Skip to content

Commit ab7fdb2

Browse files
vello: make Scene clip/layers honor fill rule (even-odd clips)
Scene layer APIs now take a `Fill` for the clip shape (`push_layer`, `push_clip_layer`, `push_luminance_mask_layer`), fixing the long-standing hardcoded `Fill::NonZero` clip behavior and aligning with sparse-strips renderers. This is required for correctness in CSS/SVG semantics (e.g. `clip-rule`, `fill-rule`, and masking/clipPath content that relies on even-odd holes). To make even-odd clipping actually work end-to-end, plumb the fill rule through the clip pipeline: - Give `BEGIN_CLIP` a 1-word info slot so clip draw flags can be stored. - Populate clip `draw_flags` for `BEGIN_CLIP` in `draw_leaf`. - In `clip_leaf`, propagate `info_offset` from `BEGIN_CLIP` to the corresponding `END_CLIP`. - In coarse, apply clip layers with the correct fill rule by propagating `BEGIN_CLIP` draw flags through to `END_CLIP`, and by updating the “clip-zero” fast path to respect even-odd backdrop parity (GPU + CPU paths).
1 parent c15340d commit ab7fdb2

File tree

13 files changed

+173
-27
lines changed

13 files changed

+173
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This release has an [MSRV][] of 1.88.
1818
### Changed
1919

