diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f0c8017..38928b5d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ This release has an [MSRV][] of 1.88. ### Changed - Breaking change: wgpu has been updated to wgpu 27. ([#1280][] by [@theoparis][]) +- Breaking change: Make `Scene` clip / layers honor fill rule (even-odd clips). ([#1332][] by [@waywardmonkeys][]) + When pushing a layer, you should use `Fill::NonZero` as the clip fill rule to achieve the same behavior as previous versions. ### Fixed @@ -383,6 +385,7 @@ This release has an [MSRV][] of 1.75. [#1273]: https://github.com/linebender/vello/pull/1273 [#1280]: https://github.com/linebender/vello/pull/1280 [#1323]: https://github.com/linebender/vello/pull/1323 +[#1332]: https://github.com/linebender/vello/pull/1332 [Unreleased]: https://github.com/linebender/vello/compare/v0.5.0...HEAD diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 9703872f0..836cadf84 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -769,7 +769,7 @@ mod impls { &rect, ); let alpha = params.time.sin() as f32 * 0.5 + 0.5; - scene.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect); + scene.push_layer(Fill::NonZero, Mix::Normal, alpha, Affine::IDENTITY, &rect); scene.fill( Fill::NonZero, Affine::translate((100.0, 100.0)) * Affine::scale(0.2), @@ -1125,6 +1125,7 @@ mod impls { let mut depth = 0; for (width, color) in &options[..params.complexity.min(options.len() - 1)] { scene.push_layer( + Fill::NonZero, Mix::Normal, 0.9, Affine::IDENTITY, @@ -1152,7 +1153,7 @@ mod impls { const CLIPS_PER_FILL: usize = 3; for _ in 0..CLIPS_PER_FILL { let rot = Affine::rotate(rng.random_range(0.0..PI)); - scene.push_clip_layer(translate * rot, &base_tri); + scene.push_clip_layer(Fill::NonZero, translate * rot, &base_tri); } let rot = Affine::rotate(rng.random_range(0.0..PI)); let color = Color::new([rng.random(), rng.random(), rng.random(), 1.]); @@ -1212,7 +1213,7 @@ mod impls { PathEl::LineTo((X0, Y1).into()), PathEl::ClosePath, ]; - scene.push_clip_layer(Affine::IDENTITY, &path); + scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path); } let rect = Rect::new(X0, Y0, X1, Y1); scene.fill( @@ -1243,7 +1244,11 @@ mod impls { None, &make_diamond(1024.0, 125.0), ); - scene.push_clip_layer(Affine::IDENTITY, &make_diamond(1024.0, 150.0)); + scene.push_clip_layer( + Fill::NonZero, + Affine::IDENTITY, + &make_diamond(1024.0, 150.0), + ); scene.fill( Fill::NonZero, Affine::IDENTITY, @@ -1271,11 +1276,11 @@ mod impls { scene.fill(Fill::NonZero, transform, &radial, None, &rect); } const COLORS: &[Color] = &[palette::css::RED, palette::css::LIME, palette::css::BLUE]; - scene.push_layer(Mix::Normal, 1.0, transform, &rect); + scene.push_layer(Fill::NonZero, Mix::Normal, 1.0, transform, &rect); for (i, c) in COLORS.iter().enumerate() { let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0)) .with_stops([palette::css::WHITE, *c]); - scene.push_layer(blend, 1.0, transform, &rect); + scene.push_layer(Fill::NonZero, blend, 1.0, transform, &rect); // squash the ellipse let a = transform * Affine::translate((100., 100.)) @@ -1579,7 +1584,7 @@ mod impls { PathEl::ClosePath, ] }; - scene.push_clip_layer(Affine::IDENTITY, &clip); + scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &clip); { let text_size = 60.0 + 40.0 * (params.time as f32).sin(); let s = "Some clipped text!"; @@ -1594,6 +1599,80 @@ mod impls { } scene.pop_layer(); + // Even-odd clip-layer demo: a self-intersecting star ("pentagram") has different results + // under non-zero vs even-odd fill rules (even-odd produces a hole). + let demo_rect = Rect::new(250.0, 20.0, 450.0, 220.0); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + palette::css::BLUE, + None, + &demo_rect, + ); + let mut star = BezPath::new(); + let center = Point::new(350.0, 120.0); + let outer_r = 90.0; + let start_angle = -std::f64::consts::FRAC_PI_2; + let pts: [Point; 5] = core::array::from_fn(|i| { + let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0); + center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r) + }); + let order = [0_usize, 2, 4, 1, 3]; + star.move_to(pts[order[0]]); + for &idx in &order[1..] { + star.line_to(pts[idx]); + } + star.close_path(); + + scene.push_clip_layer(Fill::EvenOdd, Affine::IDENTITY, &star); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + palette::css::RED, + None, + &demo_rect, + ); + scene.pop_layer(); + + // Stroke clip demo: clip to the stroked outline of a path. + let stroke_demo_rect = Rect::new(250.0, 240.0, 450.0, 440.0); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + palette::css::SLATE_GRAY, + None, + &stroke_demo_rect, + ); + let mut stroke_star = BezPath::new(); + let center = Point::new(350.0, 340.0); + let outer_r = 85.0; + let start_angle = -std::f64::consts::FRAC_PI_2; + let pts: [Point; 5] = core::array::from_fn(|i| { + let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0); + center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r) + }); + let order = [0_usize, 2, 4, 1, 3]; + stroke_star.move_to(pts[order[0]]); + for &idx in &order[1..] { + stroke_star.line_to(pts[idx]); + } + stroke_star.close_path(); + let mut stroke = Stroke::new(18.0); + stroke.join = Join::Round; + stroke.start_cap = Cap::Round; + stroke.end_cap = Cap::Round; + scene.push_clip_layer(&stroke, Affine::IDENTITY, &stroke_star); + let grad = Gradient::new_linear((250.0, 240.0), (450.0, 440.0)) + .with_stops([palette::css::MAGENTA, palette::css::CYAN]); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + &grad, + None, + &stroke_demo_rect, + ); + scene.pop_layer(); + let large_background_rect = Rect::new(-1000.0, -1000.0, 2000.0, 2000.0); let inside_clip_rect = Rect::new(11.0, 13.399999999999999, 59.0, 56.6); let outside_clip_rect = Rect::new( @@ -1606,6 +1685,7 @@ mod impls { let scale = 2.0; scene.push_layer( + Fill::NonZero, BlendMode { mix: Mix::Normal, compose: Compose::SrcOver, @@ -1926,6 +2006,7 @@ mod impls { }, ); scene.push_layer( + Fill::NonZero, BlendMode::new(Mix::Normal, Compose::SrcOver), 1.0, Affine::IDENTITY, @@ -1949,6 +2030,7 @@ mod impls { }, ); scene.push_luminance_mask_layer( + Fill::NonZero, 1.0, Affine::IDENTITY, &Rect { @@ -1993,6 +2075,7 @@ mod impls { .unwrap(); // HACK: Porter-Duff "over" the base color, restoring full alpha scene.push_layer( + Fill::NonZero, BlendMode::new(Mix::Normal, Compose::SrcOver), 1.0, Affine::IDENTITY, @@ -2028,6 +2111,7 @@ mod impls { }, ); scene.push_luminance_mask_layer( + Fill::NonZero, 1.0, Affine::IDENTITY, &Rect { diff --git a/vello/src/scene.rs b/vello/src/scene.rs index 840af2e60..c32ecaf91 100644 --- a/vello/src/scene.rs +++ b/vello/src/scene.rs @@ -82,6 +82,11 @@ impl Scene { /// Pushes a new layer clipped by the specified shape and composed with /// previous layers using the specified blend mode. /// + /// The `clip_style` controls how the `clip` shape is interpreted. + /// + /// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule. + /// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape. + /// /// Every drawing command after this call will be clipped by the shape /// until the layer is [popped](Self::pop_layer). /// For layers which are only added for clipping, you should @@ -90,9 +95,14 @@ impl Scene { /// **However, the transforms are *not* saved or modified by the layer stack.** /// That is, the `transform` argument to this function only applies a transform to the `clip` shape. #[expect(deprecated, reason = "Provided by the user, need to handle correctly.")] + #[expect( + single_use_lifetimes, + reason = "False positive: https://github.com/rust-lang/rust/issues/129255" + )] #[track_caller] - pub fn push_layer( + pub fn push_layer<'a>( &mut self, + clip_style: impl Into>, blend: impl Into, alpha: f32, transform: Affine, @@ -107,6 +117,7 @@ impl Scene { } self.push_layer_inner( DrawBeginClip::new(blend, alpha.clamp(0.0, 1.0)), + clip_style.into(), transform, clip, ); @@ -118,6 +129,11 @@ impl Scene { /// That is, content drawn between this and the matching `pop_layer` call will serve /// as a luminance mask for the prior content in this layer. /// + /// The `clip_style` controls how the `clip` shape is interpreted. + /// + /// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule. + /// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape. + /// /// Every drawing command after this call will be clipped by the shape /// until the layer is [popped](Self::pop_layer). /// @@ -135,9 +151,20 @@ impl Scene { /// This issue only occurs if there are no intermediate opaque layers, so can be worked around /// by drawing something opaque (or having an opaque `base_color`), then putting a layer around your entire scene /// with a [`Compose::SrcOver`]. - pub fn push_luminance_mask_layer(&mut self, alpha: f32, transform: Affine, clip: &impl Shape) { + #[expect( + single_use_lifetimes, + reason = "False positive: https://github.com/rust-lang/rust/issues/129255" + )] + pub fn push_luminance_mask_layer<'a>( + &mut self, + clip_style: impl Into>, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { self.push_layer_inner( DrawBeginClip::luminance_mask(alpha.clamp(0.0, 1.0)), + clip_style.into(), transform, clip, ); @@ -145,6 +172,11 @@ impl Scene { /// Pushes a new layer clipped by the specified `clip` shape. /// + /// The `clip_style` controls how the `clip` shape is interpreted. + /// + /// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule. + /// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape. + /// /// The pushed layer is intended to not impact the "source" for blending; that is, any blends /// within this layer will still include content from before this method was called in the "source" /// of that blend operation. @@ -157,21 +189,55 @@ impl Scene { /// /// **However, the transforms are *not* saved or modified by the layer stack.** /// That is, the `transform` argument to this function only applies a transform to the `clip` shape. - pub fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { - self.push_layer_inner(DrawBeginClip::clip(), transform, clip); + #[expect( + single_use_lifetimes, + reason = "False positive: https://github.com/rust-lang/rust/issues/129255" + )] + pub fn push_clip_layer<'a>( + &mut self, + clip_style: impl Into>, + transform: Affine, + clip: &impl Shape, + ) { + self.push_layer_inner(DrawBeginClip::clip(), clip_style.into(), transform, clip); } /// Helper for logic shared between [`Self::push_layer`] and [`Self::push_luminance_mask_layer`] - fn push_layer_inner( + fn push_layer_inner<'a>( &mut self, parameters: DrawBeginClip, + clip_style: StyleRef<'a>, transform: Affine, clip: &impl Shape, ) { let t = Transform::from_kurbo(&transform); self.encoding.encode_transform(t); - self.encoding.encode_fill_style(Fill::NonZero); - if !self.encoding.encode_shape(clip, true) { + let (is_fill, stroke_for_estimate) = match clip_style { + StyleRef::Fill(fill) => { + self.encoding.encode_fill_style(fill); + (true, None) + } + StyleRef::Stroke(stroke) => { + let encoded_stroke = self.encoding.encode_stroke_style(stroke); + (false, encoded_stroke.then_some(stroke)) + } + }; + if stroke_for_estimate.is_none() && matches!(clip_style, StyleRef::Stroke(_)) { + // If the stroke has zero width, encode a valid empty path. This suppresses + // all drawing until the layer is popped. + self.encoding.encode_fill_style(Fill::NonZero); + self.encoding.encode_empty_shape(); + #[cfg(feature = "bump_estimate")] + { + use peniko::kurbo::PathEl; + let path = [PathEl::MoveTo(Point::ZERO), PathEl::LineTo(Point::ZERO)]; + self.estimator.count_path(path.into_iter(), &t, None); + } + self.encoding.encode_begin_clip(parameters); + return; + } + + if !self.encoding.encode_shape(clip, is_fill) { // If the layer shape is invalid, encode a valid empty path. This suppresses // all drawing until the layer is popped. self.encoding.encode_empty_shape(); @@ -183,7 +249,8 @@ impl Scene { } } else { #[cfg(feature = "bump_estimate")] - self.estimator.count_path(clip.path_elements(0.1), &t, None); + self.estimator + .count_path(clip.path_elements(0.1), &t, stroke_for_estimate); } self.encoding.encode_begin_clip(parameters); } @@ -894,7 +961,7 @@ impl ColorPainter for DrawColorGlyphs<'_> { }; self.clip_depth += 1; self.scene - .push_clip_layer(self.last_transform().to_kurbo(), &path.0); + .push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &path.0); } fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox) { @@ -909,7 +976,7 @@ impl ColorPainter for DrawColorGlyphs<'_> { } self.clip_depth += 1; self.scene - .push_clip_layer(self.last_transform().to_kurbo(), &clip_box); + .push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &clip_box); } fn pop_clip(&mut self) { @@ -949,8 +1016,13 @@ impl ColorPainter for DrawColorGlyphs<'_> { // TODO: _ => Compose::SrcOver, }; - self.scene - .push_layer(blend, 1.0, self.last_transform().to_kurbo(), &self.clip_box); + self.scene.push_layer( + Fill::NonZero, + blend, + 1.0, + self.last_transform().to_kurbo(), + &self.clip_box, + ); } fn pop_layer(&mut self) { diff --git a/vello_encoding/src/draw.rs b/vello_encoding/src/draw.rs index 7b2fdcc62..024324f33 100644 --- a/vello_encoding/src/draw.rs +++ b/vello_encoding/src/draw.rs @@ -37,7 +37,7 @@ impl DrawTag { pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect) /// Begin layer/clip. - pub const BEGIN_CLIP: Self = Self(0x9); + pub const BEGIN_CLIP: Self = Self(0x49); /// End layer/clip. pub const END_CLIP: Self = Self(0x21); diff --git a/vello_shaders/shader/clip_leaf.wgsl b/vello_shaders/shader/clip_leaf.wgsl index 0fe022948..20eda44ff 100644 --- a/vello_shaders/shader/clip_leaf.wgsl +++ b/vello_shaders/shader/clip_leaf.wgsl @@ -193,6 +193,8 @@ fn main( draw_monoids[ix].path_ix = u32(path_ix); // Make EndClip point to the same draw data as BeginClip draw_monoids[ix].scene_offset = draw_monoids[parent_ix].scene_offset; + // Make EndClip point to the same info (draw flags) as BeginClip + draw_monoids[ix].info_offset = draw_monoids[parent_ix].info_offset; if grandparent >= 0 { bbox = sh_bbox[grandparent]; } else if grandparent + i32(stack_size) >= 0 { diff --git a/vello_shaders/shader/coarse.wgsl b/vello_shaders/shader/coarse.wgsl index 706032376..907f6e947 100644 --- a/vello_shaders/shader/coarse.wgsl +++ b/vello_shaders/shader/coarse.wgsl @@ -410,7 +410,9 @@ fn main( write_image(di + 1u); } case DRAWTAG_BEGIN_CLIP: { - if tile.segment_count_or_ix == 0u && tile.backdrop == 0 { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; + if tile.segment_count_or_ix == 0u && backdrop_clear { clip_zero_depth = clip_depth + 1u; } else { write_begin_clip(); @@ -421,8 +423,7 @@ fn main( } case DRAWTAG_END_CLIP: { clip_depth -= 1u; - // A clip shape is always a non-zero fill (draw_flags=0). - write_path(tile, tile_ix, /*draw_flags=*/0u); + write_path(tile, tile_ix, draw_flags); let blend = scene[dd]; let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); diff --git a/vello_shaders/shader/draw_leaf.wgsl b/vello_shaders/shader/draw_leaf.wgsl index 415aa81f0..b5514b5ae 100644 --- a/vello_shaders/shader/draw_leaf.wgsl +++ b/vello_shaders/shader/draw_leaf.wgsl @@ -131,6 +131,9 @@ fn main( case DRAWTAG_FILL_COLOR: { info[di] = draw_flags; } + case DRAWTAG_BEGIN_CLIP: { + info[di] = draw_flags; + } case DRAWTAG_FILL_LIN_GRADIENT: { info[di] = draw_flags; var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); diff --git a/vello_shaders/shader/shared/drawtag.wgsl b/vello_shaders/shader/shared/drawtag.wgsl index 8a3bdda8a..6020e9f78 100644 --- a/vello_shaders/shader/shared/drawtag.wgsl +++ b/vello_shaders/shader/shared/drawtag.wgsl @@ -23,7 +23,7 @@ const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu; const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u; const DRAWTAG_FILL_IMAGE = 0x28Cu; const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u; -const DRAWTAG_BEGIN_CLIP = 0x9u; +const DRAWTAG_BEGIN_CLIP = 0x49u; const DRAWTAG_END_CLIP = 0x21u; /// The first word of each draw info stream entry contains the flags. This is not a part of the diff --git a/vello_shaders/src/cpu/clip_leaf.rs b/vello_shaders/src/cpu/clip_leaf.rs index 62d3bcdb3..806331008 100644 --- a/vello_shaders/src/cpu/clip_leaf.rs +++ b/vello_shaders/src/cpu/clip_leaf.rs @@ -66,6 +66,8 @@ fn clip_leaf_main( draw_monoids[clip_el.ix as usize].path_ix = tos.path_ix; draw_monoids[clip_el.ix as usize].scene_offset = draw_monoids[tos.parent_ix as usize].scene_offset; + draw_monoids[clip_el.ix as usize].info_offset = + draw_monoids[tos.parent_ix as usize].info_offset; } } } diff --git a/vello_shaders/src/cpu/coarse.rs b/vello_shaders/src/cpu/coarse.rs index 48c6b12ce..acc3cca6d 100644 --- a/vello_shaders/src/cpu/coarse.rs +++ b/vello_shaders/src/cpu/coarse.rs @@ -334,7 +334,13 @@ fn coarse_main( tile_state.write_blur_rect(config, bump, ptcl, rgba_color, di + 1); } DrawTag::BEGIN_CLIP => { - if tile.segment_count_or_ix == 0 && tile.backdrop == 0 { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0; + let backdrop_clear = if even_odd { + tile.backdrop.abs() & 1 == 0 + } else { + tile.backdrop == 0 + }; + if tile.segment_count_or_ix == 0 && backdrop_clear { clip_zero_depth = clip_depth + 1; } else { tile_state.write_begin_clip(config, bump, ptcl); @@ -347,8 +353,7 @@ fn coarse_main( } DrawTag::END_CLIP => { clip_depth -= 1; - // A clip shape is always a non-zero fill (draw_flags=0). - tile_state.write_path(config, bump, ptcl, tile, 0); + tile_state.write_path(config, bump, ptcl, tile, draw_flags); let blend = scene[dd as usize]; let alpha = f32::from_bits(scene[dd as usize + 1]); tile_state.write_end_clip(config, bump, ptcl, blend, alpha); diff --git a/vello_shaders/src/cpu/draw_leaf.rs b/vello_shaders/src/cpu/draw_leaf.rs index 13d67fa0f..6f709010b 100644 --- a/vello_shaders/src/cpu/draw_leaf.rs +++ b/vello_shaders/src/cpu/draw_leaf.rs @@ -191,7 +191,9 @@ fn draw_leaf_main( info[di + 9] = scene[dd as usize + 3]; info[di + 10] = scene[dd as usize + 4]; } - DrawTag::BEGIN_CLIP => (), + DrawTag::BEGIN_CLIP => { + info[di] = draw_flags; + } _ => todo!("unhandled draw tag {:x}", tag_word.0), } } diff --git a/vello_tests/snapshots/clip_test.png b/vello_tests/snapshots/clip_test.png new file mode 100644 index 000000000..ec78edd78 --- /dev/null +++ b/vello_tests/snapshots/clip_test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7ef8db8960774793d3be59d29a9972966994088bacc7d7de3c09aa02b7d561a +size 13198 diff --git a/vello_tests/tests/known_issues.rs b/vello_tests/tests/known_issues.rs index 56ea71d63..8188183d0 100644 --- a/vello_tests/tests/known_issues.rs +++ b/vello_tests/tests/known_issues.rs @@ -97,6 +97,7 @@ fn test_layer_size() { &Rect::from_origin_size((20.0, 20.0), (20., 20.)), ); scene.push_layer( + vello::peniko::Fill::NonZero, vello::peniko::Compose::Clear, 1.0, vello::kurbo::Affine::IDENTITY, @@ -205,8 +206,14 @@ fn clip_blends() { &Rect::from_origin_size((0., 0.), (100., 100.)), ); let layer_shape = Triangle::from_coords((50., 0.), (0., 100.), (100., 100.)); - scene.push_clip_layer(Affine::IDENTITY, &layer_shape); - scene.push_layer(Mix::Multiply, 1.0, Affine::IDENTITY, &layer_shape); + scene.push_clip_layer(vello::peniko::Fill::NonZero, Affine::IDENTITY, &layer_shape); + scene.push_layer( + vello::peniko::Fill::NonZero, + Mix::Multiply, + 1.0, + Affine::IDENTITY, + &layer_shape, + ); scene.fill( vello::peniko::Fill::EvenOdd, Affine::IDENTITY, diff --git a/vello_tests/tests/snapshot_test_scenes.rs b/vello_tests/tests/snapshot_test_scenes.rs index 6c9ab1071..204b8ab55 100644 --- a/vello_tests/tests/snapshot_test_scenes.rs +++ b/vello_tests/tests/snapshot_test_scenes.rs @@ -98,6 +98,14 @@ fn snapshot_many_clips() { snapshot_test_scene(test_scene, params); } +#[test] +#[cfg_attr(skip_gpu_tests, ignore)] +fn snapshot_clip_test() { + let test_scene = test_scenes::clip_test(); + let params = TestParams::new("clip_test", 512, 512); + snapshot_test_scene(test_scene, params); +} + #[test] #[cfg_attr(skip_gpu_tests, ignore)] fn snapshot_blurred_rounded_rect() {