blob: cf702419e7a72b2d8425c5a36b42b8cd2da65808 [file]
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use super::cmd::{DepthFill, LayerFill, LayerFillAttrs, PaintFill, PaintFillAttrs, RenderCmd};
use super::depth::{DepthSegment, DepthState};
use crate::coarse::depth;
use crate::filter::context::FilterContext;
use crate::kurbo::{Affine, Vec2};
use crate::peniko::{BlendMode, Extend, ImageQuality, ImageSampler};
use crate::record::RecordedFill;
use crate::util::Span;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use vello_common::encode::{EncodedImage, EncodedPaint};
use vello_common::filter::FilterLayerPlacement;
use vello_common::geometry::RectU16;
use vello_common::mask::Mask;
use vello_common::paint::{ImageSource, IndexedPaint, Paint};
use vello_common::pixmap::Pixmap;
use vello_common::record::{LayerClip, LayerProps, Node, RecordedLayer, RecordedLayerKind};
use vello_common::strip::{Strip, visit_strip_fill_segments};
use vello_common::tile::Tile;
use vello_common::util::{Clear, RectExt, RetainVec, VecPool};
/// State for a single row of strips.
#[derive(Debug, Default)]
pub(crate) struct RowState {
/// Normal render commands rendered in back-to-front with depth buffer read.
pub(crate) render_cmds: Vec<RenderCmd>,
/// Opaque fill commands rendered front-to-back with depth buffer read and write.
pub(crate) depth_cmds: Vec<DepthFill>,
/// State of the depth buffer for this row.
depth: DepthState,
/// Current layer depth.
pub(super) layer_depth: usize,
layer_stack: Vec<RowLayerState>,
}
/// Each recorded layer by itself already stores a bounding box.
/// However, we want to take this one step further and track a horizontal bounding
/// box of each row band individually. This allows us to save a lot of work when clearing buffers
/// during fine rasterization.
///
/// For example, let's say that we are rendering a triangle with the points
/// (0, 0), (500, 0) and (250, 250). The bounding box would be (0, 0) and (500, 500). However,
/// the triangle gets much more narrow as we advance through the rows. If we just chose
/// the coarse bounding box, we would always end up clearing a whole width of 500 pixels during
/// fine rasterization. By tracking the bounding box per row, we can reduce the work to the
/// absolute minimum necessary for each row.
#[derive(Debug)]
struct RowLayerState {
/// The index in the command stream where the `PushBuf` command of the corresponding
/// layer lives.
push_cmd_idx: usize,
/// The horizontal span of the layer.
span: Option<Span>,
}
impl RowState {
pub(super) fn new() -> Self {
Self::default()
}
fn clear(&mut self) {
self.render_cmds.clear();
self.depth_cmds.clear();
self.depth.reset();
self.layer_depth = 0;
self.layer_stack.clear();
}
#[inline]
pub(super) fn push_cmd(&mut self, cmd: RenderCmd) {
match cmd {
RenderCmd::PaintFill(cmd) => self.include_current_span(cmd.span),
RenderCmd::LayerFill(cmd) => self.include_current_span(cmd.span),
RenderCmd::PushBuf(_) | RenderCmd::PopBuf => {}
}
self.render_cmds.push(cmd);
}
#[inline]
pub(super) fn push_buf(&mut self) {
let push_cmd_idx = self.render_cmds.len();
self.render_cmds.push(RenderCmd::PushBuf(None));
self.layer_stack.push(RowLayerState {
push_cmd_idx,
span: None,
});
self.layer_depth += 1;
}
#[inline]
pub(super) fn pop_buf(&mut self) {
let layer = self.layer_stack.pop().unwrap();
match &mut self.render_cmds[layer.push_cmd_idx] {
RenderCmd::PushBuf(push_span) => *push_span = layer.span,
_ => unreachable!("layer stack must point to a PushBuf command"),
}
self.render_cmds.push(RenderCmd::PopBuf);
self.layer_depth -= 1;
}
#[inline]
pub(super) fn push_depth_fill(&mut self, cmd: DepthFill, draw_id: u32) {
let span = cmd.span();
self.depth.include_span(span, draw_id);
self.depth_cmds.push(cmd);
}
#[inline]
pub(crate) fn can_skip_depth(&self, span: Span, draw_id: u32) -> bool {
self.depth.can_skip(span, draw_id)
}
#[inline]
fn include_current_span(&mut self, span: Span) {
if let Some(layer) = self.layer_stack.last_mut() {
match &mut layer.span {
Some(layer_span) => layer_span.extend(span),
None => layer.span = Some(span),
}
}
}
}
impl Clear for RowState {
fn clear(&mut self) {
Self::clear(self);
}
}
fn debug_assert_tile_aligned(point: (u16, u16), description: &str) {
debug_assert_eq!(
point.0 % Tile::WIDTH,
0,
"{description} must be tile-width aligned",
);
debug_assert_eq!(
point.1 % Tile::HEIGHT,
0,
"{description} must be tile-height aligned",
);
}
/// A bucketer that groups commands into strip-row-sized buckets.
#[derive(Debug)]
pub(crate) struct CommandBucketer {
/// The viewport of the root/filter layer we are currently bucketing.
viewport: RectU16,
/// The currently active stack of clip bboxes (from layer clips), always anchored at the
/// (0, 0) origin, regardless of the viewport, tracked for two reasons:
/// - So we can clamp fill commands to the bbox and avoid unnecessary rendering work.
/// - In case we have a filter layer with a clip, it all happens in three steps:
/// 1) We first push a new intermediate layer with the clip.
/// 2) We composite the whole filter layer **cropped to the active clip bbox**. Note that
/// the filter layer itself is _not_ affected by the clip bbox, as filter layers should
/// always be fully rendered before applying clipping. However, by limiting the composition
/// to the coarse clip bbox we might be able to save a lot of work if only a small part
/// of the filter layer is visible.
/// 3) Pop the intermediate layer, which will take care of doing the fine-grained clipping
/// (e.g. applying anti-aliasing from the clip path).
pub(super) clip_bboxes: Vec<RectU16>,
/// The actual render commands for each strip row.
///
/// Since this is essentially a 2D-array, we use [`RetainVec`] to preserve inner allocations
/// upon resetting.
pub(super) rows: RetainVec<RowState>,
pub(crate) paint_fill_attrs: Vec<PaintFillAttrs>,
pub(crate) layer_fill_attrs: Vec<LayerFillAttrs>,
pub(crate) filter_paints: Vec<EncodedPaint>,
/// Keeping track of currently active layers to enable lazy layer pushing.
pub(super) active_layers: Vec<ActiveLayer>,
/// A vector pool for keeping track of occupied rows in a layer.
occupied_rows_pool: VecPool<usize>,
/// Scratch space used when replaying clip strips while popping clipped layers.
occupied_rows_bool_scratch: Vec<bool>,
/// A counter to assign monotonically increasing IDs to draws to enable depth buffer rendering.
pub(super) next_draw_id: u32,
}
impl CommandBucketer {
pub(crate) fn from_wh(width: u16, height: u16) -> Self {
Self::new(RectU16::new(0, 0, width, height))
}
pub(crate) fn new(mut viewport: RectU16) -> Self {
// It's _very_ important that we snap to tile coordinates. Fine rasterization assumes
// that the width is a multiple of the tile width, so if that's not the case bad things
// could happen!
viewport = viewport.snap_to_tile_coordinates();
let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
// Note: `clip_bbox` is already snapped to tile coordinates because `viewport` is, so no
// need to `div_ceil` here.
let num_rows = usize::from(clip_bbox.height() / Tile::HEIGHT);
Self {
viewport,
clip_bboxes: vec![clip_bbox],
rows: RetainVec::with_len(num_rows, RowState::new),
paint_fill_attrs: Vec::new(),
layer_fill_attrs: Vec::new(),
filter_paints: Vec::new(),
active_layers: Vec::new(),
occupied_rows_pool: VecPool::default(),
occupied_rows_bool_scratch: vec![false; num_rows],
// It is important to start at 1, because the depth buffer uses 0 for "no entries yet".
next_draw_id: 1,
}
}
fn bbox_span(bbox: RectU16) -> Span {
// Bbox might be empty vertically but not horizontally. In this case,
// it should still be considered a zero-sized span, though.
// TODO: Discard empty layers in an earlier stage.
if bbox.is_empty() {
Span::new(bbox.x0, 0)
} else {
Span::new(bbox.x0, bbox.x1 - bbox.x0)
}
}
pub(crate) fn rows(&self) -> &[RowState] {
self.rows.as_slice()
}
pub(crate) fn width(&self) -> u16 {
self.clip_bboxes[0].width()
}
fn viewport_origin(&self) -> (u16, u16) {
(self.viewport.x0, self.viewport.y0)
}
pub(crate) fn reset(&mut self, mut viewport: RectU16) {
// See comments in `CommandBucketer::reset`.
viewport = viewport.snap_to_tile_coordinates();
let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
let num_rows = usize::from(viewport.height() / Tile::HEIGHT);
self.rows.clear();
self.rows.resize_with(num_rows, RowState::new);
self.paint_fill_attrs.clear();
self.layer_fill_attrs.clear();
self.filter_paints.clear();
for layer in self.active_layers.drain(..) {
self.occupied_rows_pool.submit(layer.occupied_rows);
}
self.occupied_rows_bool_scratch.clear();
self.occupied_rows_bool_scratch.resize(num_rows, false);
self.next_draw_id = 1;
self.viewport = viewport;
self.clip_bboxes.truncate(1);
self.clip_bboxes[0] = clip_bbox;
}
fn next_draw_id(&mut self) -> u32 {
let draw_id = self.next_draw_id;
self.next_draw_id += 1;
draw_id
}
#[inline(always)]
pub(super) fn ensure_row_layers(&mut self, row_idx: usize) {
let layer_depth = self.rows[row_idx].layer_depth;
if layer_depth == self.active_layers.len() {
return;
}
for layer_idx in layer_depth..self.active_layers.len() {
self.rows[row_idx].push_buf();
self.active_layers[layer_idx].occupied_rows.push(row_idx);
}
}
pub(crate) fn bucket_commands(
&mut self,
nodes: &[Node],
draws: &[RecordedFill],
layers: &[RecordedLayer],
strips: &[Strip],
encoded_paints: &[EncodedPaint],
filter_ctx: &FilterContext,
) {
// When rendering filter layers, we always anchor them so that the top-left of the bounding
// box lands at (0, 0), even if the bounding box's top-left is for example at (200, 200).
// This is to ensure that the pixmap is as small as possible. Otherwise, if we for example
// had a filter layer that draws something small in the bottom right but nowhere else, we
// would still need to allocate a full viewport-sized pixmap!
// Therefore, we need to keep track of this offset so that for example paints know that
// they should actually be sampled at (200, 200) instead of (0, 0).
debug_assert_tile_aligned(self.viewport_origin(), "viewport origin");
for node in nodes {
for RecordedFill {
thread_idx,
strip_range,
paint,
blend_mode,
mask,
} in node.draws_in(draws)
{
let draw_id = self.next_draw_id();
let attrs = PaintFillAttrs {
paint: paint.clone(),
blend_mode: *blend_mode,
mask: mask.clone(),
draw_id,
thread_idx: *thread_idx,
origin: self.viewport_origin(),
};
self.generate_fill(&strips[strip_range.clone()], &attrs, encoded_paints);
}
if let Some(id) = node.layer {
let layer = &layers[id as usize];
let props = &layer.props;
match &layer.kind {
RecordedLayerKind::Regular => {
// Regular layers are inlined and bucketed into the same command stream.
self.push_layer(props);
// TODO: Avoid recursion to prevent stack overflows for deeply nested
// layers.
self.bucket_commands(
&layer.nodes,
draws,
layers,
strips,
encoded_paints,
filter_ctx,
);
self.pop_layer(strips);
}
RecordedLayerKind::Filter { placement, .. } => {
let needs_layer = props.blend_mode != BlendMode::default()
|| props.opacity != 1.0
|| props.mask.is_some()
|| props.clip_path.is_some();
if needs_layer {
self.push_layer(props);
}
// Note: At this point, we've already rasterized all dependent
// filter layers, so this should never fail.
if let Some(pixmap) = filter_ctx.filter_layer(id as usize) {
self.generate_filter_layer_fill(
pixmap,
*placement,
encoded_paints.len(),
);
}
if needs_layer {
self.pop_layer(strips);
}
}
}
}
}
}
pub(crate) fn push_layer(&mut self, props: &LayerProps) {
let parent_bbox = *self.clip_bboxes.last().unwrap();
let bbox = props
.clip_path
.as_ref()
.map(|clip| {
// Make sure to translate the clip path from viewport-space to local space, since
// `clip_bboxes` uses this coordinate system.
let clip_bbox = clip.bbox.relative_to_origin(self.viewport_origin());
clip_bbox.intersect(parent_bbox)
})
.unwrap_or(parent_bbox);
if props.clip_path.is_some() {
self.clip_bboxes.push(bbox);
}
self.active_layers.push(ActiveLayer {
// TODO: Masks are currently probably broken if they are used inside of a filter layer,
// since they aren't shifted and also only work if the mask has the same dimensions as
// our allocated filter layer.
mask: props.mask.clone(),
blend_mode: props.blend_mode,
opacity: props.opacity,
clip: props.clip_path.clone(),
span: Self::bbox_span(bbox),
occupied_rows: self.occupied_rows_pool.take(),
});
// If the blend mode is destructive, we need to eagerly push to all rows in the clip bbox,
// since even areas where we didn't draw anything need to be blended with the destructive
// blend mode.
if props.blend_mode.is_destructive() && !bbox.is_empty() {
let row_start = usize::from(bbox.y0 / Tile::HEIGHT);
let row_end = usize::from(bbox.y1.div_ceil(Tile::HEIGHT)).min(self.rows.len());
for row_idx in row_start..row_end {
self.ensure_row_layers(row_idx);
}
}
}
pub(crate) fn pop_layer(&mut self, strips: &[Strip]) {
let mut layer = self.active_layers.pop().unwrap();
let opacity = layer.opacity;
let blend_mode = layer.blend_mode;
// Two cases that need to be distinguished.
//
// If no clip was associated, we simply iterate over all rows that
// were lazily associated with some rendered contents and emit a `LayerFill`
// instructions across the whole width of the bounding box of the layer.
//
// If there _was_ a clip, things get trickier because the `LayerFill` commands
// need to be generated on a more fine-grained basis, and might in certain cases
// also require anti-aliasing.
if let Some(clip) = layer.clip {
let attrs_idx = self.layer_fill_attrs.len() as u32;
let draw_id = self.next_draw_id();
self.layer_fill_attrs.push(LayerFillAttrs {
blend_mode,
opacity,
mask: layer.mask.clone(),
draw_id,
thread_idx: clip.thread_idx,
});
self.clip_bboxes.pop();
// Note: The clip strips themselves are still in viewport space, not in local space.
// They will be converted to local space when calling `generate`.
let clip_strips = &strips[clip.strip_range];
// We need to make sure that we only emit `LayerFill` commands for rows that actually
// have been pushed into. If we don't have a destructive blend mode and only parts of
// the layer were painted, it can happen that the clip path itself covers a row
// that actually doesn't have an associated `PushBuf` command.
// For such rows, we don't want to emit any layer fill commands, because there is
// nothing to fill into in the first place!
// `occupied_rows` stores the indices of all rows that have been marked, but they
// are not sorted or anything. Therefore, we convert it into an indexable array
// such that in the `generate` closure, we can easily check whether the row is included
// or now.
for &row_idx in &layer.occupied_rows {
self.occupied_rows_bool_scratch[row_idx] = true;
}
// Only generate layer fill commands if it actually lies within a row that has
// been touched by the contents of the layer.
self.generate(
clip_strips,
|bucketer, fill| {
if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
fill.span, None, attrs_idx,
)));
}
},
|bucketer, fill| {
if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
fill.span,
Some(fill.alpha_idx),
attrs_idx,
)));
}
},
);
for row_idx in layer.occupied_rows.drain(..) {
self.rows[row_idx].pop_buf();
// Make sure to reset it so that by the end, the vector is all `false` again.
self.occupied_rows_bool_scratch[row_idx] = false;
}
self.occupied_rows_pool.submit(layer.occupied_rows);
} else {
let attrs_idx = self.layer_fill_attrs.len() as u32;
let draw_id = self.next_draw_id();
self.layer_fill_attrs.push(LayerFillAttrs {
blend_mode,
opacity,
mask: layer.mask.clone(),
draw_id,
thread_idx: 0,
});
for row_idx in layer.occupied_rows.drain(..) {
let row = &mut self.rows[row_idx];
// TODO: Instead of always pushing the full layer bbox across all rows, it
// would be nice to instead only emit the per-row bounding box.
row.push_cmd(RenderCmd::LayerFill(LayerFill::new(
layer.span, None, attrs_idx,
)));
row.pop_buf();
}
self.occupied_rows_pool.submit(layer.occupied_rows);
}
}
pub(crate) fn generate_filter_layer_fill(
&mut self,
pixmap: Arc<Pixmap>,
placement: FilterLayerPlacement,
static_paint_count: usize,
) {
let origin = self.viewport_origin();
let dest_bbox = placement.dest_bbox;
let src_sample_shift = placement.src_origin();
// Here, we want to determine the absolute transform that needs to be applied to the filter
// image for it to be placed correctly. On the one hand, we position it such that it starts
// at the top-left of `dest_bbox`, which is the intended location the filter layer should
// be composited into. On the other hand, we optionally apply a correction to ensure we
// are sampling from the right location of the filter pixmap (see
// `FilterLayerPlacement::new` for a more detailed description of how/what this offset
// represents).
let src_offset = (
i32::from(src_sample_shift.0) - i32::from(dest_bbox.x0),
i32::from(src_sample_shift.1) - i32::from(dest_bbox.y0),
);
// Now that we have determined the image transform, we next need to determine which
// strip rows in the bucketer should actually be emitted. Keep in mind that regardless
// of what `viewport_origin` is, when actually rendering we always shift the origin to
// (0, 0). (Note: We did _not_ do this for computing `src_offset` because the shift for the
// paint itself will be applied later when resolving the indexed paint)
let dest_bbox = dest_bbox.relative_to_origin(origin);
let clip_bbox = *self.clip_bboxes.last().unwrap();
// As noted in [`CommandBucketer::clip_bboxes`], we only need to composite the parts
// of the filter layer that actually lie within clip bounding box.
// `clip_bbox` already is in viewport-local coordinates, so we don't need to shift it
// like we did for `dest_bbox`.
let clipped_dest_bbox = dest_bbox.intersect(clip_bbox);
if clipped_dest_bbox.is_empty() {
return;
}
let draw_id = self.next_draw_id();
let span = Self::bbox_span(clipped_dest_bbox);
let paint_idx = static_paint_count + self.filter_paints.len();
self.filter_paints.push(EncodedPaint::Image(EncodedImage {
source: ImageSource::Pixmap(pixmap),
sampler: ImageSampler {
x_extend: Extend::Pad,
y_extend: Extend::Pad,
quality: ImageQuality::Low,
alpha: 1.0,
},
may_have_transparency: true,
transform: Affine::translate((f64::from(src_offset.0), f64::from(src_offset.1))),
x_advance: Vec2::new(1.0, 0.0),
y_advance: Vec2::new(0.0, 1.0),
tint: None,
}));
let attrs_idx = self.paint_fill_attrs.len() as u32;
self.paint_fill_attrs.push(PaintFillAttrs {
paint: Paint::Indexed(IndexedPaint::new(paint_idx)),
blend_mode: BlendMode::default(),
mask: None,
draw_id,
thread_idx: 0,
origin,
});
let row_start = usize::from(clipped_dest_bbox.y0 / Tile::HEIGHT);
let row_end = usize::from(clipped_dest_bbox.y1.div_ceil(Tile::HEIGHT));
for row_idx in row_start..row_end {
self.push_fill(GeneratedFill { row_idx, span }, attrs_idx, None);
}
}
pub(crate) fn generate_fill(
&mut self,
strip_buf: &[Strip],
attrs: &PaintFillAttrs,
encoded_paints: &[EncodedPaint],
) {
if strip_buf.is_empty() {
return;
}
debug_assert_ne!(attrs.draw_id, 0, "fill draw IDs should start at 1");
let attrs_idx = self.paint_fill_attrs.len() as u32;
self.paint_fill_attrs.push(attrs.clone());
let draw_id =
// While in certain cases it _might_ be okay to use depth culling while inside of
// a layer, it can get very finicky with blend modes etc., so we just outright
// reject those for now.
(self.active_layers.is_empty()
&& attrs.blend_mode == BlendMode::default()
&& attrs.mask.is_none()
&& !attrs.paint.may_have_transparency(encoded_paints))
.then_some(attrs.draw_id);
self.generate(
strip_buf,
|bucketer, fill| {
// `push_fill` already calls `ensure_row_layers` so no need to call it twice.
bucketer.push_fill(fill, attrs_idx, draw_id);
},
|bucketer, fill| {
let row_idx = fill.row_idx;
bucketer.ensure_row_layers(row_idx);
bucketer.rows[row_idx].push_cmd(RenderCmd::PaintFill(PaintFill::new(
fill.span,
Some(fill.alpha_idx),
attrs_idx,
)));
},
);
}
pub(crate) fn generate<F, A>(
&mut self,
strip_buf: &[Strip],
mut fill_cmd: F,
mut alpha_fill_cmd: A,
) where
F: FnMut(&mut Self, GeneratedFill),
A: FnMut(&mut Self, GeneratedAlphaFill),
{
if strip_buf.is_empty() {
return;
}
let clip_bbox = *self.clip_bboxes.last().unwrap();
// TODO: Don't emit layers with empty clip bboxes (and non-destructive blend modes)
// in the first place during recordings.
if clip_bbox.is_empty() {
return;
}
// Note: Those will always be aligned to tile coordinates.
let clip_x0 = clip_bbox.x0;
let clip_x1 = clip_bbox.x1;
debug_assert_tile_aligned((clip_x0, clip_bbox.y0), "clip start");
debug_assert_tile_aligned((clip_x1, clip_bbox.y1), "clip end");
let origin = self.viewport_origin();
debug_assert_tile_aligned(origin, "viewport origin");
// Note: the viewport of a filter layer is based on the bounds of its rendered contents.
// Therefore, those are always guaranteed to be within the viewport rect. However, this does not
// apply to any clip paths associated with the filter layer. Including those in the filter
// layer bbox could unnecessarily balloon the size of the pixmap we allocate if it is
// very large, since those don't actually contribute any visible output. However, this does
// mean that this method might be called with strips that do not lie within the viewport.
// Therefore, we need to make sure to clip those appropriately.
let origin_tile_x = origin.0 / Tile::WIDTH;
let origin_tile_y = origin.1 / Tile::HEIGHT;
let clip_scene_y0 = origin.1.saturating_add(clip_bbox.y0);
let clip_scene_y1 = origin.1.saturating_add(clip_bbox.y1);
// Convert to scene coordinates.
let clip_scene_x0 = origin.0.saturating_add(clip_x0);
let clip_scene_x1 = origin.0.saturating_add(clip_x1);
// Clip bounding box in tile units.
let tile_bounds = RectU16::new(
clip_scene_x0 / Tile::WIDTH,
clip_scene_y0 / Tile::HEIGHT,
clip_scene_x1 / Tile::WIDTH,
clip_scene_y1 / Tile::HEIGHT,
);
visit_strip_fill_segments(
strip_buf,
tile_bounds,
self,
|bucketer, segment| {
let row_idx = usize::from(segment.tile_y - origin_tile_y);
let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
alpha_fill_cmd(
bucketer,
GeneratedAlphaFill {
row_idx,
span: Span::new(x0, x1 - x0),
alpha_idx: segment.alpha_idx,
},
);
},
|bucketer, segment| {
let row_idx = usize::from(segment.tile_y - origin_tile_y);
let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
fill_cmd(
bucketer,
GeneratedFill {
row_idx,
span: Span::new(x0, x1 - x0),
},
);
},
);
}
/// Note: If depth-culling should be disabled, pass `None` to `draw_id`.
fn push_fill(&mut self, fill: GeneratedFill, attrs_idx: u32, draw_id: Option<u32>) {
self.ensure_row_layers(fill.row_idx);
let row = &mut self.rows[fill.row_idx];
let draw_id = draw_id.filter(|_| row.layer_depth == 0);
let Some(draw_id) = draw_id else {
// If depth-culling is disabled, we can just push it as a single contiguous command.
row.push_cmd(RenderCmd::PaintFill(PaintFill::new(
fill.span, None, attrs_idx,
)));
return;
};
depth::split_opaque_span(fill.span, |segment| match segment {
DepthSegment::Regular(span) => {
row.push_cmd(RenderCmd::PaintFill(PaintFill::new(span, None, attrs_idx)));
}
DepthSegment::Opaque(bucket_range) => {
row.push_depth_fill(DepthFill::new(bucket_range, attrs_idx), draw_id);
}
});
}
}
/// Metadata about the currently active layer.
#[derive(Debug, Clone)]
pub(crate) struct ActiveLayer {
pub(crate) mask: Option<Mask>,
pub(crate) blend_mode: BlendMode,
pub(crate) opacity: f32,
pub(crate) clip: Option<LayerClip>,
pub(crate) span: Span,
/// Which rows have been drawn into and thus contain lazily-allocated `PushBuf` instructions.
pub(crate) occupied_rows: Vec<usize>,
}
/// A generic fill to allow using `generate_fill` to create either paint fills or blend fills.
#[derive(Debug, Clone, Copy)]
pub(crate) struct GeneratedFill {
pub(crate) row_idx: usize,
pub(crate) span: Span,
}
/// A generic alpha fill to allow using `generate_fill` to create either paint fills or blend fills.
#[derive(Debug, Clone, Copy)]
pub(crate) struct GeneratedAlphaFill {
pub(crate) row_idx: usize,
pub(crate) span: Span,
pub(crate) alpha_idx: u32,
}
#[cfg(test)]
mod tests {
use super::LayerClip;
use crate::coarse::CommandBucketer;
use crate::coarse::cmd::{PaintFillAttrs, RenderCmd};
use crate::coarse::depth::{BucketRange, DEPTH_BUCKET_WIDTH};
use vello_common::color::palette::css::{BLUE, RED};
use vello_common::color::{AlphaColor, Srgb};
use vello_common::geometry::RectU16;
use vello_common::paint::{Paint, PremulColor};
use vello_common::peniko::{BlendMode, Compose, Mix};
use vello_common::record::LayerProps;
use vello_common::strip::Strip;
use vello_common::tile::Tile;
fn color(alpha: AlphaColor<Srgb>) -> PremulColor {
PremulColor::from_alpha_color(alpha)
}
fn fill_attrs(paint: Paint) -> PaintFillAttrs {
PaintFillAttrs {
paint,
blend_mode: BlendMode::default(),
mask: None,
draw_id: 1,
thread_idx: 0,
origin: (0, 0),
}
}
fn layer_props() -> LayerProps {
LayerProps {
blend_mode: BlendMode::default(),
opacity: 1.0,
mask: None,
clip_path: None,
}
}
fn clipped_layer_props(bbox: RectU16) -> LayerProps {
LayerProps {
blend_mode: BlendMode::default(),
opacity: 1.0,
mask: None,
clip_path: Some(LayerClip {
strip_range: 0..0,
thread_idx: 0,
bbox,
}),
}
}
fn destructive_clipped_layer_props(bbox: RectU16) -> LayerProps {
LayerProps {
blend_mode: BlendMode::new(Mix::Normal, Compose::Clear),
..clipped_layer_props(bbox)
}
}
fn clipped_layer_props_with_strips(
bbox: RectU16,
strip_range: core::ops::Range<usize>,
) -> LayerProps {
LayerProps {
blend_mode: BlendMode::default(),
opacity: 1.0,
mask: None,
clip_path: Some(LayerClip {
strip_range,
thread_idx: 0,
bbox,
}),
}
}
fn count_layer_fills(cmds: &[RenderCmd]) -> usize {
cmds.iter()
.filter(|cmd| matches!(cmd, RenderCmd::LayerFill(_)))
.count()
}
#[test]
fn opaque_fill_inside_layer_does_not_use_depth_write() {
let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
let strips = [
Strip::new(0, 0, 0, false),
Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
];
bucketer.push_layer(&layer_props());
bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
let row = &bucketer.rows()[0];
assert_eq!(row.depth_cmds.len(), 0);
assert_eq!(row.render_cmds.len(), 2);
assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
assert!(
matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
);
}
#[test]
fn alpha_fill_is_clipped_to_active_layer_bbox() {
let mut bucketer = CommandBucketer::from_wh(8, 4);
let strips = [Strip::new(0, 0, 0, false), Strip::new(12, 0, 48, false)];
bucketer.push_layer(&clipped_layer_props(RectU16::new(4, 0, 8, 4)));
bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
let row = &bucketer.rows()[0];
assert_eq!(row.render_cmds.len(), 2);
assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
assert!(matches!(
row.render_cmds[1],
RenderCmd::PaintFill(cmd)
if cmd.span.pixel_x() == 4
&& cmd.span.pixel_width() == 4
&& cmd.alpha_idx() == Some(u32::from(4 * Tile::HEIGHT))
));
}
#[test]
fn disjoint_nested_clip_bounds_do_not_emit_commands() {
let mut bucketer = CommandBucketer::from_wh(16, 4);
let strips = [Strip::new(0, 0, 0, false), Strip::new(16, 0, 0, true)];
bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
bucketer.push_layer(&clipped_layer_props(RectU16::new(8, 0, 12, 4)));
bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
}
#[test]
fn empty_destructive_clip_does_not_push_rows() {
let mut bucketer = CommandBucketer::from_wh(16, 4);
bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
bucketer.push_layer(&destructive_clipped_layer_props(RectU16::new(8, 0, 12, 4)));
assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
}
#[test]
fn opaque_fill_uses_depth_write_when_possible() {
let end = DEPTH_BUCKET_WIDTH * 2 + 4;
let mut bucketer = CommandBucketer::from_wh(end, 4);
let strips = [Strip::new(4, 0, 0, false), Strip::new(end, 0, 0, true)];
bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
let row = &bucketer.rows()[0];
assert_eq!(row.depth_cmds.len(), 1);
assert_eq!(row.depth_cmds[0].bucket_range(), BucketRange::new(1, 2));
assert_eq!(row.render_cmds.len(), 2);
assert!(
matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 4 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH - 4)
);
assert!(
matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == DEPTH_BUCKET_WIDTH * 2 && cmd.span.pixel_width() == 4)
);
}
#[test]
fn non_opaque_fill_uses_regular_commands() {
let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
let strips = [
Strip::new(0, 0, 0, false),
Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
];
bucketer.generate_fill(
&strips,
&fill_attrs(Paint::Solid(color(BLUE.with_alpha(0.5)))),
&[],
);
let row = &bucketer.rows()[0];
assert_eq!(row.depth_cmds.len(), 0);
assert_eq!(row.render_cmds.len(), 1);
assert!(
matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
);
}
#[test]
fn clips_fills_correctly_inside_nonzero_origin_viewport() {
// Viewport spans scene (32, 32) to (96, 96). Local space is 64x64, the origin at (32, 32).
let mut bucketer = CommandBucketer::new(RectU16::new(32, 32, 96, 96));
// Clip bbox in scene coordinates: (40, 32)..(72, 96) => local (8, 0)..(40, 64).
bucketer.push_layer(&clipped_layer_props(RectU16::new(40, 32, 72, 96)));
// A 32px-wide alpha strip at scene (40, 32) => local (8, 0), fully inside the clip.
let strips = [
Strip::new(40, 32, 0, false),
Strip::new(72, 32, 32 * u32::from(Tile::HEIGHT), false),
];
bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
let row = &bucketer.rows()[0];
assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
// The fill inside should not have been clipped.
assert!(matches!(
row.render_cmds[1],
RenderCmd::PaintFill(cmd)
if cmd.span.pixel_x() == 8
&& cmd.span.pixel_width() == 32
&& cmd.alpha_idx() == Some(0)
));
}
#[test]
fn culls_clip_strips_above_viewport_origin() {
// Viewport spans scene (0, 32) to (64, 96). Local space is 64x64, the origin at (0, 32).
let mut bucketer = CommandBucketer::new(RectU16::new(0, 32, 64, 96));
let alpha = u32::from(Tile::HEIGHT);
let strips = [
// Content: 16px alpha strip at scene (0, 32) => local row 0.
Strip::new(0, 32, 0, false),
Strip::new(16, 32, 16 * alpha, false),
// Clip strip above the viewport origin at scene y = 0, covering nothing visible.
Strip::new(0, 0, 16 * alpha, false),
Strip::new(16, 0, 32 * alpha, false),
// Clip strip at scene y = 32 => local row 0: the real coverage.
Strip::new(0, 32, 32 * alpha, false),
Strip::new(16, 32, 48 * alpha, false),
];
// Clip bbox in scene coordinates: (0, 0)..(16, 40) => local (0, 0)..(16, 8).
bucketer.push_layer(&clipped_layer_props_with_strips(
RectU16::new(0, 0, 16, 40),
2..6,
));
bucketer.generate_fill(&strips[0..2], &fill_attrs(Paint::Solid(color(RED))), &[]);
bucketer.pop_layer(&strips);
let row = &bucketer.rows()[0];
assert_eq!(
count_layer_fills(&row.render_cmds),
1,
"row 0 must be composited exactly once, got {:?}",
row.render_cmds
);
}
}