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::Blur {
530 radius_x, radius_y, ..
531 } => radius_x.abs().max(radius_y.abs()),
532 RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
533 RenderEffect::Shader { shader } => shader.input_padding(),
534 RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use crate::RoundedCornerShape;
543
544 #[test]
545 fn runtime_shader_set_uniforms() {
546 let mut shader = RuntimeShader::new("// test");
547 shader.set_float(0, 1.0);
548 shader.set_float2(2, 3.0, 4.0);
549 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
550
551 assert_eq!(shader.uniforms()[0], 1.0);
552 assert_eq!(shader.uniforms()[1], 0.0); assert_eq!(shader.uniforms()[2], 3.0);
554 assert_eq!(shader.uniforms()[3], 4.0);
555 assert_eq!(shader.uniforms()[4], 5.0);
556 assert_eq!(shader.uniforms()[5], 6.0);
557 assert_eq!(shader.uniforms()[6], 7.0);
558 assert_eq!(shader.uniforms()[7], 8.0);
559 }
560
561 #[test]
562 fn runtime_shader_padded() {
563 let mut shader = RuntimeShader::new("// test");
564 shader.set_float(0, 42.0);
565 let padded = shader.uniforms_padded();
566 assert_eq!(padded[0], 42.0);
567 assert_eq!(padded[1], 0.0);
568 assert_eq!(padded[255], 0.0);
569 }
570
571 #[test]
572 fn blur_and_offset_declare_input_padding() {
573 assert_eq!(
574 RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
575 12.0
576 );
577 assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
578 }
579
580 #[test]
581 fn chained_effect_padding_accumulates_sampling_ranges() {
582 let mut shader = RuntimeShader::new("// test");
583 shader.set_input_padding(9.0);
584 let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
585 .then(RenderEffect::runtime_shader(shader))
586 .then(RenderEffect::offset(2.0, -5.0));
587
588 assert_eq!(effect.input_padding(), 20.0);
589 }
590
591 #[test]
592 fn runtime_shader_keeps_common_uniform_payload_inline() {
593 let mut shader = RuntimeShader::new("// test");
594 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
595 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
596 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
597 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
598
599 assert!(shader.uniforms.is_inline());
600 assert_eq!(shader.uniforms().len(), 16);
601
602 shader.set_float(16, 17.0);
603 assert!(!shader.uniforms.is_inline());
604 assert_eq!(shader.uniforms()[16], 17.0);
605 }
606
607 #[test]
608 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
609 let mut shader = RuntimeShader::new("// test");
610
611 let err = shader
612 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
613 .unwrap_err();
614 assert_eq!(
615 err,
616 RuntimeShaderUniformError::OutOfUserRange {
617 index: RuntimeShader::RESERVED_UNIFORM_START,
618 width: 1,
619 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
620 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
621 max_uniforms: RuntimeShader::MAX_UNIFORMS,
622 }
623 );
624 assert!(shader.uniforms().is_empty());
625
626 let err = shader
627 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
628 .unwrap_err();
629 assert_eq!(
630 err,
631 RuntimeShaderUniformError::OutOfUserRange {
632 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
633 width: 4,
634 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
635 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
636 max_uniforms: RuntimeShader::MAX_UNIFORMS,
637 }
638 );
639 }
640
641 #[test]
642 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
643 let mut shader = RuntimeShader::new("// test");
644 shader.set_float(0, 7.0);
645
646 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
647 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
648
649 assert_eq!(shader.uniforms(), &[7.0]);
650 }
651
652 #[test]
653 fn render_effect_chaining() {
654 let blur = RenderEffect::blur(10.0);
655 let offset = RenderEffect::offset(5.0, 5.0);
656 let chained = blur.then(offset);
657 match chained {
658 RenderEffect::Chain { first, second } => {
659 assert!(matches!(*first, RenderEffect::Blur { .. }));
660 assert!(matches!(*second, RenderEffect::Offset { .. }));
661 }
662 _ => panic!("expected Chain"),
663 }
664 }
665
666 #[test]
667 fn blur_convenience() {
668 let effect = RenderEffect::blur(15.0);
669 match effect {
670 RenderEffect::Blur {
671 radius_x,
672 radius_y,
673 edge_treatment,
674 } => {
675 assert_eq!(radius_x, 15.0);
676 assert_eq!(radius_y, 15.0);
677 assert_eq!(edge_treatment, TileMode::Clamp);
678 }
679 _ => panic!("expected Blur"),
680 }
681 }
682
683 #[test]
684 fn blur_with_edge_treatment_uses_explicit_mode() {
685 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
686 match effect {
687 RenderEffect::Blur {
688 radius_x,
689 radius_y,
690 edge_treatment,
691 } => {
692 assert_eq!(radius_x, 6.0);
693 assert_eq!(radius_y, 6.0);
694 assert_eq!(edge_treatment, TileMode::Decal);
695 }
696 _ => panic!("expected Blur"),
697 }
698 }
699
700 #[test]
701 fn source_hash_consistent() {
702 let s1 = RuntimeShader::new("fn main() {}");
703 let s2 = RuntimeShader::new("fn main() {}");
704 assert_eq!(s1.source_hash(), s2.source_hash());
705 }
706
707 #[test]
708 fn runtime_shader_from_shared_source_reuses_shared_source() {
709 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
710 let s1 = RuntimeShader::from_shared_source(source.clone());
711 let s2 = RuntimeShader::from_shared_source(source);
712
713 assert!(Arc::ptr_eq(&s1.source, &s2.source));
714 assert_eq!(s1.source_hash(), s2.source_hash());
715 }
716
717 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
718 RuntimeShader::new(source)
719 }
720
721 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
722 RuntimeShader::new(source)
723 }
724
725 #[test]
726 fn runtime_shader_new_reuses_same_callsite_source() {
727 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
728 let s1 = runtime_shader_from_reuse_callsite(source);
729 let s2 = runtime_shader_from_reuse_callsite(source);
730
731 assert!(Arc::ptr_eq(&s1.source, &s2.source));
732 assert_eq!(s1.source_hash(), s2.source_hash());
733 }
734
735 #[test]
736 fn runtime_shader_new_replaces_changed_callsite_source() {
737 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
738 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
739
740 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
741 assert_ne!(s1.source_hash(), s2.source_hash());
742 assert_eq!(s2.source(), "fn b() {}");
743 }
744
745 #[test]
746 fn runtime_shader_source_storage_has_no_process_global_interner() {
747 let source = include_str!("render_effect.rs");
748 let blocked_static = ["static ", "INTERNER"].concat();
749 let blocked_type = ["ShaderSource", "Interner"].concat();
750
751 assert!(
752 !source.contains(&blocked_static) && !source.contains(&blocked_type),
753 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
754 );
755 }
756
757 #[test]
758 fn blur_xy_preserves_tile_mode() {
759 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
760 match effect {
761 RenderEffect::Blur {
762 radius_x,
763 radius_y,
764 edge_treatment,
765 } => {
766 assert_eq!(radius_x, 3.0);
767 assert_eq!(radius_y, 7.0);
768 assert_eq!(edge_treatment, TileMode::Clamp);
769 }
770 _ => panic!("expected Blur"),
771 }
772 }
773
774 #[test]
775 fn offset_constructor_sets_components() {
776 let effect = RenderEffect::offset(11.0, -5.0);
777 match effect {
778 RenderEffect::Offset { offset_x, offset_y } => {
779 assert_eq!(offset_x, 11.0);
780 assert_eq!(offset_y, -5.0);
781 }
782 _ => panic!("expected Offset"),
783 }
784 }
785
786 #[test]
787 fn runtime_shader_equality_is_source_value_based() {
788 let mut s1 = RuntimeShader::new("fn main() {}");
789 let mut s2 = RuntimeShader::new("fn main() {}");
790 s1.set_float(0, 1.0);
791 s2.set_float(0, 1.0);
792 assert_eq!(s1, s2);
793 }
794
795 #[test]
796 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
797 let treatment = BlurredEdgeTreatment::default();
798 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
799 assert!(treatment.clip());
800 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
801 }
802
803 #[test]
804 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
805 let treatment = BlurredEdgeTreatment::UNBOUNDED;
806 assert_eq!(treatment.shape(), None);
807 assert!(!treatment.clip());
808 assert_eq!(treatment.tile_mode(), TileMode::Decal);
809 }
810
811 #[test]
812 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
813 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
814 let treatment = BlurredEdgeTreatment::with_shape(rounded);
815 assert_eq!(treatment.shape(), Some(rounded));
816 assert!(treatment.clip());
817 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
818 }
819}