blob: c7d497ed80f41730c733d8089a16b6880efed5c5 [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Generating and processing wide tiles.
use crate::color::palette::css::TRANSPARENT;
use crate::mask::Mask;
use crate::paint::{Paint, PremulColor};
use crate::peniko::{BlendMode, Compose, Fill, Mix};
use crate::{strip::Strip, tile::Tile};
use alloc::vec;
use alloc::{boxed::Box, vec::Vec};
#[derive(Debug)]
struct Layer {
/// Whether the layer has a clip associated with it.
clip: bool,
/// The blend mode with which this layer should be blended into
/// the previous layer.
blend_mode: BlendMode,
/// An opacity to apply to the whole layer before blending it
/// into the backdrop.
opacity: f32,
/// A mask to apply to the layer before blending it back into
/// the backdrop.
mask: Option<Mask>,
}
impl Layer {
/// Whether the layer actually requires allocating a new scratch buffer
/// for drawing its contents.
fn needs_buf(&self) -> bool {
self.blend_mode.mix != Mix::Normal
|| self.blend_mode.compose != Compose::SrcOver
|| self.opacity != 1.0
|| self.mask.is_some()
|| !self.clip
}
}
/// A container for wide tiles.
#[derive(Debug)]
pub struct Wide {
/// The width of the container.
pub width: u16,
/// The height of the container.
pub height: u16,
/// The wide tiles in the container.
pub tiles: Vec<WideTile>,
/// The stack of layers.
layer_stack: Vec<Layer>,
/// The stack of active clip regions.
clip_stack: Vec<Clip>,
}
/// A clip region.
#[derive(Debug)]
struct Clip {
/// The intersected bounding box after clip
pub clip_bbox: Bbox,
/// The rendered path in sparse strip representation
pub strips: Box<[Strip]>,
/// The fill rule used for this clip
pub fill_rule: Fill,
}
/// A bounding box
///
/// The first two values represent the x0 and y0 coordinates, respectively.
/// The last two values represent the x1 and y1 coordinates, respectively.
/// x0, y0 — the top-left corner of the bounding box,
/// x1, y1 — the bottom-right corner of the bounding box.
#[derive(Debug, Clone)]
struct Bbox {
pub bbox: [u16; 4],
}
impl Bbox {
pub(crate) fn new(bbox: [u16; 4]) -> Self {
Self { bbox }
}
/// Get the x0 coordinate of the bounding box.
#[inline]
pub(crate) fn x0(&self) -> u16 {
self.bbox[0]
}
/// Get the y0 coordinate of the bounding box.
#[inline]
pub(crate) fn y0(&self) -> u16 {
self.bbox[1]
}
/// Get the x1 coordinate of the bounding box.
#[inline]
pub(crate) fn x1(&self) -> u16 {
self.bbox[2]
}
/// Get the y1 coordinate of the bounding box.
#[inline]
pub(crate) fn y1(&self) -> u16 {
self.bbox[3]
}
/// Create an empty bounding box (zero area).
#[inline]
pub(crate) fn empty() -> Self {
Self::new([0, 0, 0, 0])
}
/// Calculate the intersection of this bounding box with another.
#[inline]
pub(crate) fn intersect(&self, other: &Self) -> Self {
Self::new([
self.x0().max(other.x0()),
self.y0().max(other.y0()),
self.x1().min(other.x1()),
self.y1().min(other.y1()),
])
}
}
impl Wide {
/// Create a new container for wide tiles.
pub fn new(width: u16, height: u16) -> Self {
let width_tiles = width.div_ceil(WideTile::WIDTH);
let height_tiles = height.div_ceil(Tile::HEIGHT);
let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles));
for h in 0..height_tiles {
for w in 0..width_tiles {
tiles.push(WideTile::new(w * WideTile::WIDTH, h * Tile::HEIGHT));
}
}
Self {
tiles,
width,
height,
layer_stack: vec![],
clip_stack: vec![],
}
}
/// Whether there are any existing layers that haven't been popped yet.
pub fn has_layers(&self) -> bool {
!self.layer_stack.is_empty()
}
/// Reset all tiles in the container.
pub fn reset(&mut self) {
for tile in &mut self.tiles {
tile.bg = PremulColor::from_alpha_color(TRANSPARENT);
tile.cmds.clear();
}
self.layer_stack.clear();
self.clip_stack.clear();
}
/// Return the number of horizontal tiles.
pub fn width_tiles(&self) -> u16 {
self.width.div_ceil(WideTile::WIDTH)
}
/// Return the number of vertical tiles.
pub fn height_tiles(&self) -> u16 {
self.height.div_ceil(Tile::HEIGHT)
}
/// Get the wide tile at a certain index.
///
/// Panics if the index is out-of-range.
pub fn get(&self, x: u16, y: u16) -> &WideTile {
assert!(
x < self.width_tiles() && y < self.height_tiles(),
"attempted to access out-of-bounds wide tile"
);
&self.tiles[usize::from(y) * usize::from(self.width_tiles()) + usize::from(x)]
}
/// Get mutable access to the wide tile at a certain index.
///
/// Panics if the index is out-of-range.
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile {
assert!(
x < self.width_tiles() && y < self.height_tiles(),
"attempted to access out-of-bounds wide tile"
);
let idx = usize::from(y) * usize::from(self.width_tiles()) + usize::from(x);
&mut self.tiles[idx]
}
/// Return a reference to all wide tiles.
pub fn tiles(&self) -> &[WideTile] {
self.tiles.as_slice()
}
/// Generate wide tile commands from the strip buffer.
///
/// This method processes a buffer of strips that represent a path, applies the fill rule,
/// and generates appropriate drawing commands for each affected wide tile.
///
/// # Algorithm overview:
/// 1. For each strip in the buffer:
/// - Calculate its position and width in pixels
/// - Determine which wide tiles the strip intersects
/// - Generate alpha fill commands for the intersected wide tiles
/// 2. For active fill regions (determined by fill rule):
/// - Generate solid fill commands for the regions between strips
pub fn generate(&mut self, strip_buf: &[Strip], fill_rule: Fill, paint: Paint) {
if strip_buf.is_empty() {
return;
}
// Get current clip bounding box or full viewport if no clip is active
let bbox = self.get_bbox();
for i in 0..strip_buf.len() - 1 {
let strip = &strip_buf[i];
debug_assert!(
strip.y < self.height,
"Strips below the viewport should have been culled prior to this stage."
);
// Don't render strips that are outside the viewport width
if strip.x >= self.width {
continue;
}
let next_strip = &strip_buf[i + 1];
let x0 = strip.x;
let strip_y = strip.strip_y();
// Skip strips outside the current clip bounding box
if strip_y < bbox.y0() {
continue;
}
if strip_y >= bbox.y1() {
// The rest of our strips must be outside the clip, so we can break early.
break;
}
// Calculate the width of the strip in columns
let mut col = strip.alpha_idx / u32::from(Tile::HEIGHT);
let next_col = next_strip.alpha_idx / u32::from(Tile::HEIGHT);
// Can potentially be 0 if strip only changes winding without covering pixels
let strip_width = next_col.saturating_sub(col) as u16;
let x1 = x0 + strip_width;
// Calculate which wide tiles this strip intersects
let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0());
// It's possible that a strip extends into a new wide tile, but we don't actually
// have as many wide tiles (e.g. because the pixmap width is only 512, but
// strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
let wtile_x1 = x1.div_ceil(WideTile::WIDTH).min(bbox.x1());
// Adjust column starting position if needed to respect clip boundaries
let mut x = x0;
let clip_x = bbox.x0() * WideTile::WIDTH;
if clip_x > x {
col += u32::from(clip_x - x);
x = clip_x;
}
// Generate alpha fill commands for each wide tile intersected by this strip
for wtile_x in wtile_x0..wtile_x1 {
let x_wtile_rel = x % WideTile::WIDTH;
// Restrict the width of the fill to the width of the wide tile
let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
let cmd = CmdAlphaFill {
x: x_wtile_rel,
width,
alpha_idx: (col * u32::from(Tile::HEIGHT)) as usize,
paint: paint.clone(),
blend_mode: None,
};
x += width;
col += u32::from(width);
self.get_mut(wtile_x, strip_y).strip(cmd);
}
// Determine if the region between this strip and the next should be filled
// based on the fill rule (NonZero or EvenOdd)
let active_fill = match fill_rule {
Fill::NonZero => next_strip.winding != 0,
Fill::EvenOdd => next_strip.winding % 2 != 0,
};
// If region should be filled and both strips are on the same row,
// generate fill commands for the region between them
if active_fill && strip_y == next_strip.strip_y() {
// Clamp the fill to the clip bounding box
x = x1.max(bbox.x0() * WideTile::WIDTH);
let x2 = next_strip
.x
.min(self.width.next_multiple_of(WideTile::WIDTH));
let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0());
let wfxt1 = x2.div_ceil(WideTile::WIDTH).min(bbox.x1());
// Generate fill commands for each wide tile in the fill region
for wtile_x in wfxt0..wfxt1 {
let x_wtile_rel = x % WideTile::WIDTH;
let width = x2.min((wtile_x + 1) * WideTile::WIDTH) - x;
x += width;
self.get_mut(wtile_x, strip_y)
.fill(x_wtile_rel, width, paint.clone());
}
}
}
}
/// Push a new layer with the given properties.
pub fn push_layer(
&mut self,
clip_path: Option<(impl Into<Box<[Strip]>>, Fill)>,
blend_mode: BlendMode,
mask: Option<Mask>,
opacity: f32,
) {
// Some explanations about what is going on here: We support the concept of
// layers, where a user can push a new layer (with certain properties), draw some
// stuff, and finally pop the layer, as part of which the layer as a whole will be
// blended into the previous layer.
// There are 3 "straightforward" properties that can be set for each layer:
// 1) The blend mode that should be used to blend the layer into the backdrop.
// 2) A mask that will be applied to the whole layer in the very end before blending.
// 3) An optional opacity that will be applied to the whole layer before blending (this
// could in theory be simulated with an alpha mask, but since it's a common operation and
// we only have a single opacity, this can easily be optimized.
//
// Finally, you can also add a clip path to the layer. However, clipping has its own
// more complicated logic for pushing/popping buffers where drawing is also suppressed
// in clipped-out wide tiles. Because of this, in case we have one of the above properties
// AND a clipping path, we will actually end up pushing two buffers, the first one handles
// the three properties and the second one is just for clip paths. That is a bit wasteful
// and I believe it should be possible to process them all in just one go, but for now
// this is good enough, and it allows us to implement blending without too deep changes to
// the original clipping implementation.
let layer = Layer {
clip: clip_path.is_some(),
blend_mode,
opacity,
mask,
};
let needs_buf = layer.needs_buf();
// In case we do blending, masking or opacity, push one buffer per wide tile.
if needs_buf {
for x in 0..self.width_tiles() {
for y in 0..self.height_tiles() {
self.get_mut(x, y).push_buf();
}
}
}
// If we have a clip path, push another buffer in the affected wide tiles.
// Note that it is important that we FIRST push the buffer for blending etc. and
// only then for clipping, otherwise we will use the empty clip buffer as the backdrop
// for blending!
if let Some((clip, fill)) = clip_path {
self.push_clip(clip, fill);
}
self.layer_stack.push(layer);
}
/// Pop a previously pushed layer.
pub fn pop_layer(&mut self) {
// This method basically unwinds everything we did in `push_layer`.
let layer = self.layer_stack.pop().unwrap();
if layer.clip {
self.pop_clip();
}
let needs_buf = layer.needs_buf();
if needs_buf {
for x in 0..self.width_tiles() {
for y in 0..self.height_tiles() {
let t = self.get_mut(x, y);
if let Some(mask) = layer.mask.clone() {
t.mask(mask);
}
t.opacity(layer.opacity);
t.blend(layer.blend_mode);
t.pop_buf();
}
}
}
}
/// Adds a clipping region defined by the provided strips.
///
/// This method takes a vector of strips representing a clip path, calculates the
/// intersection with the current clip region, and updates the clip stack.
///
/// # Algorithm overview:
/// 1. Calculate bounding box of the clip path
/// 2. Intersect with current clip bounding box
/// 3. For each tile in the intersected bounding box:
/// - If covered by zero winding: `push_zero_clip`
/// - If fully covered by non-zero winding: do nothing (clip is a no-op)
/// - If partially covered: `push_clip`
pub fn push_clip(&mut self, strips: impl Into<Box<[Strip]>>, fill_rule: Fill) {
let strips = strips.into();
let n_strips = strips.len();
// Calculate the bounding box of the clip path in strip coordinates
let path_bbox = if n_strips <= 1 {
Bbox::empty()
} else {
// Calculate the y range from first to last strip in wide tile coordinates
let wtile_y0 = strips[0].strip_y();
let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1;
// Calculate the x range by examining all strips in wide tile coordinates
let mut wtile_x0 = strips[0].x / WideTile::WIDTH;
let mut wtile_x1 = wtile_x0;
for i in 0..n_strips.saturating_sub(1) {
let strip = &strips[i];
let next_strip = &strips[i + 1];
let width =
((next_strip.alpha_idx - strip.alpha_idx) / u32::from(Tile::HEIGHT)) as u16;
let x = strip.x;
wtile_x0 = wtile_x0.min(x / WideTile::WIDTH);
wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH));
}
Bbox::new([wtile_x0, wtile_y0, wtile_x1, wtile_y1])
};
let parent_bbox = self.get_bbox();
// Calculate the intersection of the parent clip bounding box and the path bounding box.
let clip_bbox = parent_bbox.intersect(&path_bbox);
let mut cur_wtile_x = clip_bbox.x0();
let mut cur_wtile_y = clip_bbox.y0();
// Process strips to determine the clipping state for each wide tile
for i in 0..n_strips.saturating_sub(1) {
let strip = &strips[i];
let strip_y = strip.strip_y();
// Skip strips before current wide tile row
if strip_y < cur_wtile_y {
continue;
}
// Process wide tiles in rows before this strip's row
// These wide tiles are all zero-winding (outside the path)
while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
for wtile_x in cur_wtile_x..clip_bbox.x1() {
self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
}
// Reset x to the left edge of the clip bounding box
cur_wtile_x = clip_bbox.x0();
// Move to the next row
cur_wtile_y += 1;
}
// If we've reached the bottom of the clip bounding box, stop processing
if cur_wtile_y == clip_bbox.y1() {
break;
}
// Process wide tiles to the left of this strip in the same row
let x = strip.x;
let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1());
if cur_wtile_x < wtile_x_clamped {
// If winding is zero or doesn't match fill rule, these wide tiles are outside the path
let is_inside = match fill_rule {
Fill::NonZero => strip.winding != 0,
Fill::EvenOdd => strip.winding % 2 != 0,
};
if !is_inside {
for wtile_x in cur_wtile_x..wtile_x_clamped {
self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
}
}
// If winding is nonzero, then wide tiles covered entirely
// by sparse fill are no-op (no clipping is applied).
cur_wtile_x = wtile_x_clamped;
}
// Process wide tiles covered by the strip - these need actual clipping
let next_strip = &strips[i + 1];
let width = ((next_strip.alpha_idx - strip.alpha_idx) / u32::from(Tile::HEIGHT)) as u16;
let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
if cur_wtile_x < wtile_x1 {
for wtile_x in cur_wtile_x..wtile_x1 {
self.get_mut(wtile_x, cur_wtile_y).push_clip();
}
cur_wtile_x = wtile_x1;
}
}
// Process any remaining wide tiles in the bounding box (all zero-winding)
while cur_wtile_y < clip_bbox.y1() {
for wtile_x in cur_wtile_x..clip_bbox.x1() {
self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
}
cur_wtile_x = clip_bbox.x0();
cur_wtile_y += 1;
}
self.clip_stack.push(Clip {
clip_bbox,
strips,
fill_rule,
});
}
/// Get the bounding box of the current clip region or the entire viewport if no clip regions are active.
fn get_bbox(&self) -> Bbox {
if let Some(top) = self.clip_stack.last() {
top.clip_bbox.clone()
} else {
// Convert pixel dimensions to wide tile coordinates
Bbox::new([0, 0, self.width_tiles(), self.height_tiles()])
}
}
/// Removes the most recently added clip region.
///
/// This is the inverse operation of `push_clip`, carefully undoing all the clipping
/// operations while also handling any rendering needed for the clip region itself.
///
/// # Algorithm overview:
/// 1. Retrieve the top clip from the stack
/// 2. For each wide tile in the clip's bounding box:
/// - If covered by zero winding: `pop_zero_clip`
/// - If fully covered by non-zero winding: do nothing (was no-op)
/// - If partially covered: render the clip and `pop_clip`
///
/// This operation must be symmetric with `push_clip` to maintain a balanced clip stack.
fn pop_clip(&mut self) {
let Clip {
clip_bbox,
strips,
fill_rule,
} = self.clip_stack.pop().unwrap();
let n_strips = strips.len();
let mut cur_wtile_x = clip_bbox.x0();
let mut cur_wtile_y = clip_bbox.y0();
let mut pop_pending = false;
// Process each strip to determine the clipping state for each tile
for i in 0..n_strips.saturating_sub(1) {
let strip = &strips[i];
let strip_y = strip.strip_y();
// Skip strips before current tile row
if strip_y < cur_wtile_y {
continue;
}
// Process tiles in rows before this strip's row
// These tiles all had zero-winding clips
while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
// Handle any pending clip pop from previous iteration
if core::mem::take(&mut pop_pending) {
self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
cur_wtile_x += 1;
}
// Pop zero clips for all remaining tiles in this row
for wtile_x in cur_wtile_x..clip_bbox.x1() {
self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
}
cur_wtile_x = clip_bbox.x0();
cur_wtile_y += 1;
}
// If we've reached the bottom of the clip bounding box, stop processing
if cur_wtile_y == clip_bbox.y1() {
break;
}
// Process tiles to the left of this strip in the same row
let x0 = strip.x;
let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1());
if cur_wtile_x < wtile_x_clamped {
// Handle any pending clip pop from previous iteration
if core::mem::take(&mut pop_pending) {
self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
cur_wtile_x += 1;
}
// Pop zero clips for tiles that had zero winding or didn't match fill rule
// TODO: The winding check is probably not needed; if there was a fill,
// the logic below should have advanced wtile_x.
let is_inside = match fill_rule {
Fill::NonZero => strip.winding != 0,
Fill::EvenOdd => strip.winding % 2 != 0,
};
if !is_inside {
for wtile_x in cur_wtile_x..wtile_x_clamped {
self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
}
}
cur_wtile_x = wtile_x_clamped;
}
// Process tiles covered by the strip - render clip content and pop
let next_strip = &strips[i + 1];
let strip_width =
((next_strip.alpha_idx - strip.alpha_idx) / u32::from(Tile::HEIGHT)) as u16;
let x1 = x0 + strip_width;
let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0());
let wtile_x1 = x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
// Calculate starting position and column for alpha mask
let mut x = x0;
let mut col = strip.alpha_idx / u32::from(Tile::HEIGHT);
let clip_x = clip_bbox.x0() * WideTile::WIDTH;
if clip_x > x {
col += u32::from(clip_x - x);
x = clip_x;
}
// Render clip strips for each affected tile and mark for popping
for wtile_x in wtile_x0..wtile_x1 {
// If we've moved past tile_x and have a pending pop, do it now
if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) {
self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
}
// Calculate the portion of the strip that affects this tile
let x_rel = u32::from(x % WideTile::WIDTH);
let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
// Create clip strip command for rendering the partial coverage
let cmd = CmdClipAlphaFill {
x: x_rel,
width: u32::from(width),
alpha_idx: col as usize * Tile::HEIGHT as usize,
};
x += width;
col += u32::from(width);
// Apply the clip strip command and update state
self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd);
cur_wtile_x = wtile_x;
// Only request a pop if the x coordinate is actually inside the bounds.
if cur_wtile_x < clip_bbox.x1() {
pop_pending = true;
}
}
// Handle fill regions between strips based on fill rule
let is_inside = match fill_rule {
Fill::NonZero => next_strip.winding != 0,
Fill::EvenOdd => next_strip.winding % 2 != 0,
};
if is_inside && strip_y == next_strip.strip_y() {
if cur_wtile_x >= clip_bbox.x1() {
continue;
}
let x2 = next_strip.x;
let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH);
let width = clipped_x2.saturating_sub(x1);
// If there's a gap, fill it
if width > 0 {
let x_rel = u32::from(x1 % WideTile::WIDTH);
self.get_mut(cur_wtile_x, cur_wtile_y)
.clip_fill(x_rel, u32::from(width));
}
// If the next strip is a sentinel, skip the fill
// It's a sentinel in the row if there is non-zero winding for the sparse fill
// Look more into this in the strip.rs render function
if x2 == u16::MAX {
continue;
}
// If fill extends to next tile, pop current and handle next
if x2 > (cur_wtile_x + 1) * WideTile::WIDTH {
self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
pop_pending = false;
let width2 = x2 % WideTile::WIDTH;
cur_wtile_x = x2 / WideTile::WIDTH;
// If the strip is outside the clipping box, we don't need to do any
// filling, so we continue (also to prevent out-of-bounds access).
if cur_wtile_x >= clip_bbox.x1() {
continue;
}
if width2 > 0 {
// An important thing to note: Note that we are only applying
// `clip_fill` to the wide tile that is actually covered by the next
// strip, and not the ones in-between! For example, if the first strip
// is in wide tile 1 and the second in wide tile 4, we will do a clip
// fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is
// that any tile in-between is fully covered and thus no clipping is
// necessary at all. See also the `push_clip` function, where we don't
// push a new buffer for such tiles.
self.get_mut(cur_wtile_x, cur_wtile_y)
.clip_fill(0, u32::from(width2));
}
}
}
}
// Handle any pending clip pop from the last iteration
if core::mem::take(&mut pop_pending) {
self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
cur_wtile_x += 1;
}
// Process any remaining tiles in the bounding box (all zero-winding)
while cur_wtile_y < clip_bbox.y1() {
for wtile_x in cur_wtile_x..clip_bbox.x1() {
self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
}
cur_wtile_x = clip_bbox.x0();
cur_wtile_y += 1;
}
}
}
/// A wide tile.
#[derive(Debug)]
pub struct WideTile {
/// The x coordinate of the wide tile.
pub x: u16,
/// The y coordinate of the wide tile.
pub y: u16,
/// The background of the tile.
pub bg: PremulColor,
/// The draw commands of the tile.
pub cmds: Vec<Cmd>,
/// The number of zero-winding clips.
pub n_zero_clip: usize,
/// The number of non-zero-winding clips.
pub n_clip: usize,
/// The number of pushed buffers.
pub n_bufs: usize,
}
impl WideTile {
/// The width of a wide tile in pixels.
pub const WIDTH: u16 = 256;
/// Create a new wide tile.
pub fn new(x: u16, y: u16) -> Self {
Self {
x,
y,
bg: PremulColor::from_alpha_color(TRANSPARENT),
cmds: vec![],
n_zero_clip: 0,
n_clip: 0,
n_bufs: 0,
}
}
pub(crate) fn fill(&mut self, x: u16, width: u16, paint: Paint) {
if !self.is_zero_clip() {
let bg = if let Paint::Solid(s) = &paint {
// Note that we could be more aggressive in optimizing a whole-tile opaque fill
// even with a clip stack. It would be valid to elide all drawing commands from
// the enclosing clip push up to the fill. Further, we could extend the clip
// push command to include a background color, rather than always starting with
// a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
// be replaced with strip/fill with the color (the latter is true even with a
// non-opaque color).
//
// However, the extra cost of tracking such optimizations may outweigh the
// benefit, especially in hybrid mode with GPU painting.
let can_override = x == 0
&& width == Self::WIDTH
&& s.is_opaque()
&& self.n_clip == 0
&& self.n_bufs == 0;
can_override.then_some(*s)
} else {
// TODO: Implement for indexed paints.
None
};
if let Some(bg) = bg {
self.cmds.clear();
self.bg = bg;
} else {
self.cmds.push(Cmd::Fill(CmdFill {
x,
width,
paint,
blend_mode: None,
}));
}
}
}
pub(crate) fn strip(&mut self, cmd_strip: CmdAlphaFill) {
if !self.is_zero_clip() {
self.cmds.push(Cmd::AlphaFill(cmd_strip));
}
}
/// Adds a new clip region to the current wide tile.
pub fn push_clip(&mut self) {
if !self.is_zero_clip() {
self.push_buf();
self.n_clip += 1;
}
}
/// Removes the most recently added clip region from the current wide tile.
pub fn pop_clip(&mut self) {
if !self.is_zero_clip() {
self.pop_buf();
self.n_clip -= 1;
}
}
/// Adds a zero-winding clip region to the stack.
pub fn push_zero_clip(&mut self) {
self.n_zero_clip += 1;
}
/// Removes the most recently added zero-winding clip region.
pub fn pop_zero_clip(&mut self) {
self.n_zero_clip -= 1;
}
/// Checks if the current clip region is a zero-winding clip.
pub fn is_zero_clip(&mut self) -> bool {
self.n_zero_clip > 0
}
/// Applies a clip strip operation with the given parameters.
pub fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) {
if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
self.cmds.push(Cmd::ClipStrip(cmd_clip_strip));
}
}
/// Applies a clip fill operation at the specified position and width.
pub fn clip_fill(&mut self, x: u32, width: u32) {
if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width }));
}
}
/// Push a buffer.
pub fn push_buf(&mut self) {
self.cmds.push(Cmd::PushBuf);
self.n_bufs += 1;
}
/// Pop the most recent buffer.
pub fn pop_buf(&mut self) {
if matches!(self.cmds.last(), Some(&Cmd::PushBuf)) {
// Optimization: If no drawing happened between the last `PushBuf`,
// we can just pop it instead.
self.cmds.pop();
} else {
if self.cmds.len() >= 3 {
// If we have a non-destructive blend mode with just a single fill/strip,
// inline the blend mode instead.
let (_, tail) = self.cmds.split_at(self.cmds.len() - 3);
let updated = match tail {
[Cmd::PushBuf, Cmd::AlphaFill(a), Cmd::Blend(b)] => {
if !b.is_destructive() && a.blend_mode.is_none() {
let mut blended = a.clone();
blended.blend_mode = Some(*b);
Some(Cmd::AlphaFill(blended))
} else {
None
}
}
[Cmd::PushBuf, Cmd::Fill(a), Cmd::Blend(b)] => {
if !b.is_destructive() && a.blend_mode.is_none() {
let mut blended = a.clone();
blended.blend_mode = Some(*b);
Some(Cmd::Fill(blended))
} else {
None
}
}
_ => None,
};
if let Some(updated) = updated {
self.cmds.pop();
self.cmds.pop();
self.cmds.pop();
self.cmds.push(updated);
return;
}
}
self.cmds.push(Cmd::PopBuf);
}
self.n_bufs -= 1;
}
/// Apply an opacity to the whole buffer.
pub fn opacity(&mut self, opacity: f32) {
if opacity != 1.0 {
self.cmds.push(Cmd::Opacity(opacity));
}
}
/// Apply a mask to the whole buffer.
pub fn mask(&mut self, mask: Mask) {
self.cmds.push(Cmd::Mask(mask));
}
/// Blend the current buffer into the previous buffer in the stack.
pub fn blend(&mut self, blend_mode: BlendMode) {
// Optimization: If no drawing happened since the last `PushBuf` and the blend mode
// is not destructive, we do not need to do any blending at all.
if !matches!(self.cmds.last(), Some(&Cmd::PushBuf)) || blend_mode.is_destructive() {
self.cmds.push(Cmd::Blend(blend_mode));
}
}
}
/// A drawing command.
#[derive(Debug, PartialEq)]
pub enum Cmd {
/// A fill command.
Fill(CmdFill),
/// A fill command with alpha mask.
AlphaFill(CmdAlphaFill),
/// Pushes a new buffer for drawing.
PushBuf,
/// Pops the most recent buffer.
PopBuf,
/// A fill command within a clipping region.
///
/// This command will blend the contents of the current buffer within the clip fill region
/// into the previous buffer in the stack.
ClipFill(CmdClipFill),
/// A fill command with alpha mask within a clipping region.
///
/// This command will blend the contents of the current buffer within the clip fill region
/// into the previous buffer in the stack, with an additional alpha mask.
ClipStrip(CmdClipAlphaFill),
/// Apply a blend.
///
/// This command will blend the contents of the current buffer into the previous buffer in
/// the stack.
Blend(BlendMode),
/// Apply an opacity mask to the current buffer.
Opacity(f32),
/// Apply a mask to the current buffer.
Mask(Mask),
}
/// Fill a consecutive region of a wide tile.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdFill {
/// The horizontal start position of the command in pixels.
pub x: u16,
/// The width of the command in pixels.
pub width: u16,
/// The paint that should be used to fill the area.
pub paint: Paint,
/// The blend mode to apply before drawing the contents.
pub blend_mode: Option<BlendMode>,
}
/// Fill a consecutive region of a wide tile with an alpha mask.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdAlphaFill {
/// The horizontal start position of the command in pixels.
pub x: u16,
/// The width of the command in pixels.
pub width: u16,
/// The start index into the alpha buffer of the command.
pub alpha_idx: usize,
/// The paint that should be used to fill the area.
pub paint: Paint,
/// A blend mode to apply before drawing the contents.
pub blend_mode: Option<BlendMode>,
}
/// Same as fill, but copies top of clip stack to next on stack.
#[derive(Debug, PartialEq, Eq)]
pub struct CmdClipFill {
/// The horizontal start position of the command in pixels.
pub x: u32,
/// The width of the command in pixels.
pub width: u32,
}
/// Same as strip, but composites top of clip stack to next on stack.
#[derive(Debug, PartialEq, Eq)]
pub struct CmdClipAlphaFill {
/// The horizontal start position of the command in pixels.
pub x: u32,
/// The width of the command in pixels.
pub width: u32,
/// The start index into the alpha buffer of the command.
pub alpha_idx: usize,
}
trait BlendModeExt {
/// Whether a blend mode might cause destructive changes in the backdrop.
/// This disallows certain optimizations (like for example inlining a blend mode
/// or only applying a blend mode to the current clipping area).
fn is_destructive(&self) -> bool;
}
impl BlendModeExt for BlendMode {
fn is_destructive(&self) -> bool {
matches!(
self.compose,
Compose::Clear
| Compose::Copy
| Compose::SrcIn
| Compose::DestIn
| Compose::SrcOut
| Compose::DestAtop
)
}
}
#[cfg(test)]
mod tests {
use crate::coarse::{Cmd, CmdFill, Wide, WideTile};
use crate::color::AlphaColor;
use crate::color::palette::css::TRANSPARENT;
use crate::paint::{Paint, PremulColor};
use crate::peniko::{BlendMode, Compose, Fill, Mix};
use crate::strip::Strip;
use alloc::{boxed::Box, vec};
#[test]
fn optimize_empty_layers() {
let mut wide = WideTile::new(0, 0);
wide.push_buf();
wide.pop_buf();
assert!(wide.cmds.is_empty());
}
#[test]
fn basic_layer() {
let mut wide = WideTile::new(0, 0);
wide.push_buf();
wide.fill(
0,
10,
Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
);
wide.fill(
10,
10,
Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
);
wide.pop_buf();
assert_eq!(wide.cmds.len(), 4);
}
#[test]
fn inline_blend_with_one_fill() {
let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
30, 30, 30, 255,
)));
let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
let mut wide = WideTile::new(0, 0);
wide.push_buf();
wide.fill(0, 10, paint.clone());
wide.blend(blend_mode);
wide.pop_buf();
assert_eq!(wide.cmds.len(), 1);
let expected = Cmd::Fill(CmdFill {
x: 0,
width: 10,
paint,
blend_mode: Some(blend_mode),
});
assert_eq!(wide.cmds[0], expected);
}
#[test]
fn dont_inline_blend_with_two_fills() {
let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
30, 30, 30, 255,
)));
let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
let mut wide = WideTile::new(0, 0);
wide.push_buf();
wide.fill(0, 10, paint.clone());
wide.fill(10, 10, paint.clone());
wide.blend(blend_mode);
wide.pop_buf();
assert_eq!(wide.cmds.len(), 5);
}
#[test]
fn dont_inline_destructive_blend() {
let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
30, 30, 30, 255,
)));
let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear);
let mut wide = WideTile::new(0, 0);
wide.push_buf();
wide.fill(0, 10, paint.clone());
wide.blend(blend_mode);
wide.pop_buf();
assert_eq!(wide.cmds.len(), 4);
}
#[test]
fn tile_coordinates() {
let wide = Wide::new(1000, 258);
let tile_1 = wide.get(1, 3);
assert_eq!(tile_1.x, 256);
assert_eq!(tile_1.y, 12);
let tile_2 = wide.get(2, 15);
assert_eq!(tile_2.x, 512);
assert_eq!(tile_2.y, 60);
}
#[test]
fn reset_clears_layer_and_clip_stacks() {
type ClipPath = Option<(Box<[Strip]>, Fill)>;
let mut wide = Wide::new(1000, 258);
let no_clip_path: ClipPath = None;
wide.push_layer(no_clip_path, BlendMode::default(), None, 0.5);
assert_eq!(wide.layer_stack.len(), 1);
assert_eq!(wide.clip_stack.len(), 0);
let strip = Strip {
x: 2,
y: 2,
alpha_idx: 0,
winding: 1,
};
let clip_path = Some((vec![strip].into_boxed_slice(), Fill::NonZero));
wide.push_layer(clip_path, BlendMode::default(), None, 0.09);
assert_eq!(wide.layer_stack.len(), 2);
assert_eq!(wide.clip_stack.len(), 1);
wide.reset();
assert_eq!(wide.layer_stack.len(), 0);
assert_eq!(wide.clip_stack.len(), 0);
}
}