1use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use arrayvec::ArrayVec;
9
10use crate::{LayerShape, Rect};
11
12const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16pub enum TileMode {
17 #[default]
19 Clamp,
20 Repeated,
22 Mirror,
24 Decal,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct BlurredEdgeTreatment {
35 shape: Option<LayerShape>,
36}
37
38impl BlurredEdgeTreatment {
39 pub const RECTANGLE: Self = Self {
41 shape: Some(LayerShape::Rectangle),
42 };
43
44 pub const UNBOUNDED: Self = Self { shape: None };
46
47 pub const fn with_shape(shape: LayerShape) -> Self {
49 Self { shape: Some(shape) }
50 }
51
52 pub fn shape(self) -> Option<LayerShape> {
53 self.shape
54 }
55
56 pub fn clip(self) -> bool {
57 self.shape.is_some()
58 }
59
60 pub fn tile_mode(self) -> TileMode {
61 if self.clip() {
62 TileMode::Clamp
63 } else {
64 TileMode::Decal
65 }
66 }
67}
68
69impl Default for BlurredEdgeTreatment {
70 fn default() -> Self {
71 Self::RECTANGLE
72 }
73}
74
75pub const RUNTIME_SHADER_PRELUDE_WGSL: &str = concat!(
80 include_str!("../shaders/fullscreen_quad_vs.wgsl"),
81 include_str!("../shaders/runtime_shader_bindings.wgsl"),
82);
83
84#[derive(Clone, Debug)]
124pub struct RuntimeShader {
125 source: Arc<str>,
126 source_hash: u64,
127 uniforms: RuntimeShaderUniforms,
128 specialization: Option<Arc<ShaderSpecialization>>,
129 input_padding: f32,
130 output_padding: f32,
131 batched_source: bool,
132 domains: Option<Box<ShaderDomains>>,
133}
134
135#[derive(Clone, Debug, Default)]
136struct ShaderSpecialization {
137 overrides: Vec<(&'static str, f64)>,
138 overrides_hash: OnceLock<u64>,
139 substrates: ArrayVec<SubstrateSpec, MAX_SUBSTRATES>,
140 draw_split: Option<&'static str>,
141}
142
143pub(crate) struct ShaderSpecializationCache<K, const N: usize> {
144 entries: ArrayVec<CachedShaderSpecialization<K>, N>,
145}
146
147struct CachedShaderSpecialization<K> {
148 source: Option<Arc<ShaderSpecialization>>,
149 key: K,
150 result: Option<Arc<ShaderSpecialization>>,
151}
152
153impl<K: PartialEq, const N: usize> ShaderSpecializationCache<K, N> {
154 pub(crate) const fn new() -> Self {
155 assert!(N > 0);
156 Self {
157 entries: ArrayVec::new_const(),
158 }
159 }
160
161 pub(crate) fn apply(
162 &mut self,
163 shader: &mut RuntimeShader,
164 key: K,
165 specialize: impl FnOnce(&mut RuntimeShader, &K),
166 ) {
167 let hit = self.entries.iter().rposition(|entry| {
168 entry.key == key
169 && match (&entry.source, &shader.specialization) {
170 (Some(source), Some(current)) => Arc::ptr_eq(source, current),
171 (None, None) => true,
172 _ => false,
173 }
174 });
175 if let Some(index) = hit {
176 let entry = self.entries.remove(index);
177 shader.specialization.clone_from(&entry.result);
178 self.entries.push(entry);
179 return;
180 }
181 if shader
182 .specialization
183 .as_ref()
184 .is_some_and(|source| Arc::strong_count(source) == 1)
185 {
186 specialize(shader, &key);
187 return;
188 }
189 let source = shader.specialization.clone();
190 specialize(shader, &key);
191 if self.entries.is_full() {
192 self.entries.remove(0);
193 }
194 self.entries.push(CachedShaderSpecialization {
195 source,
196 key,
197 result: shader.specialization.clone(),
198 });
199 }
200}
201
202static DEFAULT_SHADER_SPECIALIZATION: ShaderSpecialization = ShaderSpecialization {
203 overrides: Vec::new(),
204 overrides_hash: OnceLock::new(),
205 substrates: ArrayVec::new_const(),
206 draw_split: None,
207};
208
209#[derive(Clone, Copy, Debug, Default, PartialEq)]
210struct ShaderDomains {
211 output_support: Option<Rect>,
212 sample_domain: Option<Rect>,
213}
214
215fn finite_rect(rect: Option<Rect>) -> Option<Rect> {
216 rect.filter(|rect| {
217 rect.x.is_finite()
218 && rect.y.is_finite()
219 && rect.width.is_finite()
220 && rect.height.is_finite()
221 })
222}
223
224pub const MAX_SUBSTRATES: usize = 3;
226
227#[derive(Clone, Copy, Debug, PartialEq)]
230pub enum SubstrateSpec {
231 Average { block: u32 },
234 Blur { radius_px: f32 },
237}
238
239impl SubstrateSpec {
240 fn same_bits(&self, other: &Self) -> bool {
241 match (self, other) {
242 (Self::Average { block: a }, Self::Average { block: b }) => a == b,
243 (Self::Blur { radius_px: a }, Self::Blur { radius_px: b }) => {
244 a.to_bits() == b.to_bits()
245 }
246 _ => false,
247 }
248 }
249
250 fn hash_bits<H: std::hash::Hasher>(&self, state: &mut H) {
251 use std::hash::Hash;
252 match self {
253 Self::Average { block } => {
254 0u8.hash(state);
255 block.hash(state);
256 }
257 Self::Blur { radius_px } => {
258 1u8.hash(state);
259 radius_px.to_bits().hash(state);
260 }
261 }
262 }
263}
264
265#[derive(Clone, Debug, PartialEq)]
266struct RuntimeShaderUniforms {
267 len: usize,
268 inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
269 heap: Option<Vec<f32>>,
270}
271
272impl RuntimeShaderUniforms {
273 fn new() -> Self {
274 Self {
275 len: 0,
276 inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
277 heap: None,
278 }
279 }
280
281 fn as_slice(&self) -> &[f32] {
282 if let Some(heap) = &self.heap {
283 heap.as_slice()
284 } else {
285 &self.inline[..self.len]
286 }
287 }
288
289 fn len(&self) -> usize {
290 self.as_slice().len()
291 }
292
293 fn ensure_len(&mut self, min_len: usize) {
294 if let Some(heap) = &mut self.heap {
295 if heap.len() < min_len {
296 heap.resize(min_len, 0.0);
297 }
298 return;
299 }
300
301 if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
302 self.len = self.len.max(min_len);
303 return;
304 }
305
306 let mut heap = Vec::with_capacity(min_len);
307 heap.extend_from_slice(&self.inline[..self.len]);
308 heap.resize(min_len, 0.0);
309 self.heap = Some(heap);
310 }
311
312 fn set(&mut self, index: usize, value: f32) {
313 if let Some(heap) = &mut self.heap {
314 heap[index] = value;
315 } else {
316 self.inline[index] = value;
317 }
318 }
319
320 #[cfg(test)]
321 fn is_inline(&self) -> bool {
322 self.heap.is_none()
323 }
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
328pub enum RuntimeShaderUniformError {
329 #[error(
330 "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"
331 )]
332 OutOfUserRange {
333 index: usize,
334 width: usize,
335 max_user_uniforms: usize,
336 reserved_start: usize,
337 max_uniforms: usize,
338 },
339}
340
341impl RuntimeShader {
342 pub const MAX_UNIFORMS: usize = 256;
346 pub const RESERVED_UNIFORM_START: usize = 224;
348 pub const SUBSTRATE_REGION_UNIFORMS: [usize; MAX_SUBSTRATES] = [232, 228, 224];
351 pub const SOURCE_REGION_UNIFORM: usize = 236;
353 pub const MASK_RECT_UNIFORM: usize = 240;
355 pub const MASK_RADII_UNIFORM: usize = 244;
357 pub const EFFECT_RECT_UNIFORM: usize = 248;
359 pub const LOGICAL_SIZE_UNIFORM: usize = 252;
361 pub const ALPHA_UNIFORM: usize = 254;
363 pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
365
366 #[track_caller]
368 pub fn new(wgsl_source: &str) -> Self {
369 let (source, source_hash) =
370 cached_shader_source(std::panic::Location::caller(), wgsl_source);
371 Self::with_source(source, source_hash)
372 }
373
374 pub fn from_shared_source(source: Arc<str>) -> Self {
379 let source_hash = cached_shared_shader_source_hash(&source);
380 Self::with_source(source, source_hash)
381 }
382
383 fn with_source(source: Arc<str>, source_hash: u64) -> Self {
384 Self {
385 source,
386 source_hash,
387 uniforms: RuntimeShaderUniforms::new(),
388 specialization: None,
389 input_padding: 0.0,
390 output_padding: 0.0,
391 batched_source: false,
392 domains: None,
393 }
394 }
395
396 fn specialization(&self) -> &ShaderSpecialization {
397 self.specialization
398 .as_deref()
399 .unwrap_or(&DEFAULT_SHADER_SPECIALIZATION)
400 }
401
402 fn specialization_mut(&mut self) -> &mut ShaderSpecialization {
403 Arc::make_mut(self.specialization.get_or_insert_with(Arc::default))
404 }
405
406 pub fn set_override(&mut self, name: &'static str, value: f64) {
413 let position = self
414 .overrides()
415 .binary_search_by(|(existing, _)| existing.cmp(&name));
416 if position.is_ok_and(|index| self.overrides()[index].1.to_bits() == value.to_bits()) {
417 return;
418 }
419 let specialization = self.specialization_mut();
420 specialization.overrides_hash.take();
421 let overrides = &mut specialization.overrides;
422 match position {
423 Ok(index) => overrides[index].1 = value,
424 Err(index) => overrides.insert(index, (name, value)),
425 }
426 }
427
428 pub fn clear_override(&mut self, name: &str) -> bool {
430 let Ok(index) = self
431 .overrides()
432 .binary_search_by(|(existing, _)| (*existing).cmp(name))
433 else {
434 return false;
435 };
436 let specialization = self.specialization_mut();
437 specialization.overrides_hash.take();
438 specialization.overrides.remove(index);
439 true
440 }
441
442 pub fn overrides(&self) -> &[(&'static str, f64)] {
445 &self.specialization().overrides
446 }
447
448 pub fn overrides_hash(&self) -> u64 {
450 let specialization = self.specialization();
451 if specialization.overrides.is_empty() {
452 return 0;
453 }
454 *specialization.overrides_hash.get_or_init(|| {
455 #[cfg(test)]
456 OVERRIDE_HASH_COMPUTATIONS.with(|count| count.set(count.get() + 1));
457 hash_shader_bytes(specialization.overrides.iter().flat_map(|(name, value)| {
458 name.bytes().chain([0]).chain(value.to_bits().to_le_bytes())
459 }))
460 })
461 }
462
463 pub fn set_input_padding(&mut self, padding: f32) {
467 self.input_padding = if padding.is_finite() {
468 padding.max(0.0)
469 } else {
470 0.0
471 };
472 }
473
474 pub fn input_padding(&self) -> f32 {
476 self.input_padding
477 }
478
479 pub fn set_output_padding(&mut self, padding: f32) {
484 self.output_padding = if padding.is_finite() {
485 padding.max(0.0)
486 } else {
487 0.0
488 };
489 }
490
491 pub fn output_padding(&self) -> f32 {
493 self.output_padding
494 }
495
496 pub fn set_output_support(&mut self, support: Option<Rect>) {
507 self.set_domains(ShaderDomains {
508 output_support: finite_rect(support),
509 sample_domain: self.sample_domain(),
510 });
511 }
512
513 pub fn output_support(&self) -> Option<Rect> {
515 self.domains
516 .as_ref()
517 .and_then(|domains| domains.output_support)
518 }
519
520 fn set_domains(&mut self, domains: ShaderDomains) {
521 self.domains = (domains != ShaderDomains::default()).then(|| Box::new(domains));
522 }
523
524 pub fn set_sample_domain(&mut self, domain: Option<Rect>) {
533 self.set_domains(ShaderDomains {
534 output_support: self.output_support(),
535 sample_domain: finite_rect(domain),
536 });
537 }
538
539 pub fn sample_domain(&self) -> Option<Rect> {
541 self.domains
542 .as_ref()
543 .and_then(|domains| domains.sample_domain)
544 }
545
546 pub fn set_float(&mut self, index: usize, value: f32) {
551 let _ = self.try_set_float(index, value);
552 }
553
554 pub fn try_set_float(
556 &mut self,
557 index: usize,
558 value: f32,
559 ) -> Result<(), RuntimeShaderUniformError> {
560 self.try_ensure_capacity(index, 1)?;
561 self.uniforms.set(index, value);
562 Ok(())
563 }
564
565 pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
570 let _ = self.try_set_float2(index, x, y);
571 }
572
573 pub fn try_set_float2(
575 &mut self,
576 index: usize,
577 x: f32,
578 y: f32,
579 ) -> Result<(), RuntimeShaderUniformError> {
580 self.try_ensure_capacity(index, 2)?;
581 self.uniforms.set(index, x);
582 self.uniforms.set(index + 1, y);
583 Ok(())
584 }
585
586 pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
591 let _ = self.try_set_float4(index, x, y, z, w);
592 }
593
594 pub fn try_set_float4(
596 &mut self,
597 index: usize,
598 x: f32,
599 y: f32,
600 z: f32,
601 w: f32,
602 ) -> Result<(), RuntimeShaderUniformError> {
603 self.try_ensure_capacity(index, 4)?;
604 self.uniforms.set(index, x);
605 self.uniforms.set(index + 1, y);
606 self.uniforms.set(index + 2, z);
607 self.uniforms.set(index + 3, w);
608 Ok(())
609 }
610
611 pub fn set_batched_source(&mut self, batched: bool) {
616 self.batched_source = batched;
617 }
618
619 pub fn batched_source(&self) -> bool {
622 self.batched_source
623 }
624
625 pub fn set_substrates(&mut self, substrates: &[SubstrateSpec]) {
634 assert!(
635 substrates.len() <= MAX_SUBSTRATES,
636 "a runtime shader declares at most {MAX_SUBSTRATES} substrates"
637 );
638 if self.substrates().len() == substrates.len()
639 && self
640 .substrates()
641 .iter()
642 .zip(substrates)
643 .all(|(existing, incoming)| existing.same_bits(incoming))
644 {
645 return;
646 }
647 self.specialization_mut().substrates = substrates.iter().copied().collect();
648 }
649
650 pub fn substrates(&self) -> &[SubstrateSpec] {
652 &self.specialization().substrates
653 }
654
655 pub fn hash_substrates<H: std::hash::Hasher>(&self, state: &mut H) {
657 use std::hash::Hash;
658 self.substrates().len().hash(state);
659 for substrate in self.substrates() {
660 substrate.hash_bits(state);
661 }
662 self.draw_split().hash(state);
663 }
664
665 pub fn set_draw_split(&mut self, override_name: Option<&'static str>) {
672 if self.draw_split() == override_name {
673 return;
674 }
675 self.specialization_mut().draw_split = override_name;
676 }
677
678 pub fn draw_split(&self) -> Option<&'static str> {
680 self.specialization().draw_split
681 }
682
683 pub fn source(&self) -> &str {
685 &self.source
686 }
687
688 pub fn uniforms(&self) -> &[f32] {
690 self.uniforms.as_slice()
691 }
692
693 pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
695 let mut padded = [0.0f32; Self::MAX_UNIFORMS];
696 let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
697 padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
698 padded
699 }
700
701 pub fn source_hash(&self) -> u64 {
703 self.source_hash
704 }
705
706 fn try_ensure_capacity(
707 &mut self,
708 index: usize,
709 width: usize,
710 ) -> Result<(), RuntimeShaderUniformError> {
711 let min_len = index
712 .checked_add(width)
713 .ok_or_else(|| Self::uniform_range_error(index, width))?;
714 if min_len > Self::MAX_USER_UNIFORMS {
715 return Err(Self::uniform_range_error(index, width));
716 }
717 self.uniforms.ensure_len(min_len);
718 Ok(())
719 }
720
721 fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
722 RuntimeShaderUniformError::OutOfUserRange {
723 index,
724 width,
725 max_user_uniforms: Self::MAX_USER_UNIFORMS,
726 reserved_start: Self::RESERVED_UNIFORM_START,
727 max_uniforms: Self::MAX_UNIFORMS,
728 }
729 }
730}
731
732#[cfg(test)]
733thread_local! {
734 static OVERRIDE_HASH_COMPUTATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
735}
736
737impl PartialEq for RuntimeShader {
738 fn eq(&self, other: &Self) -> bool {
739 self.source_hash == other.source_hash
740 && (Arc::ptr_eq(&self.source, &other.source)
741 || self.source.as_ref() == other.source.as_ref())
742 && self.uniforms == other.uniforms
743 && self.overrides().len() == other.overrides().len()
744 && self
745 .overrides()
746 .iter()
747 .zip(other.overrides())
748 .all(|(a, b)| a.0 == b.0 && a.1.to_bits() == b.1.to_bits())
749 && self.input_padding.to_bits() == other.input_padding.to_bits()
750 && self.output_padding.to_bits() == other.output_padding.to_bits()
751 && self.batched_source == other.batched_source
752 && self.substrates() == other.substrates()
753 && self.draw_split() == other.draw_split()
754 && self.domains == other.domains
755 }
756}
757
758fn hash_shader_source(source: &str) -> u64 {
759 hash_shader_bytes(source.bytes())
760}
761
762fn hash_shader_bytes(bytes: impl IntoIterator<Item = u8>) -> u64 {
763 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
764 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
765
766 bytes.into_iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
767 (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
768 })
769}
770
771#[derive(Clone, Copy, Debug, PartialEq, Eq)]
772struct ShaderSourceCallsite {
773 file: &'static str,
774 line: u32,
775 column: u32,
776}
777
778struct CachedShaderSource {
779 callsite: ShaderSourceCallsite,
780 source_hash: u64,
781 source: Arc<str>,
782}
783
784struct CachedSharedShaderSourceHash {
785 byte_ptr: usize,
786 len: usize,
787 source_hash: u64,
788 source: Weak<str>,
789}
790
791fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
792 static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
793 let byte_ptr = source.as_ptr() as usize;
794 let len = source.len();
795 let mut cache = CACHE
796 .get_or_init(|| Mutex::new(Vec::new()))
797 .lock()
798 .unwrap_or_else(|poisoned| poisoned.into_inner());
799
800 cache.retain(|entry| entry.source.strong_count() > 0);
801 if let Some(entry) = cache.iter().find(|entry| {
802 entry.byte_ptr == byte_ptr
803 && entry.len == len
804 && entry
805 .source
806 .upgrade()
807 .is_some_and(|cached| Arc::ptr_eq(&cached, source))
808 }) {
809 return entry.source_hash;
810 }
811
812 let source_hash = hash_shader_source(source);
813 cache.push(CachedSharedShaderSourceHash {
814 byte_ptr,
815 len,
816 source_hash,
817 source: Arc::downgrade(source),
818 });
819 source_hash
820}
821
822fn cached_shader_source(
823 location: &'static std::panic::Location<'static>,
824 source: &str,
825) -> (Arc<str>, u64) {
826 static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
827 let callsite = ShaderSourceCallsite {
828 file: location.file(),
829 line: location.line(),
830 column: location.column(),
831 };
832 let mut cache = CACHE
833 .get_or_init(|| Mutex::new(Vec::new()))
834 .lock()
835 .unwrap_or_else(|poisoned| poisoned.into_inner());
836
837 if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
838 if entry.source.as_ref() == source {
839 return (entry.source.clone(), entry.source_hash);
840 }
841 let source_hash = hash_shader_source(source);
842 entry.source_hash = source_hash;
843 entry.source = Arc::<str>::from(source);
844 return (entry.source.clone(), entry.source_hash);
845 }
846
847 let source_hash = hash_shader_source(source);
848 let shared = Arc::<str>::from(source);
849 cache.push(CachedShaderSource {
850 callsite,
851 source_hash,
852 source: shared.clone(),
853 });
854 (shared, source_hash)
855}
856
857#[derive(Clone, Debug, PartialEq)]
862pub enum RenderEffect {
863 Blur {
865 radius_x: f32,
866 radius_y: f32,
867 edge_treatment: TileMode,
868 },
869 Offset { offset_x: f32, offset_y: f32 },
871 Shader {
873 shader: Arc<RuntimeShader>,
875 },
876 Chain {
880 first: Arc<RenderEffect>,
881 second: Arc<RenderEffect>,
882 },
883}
884
885impl RenderEffect {
886 pub fn blur(radius: f32) -> Self {
888 Self::blur_with_edge_treatment(radius, TileMode::default())
889 }
890
891 pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
894 Self::Blur {
895 radius_x: radius,
896 radius_y: radius,
897 edge_treatment,
898 }
899 }
900
901 pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
903 Self::Blur {
904 radius_x,
905 radius_y,
906 edge_treatment,
907 }
908 }
909
910 pub fn offset(offset_x: f32, offset_y: f32) -> Self {
912 Self::Offset { offset_x, offset_y }
913 }
914
915 pub fn runtime_shader(shader: RuntimeShader) -> Self {
917 Self::Shader {
918 shader: Arc::new(shader),
919 }
920 }
921
922 pub fn then(self, other: RenderEffect) -> Self {
924 Self::Chain {
925 first: Arc::new(self),
926 second: Arc::new(other),
927 }
928 }
929
930 pub fn contains_runtime_shader(&self) -> bool {
934 match self {
935 RenderEffect::Shader { .. } => true,
936 RenderEffect::Chain { first, second } => {
937 first.contains_runtime_shader() || second.contains_runtime_shader()
938 }
939 _ => false,
940 }
941 }
942
943 pub fn input_padding(&self) -> f32 {
945 match self {
946 RenderEffect::Blur {
947 radius_x, radius_y, ..
948 } => radius_x.abs().max(radius_y.abs()),
949 RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
950 RenderEffect::Shader { shader } => shader.input_padding(),
951 RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
952 }
953 }
954
955 pub fn output_padding(&self) -> f32 {
959 match self {
960 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
961 RenderEffect::Shader { shader } => shader.output_padding(),
962 RenderEffect::Chain { first, second } => {
963 first.output_padding() + second.output_padding()
964 }
965 }
966 }
967
968 pub fn output_support(&self) -> Option<Rect> {
973 match self {
974 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
975 RenderEffect::Shader { shader } => shader.output_support(),
976 RenderEffect::Chain { second, .. } => second.output_support(),
977 }
978 }
979
980 pub fn sample_domain(&self) -> Option<Rect> {
984 match self {
985 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => None,
986 RenderEffect::Shader { shader } => shader.sample_domain(),
987 RenderEffect::Chain { second, .. } => second.sample_domain(),
988 }
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 #[test]
995 fn overrides_stay_sorted_and_replace_by_name() {
996 let mut shader = super::RuntimeShader::new("// overrides");
997 shader.set_override("ZETA", 1.0);
998 shader.set_override("ALPHA", 0.0);
999 shader.set_override("ZETA", 2.0);
1000 assert_eq!(shader.overrides(), &[("ALPHA", 0.0), ("ZETA", 2.0)]);
1001 }
1002
1003 #[test]
1004 fn clear_override_removes_present_name_and_preserves_remaining_set() {
1005 let mut shader = super::RuntimeShader::new("");
1006 shader.set_override("ZETA", 1.0);
1007 shader.set_override("ALPHA", 2.0);
1008 let mut expected = super::RuntimeShader::new("");
1009 expected.set_override("ALPHA", 2.0);
1010 assert!(shader.clear_override("ZETA"));
1011 assert!(!shader.clear_override("MISSING"));
1012 assert_eq!(shader.overrides(), &[("ALPHA", 2.0)]);
1013 assert_eq!(shader.overrides_hash(), expected.overrides_hash());
1014 assert!(shader.clear_override("ALPHA"));
1015 assert!(shader.overrides().is_empty());
1016 assert_eq!(shader.overrides_hash(), 0);
1017 }
1018
1019 #[test]
1020 fn overrides_distinguish_otherwise_equal_shaders() {
1021 let plain = super::RuntimeShader::new("// overrides-eq");
1022 let mut raised = plain.clone();
1023 raised.set_override("FLAG", 1.0);
1024 assert_eq!(plain.overrides_hash(), 0);
1025 assert_ne!(plain.overrides_hash(), raised.overrides_hash());
1026 assert_ne!(plain, raised);
1027 let mut lowered = raised.clone();
1028 lowered.set_override("FLAG", 0.0);
1029 assert_ne!(raised.overrides_hash(), lowered.overrides_hash());
1030 assert_ne!(raised, lowered);
1031 }
1032
1033 #[test]
1034 fn unchanged_override_lookups_share_one_hash_computation() {
1035 let mut shader = RuntimeShader::new("");
1036 shader.set_override("FLAG", 1.0);
1037 shader.set_override("SCALE", 0.5);
1038 OVERRIDE_HASH_COMPUTATIONS.with(|count| count.set(0));
1039 let expected = shader.overrides_hash();
1040 for _ in 0..24 {
1041 let mut copy = shader.clone();
1042 copy.set_float(0, 7.0);
1043 copy.set_override("FLAG", 1.0);
1044 assert!(!copy.clear_override("ABSENT"));
1045 assert_eq!(copy.overrides_hash(), expected);
1046 }
1047 assert_eq!(OVERRIDE_HASH_COMPUTATIONS.with(std::cell::Cell::get), 1);
1048 }
1049
1050 #[test]
1051 fn override_hash_tracks_clone_mutations_and_float_bits() {
1052 fn independent_hash(shader: &RuntimeShader) -> u64 {
1053 let bytes: Vec<u8> = shader
1054 .overrides()
1055 .iter()
1056 .flat_map(|(name, value)| {
1057 name.bytes().chain([0]).chain(value.to_bits().to_le_bytes())
1058 })
1059 .collect();
1060 if bytes.is_empty() {
1061 0
1062 } else {
1063 hash_shader_bytes(bytes)
1064 }
1065 }
1066
1067 let mut original = RuntimeShader::new("");
1068 original.set_override("VALUE", 0.0);
1069 let first = original.overrides_hash();
1070 assert_eq!(first, independent_hash(&original));
1071 for value in [
1072 -0.0,
1073 0.5,
1074 f64::INFINITY,
1075 f64::from_bits(0x7ff8_0000_0000_0001),
1076 ] {
1077 let mut changed = original.clone();
1078 changed.set_override("VALUE", value);
1079 assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1080 assert_ne!(changed.overrides_hash(), first);
1081 changed.set_override("ADDED", 1.0);
1082 assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1083 assert!(changed.clear_override("VALUE"));
1084 assert_eq!(changed.overrides_hash(), independent_hash(&changed));
1085 assert!(changed.clear_override("ADDED"));
1086 assert_eq!(changed.overrides_hash(), 0);
1087 assert_eq!(original.overrides_hash(), first);
1088 }
1089 }
1090
1091 #[test]
1092 fn shader_clones_share_declarations_and_isolate_mutation() {
1093 let mut shader = RuntimeShader::new("fn effect_fs() {}");
1094 shader.set_override("FLAG", 1.0);
1095 shader.set_substrates(&[SubstrateSpec::Average { block: 4 }]);
1096 shader.set_draw_split(Some("SPLIT"));
1097 let support = Rect {
1098 x: 1.0,
1099 y: 2.0,
1100 width: 30.0,
1101 height: 40.0,
1102 };
1103 shader.set_output_support(Some(support));
1104 let mut cloned = shader.clone();
1105 assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1106 assert_eq!(cloned.substrates().as_ptr(), shader.substrates().as_ptr());
1107 cloned.set_float(0, 2.0);
1108 assert!(shader.uniforms().is_empty());
1109 assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1110 cloned.set_override("FLAG", 2.0);
1111 assert_eq!(cloned.substrates(), shader.substrates());
1112 assert_eq!(cloned.draw_split(), shader.draw_split());
1113 cloned.set_substrates(&[SubstrateSpec::Average { block: 8 }]);
1114 cloned.set_draw_split(None);
1115 cloned.set_output_support(None);
1116 assert_eq!(shader.overrides(), &[("FLAG", 1.0)]);
1117 assert_eq!(shader.substrates(), &[SubstrateSpec::Average { block: 4 }]);
1118 assert_eq!(shader.draw_split(), Some("SPLIT"));
1119 assert_eq!(shader.output_support(), Some(support));
1120 assert_ne!(cloned, shader);
1121 }
1122
1123 use super::*;
1124 use crate::RoundedCornerShape;
1125
1126 #[test]
1127 fn cloned_effect_chains_keep_order_and_isolate_nested_edits() {
1128 let original = RenderEffect::offset(2.0, 7.0)
1129 .then(RenderEffect::blur(3.0))
1130 .then(RenderEffect::offset(-4.0, 1.0));
1131 let mut edited = original.clone();
1132 assert_eq!(edited, original);
1133 let RenderEffect::Chain {
1134 first: original_first,
1135 second: original_second,
1136 } = &original
1137 else {
1138 panic!("chain effect")
1139 };
1140 let RenderEffect::Chain {
1141 first: edited_first,
1142 second: edited_second,
1143 } = &mut edited
1144 else {
1145 panic!("chain effect")
1146 };
1147 assert!(Arc::ptr_eq(original_first, edited_first));
1148 assert!(Arc::ptr_eq(original_second, edited_second));
1149 assert_eq!(original_second.as_ref(), &RenderEffect::offset(-4.0, 1.0));
1150 let RenderEffect::Chain { first, second } = Arc::make_mut(edited_first) else {
1151 panic!("nested chain")
1152 };
1153 assert_eq!(first.as_ref(), &RenderEffect::offset(2.0, 7.0));
1154 assert_eq!(second.as_ref(), &RenderEffect::blur(3.0));
1155 *Arc::make_mut(first) = RenderEffect::offset(12.0, 17.0);
1156 *Arc::make_mut(edited_second) = RenderEffect::blur(11.0);
1157 assert_eq!(
1158 original,
1159 RenderEffect::offset(2.0, 7.0)
1160 .then(RenderEffect::blur(3.0))
1161 .then(RenderEffect::offset(-4.0, 1.0))
1162 );
1163 assert_eq!(
1164 edited,
1165 RenderEffect::offset(12.0, 17.0)
1166 .then(RenderEffect::blur(3.0))
1167 .then(RenderEffect::blur(11.0))
1168 );
1169 }
1170
1171 #[test]
1172 fn cloned_shader_effects_preserve_configuration_and_isolate_edits() {
1173 let mut shader = RuntimeShader::new("fn effect_fs() {}");
1174 shader.set_float(20, 3.0);
1175 shader.set_override("FEATURE", -0.0);
1176 shader.set_input_padding(7.0);
1177 shader.set_substrates(&[SubstrateSpec::Blur { radius_px: 12.0 }]);
1178 shader.set_draw_split(Some("SPLIT"));
1179 let original = RenderEffect::runtime_shader(shader.clone());
1180 let mut edited = original.clone();
1181 assert_eq!(edited, original);
1182 let RenderEffect::Shader {
1183 shader: original_shader,
1184 } = &original
1185 else {
1186 panic!("shader effect")
1187 };
1188 assert_eq!(original_shader.as_ref(), &shader);
1189 let RenderEffect::Shader {
1190 shader: edited_shader,
1191 } = &mut edited
1192 else {
1193 panic!("shader effect")
1194 };
1195 assert!(Arc::ptr_eq(original_shader, edited_shader));
1196 let changed = Arc::make_mut(edited_shader);
1197 changed.set_float(20, 9.0);
1198 changed.set_override("FEATURE", 1.0);
1199 changed.set_substrates(&[]);
1200 changed.set_draw_split(None);
1201 assert_eq!(original_shader.as_ref(), &shader);
1202 assert_eq!(edited_shader.uniforms()[20], 9.0);
1203 assert_eq!(edited_shader.overrides(), &[("FEATURE", 1.0)]);
1204 assert!(edited_shader.substrates().is_empty());
1205 assert_eq!(edited_shader.draw_split(), None);
1206 assert_ne!(original, edited);
1207 }
1208
1209 #[test]
1210 fn runtime_shader_set_uniforms() {
1211 let mut shader = RuntimeShader::new("// test");
1212 shader.set_float(0, 1.0);
1213 shader.set_float2(2, 3.0, 4.0);
1214 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
1215
1216 assert_eq!(shader.uniforms()[0], 1.0);
1217 assert_eq!(shader.uniforms()[1], 0.0);
1218 assert_eq!(shader.uniforms()[2], 3.0);
1219 assert_eq!(shader.uniforms()[3], 4.0);
1220 assert_eq!(shader.uniforms()[4], 5.0);
1221 assert_eq!(shader.uniforms()[5], 6.0);
1222 assert_eq!(shader.uniforms()[6], 7.0);
1223 assert_eq!(shader.uniforms()[7], 8.0);
1224 }
1225
1226 #[test]
1227 fn runtime_shader_padded() {
1228 let mut shader = RuntimeShader::new("// test");
1229 shader.set_float(0, 42.0);
1230 let padded = shader.uniforms_padded();
1231 assert_eq!(padded[0], 42.0);
1232 assert_eq!(padded[1], 0.0);
1233 assert_eq!(padded[255], 0.0);
1234 }
1235
1236 #[test]
1237 fn blur_and_offset_declare_input_padding() {
1238 assert_eq!(
1239 RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
1240 12.0
1241 );
1242 assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
1243 }
1244
1245 #[test]
1246 fn chained_effect_padding_accumulates_sampling_ranges() {
1247 let mut shader = RuntimeShader::new("// test");
1248 shader.set_input_padding(9.0);
1249 let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
1250 .then(RenderEffect::runtime_shader(shader))
1251 .then(RenderEffect::offset(2.0, -5.0));
1252
1253 assert_eq!(effect.input_padding(), 20.0);
1254 }
1255
1256 #[test]
1257 fn runtime_shader_keeps_common_uniform_payload_inline() {
1258 let mut shader = RuntimeShader::new("// test");
1259 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
1260 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
1261 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
1262 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
1263
1264 assert!(shader.uniforms.is_inline());
1265 assert_eq!(shader.uniforms().len(), 16);
1266
1267 shader.set_float(16, 17.0);
1268 assert!(!shader.uniforms.is_inline());
1269 assert_eq!(shader.uniforms()[16], 17.0);
1270 }
1271
1272 #[test]
1273 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
1274 let mut shader = RuntimeShader::new("// test");
1275
1276 let err = shader
1277 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
1278 .unwrap_err();
1279 assert_eq!(
1280 err,
1281 RuntimeShaderUniformError::OutOfUserRange {
1282 index: RuntimeShader::RESERVED_UNIFORM_START,
1283 width: 1,
1284 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1285 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1286 max_uniforms: RuntimeShader::MAX_UNIFORMS,
1287 }
1288 );
1289 assert!(shader.uniforms().is_empty());
1290
1291 let err = shader
1292 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
1293 .unwrap_err();
1294 assert_eq!(
1295 err,
1296 RuntimeShaderUniformError::OutOfUserRange {
1297 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
1298 width: 4,
1299 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
1300 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
1301 max_uniforms: RuntimeShader::MAX_UNIFORMS,
1302 }
1303 );
1304 }
1305
1306 #[test]
1307 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
1308 let mut shader = RuntimeShader::new("// test");
1309 shader.set_float(0, 7.0);
1310
1311 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
1312 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
1313
1314 assert_eq!(shader.uniforms(), &[7.0]);
1315 }
1316
1317 #[test]
1318 fn render_effect_chaining() {
1319 let blur = RenderEffect::blur(10.0);
1320 let offset = RenderEffect::offset(5.0, 5.0);
1321 let chained = blur.then(offset);
1322 match chained {
1323 RenderEffect::Chain { first, second } => {
1324 assert!(matches!(*first, RenderEffect::Blur { .. }));
1325 assert!(matches!(*second, RenderEffect::Offset { .. }));
1326 }
1327 _ => panic!("expected Chain"),
1328 }
1329 }
1330
1331 #[test]
1332 fn blur_convenience() {
1333 let effect = RenderEffect::blur(15.0);
1334 match effect {
1335 RenderEffect::Blur {
1336 radius_x,
1337 radius_y,
1338 edge_treatment,
1339 } => {
1340 assert_eq!(radius_x, 15.0);
1341 assert_eq!(radius_y, 15.0);
1342 assert_eq!(edge_treatment, TileMode::Clamp);
1343 }
1344 _ => panic!("expected Blur"),
1345 }
1346 }
1347
1348 #[test]
1349 fn blur_with_edge_treatment_uses_explicit_mode() {
1350 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
1351 match effect {
1352 RenderEffect::Blur {
1353 radius_x,
1354 radius_y,
1355 edge_treatment,
1356 } => {
1357 assert_eq!(radius_x, 6.0);
1358 assert_eq!(radius_y, 6.0);
1359 assert_eq!(edge_treatment, TileMode::Decal);
1360 }
1361 _ => panic!("expected Blur"),
1362 }
1363 }
1364
1365 #[test]
1366 fn source_hash_consistent() {
1367 let s1 = RuntimeShader::new("fn main() {}");
1368 let s2 = RuntimeShader::new("fn main() {}");
1369 assert_eq!(s1.source_hash(), s2.source_hash());
1370 }
1371
1372 #[test]
1373 fn runtime_shader_from_shared_source_reuses_shared_source() {
1374 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
1375 let s1 = RuntimeShader::from_shared_source(source.clone());
1376 let s2 = RuntimeShader::from_shared_source(source);
1377
1378 assert!(Arc::ptr_eq(&s1.source, &s2.source));
1379 assert_eq!(s1.source_hash(), s2.source_hash());
1380 }
1381
1382 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
1383 RuntimeShader::new(source)
1384 }
1385
1386 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
1387 RuntimeShader::new(source)
1388 }
1389
1390 #[test]
1391 fn runtime_shader_new_reuses_same_callsite_source() {
1392 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
1393 let s1 = runtime_shader_from_reuse_callsite(source);
1394 let s2 = runtime_shader_from_reuse_callsite(source);
1395
1396 assert!(Arc::ptr_eq(&s1.source, &s2.source));
1397 assert_eq!(s1.source_hash(), s2.source_hash());
1398 }
1399
1400 #[test]
1401 fn runtime_shader_new_replaces_changed_callsite_source() {
1402 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
1403 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
1404
1405 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
1406 assert_ne!(s1.source_hash(), s2.source_hash());
1407 assert_eq!(s2.source(), "fn b() {}");
1408 }
1409
1410 #[test]
1411 fn runtime_shader_source_storage_has_no_process_global_interner() {
1412 let source = include_str!("render_effect.rs");
1413 let blocked_static = ["static ", "INTERNER"].concat();
1414 let blocked_type = ["ShaderSource", "Interner"].concat();
1415
1416 assert!(
1417 !source.contains(&blocked_static) && !source.contains(&blocked_type),
1418 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
1419 );
1420 }
1421
1422 #[test]
1423 fn blur_xy_preserves_tile_mode() {
1424 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
1425 match effect {
1426 RenderEffect::Blur {
1427 radius_x,
1428 radius_y,
1429 edge_treatment,
1430 } => {
1431 assert_eq!(radius_x, 3.0);
1432 assert_eq!(radius_y, 7.0);
1433 assert_eq!(edge_treatment, TileMode::Clamp);
1434 }
1435 _ => panic!("expected Blur"),
1436 }
1437 }
1438
1439 #[test]
1440 fn offset_constructor_sets_components() {
1441 let effect = RenderEffect::offset(11.0, -5.0);
1442 match effect {
1443 RenderEffect::Offset { offset_x, offset_y } => {
1444 assert_eq!(offset_x, 11.0);
1445 assert_eq!(offset_y, -5.0);
1446 }
1447 _ => panic!("expected Offset"),
1448 }
1449 }
1450
1451 #[test]
1452 fn runtime_shader_equality_is_source_value_based() {
1453 let mut s1 = RuntimeShader::new("fn main() {}");
1454 let mut s2 = RuntimeShader::new("fn main() {}");
1455 s1.set_float(0, 1.0);
1456 s2.set_float(0, 1.0);
1457 assert_eq!(s1, s2);
1458 }
1459
1460 #[test]
1461 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
1462 let treatment = BlurredEdgeTreatment::default();
1463 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
1464 assert!(treatment.clip());
1465 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1466 }
1467
1468 #[test]
1469 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
1470 let treatment = BlurredEdgeTreatment::UNBOUNDED;
1471 assert_eq!(treatment.shape(), None);
1472 assert!(!treatment.clip());
1473 assert_eq!(treatment.tile_mode(), TileMode::Decal);
1474 }
1475
1476 #[test]
1477 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
1478 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
1479 let treatment = BlurredEdgeTreatment::with_shape(rounded);
1480 assert_eq!(treatment.shape(), Some(rounded));
1481 assert!(treatment.clip());
1482 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
1483 }
1484
1485 #[test]
1486 fn an_effect_chains_output_support_is_the_support_of_the_stage_that_writes_its_output() {
1487 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1488 assert_eq!(shader.output_support(), None);
1489 let support = Rect {
1490 x: 4.0,
1491 y: 6.0,
1492 width: 30.0,
1493 height: 12.0,
1494 };
1495 shader.set_output_support(Some(support));
1496 assert_eq!(shader.output_support(), Some(support));
1497 let effect = RenderEffect::blur(3.0).then(RenderEffect::runtime_shader(shader.clone()));
1498 assert_eq!(effect.output_support(), Some(support));
1499 let effect = RenderEffect::runtime_shader(shader.clone()).then(RenderEffect::blur(3.0));
1500 assert_eq!(effect.output_support(), None);
1501 assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1502 }
1503
1504 #[test]
1505 fn a_sample_domain_is_the_writers_and_a_blur_declares_none() {
1506 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1507 let domain = Rect {
1508 x: -2.0,
1509 y: -2.0,
1510 width: 20.0,
1511 height: 12.0,
1512 };
1513 let plain = shader.clone();
1514 shader.set_sample_domain(Some(domain));
1515 assert_ne!(shader, plain);
1516 assert_eq!(shader.sample_domain(), Some(domain));
1517 let effect = RenderEffect::blur(3.0).then(RenderEffect::runtime_shader(shader.clone()));
1518 assert_eq!(effect.sample_domain(), Some(domain));
1519 assert_eq!(RenderEffect::blur(3.0).output_support(), None);
1520 assert_eq!(RenderEffect::blur(3.0).sample_domain(), None);
1521 shader.set_sample_domain(Some(Rect {
1522 x: f32::INFINITY,
1523 ..domain
1524 }));
1525 assert_eq!(shader.sample_domain(), None);
1526 }
1527
1528 #[test]
1529 fn a_non_finite_output_support_clears_the_declaration_and_a_support_tells_shaders_apart() {
1530 let mut shader = RuntimeShader::new("fn glass_fs() {}");
1531 let plain = shader.clone();
1532 shader.set_output_support(Some(Rect {
1533 x: 0.0,
1534 y: 0.0,
1535 width: 10.0,
1536 height: 10.0,
1537 }));
1538 assert_ne!(shader, plain);
1539 shader.set_output_support(Some(Rect {
1540 x: 0.0,
1541 y: 0.0,
1542 width: f32::NAN,
1543 height: 10.0,
1544 }));
1545 assert_eq!(shader.output_support(), None);
1546 assert_eq!(shader, plain);
1547 }
1548 #[test]
1549 fn specialization_cache_preserves_source_identity_and_shader_values() {
1550 let mut cache = ShaderSpecializationCache::<u32, 2>::new();
1551 let mut first = RuntimeShader::new("fn effect_fs() {}");
1552 first.set_override("CALLER", -0.0);
1553 let mut second = first.clone();
1554 second.set_override("CALLER", f64::from_bits(0x7ff8_0000_0000_0001));
1555 let sources = [first, second];
1556 for key in [1, 1, 2, 3, 1] {
1557 for source in &sources {
1558 let mut shader = source.clone();
1559 shader.set_float(0, key as f32);
1560 shader.set_input_padding(key as f32);
1561 cache.apply(&mut shader, key, |shader, &key| {
1562 shader.set_override("FEATURE", f64::from(key));
1563 shader.set_draw_split(Some("SPLIT"));
1564 shader.set_substrates(&[SubstrateSpec::Average { block: key }]);
1565 });
1566 assert_eq!(
1567 shader.overrides()[0].1.to_bits(),
1568 source.overrides()[0].1.to_bits()
1569 );
1570 assert_eq!(shader.overrides()[1], ("FEATURE", f64::from(key)));
1571 assert_eq!(
1572 shader.substrates(),
1573 &[SubstrateSpec::Average { block: key }]
1574 );
1575 assert_eq!(shader.draw_split(), Some("SPLIT"));
1576 assert_eq!(shader.uniforms(), &[key as f32]);
1577 assert_eq!(shader.input_padding(), key as f32);
1578 assert_eq!(source.overrides().len(), 1);
1579 assert!(source.substrates().is_empty());
1580 assert_eq!(source.draw_split(), None);
1581 let mut repeated = source.clone();
1582 cache.apply(&mut repeated, key, |_, _| {
1583 panic!("shared specialization missed")
1584 });
1585 assert_eq!(repeated.overrides_hash(), shader.overrides_hash());
1586 assert!(Arc::ptr_eq(
1587 repeated.specialization.as_ref().unwrap(),
1588 shader.specialization.as_ref().unwrap(),
1589 ));
1590 assert!(cache.entries.len() <= 2);
1591 }
1592 }
1593 }
1594
1595 #[test]
1596 fn specialization_cache_mutates_unique_state_without_retaining_it() {
1597 let mut cache = ShaderSpecializationCache::<(), 2>::new();
1598 let mut shader = RuntimeShader::new("fn effect_fs() {}");
1599 shader.set_override("VALUE", 1.0);
1600 let allocation = Arc::as_ptr(shader.specialization.as_ref().unwrap());
1601 cache.apply(&mut shader, (), |shader, ()| {
1602 shader.set_override("VALUE", 2.0)
1603 });
1604 assert_eq!(shader.overrides(), &[("VALUE", 2.0)]);
1605 assert_eq!(
1606 Arc::as_ptr(shader.specialization.as_ref().unwrap()),
1607 allocation
1608 );
1609 assert!(cache.entries.is_empty());
1610 }
1611
1612 #[test]
1613 fn unchanged_shader_declarations_keep_their_storage() {
1614 let mut shader = RuntimeShader::new("fn effect_fs() {}");
1615 shader.set_substrates(&[]);
1616 shader.set_draw_split(None);
1617 assert!(!shader.clear_override("MISSING"));
1618 assert!(shader.specialization.is_none());
1619 shader.set_override("FLAG", 1.0);
1620 let mut cloned = shader.clone();
1621 cloned.set_override("FLAG", 1.0);
1622 cloned.set_substrates(&[]);
1623 cloned.set_draw_split(None);
1624 assert!(!cloned.clear_override("MISSING"));
1625 assert_eq!(cloned.overrides().as_ptr(), shader.overrides().as_ptr());
1626 }
1627
1628 #[test]
1629 fn shader_substrates_preserve_order_and_ownership_across_size_changes() {
1630 let declared = [
1631 SubstrateSpec::Blur { radius_px: 12.0 },
1632 SubstrateSpec::Average { block: 4 },
1633 SubstrateSpec::Blur { radius_px: -0.0 },
1634 ];
1635 let mut source = declared;
1636 let mut original = RuntimeShader::new("fn effect_fs() {}");
1637 original.set_substrates(&source);
1638 source[0] = SubstrateSpec::Average { block: 16 };
1639 let mut changed = original.clone();
1640 for replacement in [&source[..1], &source[..2], &source[..0], &source[..]] {
1641 changed.set_substrates(replacement);
1642 assert_eq!(changed.substrates().len(), replacement.len());
1643 assert!(
1644 changed
1645 .substrates()
1646 .iter()
1647 .zip(replacement)
1648 .all(|(actual, expected)| actual.same_bits(expected))
1649 );
1650 assert_eq!(original.substrates().len(), declared.len());
1651 assert!(
1652 original
1653 .substrates()
1654 .iter()
1655 .zip(&declared)
1656 .all(|(actual, expected)| actual.same_bits(expected))
1657 );
1658 }
1659 }
1660
1661 #[test]
1662 fn shader_substrate_setters_preserve_float_bits_when_detaching() {
1663 let mut original = RuntimeShader::new("fn effect_fs() {}");
1664 original.set_substrates(&[SubstrateSpec::Blur { radius_px: 0.0 }]);
1665 let mut cloned = original.clone();
1666 cloned.set_substrates(&[SubstrateSpec::Blur { radius_px: 0.0 }]);
1667 assert_eq!(cloned.substrates().as_ptr(), original.substrates().as_ptr());
1668 cloned.set_substrates(&[SubstrateSpec::Blur { radius_px: -0.0 }]);
1669 let [SubstrateSpec::Blur { radius_px }] = cloned.substrates() else {
1670 panic!("one blur substrate");
1671 };
1672 assert_eq!(radius_px.to_bits(), (-0.0_f32).to_bits());
1673 let [SubstrateSpec::Blur { radius_px }] = original.substrates() else {
1674 panic!("original blur substrate");
1675 };
1676 assert_eq!(radius_px.to_bits(), 0.0_f32.to_bits());
1677 }
1678}