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