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::encode::EncodedPaint;
8use crate::filter_effects::Filter;
9use crate::kurbo::{Affine, Rect};
10use crate::mask::Mask;
11use crate::paint::{Paint, PremulColor};
12use crate::peniko::{BlendMode, Compose, Mix};
13use crate::render_graph::{DependencyKind, LayerId, RenderGraph, RenderNodeKind};
14use crate::{strip::Strip, tile::Tile};
15use alloc::vec;
16use alloc::{boxed::Box, vec::Vec};
17#[cfg(debug_assertions)]
18use alloc::{format, string::String};
19use core::ops::Range;
20use hashbrown::HashMap;
21#[cfg(not(feature = "std"))]
22use peniko::kurbo::common::FloatFuncs as _;
23
24#[derive(Debug)]
25struct Layer {
26 /// The layer's ID.
27 layer_id: LayerId,
28 /// Whether the layer has a clip associated with it.
29 clip: bool,
30 /// The blend mode with which this layer should be blended into
31 /// the previous layer.
32 blend_mode: BlendMode,
33 /// An opacity to apply to the whole layer before blending it
34 /// into the backdrop.
35 opacity: f32,
36 /// A mask to apply to the layer before blending it back into
37 /// the backdrop.
38 mask: Option<Mask>,
39 /// A filter effect to apply to the layer before other operations.
40 filter: Option<Filter>,
41 /// Bounding box of wide tiles containing geometry.
42 /// Starts with inverted bounds, shrinks to actual content during drawing.
43 wtile_bbox: WideTilesBbox,
44}
45
46impl Layer {
47 /// Whether the layer actually requires allocating a new scratch buffer
48 /// for drawing its contents.
49 fn needs_buf(&self) -> bool {
50 self.blend_mode.mix != Mix::Normal
51 || self.blend_mode.compose != Compose::SrcOver
52 || self.opacity != 1.0
53 || self.mask.is_some()
54 || self.filter.is_some()
55 || !self.clip
56 }
57}
58
59/// `MODE_CPU` allows compile time optimizations to be applied to wide tile draw command generation
60/// specific to `vello_cpu`.
61pub const MODE_CPU: u8 = 0;
62/// `MODE_HYBRID` allows compile time optimizations to be applied to wide tile draw command
63/// generation specific for `vello_hybrid`.
64pub const MODE_HYBRID: u8 = 1;
65
66/// A container for wide tiles.
67#[derive(Debug)]
68pub struct Wide<const MODE: u8 = MODE_CPU> {
69 /// The width of the container.
70 pub width: u16,
71 /// The height of the container.
72 pub height: u16,
73 /// The wide tiles in the container.
74 pub tiles: Vec<WideTile<MODE>>,
75 /// Shared command properties, referenced by index from fill and clip commands.
76 pub attrs: CommandAttrs,
77 /// The stack of layers.
78 layer_stack: Vec<Layer>,
79 /// The stack of active clip regions.
80 clip_stack: Vec<Clip>,
81 /// Stack of filter layer node IDs for render graph dependency tracking.
82 /// Initialized with node 0 (the root node representing the final output).
83 /// As layers with filters are pushed, their node IDs are added to this stack.
84 filter_node_stack: Vec<usize>,
85 /// Count of nested filtered layers with clip paths.
86 /// When > 0, command generation uses full viewport bounds instead of clip bounds
87 /// to ensure filter effects can process the full layer before applying the clip.
88 clipped_filter_layer_depth: u32,
89}
90
91/// A clip region.
92#[derive(Debug)]
93struct Clip {
94 /// The intersected bounding box after clip
95 pub clip_bbox: WideTilesBbox,
96 /// The rendered path in sparse strip representation
97 pub strips: Box<[Strip]>,
98 /// The index of the thread that owns the alpha buffer.
99 /// Always 0 in single-threaded mode.
100 pub thread_idx: u8,
101}
102
103/// An axis-aligned bounding box represented by top-left and bottom-right corners.
104///
105/// The coordinates are stored as `[x0, y0, x1, y1]` in wide tile coordinates,
106/// where `(x0, y0)` is the top-left corner and `(x1, y1)` is the bottom-right corner.
107#[derive(Debug, Clone, Copy)]
108pub struct WideTilesBbox {
109 /// The bounding box coordinates.
110 pub bbox: [u16; 4],
111}
112
113impl WideTilesBbox {
114 /// Create a new bounding box.
115 pub fn new(bbox: [u16; 4]) -> Self {
116 Self { bbox }
117 }
118
119 /// Get the x0 coordinate of the bounding box.
120 #[inline(always)]
121 pub fn x0(&self) -> u16 {
122 self.bbox[0]
123 }
124
125 /// Get the y0 coordinate of the bounding box.
126 #[inline(always)]
127 pub fn y0(&self) -> u16 {
128 self.bbox[1]
129 }
130
131 /// Get the x1 coordinate of the bounding box.
132 #[inline(always)]
133 pub fn x1(&self) -> u16 {
134 self.bbox[2]
135 }
136
137 /// Get the y1 coordinate of the bounding box.
138 #[inline(always)]
139 pub fn y1(&self) -> u16 {
140 self.bbox[3]
141 }
142
143 /// Get the width of the bounding box (x1 - x0).
144 #[inline(always)]
145 pub fn width_tiles(&self) -> u16 {
146 self.x1().saturating_sub(self.x0())
147 }
148
149 /// Get the width of the bounding box in pixels.
150 #[inline(always)]
151 pub fn width_px(&self) -> u16 {
152 self.width_tiles() * WideTile::WIDTH
153 }
154
155 /// Get the height of the bounding box (y1 - y0).
156 #[inline(always)]
157 pub fn height_tiles(&self) -> u16 {
158 self.y1().saturating_sub(self.y0())
159 }
160
161 /// Get the height of the bounding box in pixels.
162 #[inline(always)]
163 pub fn height_px(&self) -> u16 {
164 self.height_tiles() * Tile::HEIGHT
165 }
166
167 /// Check if a point (x, y) is contained within this bounding box.
168 ///
169 /// Returns `true` if x0 <= x < x1 and y0 <= y < y1.
170 #[inline(always)]
171 pub fn contains(&self, x: u16, y: u16) -> bool {
172 let [x0, y0, x1, y1] = self.bbox;
173 (x >= x0) & (x < x1) & (y >= y0) & (y < y1)
174 }
175
176 /// Create an empty bounding box (zero area).
177 #[inline(always)]
178 pub(crate) fn empty() -> Self {
179 Self::new([0, 0, 0, 0])
180 }
181
182 /// Calculate the intersection of this bounding box with another.
183 #[inline(always)]
184 pub(crate) fn intersect(self, other: Self) -> Self {
185 Self::new([
186 self.x0().max(other.x0()),
187 self.y0().max(other.y0()),
188 self.x1().min(other.x1()),
189 self.y1().min(other.y1()),
190 ])
191 }
192
193 /// Update this bounding box to include another bounding box (union in place).
194 #[inline(always)]
195 pub(crate) fn union(&mut self, other: Self) {
196 if !other.is_inverted() {
197 if self.is_inverted() {
198 // If self is empty, just copy other
199 self.bbox = other.bbox;
200 } else {
201 // Otherwise compute the union
202 self.bbox[0] = self.bbox[0].min(other.x0());
203 self.bbox[1] = self.bbox[1].min(other.y0());
204 self.bbox[2] = self.bbox[2].max(other.x1());
205 self.bbox[3] = self.bbox[3].max(other.y1());
206 }
207 }
208 }
209
210 /// Create an inverted bounding box for incremental updates.
211 #[inline(always)]
212 pub(crate) fn inverted() -> Self {
213 Self::new([u16::MAX, u16::MAX, 0, 0])
214 }
215
216 /// Check if the bbox is still in its inverted state (no updates yet).
217 #[inline(always)]
218 pub(crate) fn is_inverted(self) -> bool {
219 self.bbox[0] == u16::MAX && self.bbox[1] == u16::MAX
220 }
221
222 /// Update the bbox to include the given tile coordinates.
223 #[inline(always)]
224 pub(crate) fn include_tile(&mut self, wtile_x: u16, wtile_y: u16) {
225 self.bbox[0] = self.bbox[0].min(wtile_x);
226 self.bbox[1] = self.bbox[1].min(wtile_y);
227 self.bbox[2] = self.bbox[2].max(wtile_x + 1);
228 self.bbox[3] = self.bbox[3].max(wtile_y + 1);
229 }
230
231 /// Scale this bounding box by the given scale factors.
232 ///
233 /// Multiplies each coordinate by the corresponding scale factor to convert
234 /// from one coordinate system to another.
235 #[inline(always)]
236 pub fn scale(&self, scale_x: u16, scale_y: u16) -> [u32; 4] {
237 [
238 u32::from(self.x0()) * u32::from(scale_x),
239 u32::from(self.y0()) * u32::from(scale_y),
240 u32::from(self.x1()) * u32::from(scale_x),
241 u32::from(self.y1()) * u32::from(scale_y),
242 ]
243 }
244
245 /// Expands the bounding box outward by the given pixel amounts in each direction.
246 ///
247 /// Pixel values are converted to tile coordinates (rounding up) and clamped to the
248 /// valid range `[0, max_x)` × `[0, max_y)`. The result is a new bounding box in
249 /// wide tile coordinates.
250 pub fn expand_by_pixels(&self, expansion: Rect, max_x: u16, max_y: u16) -> Self {
251 // The expansion rect is centered at origin:
252 // - Negative coordinates (x0, y0) represent left/top expansion
253 // - Positive coordinates (x1, y1) represent right/bottom expansion
254 let left_px = (-expansion.x0).max(0.0).ceil() as u16;
255 let top_px = (-expansion.y0).max(0.0).ceil() as u16;
256 let right_px = expansion.x1.max(0.0).ceil() as u16;
257 let bottom_px = expansion.y1.max(0.0).ceil() as u16;
258
259 // Convert pixel expansion to tile expansion (round up)
260 let left_tiles = left_px.div_ceil(WideTile::WIDTH);
261 let top_tiles = top_px.div_ceil(Tile::HEIGHT);
262 let right_tiles = right_px.div_ceil(WideTile::WIDTH);
263 let bottom_tiles = bottom_px.div_ceil(Tile::HEIGHT);
264
265 Self::new([
266 self.x0().saturating_sub(left_tiles),
267 self.y0().saturating_sub(top_tiles),
268 (self.x1() + right_tiles).min(max_x),
269 (self.y1() + bottom_tiles).min(max_y),
270 ])
271 }
272}
273
274impl Wide<MODE_CPU> {
275 /// Create a new container for wide tiles.
276 pub fn new(width: u16, height: u16) -> Self {
277 Self::new_internal(width, height)
278 }
279}
280
281impl Wide<MODE_HYBRID> {
282 /// Create a new container for wide tiles.
283 pub fn new(width: u16, height: u16) -> Self {
284 Self::new_internal(width, height)
285 }
286}
287
288impl<const MODE: u8> Wide<MODE> {
289 /// Create a new container for wide tiles.
290 fn new_internal(width: u16, height: u16) -> Self {
291 let width_tiles = width.div_ceil(WideTile::WIDTH);
292 let height_tiles = height.div_ceil(Tile::HEIGHT);
293 let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles));
294
295 for h in 0..height_tiles {
296 for w in 0..width_tiles {
297 tiles.push(WideTile::<MODE>::new_internal(
298 w * WideTile::WIDTH,
299 h * Tile::HEIGHT,
300 ));
301 }
302 }
303
304 Self {
305 tiles,
306 width,
307 height,
308 attrs: CommandAttrs::default(),
309 layer_stack: vec![],
310 clip_stack: vec![],
311 // Start with root node 0.
312 filter_node_stack: vec![0],
313 clipped_filter_layer_depth: 0,
314 }
315 }
316
317 /// Whether there are any existing layers that haven't been popped yet.
318 pub fn has_layers(&self) -> bool {
319 !self.layer_stack.is_empty()
320 }
321
322 /// Reset all tiles in the container.
323 pub fn reset(&mut self) {
324 for tile in &mut self.tiles {
325 tile.bg = PremulColor::from_alpha_color(TRANSPARENT);
326 tile.cmds.clear();
327 tile.layer_ids.truncate(1);
328 tile.layer_cmd_ranges.clear();
329 tile.layer_cmd_ranges
330 .insert(0, LayerCommandRanges::default());
331 }
332 self.attrs.clear();
333 self.layer_stack.clear();
334 self.clip_stack.clear();
335 self.filter_node_stack.truncate(1);
336 self.clipped_filter_layer_depth = 0;
337 }
338
339 /// Return the number of horizontal tiles.
340 pub fn width_tiles(&self) -> u16 {
341 self.width.div_ceil(WideTile::WIDTH)
342 }
343
344 /// Return the number of vertical tiles.
345 pub fn height_tiles(&self) -> u16 {
346 self.height.div_ceil(Tile::HEIGHT)
347 }
348
349 /// Get the wide tile at a certain index.
350 ///
351 /// Panics if the index is out-of-range.
352 pub fn get(&self, x: u16, y: u16) -> &WideTile<MODE> {
353 assert!(
354 x < self.width_tiles() && y < self.height_tiles(),
355 "attempted to access out-of-bounds wide tile"
356 );
357
358 &self.tiles[usize::from(y) * usize::from(self.width_tiles()) + usize::from(x)]
359 }
360
361 /// Get mutable access to the wide tile at a certain index.
362 ///
363 /// Panics if the index is out-of-range.
364 pub fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile<MODE> {
365 assert!(
366 x < self.width_tiles() && y < self.height_tiles(),
367 "attempted to access out-of-bounds wide tile"
368 );
369
370 let idx = usize::from(y) * usize::from(self.width_tiles()) + usize::from(x);
371 &mut self.tiles[idx]
372 }
373
374 /// Return a reference to all wide tiles.
375 pub fn tiles(&self) -> &[WideTile<MODE>] {
376 self.tiles.as_slice()
377 }
378
379 /// Get the current layer Id.
380 #[inline(always)]
381 pub fn get_current_layer_id(&self) -> LayerId {
382 self.layer_stack.last().map_or(0, |l| l.layer_id)
383 }
384
385 /// Update the bounding box of the current layer to include the given tile.
386 /// Should be called whenever a command is generated for a tile.
387 #[inline]
388 fn update_current_layer_bbox(&mut self, wtile_x: u16, wtile_y: u16) {
389 if let Some(layer) = self.layer_stack.last_mut() {
390 layer.wtile_bbox.include_tile(wtile_x, wtile_y);
391 }
392 }
393
394 /// Generate wide tile commands from the strip buffer.
395 ///
396 /// This method processes a buffer of strips that represent a path, applies the fill rule,
397 /// and generates appropriate drawing commands for each affected wide tile.
398 ///
399 /// # Algorithm overview:
400 /// 1. For each strip in the buffer:
401 /// - Calculate its position and width in pixels
402 /// - Determine which wide tiles the strip intersects
403 /// - Generate alpha fill commands for the intersected wide tiles
404 /// 2. For active fill regions (determined by fill rule):
405 /// - Generate solid fill commands for the regions between strips
406 pub fn generate(
407 &mut self,
408 strip_buf: &[Strip],
409 paint: Paint,
410 blend_mode: BlendMode,
411 thread_idx: u8,
412 mask: Option<Mask>,
413 encoded_paints: &[EncodedPaint],
414 ) {
415 if strip_buf.is_empty() {
416 return;
417 }
418
419 let alpha_base_idx = strip_buf[0].alpha_idx();
420
421 // Create shared attributes for all commands from this path
422 let attrs_idx = self.attrs.fill.len() as u32;
423 self.attrs.fill.push(FillAttrs {
424 thread_idx,
425 paint,
426 blend_mode,
427 mask,
428 alpha_base_idx,
429 });
430
431 // Get current clip bounding box or full viewport if no clip is active
432 let bbox = self.active_bbox();
433
434 // Save current_layer_id to avoid borrowing issues
435 let current_layer_id = self.get_current_layer_id();
436
437 for i in 0..strip_buf.len() - 1 {
438 let strip = &strip_buf[i];
439
440 debug_assert!(
441 strip.y < self.height,
442 "Strips below the viewport should have been culled prior to this stage."
443 );
444
445 // Don't render strips that are outside the viewport width
446 if strip.x >= self.width {
447 continue;
448 }
449
450 let next_strip = &strip_buf[i + 1];
451 let x0 = strip.x;
452 let strip_y = strip.strip_y();
453
454 // Skip strips outside the current clip bounding box
455 if strip_y < bbox.y0() {
456 continue;
457 }
458 if strip_y >= bbox.y1() {
459 // The rest of our strips must be outside the clip, so we can break early.
460 break;
461 }
462
463 // Calculate the width of the strip in columns
464 let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
465 let next_col = next_strip.alpha_idx() / u32::from(Tile::HEIGHT);
466 // Can potentially be 0 if strip only changes winding without covering pixels
467 let strip_width = next_col.saturating_sub(col) as u16;
468 let x1 = x0.saturating_add(strip_width);
469
470 // Calculate which wide tiles this strip intersects
471 let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0());
472 // It's possible that a strip extends into a new wide tile, but we don't actually
473 // have as many wide tiles (e.g. because the pixmap width is only 512, but
474 // strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
475 let wtile_x1 = x1
476 .div_ceil(WideTile::WIDTH)
477 .min(bbox.x1())
478 .min(WideTile::MAX_WIDE_TILE_COORD);
479
480 // Adjust column starting position if needed to respect clip boundaries
481 let mut x = x0;
482 let clip_x = bbox.x0() * WideTile::WIDTH;
483 if clip_x > x {
484 col += u32::from(clip_x - x);
485 x = clip_x;
486 }
487
488 // Generate alpha fill commands for each wide tile intersected by this strip
489 for wtile_x in wtile_x0..wtile_x1 {
490 let x_wtile_rel = x % WideTile::WIDTH;
491 // Restrict the width of the fill to the width of the wide tile
492 let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
493 let cmd = CmdAlphaFill {
494 x: x_wtile_rel,
495 width,
496 alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx,
497 attrs_idx,
498 };
499 x += width;
500 col += u32::from(width);
501 self.get_mut(wtile_x, strip_y).strip(cmd, current_layer_id);
502 self.update_current_layer_bbox(wtile_x, strip_y);
503 }
504
505 // Determine if the region between this strip and the next should be filled.
506 let active_fill = next_strip.fill_gap();
507
508 // If region should be filled and both strips are on the same row,
509 // generate fill commands for the region between them
510 if active_fill && strip_y == next_strip.strip_y() {
511 // Clamp the fill to the clip bounding box
512 x = x1.max(bbox.x0() * WideTile::WIDTH);
513 let x2 = next_strip.x.min(
514 self.width
515 .checked_next_multiple_of(WideTile::WIDTH)
516 .unwrap_or(u16::MAX),
517 );
518 let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0());
519 let wfxt1 = x2
520 .div_ceil(WideTile::WIDTH)
521 .min(bbox.x1())
522 .min(WideTile::MAX_WIDE_TILE_COORD);
523
524 // Compute fill hint based on paint type
525 let fill_attrs = &self.attrs.fill[attrs_idx as usize];
526 let fill_hint = if fill_attrs.mask.is_none() {
527 match &fill_attrs.paint {
528 Paint::Solid(s) if s.is_opaque() => FillHint::OpaqueSolid(*s),
529 Paint::Indexed(idx) => {
530 if let Some(EncodedPaint::Image(img)) = encoded_paints.get(idx.index())
531 && !img.may_have_opacities
532 && img.sampler.alpha == 1.0
533 {
534 FillHint::OpaqueImage
535 } else {
536 FillHint::None
537 }
538 }
539 _ => FillHint::None,
540 }
541 } else {
542 FillHint::None
543 };
544
545 // Generate fill commands for each wide tile in the fill region
546 for wtile_x in wfxt0..wfxt1 {
547 let x_wtile_rel = x % WideTile::WIDTH;
548 let width = x2.min(
549 (wtile_x
550 .checked_add(1)
551 .unwrap_or(WideTile::MAX_WIDE_TILE_COORD))
552 * WideTile::WIDTH,
553 ) - x;
554 x += width;
555 self.get_mut(wtile_x, strip_y).fill(
556 x_wtile_rel,
557 width,
558 attrs_idx,
559 current_layer_id,
560 fill_hint,
561 );
562 // TODO: This bbox update might be redundant since filled regions are always
563 // bounded by strip regions (which already update the bbox). Consider removing
564 // this in a follow-up with proper benchmarks to verify correctness.
565 self.update_current_layer_bbox(wtile_x, strip_y);
566 }
567 }
568 }
569 }
570
571 /// Push a new layer with the given properties.
572 ///
573 /// Rendering will be directed to the layer storage identified by `layer_id`.
574 /// This is used for filter effects that require access to a fully-rendered layer.
575 ///
576 /// If `filter` is Some, builds render graph nodes for filter effects.
577 pub fn push_layer(
578 &mut self,
579 layer_id: LayerId,
580 clip_path: Option<impl Into<Box<[Strip]>>>,
581 blend_mode: BlendMode,
582 mask: Option<Mask>,
583 opacity: f32,
584 filter: Option<Filter>,
585 transform: Affine,
586 render_graph: &mut RenderGraph,
587 thread_idx: u8,
588 ) {
589 // Some explanations about what is going on here: We support the concept of
590 // layers, where a user can push a new layer (with certain properties), draw some
591 // stuff, and finally pop the layer, as part of which the layer as a whole will be
592 // blended into the previous layer.
593 // There are 3 "straightforward" properties that can be set for each layer:
594 // 1) The blend mode that should be used to blend the layer into the backdrop.
595 // 2) A mask that will be applied to the whole layer in the very end before blending.
596 // 3) An optional opacity that will be applied to the whole layer before blending (this
597 // could in theory be simulated with an alpha mask, but since it's a common operation and
598 // we only have a single opacity, this can easily be optimized.
599 //
600 // Finally, you can also add a clip path to the layer. However, clipping has its own
601 // more complicated logic for pushing/popping buffers where drawing is also suppressed
602 // in clipped-out wide tiles. Because of this, in case we have one of the above properties
603 // AND a clipping path, we will actually end up pushing two buffers, the first one handles
604 // the three properties and the second one is just for clip paths. That is a bit wasteful
605 // and I believe it should be possible to process them all in just one go, but for now
606 // this is good enough, and it allows us to implement blending without too deep changes to
607 // the original clipping implementation.
608
609 // Build render graph node ONLY if we have a filter.
610 // The render graph tracks dependencies and execution order for filter effects.
611 if let Some(filter) = &filter {
612 // Create a FilterLayer node that combines render + filter + other operations
613 let child_node = render_graph.add_node(RenderNodeKind::FilterLayer {
614 layer_id,
615 filter: filter.clone(),
616 // Bounding box starts inverted and will be updated in pop_layer with actual bounds
617 wtile_bbox: WideTilesBbox::inverted(),
618 transform,
619 });
620
621 // Connect to parent node if there is one
622 if let Some(&parent_node) = self.filter_node_stack.last() {
623 render_graph.add_edge(
624 child_node,
625 parent_node,
626 DependencyKind::Sequential { layer_id },
627 );
628 }
629
630 // Push this filter node onto the stack so subsequent filters depend on it
631 self.filter_node_stack.push(child_node);
632 }
633
634 let has_filter = filter.is_some();
635 let has_clip = clip_path.is_some();
636 let layer_kind = if has_filter {
637 LayerKind::Filtered(layer_id)
638 } else {
639 LayerKind::Regular(layer_id)
640 };
641
642 // Filtered layers with clipping require special handling: normally, tiles with
643 // zero-winding clips suppress all drawing. However, filters need the full layer
644 // content rendered (including zero-clipped areas) before applying the clip as a mask.
645 // When this flag is true, we generate explicit drawing commands instead of just counters.
646 let in_clipped_filter_layer = has_filter && has_clip;
647
648 // Increment the depth counter so that active_bbox() returns the full viewport
649 // instead of the clipped bbox. This ensures command generation covers all tiles,
650 // allowing the filter to process the entire layer before the clip is applied.
651 if in_clipped_filter_layer {
652 self.clipped_filter_layer_depth += 1;
653 }
654
655 let layer = Layer {
656 layer_id,
657 clip: has_clip,
658 blend_mode,
659 opacity,
660 mask,
661 filter,
662 wtile_bbox: WideTilesBbox::inverted(),
663 };
664
665 // In case we do blending, masking, opacity, or filtering, push one buffer per wide tile.
666 //
667 // Layers require buffers for different reasons:
668 // - Blending, opacity, and masking: Need to composite results with the backdrop
669 // - Filtering: Content must be rendered to a buffer before applying filter effects
670 //
671 // The layer_kind parameter distinguishes how buffers are managed:
672 // - Regular layers use the local blend_buf stack
673 // - Filtered layers are materialized in persistent layer storage for filter processing
674 // - Clip layers have special handling for clipping operations
675 if layer.needs_buf() {
676 for x in 0..self.width_tiles() {
677 for y in 0..self.height_tiles() {
678 let tile = self.get_mut(x, y);
679 tile.push_buf(layer_kind);
680 // Mark tiles that are in a clipped filter layer so they generate
681 // explicit clip commands for proper filter processing.
682 tile.in_clipped_filter_layer = in_clipped_filter_layer;
683 }
684 }
685 }
686
687 // If we have a clip path, push another buffer in the affected wide tiles.
688 // Note that it is important that we FIRST push the buffer for blending etc. and
689 // only then for clipping, otherwise we will use the empty clip buffer as the backdrop
690 // for blending!
691 if let Some(clip) = clip_path {
692 self.push_clip(clip, layer_id, thread_idx);
693 }
694
695 self.layer_stack.push(layer);
696 }
697
698 /// Pop a previously pushed layer.
699 ///
700 /// This method finalizes the layer by:
701 /// - Expanding the bounding box if filter effects are present
702 /// - Updating the parent layer's bounding box to include this layer's bounds
703 /// - Completing render graph nodes for filter effects
704 /// - Generating filter commands for each tile
705 /// - Popping any associated clip
706 /// - Applying mask, opacity, and blend mode operations if needed
707 pub fn pop_layer(&mut self, render_graph: &mut RenderGraph) {
708 // This method basically unwinds everything we did in `push_layer`.
709 let mut layer = self.layer_stack.pop().unwrap();
710
711 if let Some(filter) = &layer.filter {
712 // Update render graph node with final bounding box
713 if let Some(node_id) = self.filter_node_stack.pop() {
714 // Get the transform from the FilterLayer node and scale the expansion by it
715 if let Some(node) = render_graph.nodes.get_mut(node_id)
716 && let RenderNodeKind::FilterLayer {
717 wtile_bbox,
718 transform,
719 ..
720 } = &mut node.kind
721 {
722 // Calculate expansion in device/pixel space, accounting for the full transform.
723 // This ensures that rotated filters (e.g., drop shadows) have correct bounds.
724 let expansion = filter.bounds_expansion(transform);
725 let expanded_bbox = layer.wtile_bbox.expand_by_pixels(
726 expansion,
727 self.width_tiles(),
728 self.height_tiles(),
729 );
730 let clip_bbox = self.active_bbox();
731 let final_bbox = expanded_bbox.intersect(clip_bbox);
732
733 // Update both the local layer and the render graph node
734 layer.wtile_bbox = final_bbox;
735 *wtile_bbox = final_bbox;
736 }
737 // Record this node in execution order (children before parents)
738 render_graph.record_node_for_execution(node_id);
739 }
740
741 // Generate filter commands for each tile (used for non-graph path rendering)
742 // Apply filter BEFORE clipping (per SVG spec: filter → clip → mask → opacity → blend)
743 for x in 0..self.width_tiles() {
744 for y in 0..self.height_tiles() {
745 self.get_mut(x, y).filter(layer.layer_id, filter.clone());
746 }
747 }
748 }
749
750 // Union this layer's bbox into the parent layer's bbox.
751 // This ensures the parent knows about all tiles used by this child layer,
752 // which is important for filter effects that may expand beyond the original content bounds.
753 if let Some(parent_layer) = self.layer_stack.last_mut() {
754 parent_layer.wtile_bbox.union(layer.wtile_bbox);
755 }
756
757 if layer.clip {
758 self.pop_clip();
759 }
760
761 let needs_buf = layer.needs_buf();
762
763 if needs_buf {
764 for x in 0..self.width_tiles() {
765 for y in 0..self.height_tiles() {
766 let t = self.get_mut(x, y);
767
768 if let Some(mask) = layer.mask.clone() {
769 t.mask(mask);
770 }
771 t.opacity(layer.opacity);
772 t.blend(layer.blend_mode);
773 t.pop_buf();
774 }
775 }
776 }
777
778 let in_clipped_filter_layer = layer.filter.is_some() && layer.clip;
779 // Decrement the depth counter after popping a filtered layer with clip
780 if in_clipped_filter_layer {
781 self.clipped_filter_layer_depth -= 1;
782 }
783 }
784
785 /// Adds a clipping region defined by the provided strips.
786 ///
787 /// This method takes a vector of strips representing a clip path, calculates the
788 /// intersection with the current clip region, and updates the clip stack.
789 ///
790 /// # Algorithm overview:
791 /// 1. Calculate bounding box of the clip path
792 /// 2. Intersect with current clip bounding box
793 /// 3. For each tile in the intersected bounding box:
794 /// - If covered by zero winding: `push_zero_clip`
795 /// - If fully covered by non-zero winding: do nothing (clip is a no-op)
796 /// - If partially covered: `push_clip`
797 pub fn push_clip(
798 &mut self,
799 strips: impl Into<Box<[Strip]>>,
800 layer_id: LayerId,
801 thread_idx: u8,
802 ) {
803 let strips = strips.into();
804 let n_strips = strips.len();
805
806 // Calculate the bounding box of the clip path in strip coordinates
807 let path_bbox = if n_strips <= 1 {
808 WideTilesBbox::empty()
809 } else {
810 // Calculate the y range from first to last strip in wide tile coordinates
811 let wtile_y0 = strips[0].strip_y();
812 let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1;
813
814 // Calculate the x range by examining all strips in wide tile coordinates
815 let mut wtile_x0 = strips[0].x / WideTile::WIDTH;
816 let mut wtile_x1 = wtile_x0;
817 for i in 0..n_strips.saturating_sub(1) {
818 let strip = &strips[i];
819 let next_strip = &strips[i + 1];
820 let width =
821 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
822 let x = strip.x;
823 wtile_x0 = wtile_x0.min(x / WideTile::WIDTH);
824 wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH));
825 }
826 WideTilesBbox::new([wtile_x0, wtile_y0, wtile_x1, wtile_y1])
827 };
828
829 let parent_bbox = self.active_bbox();
830 // Determine which tiles need clip processing:
831 // - For clipped filter layers: active_bbox() returns the full viewport, so parent_bbox
832 // already covers all tiles. We need to process all of them because the filter needs
833 // the entire layer rendered, and tiles outside the clip path must get `PushZeroClip`
834 // commands to properly suppress their content after filtering.
835 // - For normal clips: Intersect with the path bounds to only process tiles that are
836 // actually affected by the clip path, avoiding unnecessary work.
837 let clip_bbox = if self.clipped_filter_layer_depth > 0 {
838 // Use parent_bbox as-is (full viewport) to process all tiles
839 parent_bbox
840 } else {
841 // Optimize by processing only the intersection of parent and path bounds
842 parent_bbox.intersect(path_bbox)
843 };
844
845 let mut cur_wtile_x = clip_bbox.x0();
846 let mut cur_wtile_y = clip_bbox.y0();
847
848 // Process strips to determine the clipping state for each wide tile
849 for i in 0..n_strips.saturating_sub(1) {
850 let strip = &strips[i];
851 let strip_y = strip.strip_y();
852
853 // Skip strips before current wide tile row
854 if strip_y < cur_wtile_y {
855 continue;
856 }
857
858 // Process wide tiles in rows before this strip's row
859 // These wide tiles are all zero-winding (outside the path)
860 while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
861 for wtile_x in cur_wtile_x..clip_bbox.x1() {
862 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
863 }
864 // Reset x to the left edge of the clip bounding box
865 cur_wtile_x = clip_bbox.x0();
866 // Move to the next row
867 cur_wtile_y += 1;
868 }
869
870 // If we've reached the bottom of the clip bounding box, stop processing.
871 // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
872 // is zero-area (see issue 1072).
873 if cur_wtile_y >= clip_bbox.y1() {
874 break;
875 }
876
877 // Process wide tiles to the left of this strip in the same row
878 let x = strip.x;
879 let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1());
880 if cur_wtile_x < wtile_x_clamped {
881 // If winding is zero or doesn't match fill rule, these wide tiles are outside the path
882 let is_inside = strip.fill_gap();
883 if !is_inside {
884 for wtile_x in cur_wtile_x..wtile_x_clamped {
885 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
886 }
887 }
888 // If winding is nonzero, then wide tiles covered entirely
889 // by sparse fill are no-op (no clipping is applied).
890 cur_wtile_x = wtile_x_clamped;
891 }
892
893 // Process wide tiles covered by the strip - these need actual clipping
894 let next_strip = &strips[i + 1];
895 let width =
896 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
897 let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
898 if cur_wtile_x < wtile_x1 {
899 for wtile_x in cur_wtile_x..wtile_x1 {
900 self.get_mut(wtile_x, cur_wtile_y).push_clip(layer_id);
901 }
902 cur_wtile_x = wtile_x1;
903 }
904 }
905
906 // Process any remaining wide tiles in the bounding box (all zero-winding)
907 while cur_wtile_y < clip_bbox.y1() {
908 for wtile_x in cur_wtile_x..clip_bbox.x1() {
909 self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
910 }
911 cur_wtile_x = clip_bbox.x0();
912 cur_wtile_y += 1;
913 }
914
915 self.clip_stack.push(Clip {
916 clip_bbox,
917 strips,
918 thread_idx,
919 });
920 }
921
922 /// Get the bounding box of the current clip region or the entire viewport if no clip regions are active.
923 fn active_bbox(&self) -> WideTilesBbox {
924 // When in a clipped filter layer, use full viewport to allow
925 // filter to process the complete layer before applying clip as mask
926 if self.clipped_filter_layer_depth > 0 {
927 return self.full_viewport_bbox();
928 }
929
930 self.clip_stack
931 .last()
932 .map(|top| top.clip_bbox)
933 .unwrap_or_else(|| self.full_viewport_bbox())
934 }
935
936 /// Returns the bounding box covering the entire viewport in wide tile coordinates.
937 fn full_viewport_bbox(&self) -> WideTilesBbox {
938 WideTilesBbox::new([0, 0, self.width_tiles(), self.height_tiles()])
939 }
940
941 /// Removes the most recently added clip region.
942 ///
943 /// This is the inverse operation of `push_clip`, carefully undoing all the clipping
944 /// operations while also handling any rendering needed for the clip region itself.
945 ///
946 /// # Algorithm overview:
947 /// 1. Retrieve the top clip from the stack
948 /// 2. For each wide tile in the clip's bounding box:
949 /// - If covered by zero winding: `pop_zero_clip`
950 /// - If fully covered by non-zero winding: do nothing (was no-op)
951 /// - If partially covered: render the clip and `pop_clip`
952 ///
953 /// This operation must be symmetric with `push_clip` to maintain a balanced clip stack.
954 fn pop_clip(&mut self) {
955 let Clip {
956 clip_bbox,
957 strips,
958 thread_idx,
959 } = self.clip_stack.pop().unwrap();
960 let n_strips = strips.len();
961
962 if n_strips == 0 {
963 return;
964 }
965
966 // Compute base alpha index and create shared clip attributes
967 let alpha_base_idx = strips[0].alpha_idx();
968 let clip_attrs_idx = self.attrs.clip.len() as u32;
969 self.attrs.clip.push(ClipAttrs {
970 thread_idx,
971 alpha_base_idx,
972 });
973
974 let mut cur_wtile_x = clip_bbox.x0();
975 let mut cur_wtile_y = clip_bbox.y0();
976 let mut pop_pending = false;
977
978 // Process each strip to determine the clipping state for each tile
979 for i in 0..n_strips.saturating_sub(1) {
980 let strip = &strips[i];
981 let strip_y = strip.strip_y();
982
983 // Skip strips before current tile row
984 if strip_y < cur_wtile_y {
985 continue;
986 }
987
988 // Process tiles in rows before this strip's row
989 // These tiles all had zero-winding clips
990 while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
991 // Handle any pending clip pop from previous iteration
992 if core::mem::take(&mut pop_pending) {
993 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
994 cur_wtile_x += 1;
995 }
996
997 // Pop zero clips for all remaining tiles in this row
998 for wtile_x in cur_wtile_x..clip_bbox.x1() {
999 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1000 }
1001 cur_wtile_x = clip_bbox.x0();
1002 cur_wtile_y += 1;
1003 }
1004
1005 // If we've reached the bottom of the clip bounding box, stop processing
1006 // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
1007 // is zero-area (see issue 1072).
1008 if cur_wtile_y >= clip_bbox.y1() {
1009 break;
1010 }
1011
1012 // Process tiles to the left of this strip in the same row
1013 let x0 = strip.x;
1014 let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1());
1015 if cur_wtile_x < wtile_x_clamped {
1016 // Handle any pending clip pop from previous iteration
1017 if core::mem::take(&mut pop_pending) {
1018 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1019 cur_wtile_x += 1;
1020 }
1021
1022 // Pop zero clips for tiles that had zero winding or didn't match fill rule
1023 // TODO: The winding check is probably not needed; if there was a fill,
1024 // the logic below should have advanced wtile_x.
1025 let is_inside = strip.fill_gap();
1026 if !is_inside {
1027 for wtile_x in cur_wtile_x..wtile_x_clamped {
1028 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1029 }
1030 }
1031 cur_wtile_x = wtile_x_clamped;
1032 }
1033
1034 // Process tiles covered by the strip - render clip content and pop
1035 let next_strip = &strips[i + 1];
1036 let strip_width =
1037 ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
1038 let mut clipped_x1 = x0 + strip_width;
1039 let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0());
1040 let wtile_x1 = clipped_x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
1041
1042 // Calculate starting position and column for alpha mask
1043 let mut x = x0;
1044 let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
1045 let clip_x = clip_bbox.x0() * WideTile::WIDTH;
1046 if clip_x > x {
1047 col += u32::from(clip_x - x);
1048 x = clip_x;
1049 clipped_x1 = clip_x.max(clipped_x1);
1050 }
1051
1052 // Render clip strips for each affected tile and mark for popping
1053 for wtile_x in wtile_x0..wtile_x1 {
1054 // If we've moved past tile_x and have a pending pop, do it now
1055 if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) {
1056 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1057 }
1058
1059 // Calculate the portion of the strip that affects this tile
1060 let x_rel = x % WideTile::WIDTH;
1061 let width = clipped_x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
1062
1063 // Create clip strip command for rendering the partial coverage
1064 let cmd = CmdClipAlphaFill {
1065 x: x_rel,
1066 width,
1067 alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx,
1068 attrs_idx: clip_attrs_idx,
1069 };
1070 x += width;
1071 col += u32::from(width);
1072
1073 // Apply the clip strip command and update state
1074 self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd);
1075 cur_wtile_x = wtile_x;
1076
1077 // Only request a pop if the x coordinate is actually inside the bounds.
1078 if cur_wtile_x < clip_bbox.x1() {
1079 pop_pending = true;
1080 }
1081 }
1082
1083 // Handle fill regions between strips based on fill rule
1084 let is_inside = next_strip.fill_gap();
1085 if is_inside && strip_y == next_strip.strip_y() {
1086 if cur_wtile_x >= clip_bbox.x1() {
1087 continue;
1088 }
1089
1090 let x2 = next_strip.x;
1091 let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH);
1092 let width = clipped_x2.saturating_sub(clipped_x1);
1093
1094 // If there's a gap, fill it. Only do this if the fill wouldn't cover the
1095 // whole tile, as such clips are skipped by the `push_clip` function. See
1096 // <https://github.com/linebender/vello/blob/de0659e4df9842c8857153841a2b4ba6f1020bb0/sparse_strips/vello_common/src/coarse.rs#L504-L516>
1097 if width > 0 && width < WideTile::WIDTH {
1098 let x_rel = clipped_x1 % WideTile::WIDTH;
1099 self.get_mut(cur_wtile_x, cur_wtile_y)
1100 .clip_fill(x_rel, width);
1101 }
1102
1103 // If the next strip is a sentinel, skip the fill
1104 // It's a sentinel in the row if there is non-zero winding for the sparse fill
1105 // Look more into this in the strip.rs render function
1106 if x2 == u16::MAX {
1107 continue;
1108 }
1109
1110 // If fill extends to next tile, pop current and handle next
1111 if x2 > (cur_wtile_x + 1) * WideTile::WIDTH {
1112 if core::mem::take(&mut pop_pending) {
1113 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1114 }
1115
1116 let width2 = x2 % WideTile::WIDTH;
1117 cur_wtile_x = x2 / WideTile::WIDTH;
1118
1119 // If the strip is outside the clipping box, we don't need to do any
1120 // filling, so we continue (also to prevent out-of-bounds access).
1121 if cur_wtile_x >= clip_bbox.x1() {
1122 continue;
1123 }
1124
1125 if width2 > 0 {
1126 // An important thing to note: Note that we are only applying
1127 // `clip_fill` to the wide tile that is actually covered by the next
1128 // strip, and not the ones in-between! For example, if the first strip
1129 // is in wide tile 1 and the second in wide tile 4, we will do a clip
1130 // fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is
1131 // that any tile in-between is fully covered and thus no clipping is
1132 // necessary at all. See also the `push_clip` function, where we don't
1133 // push a new buffer for such tiles.
1134 self.get_mut(cur_wtile_x, cur_wtile_y).clip_fill(0, width2);
1135 }
1136 }
1137 }
1138 }
1139
1140 // Handle any pending clip pop from the last iteration
1141 if core::mem::take(&mut pop_pending) {
1142 self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1143 cur_wtile_x += 1;
1144 }
1145
1146 // Process any remaining tiles in the bounding box (all zero-winding)
1147 while cur_wtile_y < clip_bbox.y1() {
1148 for wtile_x in cur_wtile_x..clip_bbox.x1() {
1149 self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1150 }
1151 cur_wtile_x = clip_bbox.x0();
1152 cur_wtile_y += 1;
1153 }
1154 }
1155}
1156
1157/// A wide tile.
1158#[derive(Debug)]
1159pub struct WideTile<const MODE: u8 = MODE_CPU> {
1160 /// The x coordinate of the wide tile.
1161 pub x: u16,
1162 /// The y coordinate of the wide tile.
1163 pub y: u16,
1164 /// The background of the tile.
1165 pub bg: PremulColor,
1166 /// The draw commands of the tile.
1167 pub cmds: Vec<Cmd>,
1168 /// The number of zero-winding clips.
1169 pub n_zero_clip: usize,
1170 /// The number of non-zero-winding clips.
1171 pub n_clip: usize,
1172 /// The number of pushed buffers.
1173 pub n_bufs: usize,
1174 /// True when this tile is in a filtered layer with clipping applied.
1175 /// When set, clip operations generate explicit commands instead of just
1176 /// tracking counters, allowing filters to process clipped content correctly.
1177 pub in_clipped_filter_layer: bool,
1178 /// Maps layer Id to command ranges for this tile.
1179 pub layer_cmd_ranges: HashMap<LayerId, LayerCommandRanges>,
1180 /// Vector of layer IDs this tile participates in.
1181 pub layer_ids: Vec<LayerKind>,
1182}
1183
1184impl WideTile {
1185 /// The width of a wide tile in pixels.
1186 pub const WIDTH: u16 = 256;
1187 /// The maximum coordinate of a wide tile.
1188 pub const MAX_WIDE_TILE_COORD: u16 = u16::MAX / Self::WIDTH;
1189}
1190
1191impl WideTile<MODE_CPU> {
1192 /// Create a new wide tile.
1193 pub fn new(width: u16, height: u16) -> Self {
1194 Self::new_internal(width, height)
1195 }
1196}
1197
1198impl WideTile<MODE_HYBRID> {
1199 /// Create a new wide tile.
1200 pub fn new(width: u16, height: u16) -> Self {
1201 Self::new_internal(width, height)
1202 }
1203}
1204
1205impl<const MODE: u8> WideTile<MODE> {
1206 /// Create a new wide tile.
1207 fn new_internal(x: u16, y: u16) -> Self {
1208 let mut layer_cmd_ranges = HashMap::new();
1209 layer_cmd_ranges.insert(0, LayerCommandRanges::default());
1210 Self {
1211 x,
1212 y,
1213 bg: PremulColor::from_alpha_color(TRANSPARENT),
1214 cmds: vec![],
1215 n_zero_clip: 0,
1216 n_clip: 0,
1217 n_bufs: 0,
1218 in_clipped_filter_layer: false,
1219 layer_cmd_ranges,
1220 layer_ids: vec![LayerKind::Regular(0)],
1221 }
1222 }
1223
1224 /// Fill a rectangular region with a paint.
1225 ///
1226 /// Generates fill commands unless the tile is in a zero-clip region (fully clipped out).
1227 /// For clipped filter layers, commands are always generated since filters need the full
1228 /// layer content rendered before applying the clip as a mask.
1229 ///
1230 /// The `fill_hint` parameter is pre-computed by the caller based on paint type:
1231 /// - `OpaqueSolid(color)`: Paint is an opaque solid color, can replace background
1232 /// - `OpaqueImage`: Paint is an opaque image, can clear previous commands
1233 /// - `None`: No optimization available
1234 pub(crate) fn fill(
1235 &mut self,
1236 x: u16,
1237 width: u16,
1238 attrs_idx: u32,
1239 current_layer_id: LayerId,
1240 fill_hint: FillHint,
1241 ) {
1242 if !self.is_zero_clip() || self.in_clipped_filter_layer {
1243 match MODE {
1244 MODE_CPU => {
1245 // Check if we can apply overdraw elimination optimization.
1246 // This requires filling the entire tile width with no clip/buffer stack.
1247 //
1248 // Note that we could be more aggressive in optimizing a whole-tile opaque fill
1249 // even with a clip stack. It would be valid to elide all drawing commands from
1250 // the enclosing clip push up to the fill. Further, we could extend the clip
1251 // push command to include a background color, rather than always starting with
1252 // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
1253 // be replaced with strip/fill with the color (the latter is true even with a
1254 // non-opaque color).
1255 //
1256 // However, the extra cost of tracking such optimizations may outweigh the
1257 // benefit, especially in hybrid mode with GPU painting.
1258 let can_override =
1259 x == 0 && width == WideTile::WIDTH && self.n_clip == 0 && self.n_bufs == 0;
1260
1261 if can_override {
1262 match fill_hint {
1263 FillHint::OpaqueSolid(color) => {
1264 self.cmds.clear();
1265 self.bg = color;
1266 if let Some(ranges) =
1267 self.layer_cmd_ranges.get_mut(¤t_layer_id)
1268 {
1269 ranges.clear();
1270 }
1271 return;
1272 }
1273 FillHint::OpaqueImage => {
1274 // Opaque image: clear previous commands but still emit the fill.
1275 self.cmds.clear();
1276 self.bg = PremulColor::from_alpha_color(TRANSPARENT);
1277 if let Some(ranges) =
1278 self.layer_cmd_ranges.get_mut(¤t_layer_id)
1279 {
1280 ranges.clear();
1281 }
1282 // Fall through to emit the fill command below, as opposed to
1283 // solid paints where we have a return statement.
1284 }
1285 FillHint::None => {}
1286 }
1287 }
1288
1289 self.record_fill_cmd(current_layer_id, self.cmds.len());
1290 self.cmds.push(Cmd::Fill(CmdFill {
1291 x,
1292 width,
1293 attrs_idx,
1294 }));
1295 }
1296 MODE_HYBRID => {
1297 self.record_fill_cmd(current_layer_id, self.cmds.len());
1298 self.cmds.push(Cmd::Fill(CmdFill {
1299 x,
1300 width,
1301 attrs_idx,
1302 }));
1303 }
1304 _ => unreachable!(),
1305 }
1306 }
1307 }
1308
1309 /// Fill a region using an alpha mask from a strip.
1310 ///
1311 /// Generates alpha fill commands unless the tile is in a zero-clip region (fully clipped out).
1312 /// For clipped filter layers, commands are always generated since filters need the full
1313 /// layer content rendered before applying the clip as a mask.
1314 pub(crate) fn strip(&mut self, cmd_strip: CmdAlphaFill, current_layer_id: LayerId) {
1315 if !self.is_zero_clip() || self.in_clipped_filter_layer {
1316 self.record_fill_cmd(current_layer_id, self.cmds.len());
1317 self.cmds.push(Cmd::AlphaFill(cmd_strip));
1318 }
1319 }
1320
1321 /// Adds a new clip region to the current wide tile.
1322 ///
1323 /// Pushes a clip buffer unless the tile is in a zero-clip region (fully clipped out).
1324 /// For clipped filter layers, clip buffers are always pushed since filters need explicit
1325 /// clip state to process the full layer before applying the clip as a mask.
1326 pub fn push_clip(&mut self, layer_id: LayerId) {
1327 if !self.is_zero_clip() || self.in_clipped_filter_layer {
1328 self.push_buf(LayerKind::Clip(layer_id));
1329 self.n_clip += 1;
1330 }
1331 }
1332
1333 /// Removes the most recently added clip region from the current wide tile.
1334 ///
1335 /// Pops a clip buffer unless the tile is in a zero-clip region (fully clipped out).
1336 /// For clipped filter layers, clip buffers are always popped since filters need explicit
1337 /// clip state to process the full layer before applying the clip as a mask.
1338 pub fn pop_clip(&mut self) {
1339 if !self.is_zero_clip() || self.in_clipped_filter_layer {
1340 self.pop_buf();
1341 self.n_clip -= 1;
1342 }
1343 }
1344
1345 /// Adds a zero-winding clip region to the stack.
1346 ///
1347 /// Zero-winding clips represent tiles completely outside the clip path.
1348 /// Normally these just increment a counter to suppress drawing, but for
1349 /// clipped filter layers we generate explicit commands so filters can
1350 /// process the entire layer before applying the clip as a mask.
1351 pub fn push_zero_clip(&mut self, layer_id: LayerId) {
1352 if self.in_clipped_filter_layer {
1353 // Generate explicit command for filter processing
1354 self.cmds.push(Cmd::PushZeroClip(layer_id));
1355 }
1356 self.n_zero_clip += 1;
1357 }
1358
1359 /// Removes the most recently added zero-winding clip region.
1360 pub fn pop_zero_clip(&mut self) {
1361 if self.in_clipped_filter_layer {
1362 // Generate explicit command for filter processing
1363 self.cmds.push(Cmd::PopZeroClip);
1364 }
1365 self.n_zero_clip -= 1;
1366 }
1367
1368 /// Checks if the current clip region is a zero-winding clip.
1369 pub fn is_zero_clip(&mut self) -> bool {
1370 self.n_zero_clip > 0
1371 }
1372
1373 /// Applies a clip strip operation with the given parameters.
1374 ///
1375 /// Note: Unlike content operations (`strip`, `push_clip`, etc.), clip operations don't need
1376 /// the `|| self.in_clipped_filter_layer` check. Filter effects need full layer *content*
1377 /// rendered (even in zero-clip areas).
1378 pub fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) {
1379 if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(_))) {
1380 self.cmds.push(Cmd::ClipStrip(cmd_clip_strip));
1381 }
1382 }
1383
1384 /// Applies a clip fill operation at the specified position and width.
1385 pub fn clip_fill(&mut self, x: u16, width: u16) {
1386 if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(_))) {
1387 self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width }));
1388 }
1389 }
1390
1391 /// Records the fill command for a specific layer.
1392 pub fn record_fill_cmd(&mut self, layer_id: LayerId, cmd_idx: usize) {
1393 self.layer_cmd_ranges.entry(layer_id).and_modify(|ranges| {
1394 ranges.full_range.end = cmd_idx + 1;
1395 if ranges.render_range.is_empty() {
1396 ranges.render_range = cmd_idx..cmd_idx + 1;
1397 } else {
1398 ranges.render_range.end = cmd_idx + 1;
1399 }
1400 });
1401 }
1402
1403 /// Push a buffer for a new layer.
1404 ///
1405 /// Different layer kinds are handled differently:
1406 /// - Regular layers: Use local `blend_buf` stack for temporary storage
1407 /// - Filtered layers: Materialized in persistent layer storage for filter processing
1408 /// - Clip layers: Special handling for clipping operations
1409 pub fn push_buf(&mut self, layer_kind: LayerKind) {
1410 let top_layer = layer_kind.id();
1411 if matches!(layer_kind, LayerKind::Filtered(_)) {
1412 self.layer_cmd_ranges.insert(
1413 top_layer,
1414 LayerCommandRanges {
1415 full_range: self.cmds.len()..self.cmds.len() + 1,
1416 // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`.
1417 render_range: self.cmds.len() + 1..self.cmds.len() + 1,
1418 },
1419 );
1420 } else if matches!(layer_kind, LayerKind::Clip(_)) {
1421 self.layer_cmd_ranges.entry(top_layer).and_modify(|ranges| {
1422 ranges.full_range.end = self.cmds.len() + 1;
1423 // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`.
1424 ranges.render_range = self.cmds.len() + 1..self.cmds.len() + 1;
1425 });
1426 }
1427 self.cmds.push(Cmd::PushBuf(layer_kind));
1428 self.layer_ids.push(layer_kind);
1429 self.n_bufs += 1;
1430 }
1431
1432 /// Pop the most recent buffer.
1433 pub fn pop_buf(&mut self) {
1434 let top_layer = self.layer_ids.pop().unwrap();
1435 let mut next_layer = *self.layer_ids.last().unwrap();
1436
1437 if matches!(self.cmds.last(), Some(&Cmd::PushBuf(_))) {
1438 // Optimization: If no drawing happened between the last `PushBuf`,
1439 // we can just pop it instead.
1440 self.cmds.pop();
1441 } else {
1442 self.layer_cmd_ranges
1443 .entry(top_layer.id())
1444 .and_modify(|ranges| {
1445 ranges.full_range.end = self.cmds.len() + 1;
1446 });
1447 if top_layer.id() == next_layer.id() {
1448 next_layer = *self
1449 .layer_ids
1450 .get(self.layer_ids.len().saturating_sub(2))
1451 .unwrap();
1452 }
1453
1454 self.layer_cmd_ranges
1455 .entry(next_layer.id())
1456 .and_modify(|ranges| {
1457 ranges.full_range.end = self.cmds.len() + 1;
1458 ranges.render_range.end = self.cmds.len() + 1;
1459 });
1460 self.cmds.push(Cmd::PopBuf);
1461 }
1462 self.n_bufs -= 1;
1463 }
1464
1465 /// Apply an opacity to the whole buffer.
1466 pub fn opacity(&mut self, opacity: f32) {
1467 if opacity != 1.0 {
1468 self.cmds.push(Cmd::Opacity(opacity));
1469 }
1470 }
1471
1472 /// Apply a filter effect to the whole buffer.
1473 pub fn filter(&mut self, layer_id: LayerId, filter: Filter) {
1474 self.cmds.push(Cmd::Filter(layer_id, filter));
1475 }
1476
1477 /// Apply a mask to the whole buffer.
1478 pub fn mask(&mut self, mask: Mask) {
1479 self.cmds.push(Cmd::Mask(mask));
1480 }
1481
1482 /// Blend the current buffer into the previous buffer in the stack.
1483 pub fn blend(&mut self, blend_mode: BlendMode) {
1484 // Optimization: If no drawing happened since the last `PushBuf` and the blend mode
1485 // is not destructive, we do not need to do any blending at all.
1486 if !matches!(self.cmds.last(), Some(&Cmd::PushBuf(_))) || blend_mode.is_destructive() {
1487 self.cmds.push(Cmd::Blend(blend_mode));
1488 }
1489 }
1490}
1491
1492/// Debug utilities for wide tiles.
1493///
1494/// These methods are only available in debug builds (`debug_assertions`).
1495/// They provide introspection into the command buffer for debugging and logging purposes.
1496#[cfg(debug_assertions)]
1497impl<const MODE: u8> WideTile<MODE> {
1498 /// Lists all commands in this wide tile with their indices and names.
1499 ///
1500 /// Returns a formatted string with each command on a new line, showing its index
1501 /// and human-readable name. This is useful for debugging and understanding the
1502 /// command sequence.
1503 ///
1504 /// # Example
1505 ///
1506 /// ```ignore
1507 /// let commands = wide_tile.list_commands();
1508 /// println!("{}", commands);
1509 /// // Output:
1510 /// // 0: PushBuf(Regular)
1511 /// // 1: FillPath
1512 /// // 2: PushZeroClip
1513 /// // 3: FillPath
1514 /// // 4: PopBuf
1515 /// ```
1516 #[allow(dead_code, reason = "useful for debugging")]
1517 pub fn list_commands(&self) -> String {
1518 self.cmds
1519 .iter()
1520 .enumerate()
1521 .map(|(i, cmd)| format!("{}: {}", i, cmd.name()))
1522 .collect::<Vec<_>>()
1523 .join("\n")
1524 }
1525}
1526
1527/// Optimization hint for fill operations, computed in `Wide::generate` and passed to `WideTile::fill`.
1528///
1529/// This enum communicates whether a fill operation can benefit from overdraw elimination:
1530/// - For opaque solid colors: we can set the background color directly and skip the fill
1531/// - For opaque images: we can clear previous commands but still need to emit the fill
1532#[derive(Debug, Clone, Copy)]
1533pub enum FillHint {
1534 /// No optimization possible, emit fill command normally.
1535 None,
1536 /// Paint is an opaque solid color - can replace background if conditions are met.
1537 OpaqueSolid(PremulColor),
1538 /// Paint is an opaque image - can clear previous commands if conditions are met.
1539 OpaqueImage,
1540}
1541
1542/// Distinguishes between different types of layers and their storage strategies.
1543///
1544/// Each layer kind determines how the layer's content is stored and processed:
1545/// - Regular layers are blended on-the-fly using a temporary buffer stack
1546/// - Filtered layers are materialized in persistent storage for filter processing
1547/// - Clip layers are special buffers used for clipping operations
1548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1549pub enum LayerKind {
1550 /// Regular layer using local `blend_buf` stack for temporary storage.
1551 Regular(LayerId),
1552 /// Filtered layer materialized in persistent `layer_manager` storage.
1553 Filtered(LayerId),
1554 /// Clip layer for clipping operations.
1555 Clip(LayerId),
1556}
1557
1558impl LayerKind {
1559 /// Get the underlying layer ID.
1560 ///
1561 /// All layer kinds contain a layer ID that uniquely identifies the layer.
1562 pub fn id(&self) -> LayerId {
1563 match self {
1564 Self::Regular(id) | Self::Filtered(id) | Self::Clip(id) => *id,
1565 }
1566 }
1567}
1568
1569/// A drawing command for wide tiles.
1570///
1571/// Commands are executed in order to render the final image. They include
1572/// drawing operations (`Fill`, `AlphaFill`), layer management (`PushBuf`, `PopBuf`),
1573/// clipping operations (`ClipFill`, `ClipStrip`), and post-processing effects
1574/// (`Filter`, `Blend`, `Opacity`, `Mask`).
1575#[derive(Debug, PartialEq)]
1576pub enum Cmd {
1577 /// Fill a rectangular region with a solid color or paint.
1578 Fill(CmdFill),
1579 /// Fill a region with a paint, modulated by an alpha mask.
1580 AlphaFill(CmdAlphaFill),
1581 /// Pushes a new buffer for drawing.
1582 /// Regular layers use the local `blend_buf` stack.
1583 /// Filtered layers are materialized in persistent layer storage.
1584 PushBuf(LayerKind),
1585 /// Pops the most recent buffer and blends it into the previous buffer.
1586 PopBuf,
1587 /// A fill command within a clipping region.
1588 ///
1589 /// This command will blend the contents of the current buffer within the clip fill region
1590 /// into the previous buffer in the stack.
1591 ClipFill(CmdClipFill),
1592 /// A fill command with alpha mask within a clipping region.
1593 ///
1594 /// This command will blend the contents of the current buffer within the clip fill region
1595 /// into the previous buffer in the stack, with an additional alpha mask.
1596 ClipStrip(CmdClipAlphaFill),
1597 /// Marks entry into a zero-winding clip region for a clipped filter layer.
1598 ///
1599 /// Zero-winding clips represent tiles completely outside the clip path. For clipped
1600 /// filter layers, this command allows the filter to process the full layer content
1601 /// before applying the clip as a mask (per SVG spec: filter → clip → mask → blend).
1602 PushZeroClip(LayerId),
1603 /// Marks exit from a zero-winding clip region for a clipped filter layer.
1604 PopZeroClip,
1605 /// Apply a filter effect to a layer's contents.
1606 ///
1607 /// This command applies a filter (e.g., blur, drop shadow) to the specified layer's
1608 /// rendered content. Per the SVG specification, filters are applied before clipping,
1609 /// masking, blending, and opacity operations.
1610 Filter(LayerId, Filter),
1611 /// Blend the current buffer into the previous buffer.
1612 ///
1613 /// This command blends the contents of the current buffer into the previous buffer
1614 /// using the specified blend mode (e.g., multiply, screen, overlay).
1615 Blend(BlendMode),
1616 /// Apply uniform opacity to the current buffer.
1617 ///
1618 /// Multiplies the alpha channel of all pixels in the buffer by the given opacity value.
1619 Opacity(f32),
1620 /// Apply a mask to the current buffer.
1621 ///
1622 /// Modulates the alpha channel of the buffer using the provided mask.
1623 Mask(Mask),
1624}
1625
1626#[cfg(debug_assertions)]
1627impl Cmd {
1628 /// Returns a human-readable name for this command.
1629 ///
1630 /// This is useful for debugging, logging, and displaying command information
1631 /// in a user-friendly format. To get detailed paint information, use `name_with_attrs`
1632 /// which can look up the paint from the command attributes.
1633 ///
1634 /// **Note:** This method is only available in debug builds (`debug_assertions`).
1635 pub fn name(&self) -> &'static str {
1636 match self {
1637 Self::Fill(_) => "FillPath",
1638 Self::AlphaFill(_) => "AlphaFillPath",
1639 Self::PushBuf(layer_kind) => match layer_kind {
1640 LayerKind::Regular(_) => "PushBuf(Regular)",
1641 LayerKind::Filtered(_) => "PushBuf(Filtered)",
1642 LayerKind::Clip(_) => "PushBuf(Clip)",
1643 },
1644 Self::PopBuf => "PopBuf",
1645 Self::ClipFill(_) => "ClipPathFill",
1646 Self::ClipStrip(_) => "ClipPathStrip",
1647 Self::PushZeroClip(_) => "PushZeroClip",
1648 Self::PopZeroClip => "PopZeroClip",
1649 Self::Filter(_, _) => "Filter",
1650 Self::Blend(_) => "Blend",
1651 Self::Opacity(_) => "Opacity",
1652 Self::Mask(_) => "Mask",
1653 }
1654 }
1655
1656 /// Returns a human-readable name for this command with detailed paint information.
1657 ///
1658 /// This variant looks up paint details from the command attributes for fill commands.
1659 ///
1660 /// **Note:** This method is only available in debug builds (`debug_assertions`).
1661 pub fn name_with_attrs(
1662 &self,
1663 fill_attrs: &[FillAttrs],
1664 encoded_paints: &[EncodedPaint],
1665 ) -> String {
1666 match self {
1667 Self::Fill(cmd) => {
1668 if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) {
1669 format!("FillPath({})", paint_name(&attrs.paint, encoded_paints))
1670 } else {
1671 format!("FillPath(attrs_idx={})", cmd.attrs_idx)
1672 }
1673 }
1674 Self::AlphaFill(cmd) => {
1675 if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) {
1676 format!(
1677 "AlphaFillPath({})",
1678 paint_name(&attrs.paint, encoded_paints)
1679 )
1680 } else {
1681 format!("AlphaFillPath(attrs_idx={})", cmd.attrs_idx)
1682 }
1683 }
1684 _ => self.name().into(),
1685 }
1686 }
1687}
1688
1689/// Returns a human-readable description of a paint.
1690#[cfg(debug_assertions)]
1691fn paint_name(paint: &Paint, encoded_paints: &[EncodedPaint]) -> String {
1692 match paint {
1693 Paint::Solid(color) => {
1694 let rgba = color.as_premul_rgba8();
1695 format!(
1696 "Solid(#{:02x}{:02x}{:02x}{:02x})",
1697 rgba.r, rgba.g, rgba.b, rgba.a
1698 )
1699 }
1700 Paint::Indexed(idx) => {
1701 let index = idx.index();
1702 if let Some(encoded) = encoded_paints.get(index) {
1703 let kind = match encoded {
1704 EncodedPaint::Gradient(g) => match &g.kind {
1705 crate::encode::EncodedKind::Linear(_) => "LinearGradient",
1706 crate::encode::EncodedKind::Radial(_) => "RadialGradient",
1707 crate::encode::EncodedKind::Sweep(_) => "SweepGradient",
1708 },
1709 EncodedPaint::Image(_) => "Image",
1710 EncodedPaint::BlurredRoundedRect(_) => "BlurredRoundedRect",
1711 };
1712 format!("{}[{}]", kind, index)
1713 } else {
1714 format!("Indexed({})", index)
1715 }
1716 }
1717 }
1718}
1719
1720/// Shared attributes for alpha fill commands.
1721#[derive(Debug, Clone, PartialEq)]
1722pub struct FillAttrs {
1723 /// The index of the thread that owns the alpha buffer
1724 /// containing the mask values at `alpha_idx`.
1725 /// Always 0 in single-threaded mode.
1726 pub thread_idx: u8,
1727 /// The paint (color, gradient, etc.) to fill the region with.
1728 // TODO: Store premultiplied colors as indexed paints as well, to reduce
1729 // memory overhead? Or get rid of indexed paints and inline all paints?
1730 pub paint: Paint,
1731 /// The blend mode to apply before drawing the contents.
1732 pub blend_mode: BlendMode,
1733 /// A mask to apply to the command.
1734 pub mask: Option<Mask>,
1735 /// Base index into the alpha buffer for this path's commands.
1736 /// Commands store a relative offset that is added to this base.
1737 alpha_base_idx: u32,
1738}
1739
1740impl FillAttrs {
1741 /// Compute the absolute alpha buffer index from a relative offset.
1742 pub fn alpha_idx(&self, offset: u32) -> u32 {
1743 self.alpha_base_idx + offset
1744 }
1745}
1746
1747/// Shared attributes for clip alpha fill commands.
1748#[derive(Debug, Clone, PartialEq, Eq)]
1749pub struct ClipAttrs {
1750 /// The index of the thread that owns the alpha buffer
1751 /// containing the mask values at `alpha_idx`.
1752 /// Always 0 in single-threaded mode.
1753 pub thread_idx: u8,
1754 /// Base index into the alpha buffer for this clip path's commands.
1755 /// Commands store a relative offset that is added to this base.
1756 alpha_base_idx: u32,
1757}
1758
1759impl ClipAttrs {
1760 /// Compute the absolute alpha buffer index from a relative offset.
1761 pub fn alpha_idx(&self, offset: u32) -> u32 {
1762 self.alpha_base_idx + offset
1763 }
1764}
1765
1766/// Container for shared command attributes.
1767///
1768/// This struct holds the shared attributes for fill and clip commands,
1769/// allowing them to be passed together to functions that need both.
1770#[derive(Debug, Default, Clone)]
1771pub struct CommandAttrs {
1772 /// Shared attributes for fill commands, indexed by `attrs_idx` in `CmdFill`/`CmdAlphaFill`.
1773 pub fill: Vec<FillAttrs>,
1774 /// Shared attributes for clip commands, indexed by `attrs_idx` in `CmdClipAlphaFill`.
1775 pub clip: Vec<ClipAttrs>,
1776}
1777
1778impl CommandAttrs {
1779 /// Clear all attributes.
1780 pub fn clear(&mut self) {
1781 self.fill.clear();
1782 self.clip.clear();
1783 }
1784}
1785
1786/// Fill a consecutive horizontal region of a wide tile.
1787///
1788/// This command fills a rectangular region with the specified paint.
1789/// The region starts at x-coordinate `x` and extends for `width` pixels
1790/// horizontally, spanning the full height of the wide tile.
1791#[derive(Debug, Clone, PartialEq, Eq)]
1792pub struct CmdFill {
1793 /// The horizontal start position relative to the wide tile's left edge, in pixels.
1794 pub x: u16,
1795 /// The width of the filled region in pixels.
1796 pub width: u16,
1797 /// Index into the command attributes array.
1798 pub attrs_idx: u32,
1799}
1800
1801/// Fill a consecutive horizontal region with an alpha mask.
1802///
1803/// Similar to `CmdFill`, but modulates the paint by an alpha mask stored
1804/// in a separate buffer. This is used for anti-aliased edges and partial
1805/// coverage from path rasterization.
1806#[derive(Debug, Clone, PartialEq, Eq)]
1807pub struct CmdAlphaFill {
1808 /// The horizontal start position relative to the wide tile's left edge, in pixels.
1809 pub x: u16,
1810 /// The width of the filled region in pixels.
1811 pub width: u16,
1812 /// Relative offset to the alpha buffer location.
1813 /// Use `FillAttrs::alpha_idx(alpha_offset)` to compute the absolute index.
1814 pub alpha_offset: u32,
1815 /// Index into the command attributes array.
1816 pub attrs_idx: u32,
1817}
1818
1819/// Fill operation within a clipping region.
1820///
1821/// This command copies a horizontal region from the top of the clip buffer stack
1822/// to the next buffer on the stack, effectively rendering the clipped content.
1823/// Unlike `CmdFill`, this doesn't fill with a paint but transfers existing content.
1824#[derive(Debug, PartialEq, Eq)]
1825pub struct CmdClipFill {
1826 /// The horizontal start position relative to the wide tile's left edge, in pixels.
1827 pub x: u16,
1828 /// The width of the region to copy in pixels.
1829 pub width: u16,
1830}
1831
1832/// Alpha-masked fill operation within a clipping region.
1833///
1834/// This command composites a horizontal region from the top of the clip buffer stack
1835/// to the next buffer, modulated by an alpha mask. This is used for anti-aliased
1836/// clip edges.
1837#[derive(Debug, PartialEq, Eq)]
1838pub struct CmdClipAlphaFill {
1839 /// The horizontal start position relative to the wide tile's left edge, in pixels.
1840 pub x: u16,
1841 /// The width of the region to composite in pixels.
1842 pub width: u16,
1843 /// Relative offset to the alpha buffer location.
1844 /// Use `ClipAttrs::alpha_idx(alpha_offset)` to compute the absolute index.
1845 pub alpha_offset: u32,
1846 /// Index into the clip attributes array.
1847 pub attrs_idx: u32,
1848}
1849
1850trait BlendModeExt {
1851 /// Whether a blend mode might cause destructive changes in the backdrop.
1852 /// This disallows certain optimizations (like for example inlining a blend mode
1853 /// or only applying a blend mode to the current clipping area).
1854 fn is_destructive(&self) -> bool;
1855}
1856
1857impl BlendModeExt for BlendMode {
1858 fn is_destructive(&self) -> bool {
1859 matches!(
1860 self.compose,
1861 Compose::Clear
1862 | Compose::Copy
1863 | Compose::SrcIn
1864 | Compose::DestIn
1865 | Compose::SrcOut
1866 | Compose::DestAtop
1867 )
1868 }
1869}
1870
1871/// Ranges of commands for a specific layer in a specific tile.
1872///
1873/// This structure tracks two different ranges of commands:
1874/// - The full range includes all layer operations (push, draw, pop)
1875/// - The render range includes only the actual drawing commands
1876#[derive(Debug, Clone, Default)]
1877pub struct LayerCommandRanges {
1878 /// Full range including `PushBuf`, all commands, and `PopBuf`.
1879 pub full_range: Range<usize>,
1880 /// Range containing only fill commands (`Fill`, `AlphaFill`).
1881 /// This is the range to replace when sampling from a filtered layer.
1882 pub render_range: Range<usize>,
1883}
1884
1885impl LayerCommandRanges {
1886 /// Clear the full range and render range.
1887 #[inline]
1888 pub fn clear(&mut self) {
1889 self.full_range = 0..0;
1890 self.render_range = 0..0;
1891 }
1892}
1893
1894#[cfg(test)]
1895mod tests {
1896 use crate::coarse::{FillHint, LayerKind, MODE_CPU, Wide, WideTile};
1897 use crate::kurbo::Affine;
1898 use crate::peniko::{BlendMode, Compose, Mix};
1899 use crate::render_graph::RenderGraph;
1900 use crate::strip::Strip;
1901 use alloc::{boxed::Box, vec};
1902
1903 #[test]
1904 fn optimize_empty_layers() {
1905 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1906 wide.push_buf(LayerKind::Regular(0));
1907 wide.pop_buf();
1908
1909 assert!(wide.cmds.is_empty());
1910 }
1911
1912 #[test]
1913 fn basic_layer() {
1914 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1915 wide.push_buf(LayerKind::Regular(0));
1916 wide.fill(0, 10, 0, 0, FillHint::None);
1917 wide.fill(10, 10, 0, 0, FillHint::None);
1918 wide.pop_buf();
1919
1920 assert_eq!(wide.cmds.len(), 4);
1921 }
1922
1923 #[test]
1924 fn dont_inline_blend_with_two_fills() {
1925 let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
1926
1927 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1928 wide.push_buf(LayerKind::Regular(0));
1929 wide.fill(0, 10, 0, 0, FillHint::None);
1930 wide.fill(10, 10, 0, 0, FillHint::None);
1931 wide.blend(blend_mode);
1932 wide.pop_buf();
1933
1934 assert_eq!(wide.cmds.len(), 5);
1935 }
1936
1937 #[test]
1938 fn dont_inline_destructive_blend() {
1939 let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear);
1940
1941 let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1942 wide.push_buf(LayerKind::Regular(0));
1943 wide.fill(0, 10, 0, 0, FillHint::None);
1944 wide.blend(blend_mode);
1945 wide.pop_buf();
1946
1947 assert_eq!(wide.cmds.len(), 4);
1948 }
1949
1950 #[test]
1951 fn tile_coordinates() {
1952 let wide = Wide::<MODE_CPU>::new(1000, 258);
1953
1954 let tile_1 = wide.get(1, 3);
1955 assert_eq!(tile_1.x, 256);
1956 assert_eq!(tile_1.y, 12);
1957
1958 let tile_2 = wide.get(2, 15);
1959 assert_eq!(tile_2.x, 512);
1960 assert_eq!(tile_2.y, 60);
1961 }
1962
1963 #[test]
1964 fn reset_clears_layer_and_clip_stacks() {
1965 type ClipPath = Option<Box<[Strip]>>;
1966
1967 let mut wide = Wide::<MODE_CPU>::new(1000, 258);
1968 let mut render_graph = RenderGraph::new();
1969 let no_clip_path: ClipPath = None;
1970 wide.push_layer(
1971 1,
1972 no_clip_path,
1973 BlendMode::default(),
1974 None,
1975 0.5,
1976 None,
1977 Affine::IDENTITY,
1978 &mut render_graph,
1979 0,
1980 );
1981
1982 assert_eq!(wide.layer_stack.len(), 1);
1983 assert_eq!(wide.clip_stack.len(), 0);
1984
1985 let strip = Strip::new(2, 2, 0, true);
1986 let clip_path = Some(vec![strip].into_boxed_slice());
1987 wide.push_layer(
1988 2,
1989 clip_path,
1990 BlendMode::default(),
1991 None,
1992 0.09,
1993 None,
1994 Affine::IDENTITY,
1995 &mut render_graph,
1996 0,
1997 );
1998
1999 assert_eq!(wide.layer_stack.len(), 2);
2000 assert_eq!(wide.clip_stack.len(), 1);
2001
2002 wide.reset();
2003
2004 assert_eq!(wide.layer_stack.len(), 0);
2005 assert_eq!(wide.clip_stack.len(), 0);
2006 }
2007}