vello_common/coarse.rs
1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Generating and processing wide tiles.
5
6use crate::color::palette::css::TRANSPARENT;
7use crate::mask::Mask;
8use crate::paint::{Paint, PremulColor};
9use crate::peniko::{BlendMode, Compose, Mix};
10use crate::{strip::Strip, tile::Tile};
11use alloc::vec;
12use alloc::{boxed::Box, vec::Vec};
13
14#[derive(Debug)]
15struct Layer {
16 /// Whether the layer has a clip associated with it.
17 clip: bool,
18 /// The blend mode with which this layer should be blended into
19 /// the previous layer.
20 blend_mode: BlendMode,
21 /// An opacity to apply to the whole layer before blending it
22 /// into the backdrop.
23 opacity: f32,
24 /// A mask to apply to the layer before blending it back into
25 /// the backdrop.
26 mask: Option<Mask>,
27}
28
29impl Layer {
30 /// Whether the layer actually requires allocating a new scratch buffer
31 /// for drawing its contents.
32 fn needs_buf(&self) -> bool {
33 self.blend_mode.mix != Mix::Normal
34 || self.blend_mode.compose != Compose::SrcOver
35 || self.opacity != 1.0
36 || self.mask.is_some()
37 || !self.clip
38 }
39}
40
41/// `MODE_CPU` allows compile time optimizations to be applied to wide tile draw command generation
42/// specific to `vello_cpu`.
43pub const MODE_CPU: u8 = 0;
44/// `MODE_HYBRID` allows compile time optimizations to be applied to wide tile draw command
45/// generation specific for `vello_hybrid`.
46pub const MODE_HYBRID: u8 = 1;
47
48/// A container for wide tiles.
49#[derive(Debug)]
50pub struct Wide<const MODE: u8 = MODE_CPU> {
51 /// The width of the container.
52 pub width: u16,
53 /// The height of the container.
54 pub height: u16,
55 /// The wide tiles in the container.
56 pub tiles: Vec<WideTile<MODE>>,
57 /// The stack of layers.
58 layer_stack: Vec<Layer>,
59 /// The stack of active clip regions.
60 clip_stack: Vec<Clip>,
61}
62
63/// A clip region.
64#[derive(Debug)]
65struct Clip {
66 /// The intersected bounding box after clip
67 pub clip_bbox: Bbox,
68 /// The rendered path in sparse strip representation
69 pub strips: Box<[Strip]>,
70 #[cfg(feature = "multithreading")]
71 pub thread_idx: u8,
72}
73
74/// A bounding box
75///
76/// The first two values represent the x0 and y0 coordinates, respectively.
77/// The last two values represent the x1 and y1 coordinates, respectively.
78/// x0, y0 — the top-left corner of the bounding box,
79/// x1, y1 — the bottom-right corner of the bounding box.
80#[derive(Debug, Clone)]
81struct Bbox {
82 pub bbox: [u16; 4],
83}
84
85impl Bbox {
86 pub(crate) fn new(bbox: [u16; 4]) -> Self {
87 Self { bbox }
88 }
89
90 /// Get the x0 coordinate of the bounding box.
91 #[inline]
92 pub(crate) fn x0(&self) -> u16 {
93 self.bbox[0]
94 }
95
96 /// Get the y0 coordinate of the bounding box.
97 #[inline]
98 pub(crate) fn y0(&self) -> u16 {
99 self.bbox[1]
100 }
101
102 /// Get the x1 coordinate of the bounding box.
103 #[inline]
104 pub(crate) fn x1(&self) -> u16 {
105 self.bbox[2]
106 }
107
108 /// Get the y1 coordinate of the bounding box.
109 #[inline]
110 pub(crate) fn y1(&self) -> u16 {
111 self.bbox[3]
112 }
113
114 /// Create an empty bounding box (zero area).
115 #[inline]
116 pub(crate) fn empty() -> Self {
117 Self::new([0, 0, 0, 0])
118 }
119
120 /// Calculate the intersection of this bounding box with another.
121 #[inline]
122 pub(crate) fn intersect(&self, other: &Self) -> Self {
123 Self::new([
124 self.x0().max(other.x0()),
125 self.y0().max(other.y0()),
126 self.x1().min(other.x1()),
127 self.y1().min(other.y1()),
128 ])
129 }
130}
131
132impl Wide<MODE_CPU> {
133 /// Create a new container for wide tiles.
134 pub fn new(width: u16, height: u16) -> Self {
135 Self::new_internal(width, height)
136 }
137}
138
139impl Wide<MODE_HYBRID> {
140 /// Create a new container for wide tiles.
141 pub fn new(width: u16, height: u16) -> Self {
142 Self::new_internal(width, height)
143 }
144}
145
146impl<const MODE: u8> Wide<MODE> {
147 /// Create a new container for wide tiles.
148 fn new_internal(width: u16, height: u16) -> Self {
149 let width_tiles = width.div_ceil(WideTile::WIDTH);
150 let height_tiles = height.div_ceil(Tile::HEIGHT);
151 let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles));
152
153 for h in 0..height_tiles {
154 for w in 0..width_tiles {
155 tiles.push(WideTile::<MODE>::new_internal(
156 w * WideTile::WIDTH,
157 h * Tile::HEIGHT,
158 ));
159 }
160 }
161
162 Self {
163 tiles,
164 width,
165 height,
166 layer_stack: vec![],
167 clip_stack: vec![],
168 }
169 }
170
171 /// Whether there are any existing layers that haven't been popped yet.
172 pub fn has_layers(&self) -> bool {
173 !self.layer_stack.is_empty()
174 }
175
176 /// Reset all tiles in the container.
177 pub fn reset(&mut self) {
178 for tile in &mut self.tiles {
179 tile.bg = PremulColor::from_alpha_color(TRANSPARENT);
180 tile.cmds.clear();
181 }
182 self.layer_stack.clear();
183 self.clip_stack.clear();
184 }
185
186 /// Return the number of horizontal tiles.
187 pub fn width_tiles(&self) -> u16 {
188 self.width.div_ceil(WideTile::WIDTH)
189 }
190
191 /// Return the number of vertical tiles.
192 pub fn height_tiles(&self) -> u16 {
193 self.height.div_ceil(Tile::HEIGHT)
194 }
195
196 /// Get the wide tile at a certain index.
197 ///
198 /// Panics if the index is out-of-range.
199 pub fn get(&self, x: u16, y: u16) -> &WideTile<MODE> {
200 assert!(
201 x < self.width_tiles() && y < self.height_tiles(),
202 "attempted to access out-of-bounds wide tile"
203 );
204
205 &self.tiles[usize::from(y) * usize::from(self.width_tiles()) + usize::from(x)]
206 }
207
208 /// Get mutable access to the wide tile at a certain index.
209 ///
210 /// Panics if the index is out-of-range.
211 pub fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile<MODE> {
212 assert!(
213 x < self.width_tiles() && y < self.height_tiles(),
214 "attempted to access out-of-bounds wide tile"
215 );
216
217 let idx = usize::from(y) * usize::from(self.width_tiles()) + usize::from(x);
218 &mut self.tiles[idx]
219 }
220
221 /// Return a reference to all wide tiles.
222 pub fn tiles(&self) -> &[WideTile<MODE>] {
223 self.tiles.as_slice()
224 }
225
226 /// Generate wide tile commands from the strip buffer.
227 ///
228 /// This method processes a buffer of strips that represent a path, applies the fill rule,
229 /// and generates appropriate drawing commands for each affected wide tile.
230 ///
231 /// # Algorithm overview:
232 /// 1. For each strip in the buffer:
233 /// - Calculate its position and width in pixels
234 /// - Determine which wide tiles the strip intersects
235 /// - Generate alpha fill commands for the intersected wide tiles
236 /// 2. For active fill regions (determined by fill rule):
237 /// - Generate solid fill commands for the regions between strips
238 pub fn generate(&mut self, strip_buf: &[Strip], paint: Paint, thread_idx: u8) {
239 if strip_buf.is_empty() {
240 return;
241 }
242
243 // Prevent unused warning.
244 let _ = thread_idx;
245
246 // Get current clip bounding box or full viewport if no clip is active
247 let bbox = self.get_bbox();
248
249 for i in 0..strip_buf.len() - 1 {
250 let strip = &strip_buf[i];
251
252 debug_assert!(
253 strip.y < self.height,
254 "Strips below the viewport should have been culled prior to this stage."
255 );
256
257 // Don't render strips that are outside the viewport width
258 if strip.x >= self.width {
259 continue;
260 }
261
262 let next_strip = &strip_buf[i + 1];
263 let x0 = strip.x;
264 let strip_y = strip.strip_y();
265
266 // Skip strips outside the current clip bounding box
267 if strip_y < bbox.y0() {
268 continue;
269 }
270 if strip_y >= bbox.y1() {
271 // The rest of our strips must be outside the clip, so we can break early.
272 break;
273 }
274
275 // Calculate the width of the strip in columns
276 let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
277 let next_col = next_strip.alpha_idx() / u32::from(Tile::HEIGHT);
278 // Can potentially be 0 if strip only changes winding without covering pixels
279 let strip_width = next_col.saturating_sub(col) as u16;
280 let x1 = x0.saturating_add(strip_width);
281
282 // Calculate which wide tiles this strip intersects
283 let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0());
284 // It's possible that a strip extends into a new wide tile, but we don't actually
285 // have as many wide tiles (e.g. because the pixmap width is only 512, but
286 // strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
287 let wtile_x1 = x1
288 .div_ceil(WideTile::WIDTH)
289 .min(bbox.x1())
290 .min(WideTile::MAX_WIDE_TILE_COORD);
291
292 // Adjust column starting position if needed to respect clip boundaries
293 let mut x = x0;
294 let clip_x = bbox.x0() * WideTile::WIDTH;
295 if clip_x > x {
296 col += u32::from(clip_x - x);
297 x = clip_x;
298 }
299
300 // Generate alpha fill commands for each wide tile intersected by this strip
301 for wtile_x in wtile_x0..wtile_x1 {
302 let x_wtile_rel = x % WideTile::WIDTH;
303 // Restrict the width of the fill to the width of the wide tile
304 let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
305 let cmd = CmdAlphaFill {
306 x: x_wtile_rel,
307 width,
308 alpha_idx: (col * u32::from(Tile::HEIGHT)) as usize,
309 #[cfg(feature = "multithreading")]
310 thread_idx,
311 paint: paint.clone(),
312 blend_mode: None,
313 };
314 x += width;
315 col += u32::from(width);
316 self.get_mut(wtile_x, strip_y).strip(cmd);
317 }
318
319 // Determine if the region between this strip and the next should be filled.
320 let active_fill = next_strip.fill_gap();
321
322 // If region should be filled and both strips are on the same row,
323 // generate fill commands for the region between them
324 if active_fill && strip_y == next_strip.strip_y() {
325 // Clamp the fill to the clip bounding box
326 x = x1.max(bbox.x0() * WideTile::WIDTH);
327 let x2 = next_strip.x.min(
328 self.width
329 .checked_next_multiple_of(WideTile::WIDTH)
330 .unwrap_or(u16::MAX),
331 );
332 let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0());
333 let wfxt1 = x2
334 .div_ceil(WideTile::WIDTH)
335 .min(bbox.x1())
336 .min(WideTile::MAX_WIDE_TILE_COORD);
337
338 // Generate fill commands for each wide tile in the fill region
339 for wtile_x in wfxt0..wfxt1 {
340 let x_wtile_rel = x % WideTile::WIDTH;
341 let width = x2.min(
342 (wtile_x
343 .checked_add(1)
344 .unwrap_or(WideTile::MAX_WIDE_TILE_COORD))
345 * WideTile::WIDTH,
346 ) - x;
347 x += width;
348 self.get_mut(wtile_x, strip_y)
349 .fill(x_wtile_rel, width, paint.clone());
350 }
351 }
352 }
353 }
354
355 /// Push a new layer with the given properties.
356 pub fn push_layer(
357 &mut self,
358 clip_path: Option<impl Into<Box<[Strip]>>>,
359 blend_mode: BlendMode,
360 mask: Option<Mask>,
361 opacity: f32,
362 thread_idx: u8,
363 ) {
364 // Some explanations about what is going on here: We support the concept of
365 // layers, where a user can push a new layer (with certain properties), draw some
366 // stuff, and finally pop the layer, as part of which the layer as a whole will be
367 // blended into the previous layer.
368 // There are 3 "straightforward" properties that can be set for each layer:
369 // 1) The blend mode that should be used to blend the layer into the backdrop.
370 // 2) A mask that will be applied to the whole layer in the very end before blending.
371 // 3) An optional opacity that will be applied to the whole layer before blending (this
372 // could in theory be simulated with an alpha mask, but since it's a common operation and
373 // we only have a single opacity, this can easily be optimized.
374 //
375 // Finally, you can also add a clip path to the layer. However, clipping has its own
376 // more complicated logic for pushing/popping buffers where drawing is also suppressed
377 // in clipped-out wide tiles. Because of this, in case we have one of the above properties
378 // AND a clipping path, we will actually end up pushing two buffers, the first one handles
379 // the three properties and the second one is just for clip paths. That is a bit wasteful
380 // and I believe it should be possible to process them all in just one go, but for now
381 // this is good enough, and it allows us to implement blending without too deep changes to
382 // the original clipping implementation.
383
384 let layer = Layer {
385 clip: clip_path.is_some(),
386 blend_mode,
387 opacity,
388 mask,
389 };
390
391 let needs_buf = layer.needs_buf();
392
393 // In case we do blending, masking or opacity, push one buffer per wide tile.
394 if needs_buf {
395 for x in 0..self.width_tiles() {
396 for y in 0..self.height_tiles() {
397 self.get_mut(x, y).push_buf();
398 }
399 }
400 }
401
402 // If we have a clip path, push another buffer in the affected wide tiles.
403 // Note that it is important that we FIRST push the buffer for blending etc. and
404 // only then for clipping, otherwise we will use the empty clip buffer as the backdrop
405 // for blending!
406 if let Some(clip) = clip_path {
407 self.push_clip(clip, thread_idx);
408 }
409
410 self.layer_stack.push(layer);
411 }
412
413 /// Pop a previously pushed layer.
414 pub fn pop_layer(&mut self) {
415 // This method basically unwinds everything we did in `push_layer`.
416
417 let layer = self.layer_stack.pop().unwrap();
418
419 if layer.clip {
420 self.pop_clip();
421 }
422
423 let needs_buf = layer.needs_buf();
424
425 if needs_buf {
426 for x in 0..self.width_tiles() {
427 for y in 0..self.height_tiles() {
428 let t = self.get_mut(x, y);
429
430 if let Some(mask) = layer.mask.clone() {
431 t.mask(mask);
432 }
433 t.opacity(layer.opacity);
434 t.blend(layer.blend_mode);
435 t.pop_buf();
436 }
437 }
438 }
439 }
440
441 /// Adds a clipping region defined by the provided strips.
442 ///
443 /// This method takes a vector of strips representing a clip path, calculates the
444 /// intersection with the current clip region, and updates the clip stack.
445 ///
446 /// # Algorithm overview:
447 /// 1. Calculate bounding box of the clip path
448 /// 2. Intersect with current clip bounding box
449 /// 3. For each tile in the intersected bounding box:
450 /// - If covered by zero winding: `push_zero_clip`
451 /// - If fully covered by non-zero winding: do nothing (clip is a no-op)
452 /// - If partially covered: `push_clip`
453 pub fn push_clip(&mut self, strips: impl Into<Box<[Strip]>>, thread_idx: u8) {
454 let strips = strips.into();
455 let n_strips = strips.len();
456
457 // Calculate the bounding box of the clip path in strip coordinates
458 let path_bbox = if n_strips <= 1 {
459 Bbox::empty()
460 } else {
461 // Calculate the y range from first to last strip in wide tile coordinates
462 let wtile_y0 = strips[0].strip_y();
463 let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1;
464
465 // Calculate the x range by examining all strips in wide tile coordinates
466 let mut wtile_x0 = strips[0].x / WideTile::WIDTH;
467 let mut wtile_x1 = wtile_x0;
468 for i in 0..n_strips.saturating_sub(1) {
469 let strip = &strips[i];
470 let next_strip = &strips[i + 1];
471 let width =
472 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
473 let x = strip.x;
474 wtile_x0 = wtile_x0.min(x / WideTile::WIDTH);
475 wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH));
476 }
477 Bbox::new([wtile_x0, wtile_y0, wtile_x1, wtile_y1])
478 };
479
480 let parent_bbox = self.get_bbox();
481 // Calculate the intersection of the parent clip bounding box and the path bounding box.
482 let clip_bbox = parent_bbox.intersect(&path_bbox);
483
484 let mut cur_wtile_x = clip_bbox.x0();
485 let mut cur_wtile_y = clip_bbox.y0();
486
487 // Process strips to determine the clipping state for each wide tile
488 for i in 0..n_strips.saturating_sub(1) {
489 let strip = &strips[i];
490 let strip_y = strip.strip_y();
491
492 // Skip strips before current wide tile row
493 if strip_y < cur_wtile_y {
494 continue;
495 }
496
497 // Process wide tiles in rows before this strip's row
498 // These wide tiles are all zero-winding (outside the path)
499 while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
500 for wtile_x in cur_wtile_x..clip_bbox.x1() {
501 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
502 }
503 // Reset x to the left edge of the clip bounding box
504 cur_wtile_x = clip_bbox.x0();
505 // Move to the next row
506 cur_wtile_y += 1;
507 }
508
509 // If we've reached the bottom of the clip bounding box, stop processing.
510 // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
511 // is zero-area (see issue 1072).
512 if cur_wtile_y >= clip_bbox.y1() {
513 break;
514 }
515
516 // Process wide tiles to the left of this strip in the same row
517 let x = strip.x;
518 let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1());
519 if cur_wtile_x < wtile_x_clamped {
520 // If winding is zero or doesn't match fill rule, these wide tiles are outside the path
521 let is_inside = strip.fill_gap();
522 if !is_inside {
523 for wtile_x in cur_wtile_x..wtile_x_clamped {
524 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
525 }
526 }
527 // If winding is nonzero, then wide tiles covered entirely
528 // by sparse fill are no-op (no clipping is applied).
529 cur_wtile_x = wtile_x_clamped;
530 }
531
532 // Process wide tiles covered by the strip - these need actual clipping
533 let next_strip = &strips[i + 1];
534 let width =
535 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
536 let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
537 if cur_wtile_x < wtile_x1 {
538 for wtile_x in cur_wtile_x..wtile_x1 {
539 self.get_mut(wtile_x, cur_wtile_y).push_clip();
540 }
541 cur_wtile_x = wtile_x1;
542 }
543 }
544
545 // Process any remaining wide tiles in the bounding box (all zero-winding)
546 while cur_wtile_y < clip_bbox.y1() {
547 for wtile_x in cur_wtile_x..clip_bbox.x1() {
548 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
549 }
550 cur_wtile_x = clip_bbox.x0();
551 cur_wtile_y += 1;
552 }
553
554 // Prevent unused warning.
555 let _ = thread_idx;
556
557 self.clip_stack.push(Clip {
558 clip_bbox,
559 strips,
560 #[cfg(feature = "multithreading")]
561 thread_idx,
562 });
563 }
564
565 /// Get the bounding box of the current clip region or the entire viewport if no clip regions are active.
566 fn get_bbox(&self) -> Bbox {
567 if let Some(top) = self.clip_stack.last() {
568 top.clip_bbox.clone()
569 } else {
570 // Convert pixel dimensions to wide tile coordinates
571 Bbox::new([0, 0, self.width_tiles(), self.height_tiles()])
572 }
573 }
574
575 /// Removes the most recently added clip region.
576 ///
577 /// This is the inverse operation of `push_clip`, carefully undoing all the clipping
578 /// operations while also handling any rendering needed for the clip region itself.
579 ///
580 /// # Algorithm overview:
581 /// 1. Retrieve the top clip from the stack
582 /// 2. For each wide tile in the clip's bounding box:
583 /// - If covered by zero winding: `pop_zero_clip`
584 /// - If fully covered by non-zero winding: do nothing (was no-op)
585 /// - If partially covered: render the clip and `pop_clip`
586 ///
587 /// This operation must be symmetric with `push_clip` to maintain a balanced clip stack.
588 fn pop_clip(&mut self) {
589 let Clip {
590 clip_bbox,
591 strips,
592 #[cfg(feature = "multithreading")]
593 thread_idx,
594 } = self.clip_stack.pop().unwrap();
595 let n_strips = strips.len();
596
597 let mut cur_wtile_x = clip_bbox.x0();
598 let mut cur_wtile_y = clip_bbox.y0();
599 let mut pop_pending = false;
600
601 // Process each strip to determine the clipping state for each tile
602 for i in 0..n_strips.saturating_sub(1) {
603 let strip = &strips[i];
604 let strip_y = strip.strip_y();
605
606 // Skip strips before current tile row
607 if strip_y < cur_wtile_y {
608 continue;
609 }
610
611 // Process tiles in rows before this strip's row
612 // These tiles all had zero-winding clips
613 while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
614 // Handle any pending clip pop from previous iteration
615 if core::mem::take(&mut pop_pending) {
616 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
617 cur_wtile_x += 1;
618 }
619
620 // Pop zero clips for all remaining tiles in this row
621 for wtile_x in cur_wtile_x..clip_bbox.x1() {
622 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
623 }
624 cur_wtile_x = clip_bbox.x0();
625 cur_wtile_y += 1;
626 }
627
628 // If we've reached the bottom of the clip bounding box, stop processing
629 // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
630 // is zero-area (see issue 1072).
631 if cur_wtile_y >= clip_bbox.y1() {
632 break;
633 }
634
635 // Process tiles to the left of this strip in the same row
636 let x0 = strip.x;
637 let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1());
638 if cur_wtile_x < wtile_x_clamped {
639 // Handle any pending clip pop from previous iteration
640 if core::mem::take(&mut pop_pending) {
641 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
642 cur_wtile_x += 1;
643 }
644
645 // Pop zero clips for tiles that had zero winding or didn't match fill rule
646 // TODO: The winding check is probably not needed; if there was a fill,
647 // the logic below should have advanced wtile_x.
648 let is_inside = strip.fill_gap();
649 if !is_inside {
650 for wtile_x in cur_wtile_x..wtile_x_clamped {
651 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
652 }
653 }
654 cur_wtile_x = wtile_x_clamped;
655 }
656
657 // Process tiles covered by the strip - render clip content and pop
658 let next_strip = &strips[i + 1];
659 let strip_width =
660 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
661 let mut clipped_x1 = x0 + strip_width;
662 let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0());
663 let wtile_x1 = clipped_x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
664
665 // Calculate starting position and column for alpha mask
666 let mut x = x0;
667 let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
668 let clip_x = clip_bbox.x0() * WideTile::WIDTH;
669 if clip_x > x {
670 col += u32::from(clip_x - x);
671 x = clip_x;
672 clipped_x1 = clip_x.max(clipped_x1);
673 }
674
675 // Render clip strips for each affected tile and mark for popping
676 for wtile_x in wtile_x0..wtile_x1 {
677 // If we've moved past tile_x and have a pending pop, do it now
678 if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) {
679 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
680 }
681
682 // Calculate the portion of the strip that affects this tile
683 let x_rel = u32::from(x % WideTile::WIDTH);
684 let width = clipped_x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
685
686 // Create clip strip command for rendering the partial coverage
687 let cmd = CmdClipAlphaFill {
688 x: x_rel,
689 width: u32::from(width),
690 alpha_idx: col as usize * Tile::HEIGHT as usize,
691 #[cfg(feature = "multithreading")]
692 thread_idx,
693 };
694 x += width;
695 col += u32::from(width);
696
697 // Apply the clip strip command and update state
698 self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd);
699 cur_wtile_x = wtile_x;
700
701 // Only request a pop if the x coordinate is actually inside the bounds.
702 if cur_wtile_x < clip_bbox.x1() {
703 pop_pending = true;
704 }
705 }
706
707 // Handle fill regions between strips based on fill rule
708 let is_inside = next_strip.fill_gap();
709 if is_inside && strip_y == next_strip.strip_y() {
710 if cur_wtile_x >= clip_bbox.x1() {
711 continue;
712 }
713
714 let x2 = next_strip.x;
715 let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH);
716 let width = clipped_x2.saturating_sub(clipped_x1);
717
718 // If there's a gap, fill it. Only do this if the fill wouldn't cover the
719 // whole tile, as such clips are skipped by the `push_clip` function. See
720 // <https://github.com/linebender/vello/blob/de0659e4df9842c8857153841a2b4ba6f1020bb0/sparse_strips/vello_common/src/coarse.rs#L504-L516>
721 if width > 0 && width < WideTile::WIDTH {
722 let x_rel = u32::from(clipped_x1 % WideTile::WIDTH);
723 self.get_mut(cur_wtile_x, cur_wtile_y)
724 .clip_fill(x_rel, u32::from(width));
725 }
726
727 // If the next strip is a sentinel, skip the fill
728 // It's a sentinel in the row if there is non-zero winding for the sparse fill
729 // Look more into this in the strip.rs render function
730 if x2 == u16::MAX {
731 continue;
732 }
733
734 // If fill extends to next tile, pop current and handle next
735 if x2 > (cur_wtile_x + 1) * WideTile::WIDTH {
736 if core::mem::take(&mut pop_pending) {
737 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
738 }
739
740 let width2 = x2 % WideTile::WIDTH;
741 cur_wtile_x = x2 / WideTile::WIDTH;
742
743 // If the strip is outside the clipping box, we don't need to do any
744 // filling, so we continue (also to prevent out-of-bounds access).
745 if cur_wtile_x >= clip_bbox.x1() {
746 continue;
747 }
748
749 if width2 > 0 {
750 // An important thing to note: Note that we are only applying
751 // `clip_fill` to the wide tile that is actually covered by the next
752 // strip, and not the ones in-between! For example, if the first strip
753 // is in wide tile 1 and the second in wide tile 4, we will do a clip
754 // fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is
755 // that any tile in-between is fully covered and thus no clipping is
756 // necessary at all. See also the `push_clip` function, where we don't
757 // push a new buffer for such tiles.
758 self.get_mut(cur_wtile_x, cur_wtile_y)
759 .clip_fill(0, u32::from(width2));
760 }
761 }
762 }
763 }
764
765 // Handle any pending clip pop from the last iteration
766 if core::mem::take(&mut pop_pending) {
767 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
768 cur_wtile_x += 1;
769 }
770
771 // Process any remaining tiles in the bounding box (all zero-winding)
772 while cur_wtile_y < clip_bbox.y1() {
773 for wtile_x in cur_wtile_x..clip_bbox.x1() {
774 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
775 }
776 cur_wtile_x = clip_bbox.x0();
777 cur_wtile_y += 1;
778 }
779 }
780}
781
782/// A wide tile.
783#[derive(Debug)]
784pub struct WideTile<const MODE: u8 = MODE_CPU> {
785 /// The x coordinate of the wide tile.
786 pub x: u16,
787 /// The y coordinate of the wide tile.
788 pub y: u16,
789 /// The background of the tile.
790 pub bg: PremulColor,
791 /// The draw commands of the tile.
792 pub cmds: Vec<Cmd>,
793 /// The number of zero-winding clips.
794 pub n_zero_clip: usize,
795 /// The number of non-zero-winding clips.
796 pub n_clip: usize,
797 /// The number of pushed buffers.
798 pub n_bufs: usize,
799}
800
801impl WideTile {
802 /// The width of a wide tile in pixels.
803 pub const WIDTH: u16 = 256;
804 /// The maximum coordinate of a wide tile.
805 pub const MAX_WIDE_TILE_COORD: u16 = u16::MAX / Self::WIDTH;
806}
807
808impl WideTile<MODE_CPU> {
809 /// Create a new wide tile.
810 pub fn new(width: u16, height: u16) -> Self {
811 Self::new_internal(width, height)
812 }
813}
814
815impl WideTile<MODE_HYBRID> {
816 /// Create a new wide tile.
817 pub fn new(width: u16, height: u16) -> Self {
818 Self::new_internal(width, height)
819 }
820}
821
822impl<const MODE: u8> WideTile<MODE> {
823 /// Create a new wide tile.
824 fn new_internal(x: u16, y: u16) -> Self {
825 Self {
826 x,
827 y,
828 bg: PremulColor::from_alpha_color(TRANSPARENT),
829 cmds: vec![],
830 n_zero_clip: 0,
831 n_clip: 0,
832 n_bufs: 0,
833 }
834 }
835
836 pub(crate) fn fill(&mut self, x: u16, width: u16, paint: Paint) {
837 if !self.is_zero_clip() {
838 match MODE {
839 MODE_CPU => {
840 let bg = if let Paint::Solid(s) = &paint {
841 // Note that we could be more aggressive in optimizing a whole-tile opaque fill
842 // even with a clip stack. It would be valid to elide all drawing commands from
843 // the enclosing clip push up to the fill. Further, we could extend the clip
844 // push command to include a background color, rather than always starting with
845 // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
846 // be replaced with strip/fill with the color (the latter is true even with a
847 // non-opaque color).
848 //
849 // However, the extra cost of tracking such optimizations may outweigh the
850 // benefit, especially in hybrid mode with GPU painting.
851 let can_override = x == 0
852 && width == WideTile::WIDTH
853 && s.is_opaque()
854 && self.n_clip == 0
855 && self.n_bufs == 0;
856 can_override.then_some(*s)
857 } else {
858 // TODO: Implement for indexed paints.
859 None
860 };
861
862 if let Some(bg) = bg {
863 self.cmds.clear();
864 self.bg = bg;
865 } else {
866 self.cmds.push(Cmd::Fill(CmdFill {
867 x,
868 width,
869 paint,
870 blend_mode: None,
871 }));
872 }
873 }
874 MODE_HYBRID => {
875 self.cmds.push(Cmd::Fill(CmdFill {
876 x,
877 width,
878 paint,
879 blend_mode: None,
880 }));
881 }
882 _ => unreachable!(),
883 }
884 }
885 }
886
887 pub(crate) fn strip(&mut self, cmd_strip: CmdAlphaFill) {
888 if !self.is_zero_clip() {
889 self.cmds.push(Cmd::AlphaFill(cmd_strip));
890 }
891 }
892
893 /// Adds a new clip region to the current wide tile.
894 pub fn push_clip(&mut self) {
895 if !self.is_zero_clip() {
896 self.push_buf();
897 self.n_clip += 1;
898 }
899 }
900
901 /// Removes the most recently added clip region from the current wide tile.
902 pub fn pop_clip(&mut self) {
903 if !self.is_zero_clip() {
904 self.pop_buf();
905 self.n_clip -= 1;
906 }
907 }
908
909 /// Adds a zero-winding clip region to the stack.
910 pub fn push_zero_clip(&mut self) {
911 self.n_zero_clip += 1;
912 }
913
914 /// Removes the most recently added zero-winding clip region.
915 pub fn pop_zero_clip(&mut self) {
916 self.n_zero_clip -= 1;
917 }
918
919 /// Checks if the current clip region is a zero-winding clip.
920 pub fn is_zero_clip(&mut self) -> bool {
921 self.n_zero_clip > 0
922 }
923
924 /// Applies a clip strip operation with the given parameters.
925 pub fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) {
926 if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
927 self.cmds.push(Cmd::ClipStrip(cmd_clip_strip));
928 }
929 }
930
931 /// Applies a clip fill operation at the specified position and width.
932 pub fn clip_fill(&mut self, x: u32, width: u32) {
933 if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
934 self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width }));
935 }
936 }
937
938 /// Push a buffer.
939 pub fn push_buf(&mut self) {
940 self.cmds.push(Cmd::PushBuf);
941 self.n_bufs += 1;
942 }
943
944 /// Pop the most recent buffer.
945 pub fn pop_buf(&mut self) {
946 if matches!(self.cmds.last(), Some(&Cmd::PushBuf)) {
947 // Optimization: If no drawing happened between the last `PushBuf`,
948 // we can just pop it instead.
949 self.cmds.pop();
950 } else {
951 self.cmds.push(Cmd::PopBuf);
952 }
953
954 self.n_bufs -= 1;
955 }
956
957 /// Apply an opacity to the whole buffer.
958 pub fn opacity(&mut self, opacity: f32) {
959 if opacity != 1.0 {
960 self.cmds.push(Cmd::Opacity(opacity));
961 }
962 }
963
964 /// Apply a mask to the whole buffer.
965 pub fn mask(&mut self, mask: Mask) {
966 self.cmds.push(Cmd::Mask(mask));
967 }
968
969 /// Blend the current buffer into the previous buffer in the stack.
970 pub fn blend(&mut self, blend_mode: BlendMode) {
971 // Optimization: If no drawing happened since the last `PushBuf` and the blend mode
972 // is not destructive, we do not need to do any blending at all.
973 if !matches!(self.cmds.last(), Some(&Cmd::PushBuf)) || blend_mode.is_destructive() {
974 self.cmds.push(Cmd::Blend(blend_mode));
975 }
976 }
977}
978
979/// A drawing command.
980#[derive(Debug, PartialEq)]
981pub enum Cmd {
982 /// A fill command.
983 Fill(CmdFill),
984 /// A fill command with alpha mask.
985 AlphaFill(CmdAlphaFill),
986 /// Pushes a new buffer for drawing.
987 PushBuf,
988 /// Pops the most recent buffer.
989 PopBuf,
990 /// A fill command within a clipping region.
991 ///
992 /// This command will blend the contents of the current buffer within the clip fill region
993 /// into the previous buffer in the stack.
994 ClipFill(CmdClipFill),
995 /// A fill command with alpha mask within a clipping region.
996 ///
997 /// This command will blend the contents of the current buffer within the clip fill region
998 /// into the previous buffer in the stack, with an additional alpha mask.
999 ClipStrip(CmdClipAlphaFill),
1000 /// Apply a blend.
1001 ///
1002 /// This command will blend the contents of the current buffer into the previous buffer in
1003 /// the stack.
1004 Blend(BlendMode),
1005 /// Apply an opacity mask to the current buffer.
1006 Opacity(f32),
1007 /// Apply a mask to the current buffer.
1008 Mask(Mask),
1009}
1010
1011/// Fill a consecutive region of a wide tile.
1012#[derive(Debug, Clone, PartialEq)]
1013pub struct CmdFill {
1014 /// The horizontal start position of the command in pixels.
1015 pub x: u16,
1016 /// The width of the command in pixels.
1017 pub width: u16,
1018 /// The paint that should be used to fill the area.
1019 pub paint: Paint,
1020 /// The blend mode to apply before drawing the contents.
1021 pub blend_mode: Option<BlendMode>,
1022}
1023
1024/// Fill a consecutive region of a wide tile with an alpha mask.
1025#[derive(Debug, Clone, PartialEq)]
1026pub struct CmdAlphaFill {
1027 /// The horizontal start position of the command in pixels.
1028 pub x: u16,
1029 /// The width of the command in pixels.
1030 pub width: u16,
1031 /// The start index into the alpha buffer of the command.
1032 pub alpha_idx: usize,
1033 /// The index of the thread that contains the alpha values
1034 /// pointed to by `alpha_idx`.
1035 #[cfg(feature = "multithreading")]
1036 pub thread_idx: u8,
1037 /// The paint that should be used to fill the area.
1038 pub paint: Paint,
1039 /// A blend mode to apply before drawing the contents.
1040 pub blend_mode: Option<BlendMode>,
1041}
1042
1043/// Same as fill, but copies top of clip stack to next on stack.
1044#[derive(Debug, PartialEq, Eq)]
1045pub struct CmdClipFill {
1046 /// The horizontal start position of the command in pixels.
1047 pub x: u32,
1048 /// The width of the command in pixels.
1049 pub width: u32,
1050}
1051
1052/// Same as strip, but composites top of clip stack to next on stack.
1053#[derive(Debug, PartialEq, Eq)]
1054pub struct CmdClipAlphaFill {
1055 /// The horizontal start position of the command in pixels.
1056 pub x: u32,
1057 /// The width of the command in pixels.
1058 pub width: u32,
1059 /// The index of the thread that contains the alpha values
1060 /// pointed to by `alpha_idx`.
1061 #[cfg(feature = "multithreading")]
1062 pub thread_idx: u8,
1063 /// The start index into the alpha buffer of the command.
1064 pub alpha_idx: usize,
1065}
1066
1067trait BlendModeExt {
1068 /// Whether a blend mode might cause destructive changes in the backdrop.
1069 /// This disallows certain optimizations (like for example inlining a blend mode
1070 /// or only applying a blend mode to the current clipping area).
1071 fn is_destructive(&self) -> bool;
1072}
1073
1074impl BlendModeExt for BlendMode {
1075 fn is_destructive(&self) -> bool {
1076 matches!(
1077 self.compose,
1078 Compose::Clear
1079 | Compose::Copy
1080 | Compose::SrcIn
1081 | Compose::DestIn
1082 | Compose::SrcOut
1083 | Compose::DestAtop
1084 )
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use crate::coarse::{MODE_CPU, Wide, WideTile};
1091 use crate::color::AlphaColor;
1092 use crate::color::palette::css::TRANSPARENT;
1093 use crate::paint::{Paint, PremulColor};
1094 use crate::peniko::{BlendMode, Compose, Mix};
1095 use crate::strip::Strip;
1096 use alloc::{boxed::Box, vec};
1097
1098 #[test]
1099 fn optimize_empty_layers() {
1100 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1101 wide.push_buf();
1102 wide.pop_buf();
1103
1104 assert!(wide.cmds.is_empty());
1105 }
1106
1107 #[test]
1108 fn basic_layer() {
1109 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1110 wide.push_buf();
1111 wide.fill(
1112 0,
1113 10,
1114 Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
1115 );
1116 wide.fill(
1117 10,
1118 10,
1119 Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
1120 );
1121 wide.pop_buf();
1122
1123 assert_eq!(wide.cmds.len(), 4);
1124 }
1125
1126 #[test]
1127 fn dont_inline_blend_with_two_fills() {
1128 let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
1129 30, 30, 30, 255,
1130 )));
1131 let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
1132
1133 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1134 wide.push_buf();
1135 wide.fill(0, 10, paint.clone());
1136 wide.fill(10, 10, paint.clone());
1137 wide.blend(blend_mode);
1138 wide.pop_buf();
1139
1140 assert_eq!(wide.cmds.len(), 5);
1141 }
1142
1143 #[test]
1144 fn dont_inline_destructive_blend() {
1145 let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
1146 30, 30, 30, 255,
1147 )));
1148 let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear);
1149
1150 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1151 wide.push_buf();
1152 wide.fill(0, 10, paint.clone());
1153 wide.blend(blend_mode);
1154 wide.pop_buf();
1155
1156 assert_eq!(wide.cmds.len(), 4);
1157 }
1158
1159 #[test]
1160 fn tile_coordinates() {
1161 let wide = Wide::<MODE_CPU>::new(1000, 258);
1162
1163 let tile_1 = wide.get(1, 3);
1164 assert_eq!(tile_1.x, 256);
1165 assert_eq!(tile_1.y, 12);
1166
1167 let tile_2 = wide.get(2, 15);
1168 assert_eq!(tile_2.x, 512);
1169 assert_eq!(tile_2.y, 60);
1170 }
1171
1172 #[test]
1173 fn reset_clears_layer_and_clip_stacks() {
1174 type ClipPath = Option<Box<[Strip]>>;
1175
1176 let mut wide = Wide::<MODE_CPU>::new(1000, 258);
1177 let no_clip_path: ClipPath = None;
1178 wide.push_layer(no_clip_path, BlendMode::default(), None, 0.5, 0);
1179
1180 assert_eq!(wide.layer_stack.len(), 1);
1181 assert_eq!(wide.clip_stack.len(), 0);
1182
1183 let strip = Strip::new(2, 2, 0, true);
1184 let clip_path = Some(vec![strip].into_boxed_slice());
1185 wide.push_layer(clip_path, BlendMode::default(), None, 0.09, 0);
1186
1187 assert_eq!(wide.layer_stack.len(), 2);
1188 assert_eq!(wide.clip_stack.len(), 1);
1189
1190 wide.reset();
1191
1192 assert_eq!(wide.layer_stack.len(), 0);
1193 assert_eq!(wide.clip_stack.len(), 0);
1194 }
1195}