2020
- Breaking change: wgpu has been updated to wgpu 27. ([#1280][] by [@theoparis][])
21+
- Breaking change: make `Scene` clip / layers honor fill rule (even-odd clips). ([#1332][] by [@waywardmonkeys][])
2122

2223
### Fixed
2324

@@ -383,6 +384,7 @@ This release has an [MSRV][] of 1.75.
383384
[#1273]: https://github.com/linebender/vello/pull/1273
384385
[#1280]: https://github.com/linebender/vello/pull/1280
385386
[#1323]: https://github.com/linebender/vello/pull/1323
387+
[#1332]: https://github.com/linebender/vello/pull/1332
386388

387389
<!-- Note that this still comparing against 0.5.0, because 0.5.1 is a cherry-picked patch -->
388390
[Unreleased]: https://github.com/linebender/vello/compare/v0.5.0...HEAD

examples/scenes/src/test_scenes.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ mod impls {
769769
&rect,
770770
);
771771
let alpha = params.time.sin() as f32 * 0.5 + 0.5;
772-
scene.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect);
772+
scene.push_layer(Fill::NonZero, Mix::Normal, alpha, Affine::IDENTITY, &rect);
773773
scene.fill(
774774
Fill::NonZero,
775775
Affine::translate((100.0, 100.0)) * Affine::scale(0.2),
@@ -1125,6 +1125,7 @@ mod impls {
11251125
let mut depth = 0;
11261126
for (width, color) in &options[..params.complexity.min(options.len() - 1)] {
11271127
scene.push_layer(
1128+
Fill::NonZero,
11281129
Mix::Normal,
11291130
0.9,
11301131
Affine::IDENTITY,
@@ -1152,7 +1153,7 @@ mod impls {
11521153
const CLIPS_PER_FILL: usize = 3;
11531154
for _ in 0..CLIPS_PER_FILL {
11541155
let rot = Affine::rotate(rng.random_range(0.0..PI));
1155-
scene.push_clip_layer(translate * rot, &base_tri);
1156+
scene.push_clip_layer(Fill::NonZero, translate * rot, &base_tri);
11561157
}
11571158
let rot = Affine::rotate(rng.random_range(0.0..PI));
11581159
let color = Color::new([rng.random(), rng.random(), rng.random(), 1.]);
@@ -1212,7 +1213,7 @@ mod impls {
12121213
PathEl::LineTo((X0, Y1).into()),
12131214
PathEl::ClosePath,
12141215
];
1215-
scene.push_clip_layer(Affine::IDENTITY, &path);
1216+
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path);
12161217
}
12171218
let rect = Rect::new(X0, Y0, X1, Y1);
12181219
scene.fill(
@@ -1243,7 +1244,11 @@ mod impls {
12431244
None,
12441245
&make_diamond(1024.0, 125.0),
12451246
);
1246-
scene.push_clip_layer(Affine::IDENTITY, &make_diamond(1024.0, 150.0));
1247+
scene.push_clip_layer(
1248+
Fill::NonZero,
1249+
Affine::IDENTITY,
1250+
&make_diamond(1024.0, 150.0),
1251+
);
12471252
scene.fill(
12481253
Fill::NonZero,
12491254
Affine::IDENTITY,
@@ -1271,11 +1276,11 @@ mod impls {
12711276
scene.fill(Fill::NonZero, transform, &radial, None, &rect);
12721277
}
12731278
const COLORS: &[Color] = &[palette::css::RED, palette::css::LIME, palette::css::BLUE];
1274-
scene.push_layer(Mix::Normal, 1.0, transform, &rect);
1279+
scene.push_layer(Fill::NonZero, Mix::Normal, 1.0, transform, &rect);
12751280
for (i, c) in COLORS.iter().enumerate() {
12761281
let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0))
12771282
.with_stops([palette::css::WHITE, *c]);
1278-
scene.push_layer(blend, 1.0, transform, &rect);
1283+
scene.push_layer(Fill::NonZero, blend, 1.0, transform, &rect);
12791284
// squash the ellipse
12801285
let a = transform
12811286
* Affine::translate((100., 100.))
@@ -1579,7 +1584,7 @@ mod impls {
15791584
PathEl::ClosePath,
15801585
]
15811586
};
1582-
scene.push_clip_layer(Affine::IDENTITY, &clip);
1587+
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &clip);
15831588
{
15841589
let text_size = 60.0 + 40.0 * (params.time as f32).sin();
15851590
let s = "Some clipped text!";
@@ -1594,6 +1599,41 @@ mod impls {
15941599
}
15951600
scene.pop_layer();
15961601

1602+
// Even-odd clip-layer demo: a self-intersecting star ("pentagram") has different results
1603+
// under non-zero vs even-odd fill rules (even-odd produces a hole).
1604+
let demo_rect = Rect::new(250.0, 20.0, 450.0, 220.0);
1605+
scene.fill(
1606+
Fill::NonZero,
1607+
Affine::IDENTITY,
1608+
palette::css::BLUE,
1609+
None,
1610+
&demo_rect,
1611+
);
1612+
let mut star = BezPath::new();
1613+
let center = Point::new(350.0, 120.0);
1614+
let outer_r = 90.0;
1615+
let start_angle = -std::f64::consts::FRAC_PI_2;
1616+
let pts: [Point; 5] = core::array::from_fn(|i| {
1617+
let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0);
1618+
center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r)
1619+
});
1620+
let order = [0_usize, 2, 4, 1, 3];
1621+
star.move_to(pts[order[0]]);
1622+
for &idx in &order[1..] {
1623+
star.line_to(pts[idx]);
1624+
}
1625+
star.close_path();
1626+
1627+
scene.push_clip_layer(Fill::EvenOdd, Affine::IDENTITY, &star);
1628+
scene.fill(
1629+
Fill::NonZero,
1630+
Affine::IDENTITY,
1631+
palette::css::RED,
1632+
None,
1633+
&demo_rect,
1634+
);
1635+
scene.pop_layer();
1636+
15971637
let large_background_rect = Rect::new(-1000.0, -1000.0, 2000.0, 2000.0);
15981638
let inside_clip_rect = Rect::new(11.0, 13.399999999999999, 59.0, 56.6);
15991639
let outside_clip_rect = Rect::new(
@@ -1606,6 +1646,7 @@ mod impls {
16061646
let scale = 2.0;
16071647

16081648
scene.push_layer(
1649+
Fill::NonZero,
16091650
BlendMode {
16101651
mix: Mix::Normal,
16111652
compose: Compose::SrcOver,
@@ -1926,6 +1967,7 @@ mod impls {
19261967
},
19271968
);
19281969
scene.push_layer(
1970+
Fill::NonZero,
19291971
BlendMode::new(Mix::Normal, Compose::SrcOver),
19301972
1.0,
19311973
Affine::IDENTITY,
@@ -1949,6 +1991,7 @@ mod impls {
19491991
},
19501992
);
19511993
scene.push_luminance_mask_layer(
1994+
Fill::NonZero,
19521995
1.0,
19531996
Affine::IDENTITY,
19541997
&Rect {
@@ -1993,6 +2036,7 @@ mod impls {
19932036
.unwrap();
19942037
// HACK: Porter-Duff "over" the base color, restoring full alpha
19952038
scene.push_layer(
2039+
Fill::NonZero,
19962040
BlendMode::new(Mix::Normal, Compose::SrcOver),
19972041
1.0,
19982042
Affine::IDENTITY,
@@ -2028,6 +2072,7 @@ mod impls {
20282072
},
20292073
);
20302074
scene.push_luminance_mask_layer(
2075+
Fill::NonZero,
20312076
1.0,
20322077
Affine::IDENTITY,
20332078
&Rect {

vello/src/scene.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ impl Scene {
8282
/// Pushes a new layer clipped by the specified shape and composed with
8383
/// previous layers using the specified blend mode.
8484
///
85+
/// The `fill_rule` controls how the `clip` shape is interpreted.
86+
///
8587
/// Every drawing command after this call will be clipped by the shape
8688
/// until the layer is [popped](Self::pop_layer).
8789
/// For layers which are only added for clipping, you should
@@ -93,6 +95,7 @@ impl Scene {
9395
#[track_caller]
9496
pub fn push_layer(
9597
&mut self,
98+
fill_rule: Fill,
9699
blend: impl Into<BlendMode>,
97100
alpha: f32,
98101
transform: Affine,
@@ -107,6 +110,7 @@ impl Scene {
107110
}
108111
self.push_layer_inner(
109112
DrawBeginClip::new(blend, alpha.clamp(0.0, 1.0)),
113+
fill_rule,
110114
transform,
111115
clip,
112116
);
@@ -118,6 +122,8 @@ impl Scene {
118122
/// That is, content drawn between this and the matching `pop_layer` call will serve
119123
/// as a luminance mask for the prior content in this layer.
120124
///
125+
/// The `fill_rule` controls how the `clip` shape is interpreted.
126+
///
121127
/// Every drawing command after this call will be clipped by the shape
122128
/// until the layer is [popped](Self::pop_layer).
123129
///
@@ -135,16 +141,25 @@ impl Scene {
135141
/// This issue only occurs if there are no intermediate opaque layers, so can be worked around
136142
/// by drawing something opaque (or having an opaque `base_color`), then putting a layer around your entire scene
137143
/// with a [`Compose::SrcOver`].
138-
pub fn push_luminance_mask_layer(&mut self, alpha: f32, transform: Affine, clip: &impl Shape) {
144+
pub fn push_luminance_mask_layer(
145+
&mut self,
146+
fill_rule: Fill,
147+
alpha: f32,
148+
transform: Affine,
149+
clip: &impl Shape,
150+
) {
139151
self.push_layer_inner(
140152
DrawBeginClip::luminance_mask(alpha.clamp(0.0, 1.0)),
153+
fill_rule,
141154
transform,
142155
clip,
143156
);
144157
}
145158

146159
/// Pushes a new layer clipped by the specified `clip` shape.
147160
///
161+
/// The `fill_rule` controls how the `clip` shape is interpreted.
162+
///
148163
/// The pushed layer is intended to not impact the "source" for blending; that is, any blends
149164
/// within this layer will still include content from before this method was called in the "source"
150165
/// of that blend operation.
@@ -157,20 +172,21 @@ impl Scene {
157172
///
158173
/// **However, the transforms are *not* saved or modified by the layer stack.**
159174
/// That is, the `transform` argument to this function only applies a transform to the `clip` shape.
160-
pub fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
161-
self.push_layer_inner(DrawBeginClip::clip(), transform, clip);
175+
pub fn push_clip_layer(&mut self, fill_rule: Fill, transform: Affine, clip: &impl Shape) {
176+
self.push_layer_inner(DrawBeginClip::clip(), fill_rule, transform, clip);
162177
}
163178

164179
/// Helper for logic shared between [`Self::push_layer`] and [`Self::push_luminance_mask_layer`]
165180
fn push_layer_inner(
166181
&mut self,
167182
parameters: DrawBeginClip,
183+
fill_rule: Fill,
168184
transform: Affine,
169185
clip: &impl Shape,
170186
) {
171187
let t = Transform::from_kurbo(&transform);
172188
self.encoding.encode_transform(t);
173-
self.encoding.encode_fill_style(Fill::NonZero);
189+
self.encoding.encode_fill_style(fill_rule);
174190
if !self.encoding.encode_shape(clip, true) {
175191
// If the layer shape is invalid, encode a valid empty path. This suppresses
176192
// all drawing until the layer is popped.
@@ -894,7 +910,7 @@ impl ColorPainter for DrawColorGlyphs<'_> {
894910
};
895911
self.clip_depth += 1;
896912
self.scene
897-
.push_clip_layer(self.last_transform().to_kurbo(), &path.0);
913+
.push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &path.0);
898914
}
899915

900916
fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox<f32>) {
@@ -909,7 +925,7 @@ impl ColorPainter for DrawColorGlyphs<'_> {
909925
}
910926
self.clip_depth += 1;
911927
self.scene
912-
.push_clip_layer(self.last_transform().to_kurbo(), &clip_box);
928+
.push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &clip_box);
913929
}
914930

915931
fn pop_clip(&mut self) {
@@ -949,8 +965,13 @@ impl ColorPainter for DrawColorGlyphs<'_> {
949965
// TODO:
950966
_ => Compose::SrcOver,
951967
};
952-
self.scene
953-
.push_layer(blend, 1.0, self.last_transform().to_kurbo(), &self.clip_box);
968+
self.scene.push_layer(
969+
Fill::NonZero,
970+
blend,
971+
1.0,
972+
self.last_transform().to_kurbo(),
973+
&self.clip_box,
974+
);
954975
}
955976

956977
fn pop_layer(&mut self) {

vello_encoding/src/draw.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl DrawTag {
3737
pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect)
3838

3939
/// Begin layer/clip.
40-
pub const BEGIN_CLIP: Self = Self(0x9);
40+
pub const BEGIN_CLIP: Self = Self(0x49);
4141

4242
/// End layer/clip.
4343
pub const END_CLIP: Self = Self(0x21);

vello_shaders/shader/clip_leaf.wgsl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ fn main(
193193
draw_monoids[ix].path_ix = u32(path_ix);
194194
// Make EndClip point to the same draw data as BeginClip
195195
draw_monoids[ix].scene_offset = draw_monoids[parent_ix].scene_offset;
196+
// Make EndClip point to the same info (draw flags) as BeginClip
197+
draw_monoids[ix].info_offset = draw_monoids[parent_ix].info_offset;
196198
if grandparent >= 0 {
197199
bbox = sh_bbox[grandparent];
198200
} else if grandparent + i32(stack_size) >= 0 {

vello_shaders/shader/coarse.wgsl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,9 @@ fn main(
410410
write_image(di + 1u);
411411
}
412412
case DRAWTAG_BEGIN_CLIP: {
413-
if tile.segment_count_or_ix == 0u && tile.backdrop == 0 {
413+
let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u;
414+
let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0;
415+
if tile.segment_count_or_ix == 0u && backdrop_clear {
414416
clip_zero_depth = clip_depth + 1u;
415417
} else {
416418
write_begin_clip();
@@ -421,8 +423,7 @@ fn main(
421423
}
422424
case DRAWTAG_END_CLIP: {
423425
clip_depth -= 1u;
424-
// A clip shape is always a non-zero fill (draw_flags=0).
425-
write_path(tile, tile_ix, /*draw_flags=*/0u);
426+
write_path(tile, tile_ix, draw_flags);
426427
let blend = scene[dd];
427428
let alpha = bitcast<f32>(scene[dd + 1u]);
428429
write_end_clip(CmdEndClip(blend, alpha));

vello_shaders/shader/draw_leaf.wgsl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ fn main(
131131
case DRAWTAG_FILL_COLOR: {
132132
info[di] = draw_flags;
133133
}
134+
case DRAWTAG_BEGIN_CLIP: {
135+
info[di] = draw_flags;
136+
}
134137
case DRAWTAG_FILL_LIN_GRADIENT: {
135138
info[di] = draw_flags;
136139
var p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));

vello_shaders/shader/shared/drawtag.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
2323
const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u;
2424
const DRAWTAG_FILL_IMAGE = 0x28Cu;
2525
const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u;
26-
const DRAWTAG_BEGIN_CLIP = 0x9u;
26+
const DRAWTAG_BEGIN_CLIP = 0x49u;
2727
const DRAWTAG_END_CLIP = 0x21u;
2828

2929
/// The first word of each draw info stream entry contains the flags. This is not a part of the

vello_shaders/src/cpu/clip_leaf.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ fn clip_leaf_main(
6666
draw_monoids[clip_el.ix as usize].path_ix = tos.path_ix;
6767
draw_monoids[clip_el.ix as usize].scene_offset =
6868
draw_monoids[tos.parent_ix as usize].scene_offset;
69+
draw_monoids[clip_el.ix as usize].info_offset =
70+
draw_monoids[tos.parent_ix as usize].info_offset;
6971
}
7072
}
7173
}

vello_shaders/src/cpu/coarse.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,13 @@ fn coarse_main(
334334
tile_state.write_blur_rect(config, bump, ptcl, rgba_color, di + 1);
335335
}
336336
DrawTag::BEGIN_CLIP => {
337-
if tile.segment_count_or_ix == 0 && tile.backdrop == 0 {
337+
let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0;
338+
let backdrop_clear = if even_odd {
339+
tile.backdrop.abs() & 1 == 0
340+
} else {
341+
tile.backdrop == 0
342+
};
343+
if tile.segment_count_or_ix == 0 && backdrop_clear {
338344
clip_zero_depth = clip_depth + 1;
339345
} else {
340346
tile_state.write_begin_clip(config, bump, ptcl);
@@ -347,8 +353,7 @@ fn coarse_main(
347353
}
348354
DrawTag::END_CLIP => {
349355
clip_depth -= 1;
350-
// A clip shape is always a non-zero fill (draw_flags=0).
351-
tile_state.write_path(config, bump, ptcl, tile, 0);
356+
tile_state.write_path(config, bump, ptcl, tile, draw_flags);
352357
let blend = scene[dd as usize];
353358
let alpha = f32::from_bits(scene[dd as usize + 1]);
354359
tile_state.write_end_clip(config, bump, ptcl, blend, alpha);

0 commit comments

Comments
 (0)