Skip to content

Commit 44741b4

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 44741b4

File tree

12 files changed

+167
-25
lines changed

12 files changed

+167
-25
lines changed

examples/scenes/src/test_scenes.rs

Lines changed: 48 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,37 @@ mod impls {
15941599
}
15951600
scene.pop_layer();
15961601

1602+
// Even-odd clip-layer demo: two nested rectangles with the same winding should form a ring.
1603+
let demo_rect = Rect::new(250.0, 20.0, 450.0, 220.0);
1604+
scene.fill(
1605+
Fill::NonZero,
1606+
Affine::IDENTITY,
1607+
palette::css::BLUE,
1608+
None,
1609+
&demo_rect,
1610+
);
1611+
let demo_clip = [
1612+
PathEl::MoveTo((250.0, 20.0).into()),
1613+
PathEl::LineTo((450.0, 20.0).into()),
1614+
PathEl::LineTo((450.0, 220.0).into()),
1615+
PathEl::LineTo((250.0, 220.0).into()),
1616+
PathEl::ClosePath,
1617+
PathEl::MoveTo((310.0, 80.0).into()),
1618+
PathEl::LineTo((390.0, 80.0).into()),
1619+
PathEl::LineTo((390.0, 160.0).into()),
1620+
PathEl::LineTo((310.0, 160.0).into()),
1621+
PathEl::ClosePath,
1622+
];
1623+
scene.push_clip_layer(Fill::EvenOdd, Affine::IDENTITY, &demo_clip);
1624+
scene.fill(
1625+
Fill::NonZero,
1626+
Affine::IDENTITY,
1627+
palette::css::RED,
1628+
None,
1629+
&demo_rect,
1630+
);
1631+
scene.pop_layer();
1632+
15971633
let large_background_rect = Rect::new(-1000.0, -1000.0, 2000.0, 2000.0);
15981634
let inside_clip_rect = Rect::new(11.0, 13.399999999999999, 59.0, 56.6);
15991635
let outside_clip_rect = Rect::new(
@@ -1606,6 +1642,7 @@ mod impls {
16061642
let scale = 2.0;
16071643

16081644
scene.push_layer(
1645+
Fill::NonZero,
16091646
BlendMode {
16101647
mix: Mix::Normal,
16111648
compose: Compose::SrcOver,
@@ -1926,6 +1963,7 @@ mod impls {
19261963
},
19271964
);
19281965
scene.push_layer(
1966+
Fill::NonZero,
19291967
BlendMode::new(Mix::Normal, Compose::SrcOver),
19301968
1.0,
19311969
Affine::IDENTITY,
@@ -1949,6 +1987,7 @@ mod impls {
19491987
},
19501988
);
19511989
scene.push_luminance_mask_layer(
1990+
Fill::NonZero,
19521991
1.0,
19531992
Affine::IDENTITY,
19541993
&Rect {
@@ -1993,6 +2032,7 @@ mod impls {
19932032
.unwrap();
19942033
// HACK: Porter-Duff "over" the base color, restoring full alpha
19952034
scene.push_layer(
2035+
Fill::NonZero,
19962036
BlendMode::new(Mix::Normal, Compose::SrcOver),
19972037
1.0,
19982038
Affine::IDENTITY,
@@ -2028,6 +2068,7 @@ mod impls {
20282068
},
20292069
);
20302070
scene.push_luminance_mask_layer(
2071+
Fill::NonZero,
20312072
1.0,
20322073
Affine::IDENTITY,
20332074
&Rect {

vello/src/scene.rs

Lines changed: 29 additions & 7 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) {
@@ -950,7 +966,13 @@ impl ColorPainter for DrawColorGlyphs<'_> {
950966
_ => Compose::SrcOver,
951967
};
952968
self.scene
953-
.push_layer(blend, 1.0, self.last_transform().to_kurbo(), &self.clip_box);
969+
.push_layer(
970+
Fill::NonZero,
971+
blend,
972+
1.0,
973+
self.last_transform().to_kurbo(),
974+
&self.clip_box,
975+
);
954976
}
955977

956978
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
@@ -375,6 +375,8 @@ fn main(
375375
if clip_zero_depth == 0u {
376376
let tile_ix = sh_tile_base[el_ix] + sh_tile_stride[el_ix] * tile_y + tile_x;
377377
let tile = tiles[tile_ix];
378+
let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u;
379+
let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0;
378380
switch drawtag {
379381
case DRAWTAG_FILL_COLOR: {
380382
write_path(tile, tile_ix, draw_flags);
@@ -410,7 +412,7 @@ fn main(
410412
write_image(di + 1u);
411413
}
412414
case DRAWTAG_BEGIN_CLIP: {
413-
if tile.segment_count_or_ix == 0u && tile.backdrop == 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);

vello_shaders/src/cpu/draw_leaf.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ fn draw_leaf_main(
191191
info[di + 9] = scene[dd as usize + 3];
192192
info[di + 10] = scene[dd as usize + 4];
193193
}
194-
DrawTag::BEGIN_CLIP => (),
194+
DrawTag::BEGIN_CLIP => {
195+
info[di] = draw_flags;
196+
}
195197
_ => todo!("unhandled draw tag {:x}", tag_word.0),
196198
}
197199
}

0 commit comments

Comments
 (0)