blob: 54bd1acf13971b9fce941b2601b672f6f17a9972 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Fine rasterization stage of the rendering pipeline.
//!
//! This module implements the fine rasterization phase, which processes tiles at the pixel level.
//! It supports both high-precision (f32) and low-precision (u8) rendering paths, along with
//! various paint types including solid colors, gradients, images, and blurred rounded rectangles.
mod common;
mod highp;
mod lowp;
use crate::fine::common::gradient::linear::SimdLinearKind;
use crate::fine::common::gradient::radial::SimdRadialKind;
use crate::fine::common::gradient::sweep::SimdSweepKind;
use crate::fine::common::gradient::{GradientPainter, calculate_t_vals};
use crate::fine::common::image::{FilteredImagePainter, NNImagePainter, PlainNNImagePainter};
use crate::fine::common::rounded_blurred_rect::BlurredRoundedRectFiller;
use crate::layer_manager::LayerManager;
use crate::peniko::{BlendMode, ImageQuality};
use crate::region::Region;
use crate::util::EncodedImageExt;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt::Debug;
use core::iter;
use vello_common::coarse::{Cmd, CommandAttrs, WideTile};
use vello_common::encode::{
EncodedBlurredRoundedRectangle, EncodedGradient, EncodedImage, EncodedKind, EncodedPaint,
};
use vello_common::fearless_simd::{
Bytes, Simd, SimdBase, SimdFloat, SimdInt, SimdInto, f32x4, f32x8, f32x16, u8x16, u8x32, u32x4,
u32x8,
};
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
use vello_common::mask::Mask;
use vello_common::paint::{ImageResolver, ImageSource, Paint, PremulColor, Tint};
use vello_common::pixmap::Pixmap;
use vello_common::simd::Splat4thExt;
use vello_common::tile::Tile;
use vello_common::util::f32_to_u8;
pub use highp::F32Kernel;
pub use lowp::U8Kernel;
/// Offset to shift from pixel corner to pixel center for sampling.
const PIXEL_CENTER_OFFSET: f64 = 0.5;
/// Number of color components per pixel (RGBA).
pub(crate) const COLOR_COMPONENTS: usize = 4;
/// Number of color components in a single column of a tile (height * components).
pub(crate) const TILE_HEIGHT_COMPONENTS: usize = Tile::HEIGHT as usize * COLOR_COMPONENTS;
/// Size of the scratch buffer used for intermediate rendering operations.
/// Sized to hold a full wide tile with all color components.
pub const SCRATCH_BUF_SIZE: usize =
WideTile::WIDTH as usize * Tile::HEIGHT as usize * COLOR_COMPONENTS;
/// Type alias for a scratch buffer that can hold a full wide tile's worth of data.
pub type ScratchBuf<F> = [F; SCRATCH_BUF_SIZE];
/// Trait for numeric types used in fine rasterization.
///
/// This trait abstracts over `f32` and `u8` to allow the same rendering logic
/// to work with both high-precision (floating-point) and low-precision (integer)
/// representations. This enables performance optimizations while maintaining accuracy
/// where needed.
pub trait Numeric: Copy + Default + Clone + Debug + PartialEq + Send + Sync + 'static {
/// The zero value for this numeric type (0.0 for f32, 0 for u8).
const ZERO: Self;
/// The maximum opacity value for this numeric type (1.0 for f32, 255 for u8).
const ONE: Self;
}
impl Numeric for f32 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
}
impl Numeric for u8 {
const ZERO: Self = 0;
const ONE: Self = 255;
}
/// Trait for SIMD vector types that can convert between f32 and u8 representations.
///
/// This trait enables efficient batch conversions between different numeric representations
/// during rendering operations, supporting both high-precision and low-precision rendering paths.
pub trait NumericVec<S: Simd>: Copy + Clone + Send + Sync {
/// Convert from a SIMD vector of f32 values to this type.
fn from_f32(simd: S, val: f32x16<S>) -> Self;
/// Convert from a SIMD vector of u8 values to this type.
fn from_u8(simd: S, val: u8x16<S>) -> Self;
}
impl<S: Simd> NumericVec<S> for f32x16<S> {
#[inline(always)]
fn from_f32(_: S, val: Self) -> Self {
val
}
#[inline(always)]
fn from_u8(simd: S, val: u8x16<S>) -> Self {
let converted = u8_to_f32(val);
converted * Self::splat(simd, 1.0 / 255.0)
}
}
impl<S: Simd> NumericVec<S> for u8x16<S> {
#[inline(always)]
fn from_f32(simd: S, val: f32x16<S>) -> Self {
let v1 = f32x16::splat(simd, 255.0);
let v2 = f32x16::splat(simd, 0.5);
let mulled = val.mul_add(v1, v2);
f32_to_u8(mulled)
}
#[inline(always)]
fn from_u8(_: S, val: Self) -> Self {
val
}
}
/// Convert a SIMD vector of u8 values to f32 values.
///
/// This function efficiently converts 16 u8 values to their f32 equivalents using SIMD operations,
/// preserving the values without normalization (i.e., 255 becomes 255.0, not 1.0).
#[inline(always)]
pub(crate) fn u8_to_f32<S: Simd>(val: u8x16<S>) -> f32x16<S> {
let simd = val.simd;
let zeroes = u8x16::splat(simd, 0);
let zip1 = simd.zip_high_u8x16(val, zeroes);
let zip2 = simd.zip_low_u8x16(val, zeroes);
let p1 = simd
.zip_low_u8x16(zip2, zeroes)
.bitcast::<u32x4<S>>()
.to_float::<f32x4<S>>();
let p2 = simd
.zip_high_u8x16(zip2, zeroes)
.bitcast::<u32x4<S>>()
.to_float::<f32x4<S>>();
let p3 = simd
.zip_low_u8x16(zip1, zeroes)
.bitcast::<u32x4<S>>()
.to_float::<f32x4<S>>();
let p4 = simd
.zip_high_u8x16(zip1, zeroes)
.bitcast::<u32x4<S>>()
.to_float::<f32x4<S>>();
simd.combine_f32x8(simd.combine_f32x4(p1, p2), simd.combine_f32x4(p3, p4))
}
/// Trait for SIMD vector types used in compositing and blending operations.
///
/// This trait abstracts over different SIMD vector widths (f32x16 for high-precision,
/// u8x32 for low-precision) to enable efficient batch processing of pixel data during
/// blending and compositing.
pub trait CompositeType<N: Numeric, S: Simd>: Copy + Clone + Send + Sync {
/// The number of numeric values this composite type can hold.
const LENGTH: usize;
/// Load values from a slice into this composite type.
fn from_slice(simd: S, slice: &[N]) -> Self;
/// Create a composite type by repeating a single RGBA color across all elements.
fn from_color(simd: S, color: [N; 4]) -> Self;
}
impl<S: Simd> CompositeType<f32, S> for f32x16<S> {
const LENGTH: usize = 16;
#[inline(always)]
fn from_slice(simd: S, slice: &[f32]) -> Self {
<Self as SimdBase<_>>::from_slice(simd, slice)
}
#[inline(always)]
fn from_color(simd: S, color: [f32; 4]) -> Self {
Self::block_splat(f32x4::from_slice(simd, &color[..]))
}
}
impl<S: Simd> CompositeType<u8, S> for u8x32<S> {
const LENGTH: usize = 32;
#[inline(always)]
fn from_slice(simd: S, slice: &[u8]) -> Self {
<Self as SimdBase<_>>::from_slice(simd, slice)
}
#[inline(always)]
fn from_color(simd: S, color: [u8; 4]) -> Self {
u32x8::block_splat(u32x4::splat(simd, u32::from_ne_bytes(color))).to_bytes()
}
}
/// A kernel for performing fine rasterization.
///
/// This trait defines the interface for tile-level rendering operations, abstracting over
/// different numeric precisions (f32 vs u8). Implementations provide the low-level pixel
/// manipulation, blending, and painting operations needed to render tiles.
///
/// The two main implementations are:
/// - [`F32Kernel`]: High-precision rendering using 32-bit floating-point values
/// - [`U8Kernel`]: Low-precision rendering using 8-bit integer values
pub trait FineKernel<S: Simd>: Send + Sync + 'static {
/// The basic underlying numerical type of the kernel (f32 or u8).
type Numeric: Numeric;
/// The SIMD composite type used for efficient batch blending and compositing operations.
type Composite: CompositeType<Self::Numeric, S>;
/// The SIMD vector type used for conversions between u8 and f32 representations.
type NumericVec: NumericVec<S>;
/// Extract and convert a premultiplied color to the kernel's numeric type.
///
/// Converts RGBA components from the standard premultiplied color format to
/// the kernel's internal representation (e.g., 0.0-1.0 for f32, 0-255 for u8).
fn extract_color(color: PremulColor) -> [Self::Numeric; 4];
/// Pack the blend buffer contents into the output region.
///
/// Converts from the internal scratch buffer format to the output tile format,
/// writing the results to the provided region.
fn pack(simd: S, region: &mut Region<'_>, blend_buf: &[Self::Numeric]);
/// Unpack the region contents back into the blend buffer.
///
/// Performs the reverse of `pack`, reading pixel data from the tile region
/// and loading it into the scratch buffer for further processing.
fn unpack(simd: S, region: &mut Region<'_>, blend_buf: &mut [Self::Numeric]);
/// Apply a filter to a layer.
///
/// This is used for applying filters to whole layers, which is necessary for
/// spatial filters (like blur) that need to access neighboring pixels. The filter
/// is applied in-place to the provided pixmap.
///
/// The transform parameter is used to scale filter parameters based on the current
/// transformation matrix (e.g., zoom level), ensuring filters look consistent
/// regardless of scale.
fn filter_layer(
pixmap: &mut Pixmap,
filter: &Filter,
layer_manager: &mut LayerManager,
transform: Affine,
);
/// Fill the target buffer with a solid color.
///
/// Efficiently replicates the given RGBA color across all pixels in the target buffer.
fn copy_solid(simd: S, target: &mut [Self::Numeric], color: [Self::Numeric; 4]);
/// Create a painter for rendering gradients.
///
/// Returns a painter that can render linear, radial, or sweep gradients based on
/// pre-computed t values (gradient interpolation parameters).
fn gradient_painter<'a>(
simd: S,
gradient: &'a EncodedGradient,
t_vals: &'a [f32],
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| GradientPainter::new(simd, gradient, t_vals),
)
}
/// Create a painter for rendering gradients with undefined region support.
///
/// Similar to `gradient_painter`, but with support for masking undefined locations
/// (used for radial gradients that may have mathematically undefined regions).
///
/// This is intentionally a duplicate of the default [`FineKernel::gradient_painter`]
/// implementation--the `U8Kernel` overrides that method, but not this one.
fn gradient_painter_with_undefined<'a>(
simd: S,
gradient: &'a EncodedGradient,
t_vals: &'a [f32],
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| GradientPainter::new(simd, gradient, t_vals),
)
}
/// Create a painter for rendering axis-aligned nearest-neighbor images.
///
/// Optimized painter for images with `Low` quality and no skewing component in their
/// transform. This is the fastest image rendering path.
fn plain_nn_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| PlainNNImagePainter::new(simd, image, pixmap, start_x, start_y),
)
}
/// Create a painter for rendering nearest-neighbor images with transforms.
///
/// Similar to `plain_nn_image_painter`, but supports arbitrary affine transforms
/// including skewing and rotation.
fn nn_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| NNImagePainter::new(simd, image, pixmap, start_x, start_y),
)
}
/// Create a painter for rendering images with `Medium` quality filtering.
///
/// Uses bilinear filtering for smoother appearance than nearest-neighbor.
fn medium_quality_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| FilteredImagePainter::<S, 1>::new(simd, image, pixmap, start_x, start_y),
)
}
/// Create a painter for rendering axis-aligned images with `Medium` quality filtering.
///
/// Optimized painter for images with bilinear filtering and no skewing component.
fn plain_medium_quality_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| FilteredImagePainter::<S, 1>::new(simd, image, pixmap, start_x, start_y),
)
}
/// Create a painter for rendering images with `High` quality filtering.
///
/// Uses high-quality filtering for the best visual appearance.
fn high_quality_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| FilteredImagePainter::<S, 2>::new(simd, image, pixmap, start_x, start_y),
)
}
/// Create a painter for rendering blurred rounded rectangles.
///
/// Efficiently renders rounded rectangles with gaussian blur applied,
/// computing the blur analytically rather than as a post-process.
fn blurred_rounded_rectangle_painter(
simd: S,
rect: &EncodedBlurredRoundedRectangle,
start_x: f64,
start_y: f64,
) -> impl Painter {
simd.vectorize(
#[inline(always)]
|| BlurredRoundedRectFiller::new(simd, rect, start_x, start_y),
)
}
/// Apply a mask to the destination buffer.
///
/// Multiplies each pixel in the destination by the corresponding mask value,
/// effectively masking out or reducing the opacity of pixels.
fn apply_mask(simd: S, dest: &mut [Self::Numeric], src: impl Iterator<Item = Self::NumericVec>);
/// Apply a painter to render content into the destination buffer.
///
/// Invokes the painter to generate pixel values and writes them to the destination.
fn apply_painter<'a>(simd: S, dest: &mut [Self::Numeric], painter: impl Painter + 'a);
/// Apply an image tint to an already-painted buffer.
///
/// This is called as a post-pass after `apply_painter`, only when a tint is
/// present. Keeping tint application out of the per-pixel iterator avoids
/// regressing the non-tinted fast path.
fn apply_tint(simd: S, dest: &mut [Self::Numeric], tint: &Tint);
/// Perform alpha compositing with a solid color over the target buffer.
///
/// Blends a solid RGBA color over the existing contents using standard alpha compositing
/// (Porter-Duff source-over). Optionally applies additional per-pixel alpha values.
fn alpha_composite_solid(
simd: S,
target: &mut [Self::Numeric],
src: [Self::Numeric; 4],
alphas: Option<&[u8]>,
);
/// Perform alpha compositing with a source buffer over the destination buffer.
///
/// Blends the source buffer contents over the destination using standard alpha compositing.
/// Optionally applies additional per-pixel alpha values.
fn alpha_composite_buffer(
simd: S,
dest: &mut [Self::Numeric],
src: &[Self::Numeric],
alphas: Option<&[u8]>,
);
/// Blend the source into the destination with a specified blend mode.
///
/// Applies advanced blending operations (e.g., multiply, screen, overlay) as specified
/// by the blend mode. Optionally applies additional per-pixel alpha values.
fn blend(
simd: S,
dest: &mut [Self::Numeric],
start_x: u16,
start_y: u16,
src: impl Iterator<Item = Self::Composite>,
blend_mode: BlendMode,
alphas: Option<&[u8]>,
mask: Option<&Mask>,
);
}
/// Fine rasterizer for processing tiles at the pixel level.
///
/// This structure maintains the state and scratch buffers needed for tile-based rendering.
/// It processes rendering commands and manages a stack of blend buffers for layer composition.
#[derive(Debug)]
pub struct Fine<S: Simd, T: FineKernel<S>> {
/// The (x, y) coordinates of the currently active wide tile being rendered.
pub(crate) wide_coords: (u16, u16),
/// Stack of blend buffers for managing layers and composition.
///
/// Each layer pushes a new buffer onto this stack, and layers are composited
/// by popping and blending with the buffer below.
pub(crate) blend_buf: Vec<ScratchBuf<T::Numeric>>,
/// Intermediate buffer used by painters to store generated pixel data before compositing.
pub(crate) paint_buf: ScratchBuf<T::Numeric>,
/// Buffer for storing gradient interpolation parameters (t values).
///
/// Gradients pre-compute these values for efficiency before color lookup.
pub(crate) f32_buf: Vec<f32>,
/// The SIMD context used for vectorized operations.
pub(crate) simd: S,
}
impl<S: Simd, T: FineKernel<S>> Fine<S, T> {
/// Create a new fine rasterizer with the given SIMD context.
///
/// Initializes all scratch buffers and sets up the initial blend buffer.
pub fn new(simd: S) -> Self {
Self {
simd,
wide_coords: (0, 0),
blend_buf: vec![[T::Numeric::ZERO; SCRATCH_BUF_SIZE]],
f32_buf: vec![0.0; SCRATCH_BUF_SIZE / 4],
paint_buf: [T::Numeric::ZERO; SCRATCH_BUF_SIZE],
}
}
/// Set the coordinates of the wide tile currently being rendered.
///
/// This is used by painters and other operations to compute absolute pixel positions.
pub fn set_coords(&mut self, x: u16, y: u16) {
self.wide_coords = (x, y);
}
/// Clear the current blend buffer to a solid color.
///
/// This efficiently fills the entire buffer with the given premultiplied color.
pub fn clear(&mut self, premul_color: PremulColor) {
let converted_color = T::extract_color(premul_color);
let blend_buf = self.blend_buf.last_mut().unwrap();
T::copy_solid(self.simd, blend_buf, converted_color);
}
/// Writes the current blend buffer contents to the output region.
///
/// This copies pixel data from the internal scratch buffer to the tile region,
/// converting the layout from the internal representation to the output format.
pub fn pack(&self, region: &mut Region<'_>) {
let blend_buf = self.blend_buf.last().unwrap();
T::pack(self.simd, region, blend_buf);
}
/// Reads the region contents back into the blend buffer.
///
/// This copies pixel data from the tile region to the internal scratch buffer,
/// performing the reverse operation of `pack`. This is typically used when a layer
/// needs to be read back for further processing.
pub fn unpack(&mut self, region: &mut Region<'_>) {
let blend_buf = self.blend_buf.last_mut().unwrap();
T::unpack(self.simd, region, blend_buf);
}
/// Apply a filter to a layer.
///
/// This applies the filter using the kernel's implementation, mutating the layer.
pub fn filter_layer(
&self,
pixmap: &mut Pixmap,
filter: &Filter,
layer_manager: &mut LayerManager,
transform: Affine,
) {
T::filter_layer(pixmap, filter, layer_manager, transform);
}
/// Execute a rendering command on the current tile.
///
/// This is the main dispatch method that processes different command types including
/// fills, clips, blends, filters, masks, and buffer operations.
pub(crate) fn run_cmd(
&mut self,
cmd: &Cmd,
alphas: &[u8],
paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
attrs: &CommandAttrs,
) {
match cmd {
Cmd::Fill(f) => {
let fill_attrs = &attrs.fill[f.attrs_idx as usize];
self.fill(
usize::from(f.x),
usize::from(f.width),
&fill_attrs.paint,
fill_attrs.blend_mode,
paints,
image_resolver,
None,
fill_attrs.mask.as_ref(),
);
}
Cmd::AlphaFill(s) => {
let fill_attrs = &attrs.fill[s.attrs_idx as usize];
let alpha_idx = fill_attrs.alpha_idx(s.alpha_offset) as usize;
self.fill(
usize::from(s.x),
usize::from(s.width),
&fill_attrs.paint,
fill_attrs.blend_mode,
paints,
image_resolver,
Some(&alphas[alpha_idx..]),
fill_attrs.mask.as_ref(),
);
}
Cmd::Filter(_filter, _) => {
// TODO: Apply non-spatial filters here; spatial filters need layer-level processing
//
// Spatial filters (e.g., Gaussian blur) need neighboring pixels and must be
// rendered to a pixmap for layer-level processing. Non-spatial effects (e.g.,
// color matrix, component transfer) can be processed here directly on the
// blend buffer per-pixel as wide commands.
}
Cmd::PushBuf(_layer_kind, _) => {
self.blend_buf.push([T::Numeric::ZERO; SCRATCH_BUF_SIZE]);
}
Cmd::PopBuf => {
self.blend_buf.pop();
}
Cmd::ClipFill(cf) => {
self.clip(cf.x as usize, cf.width as usize, None);
}
Cmd::ClipStrip(cs) => {
let clip_attrs = &attrs.clip[cs.attrs_idx as usize];
let alpha_idx = clip_attrs.alpha_idx(cs.alpha_offset) as usize;
self.clip(cs.x as usize, cs.width as usize, Some(&alphas[alpha_idx..]));
}
Cmd::Blend(b) => self.blend(*b),
Cmd::Mask(m) => {
let start_x = self.wide_coords.0 * WideTile::WIDTH;
let start_y = self.wide_coords.1 * Tile::HEIGHT;
let blend_buf = self.blend_buf.last_mut().unwrap();
let width = (blend_buf.len() / (Tile::HEIGHT as usize * COLOR_COMPONENTS)) as u16;
let y = start_y as u32 + u32x4::from_slice(self.simd, &[0, 1, 2, 3]);
let iter = (start_x..(start_x + width)).map(|x| {
let x_in_range = x < m.width();
macro_rules! sample {
($idx:expr) => {
if x_in_range && (y[$idx] as u16) < m.height() {
m.sample(x, y[$idx] as u16)
} else {
0
}
};
}
let s1 = sample!(0);
let s2 = sample!(1);
let s3 = sample!(2);
let s4 = sample!(3);
let samples = u8x16::from_slice(
self.simd,
&[
s1, s1, s1, s1, s2, s2, s2, s2, s3, s3, s3, s3, s4, s4, s4, s4,
],
);
T::NumericVec::from_u8(self.simd, samples)
});
T::apply_mask(self.simd, blend_buf, iter);
}
Cmd::Opacity(o) => {
if *o != 1.0 {
let blend_buf = self.blend_buf.last_mut().unwrap();
let opacity = self.simd.vectorize(
#[inline(always)]
|| T::NumericVec::from_f32(self.simd, f32x16::splat(self.simd, *o)),
);
T::apply_mask(self.simd, blend_buf, iter::repeat(opacity));
}
}
Cmd::PushZeroClip(_) | Cmd::PopZeroClip => {
// These commands are handled by the dispatcher and should not reach fine rasterization
unreachable!();
}
Cmd::BatchEnd => {}
}
}
/// Fill a horizontal strip within the current tile using the given paint.
///
/// This is the core painting method that handles solid colors, gradients, images,
/// and blurred rounded rectangles. It applies the paint starting at the given x
/// coordinate with the specified width, using the provided blend mode.
///
/// Note: For short strip segments, benchmarks showed that not inlining this method
/// leads to significantly worse performance.
pub fn fill(
&mut self,
x: usize,
width: usize,
fill: &Paint,
blend_mode: BlendMode,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
alphas: Option<&[u8]>,
mask: Option<&Mask>,
) {
let blend_buf = &mut self.blend_buf.last_mut().unwrap()[x * TILE_HEIGHT_COMPONENTS..]
[..TILE_HEIGHT_COMPONENTS * width];
let default_blend = blend_mode == BlendMode::default();
match fill {
Paint::Solid(color) => {
let color = T::extract_color(*color);
// If color is completely opaque, we can just directly override
// the blend buffer.
if color[3] == T::Numeric::ONE
&& default_blend
&& alphas.is_none()
&& mask.is_none()
{
T::copy_solid(self.simd, blend_buf, color);
return;
}
if default_blend && mask.is_none() {
T::alpha_composite_solid(self.simd, blend_buf, color, alphas);
} else {
let start_x = self.wide_coords.0 * WideTile::WIDTH + x as u16;
let start_y = self.wide_coords.1 * Tile::HEIGHT;
let src = self.simd.vectorize(
#[inline(always)]
|| T::Composite::from_color(self.simd, color),
);
T::blend(
self.simd,
blend_buf,
start_x,
start_y,
iter::repeat(src),
blend_mode,
alphas,
mask,
);
}
}
Paint::Indexed(paint) => {
let color_buf = &mut self.paint_buf[x * TILE_HEIGHT_COMPONENTS..]
[..TILE_HEIGHT_COMPONENTS * width];
let encoded_paint = &encoded_paints[paint.index()];
let start_x = self.wide_coords.0 * WideTile::WIDTH + x as u16;
let start_y = self.wide_coords.1 * Tile::HEIGHT;
// Make sure sampling happens at the center of the pixel.
let sampler_x = start_x as f64 + PIXEL_CENTER_OFFSET;
let sampler_y = start_y as f64 + PIXEL_CENTER_OFFSET;
// We need to have this as a macro because closures cannot take generic arguments, and
// we would have to repeatedly provide all arguments if we made it a function.
macro_rules! fill_complex_paint {
($may_have_transparency:expr, $filler:expr) => {
fill_complex_paint!($may_have_transparency, $filler, None::<&Tint>)
};
($may_have_transparency:expr, $filler:expr, $tint:expr) => {
if $may_have_transparency
|| alphas.is_some()
|| !default_blend
|| mask.is_some()
{
T::apply_painter(self.simd, color_buf, $filler);
if let Some(t) = $tint {
T::apply_tint(self.simd, color_buf, t);
}
if default_blend && mask.is_none() {
T::alpha_composite_buffer(self.simd, blend_buf, color_buf, alphas);
} else {
T::blend(
self.simd,
blend_buf,
start_x,
start_y,
color_buf
.chunks_exact(T::Composite::LENGTH)
.map(|s| T::Composite::from_slice(self.simd, s)),
blend_mode,
alphas,
mask,
);
}
} else {
// Similarly to solid colors we can just override the previous values
// if all colors in the gradient are fully opaque.
T::apply_painter(self.simd, blend_buf, $filler);
if let Some(t) = $tint {
T::apply_tint(self.simd, blend_buf, t);
}
}
};
}
match encoded_paint {
EncodedPaint::BlurredRoundedRect(b) => {
fill_complex_paint!(
true,
T::blurred_rounded_rectangle_painter(
self.simd, b, sampler_x, sampler_y
)
);
}
EncodedPaint::Gradient(g) => {
// Note that we are calculating the t values first, store them in a separate
// buffer and then pass that buffer to the iterator instead of calculating
// the t values on the fly in the iterator. The latter would be faster, but
// it would probably increase code size a lot, because the functions for
// position calculation need to be inlined for good performance.
let f32_buf = &mut self.f32_buf[..width * Tile::HEIGHT as usize];
match &g.kind {
EncodedKind::Linear(l) => {
calculate_t_vals(
self.simd,
SimdLinearKind::new(self.simd, *l),
f32_buf,
g,
sampler_x,
sampler_y,
);
fill_complex_paint!(
g.may_have_transparency,
T::gradient_painter(self.simd, g, f32_buf)
);
}
EncodedKind::Sweep(s) => {
calculate_t_vals(
self.simd,
SimdSweepKind::new(self.simd, s),
f32_buf,
g,
sampler_x,
sampler_y,
);
fill_complex_paint!(
g.may_have_transparency,
T::gradient_painter(self.simd, g, f32_buf)
);
}
EncodedKind::Radial(r) => {
calculate_t_vals(
self.simd,
SimdRadialKind::new(self.simd, r),
f32_buf,
g,
sampler_x,
sampler_y,
);
if r.has_undefined() {
fill_complex_paint!(
g.may_have_transparency,
T::gradient_painter_with_undefined(self.simd, g, f32_buf)
);
} else {
fill_complex_paint!(
g.may_have_transparency,
T::gradient_painter(self.simd, g, f32_buf)
);
}
}
}
}
EncodedPaint::Image(i) => {
let pixmap = match &i.source {
ImageSource::Pixmap(p) => p.clone(),
ImageSource::OpaqueId { id, .. } => image_resolver
.resolve(*id)
.unwrap_or_else(|| panic!("Image {:?} not found in registry", id)),
};
let tint = i.tint.as_ref();
match (i.has_skew(), i.nearest_neighbor()) {
(false, false) => {
// Axis-aligned with filtering - use optimized plain painters
if i.sampler.quality == ImageQuality::Medium {
fill_complex_paint!(
i.may_have_transparency,
T::plain_medium_quality_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
} else {
fill_complex_paint!(
i.may_have_transparency,
T::high_quality_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
}
}
(true, false) => {
// Skewed with filtering - use generic filtered painters
if i.sampler.quality == ImageQuality::Medium {
fill_complex_paint!(
i.may_have_transparency,
T::medium_quality_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
} else {
fill_complex_paint!(
i.may_have_transparency,
T::high_quality_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
}
}
(false, true) => {
fill_complex_paint!(
i.may_have_transparency,
T::plain_nn_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
}
(true, true) => {
fill_complex_paint!(
i.may_have_transparency,
T::nn_image_painter(
self.simd, i, &pixmap, sampler_x, sampler_y
),
tint
);
}
}
}
EncodedPaint::ExternalTexture(_) => {
unimplemented!("External textures are not supported by `vello_cpu`")
}
}
}
}
}
/// Blend the top blend buffer into the buffer below it.
///
/// This pops the top buffer from the blend stack and composites it onto the
/// buffer below using the specified blend mode. This is the core operation for
/// layer composition.
pub(crate) fn blend(&mut self, blend_mode: BlendMode) {
let (source_buffer, rest) = self.blend_buf.split_last_mut().unwrap();
let target_buffer = rest.last_mut().unwrap();
if blend_mode == BlendMode::default() {
T::alpha_composite_buffer(self.simd, target_buffer, source_buffer, None);
} else {
T::blend(
self.simd,
target_buffer,
// `start_x` and `start_y` are only needed to sample the correct position
// of a mask, so we can just pass dummy values here.
0,
0,
source_buffer
.chunks_exact(T::Composite::LENGTH)
.map(|s| T::Composite::from_slice(self.simd, s)),
blend_mode,
None,
None,
);
}
}
/// Apply a clipping mask from the top buffer to the buffer below.
///
/// Uses the top buffer's alpha channel as a mask, multiplying it with the buffer
/// below. This implements clipping by masking out pixels outside the clip region.
fn clip(&mut self, x: usize, width: usize, alphas: Option<&[u8]>) {
let (source_buffer, rest) = self.blend_buf.split_last_mut().unwrap();
let target_buffer = rest.last_mut().unwrap();
let source_buffer =
&mut source_buffer[x * TILE_HEIGHT_COMPONENTS..][..TILE_HEIGHT_COMPONENTS * width];
let target_buffer =
&mut target_buffer[x * TILE_HEIGHT_COMPONENTS..][..TILE_HEIGHT_COMPONENTS * width];
T::alpha_composite_buffer(self.simd, target_buffer, source_buffer, alphas);
}
}
/// A trait for objects that can render pixel data into buffers.
///
/// Painters abstract over different content sources (gradients, images, etc.) and can
/// generate pixel data in either u8 or f32 format. Implementations should provide at least
/// one of these methods; the other can delegate through conversion.
///
/// Note: Some painters may only efficiently support one numeric type. The implementation
/// may convert between types as needed.
pub trait Painter {
/// Paint pixel data into a u8 buffer (values in 0-255 range).
fn paint_u8(&mut self, buf: &mut [u8]);
/// Paint pixel data into an f32 buffer (values in 0.0-1.0 range).
fn paint_f32(&mut self, buf: &mut [f32]);
}
/// Extension trait for creating position vectors for gradient and image sampling.
///
/// This trait provides a method to generate SIMD vectors of positions that advance
/// correctly across a tile. It's used by painters to compute per-pixel coordinates
/// for sampling operations.
pub trait PosExt<S: Simd> {
/// Create a position vector that advances appropriately across a tile.
///
/// Given a starting position and per-pixel advances in x and y directions,
/// generates a SIMD vector with the correct position for each element.
fn splat_pos(simd: S, pos: f32, x_advance: f32, y_advance: f32) -> Self;
}
impl<S: Simd> PosExt<S> for f32x4<S> {
#[inline(always)]
fn splat_pos(simd: S, pos: f32, _: f32, y_advance: f32) -> Self {
let columns: [f32; Tile::HEIGHT as usize] = [0.0, 1.0, 2.0, 3.0];
let column_mask: Self = columns.simd_into(simd);
column_mask.mul_add(Self::splat(simd, y_advance), Self::splat(simd, pos))
}
}
impl<S: Simd> PosExt<S> for f32x8<S> {
#[inline(always)]
fn splat_pos(simd: S, pos: f32, x_advance: f32, y_advance: f32) -> Self {
simd.combine_f32x4(
f32x4::splat_pos(simd, pos, x_advance, y_advance),
f32x4::splat_pos(simd, pos + x_advance, x_advance, y_advance),
)
}
}
/// Intermediate shader result with color channels stored separately for efficient processing.
///
/// This structure holds 8 pixels worth of data in planar format (separate R, G, B, A vectors).
/// The planar layout is more efficient for certain SIMD operations before final interleaving.
pub(crate) struct ShaderResultF32<S: Simd> {
/// Red channel values for 8 pixels.
pub(crate) r: f32x8<S>,
/// Green channel values for 8 pixels.
pub(crate) g: f32x8<S>,
/// Blue channel values for 8 pixels.
pub(crate) b: f32x8<S>,
/// Alpha channel values for 8 pixels.
pub(crate) a: f32x8<S>,
}
impl<S: Simd> ShaderResultF32<S> {
/// Convert from planar format to interleaved RGBA format.
///
/// Returns two f32x16 vectors containing 8 pixels (4 RGBA components each)
/// with channels interleaved in the standard RGBA order.
#[inline(always)]
pub(crate) fn get(&self) -> (f32x16<S>, f32x16<S>) {
let (r_1, r_2) = self.r.simd.split_f32x8(self.r);
let (g_1, g_2) = self.g.simd.split_f32x8(self.g);
let (b_1, b_2) = self.b.simd.split_f32x8(self.b);
let (a_1, a_2) = self.a.simd.split_f32x8(self.a);
let first = self.r.simd.combine_f32x8(
self.r.simd.combine_f32x4(r_1, g_1),
self.r.simd.combine_f32x4(b_1, a_1),
);
let second = self.r.simd.combine_f32x8(
self.r.simd.combine_f32x4(r_2, g_2),
self.r.simd.combine_f32x4(b_2, a_2),
);
(first, second)
}
}
mod macros {
/// Implements the `Painter` trait for an iterator that produces f32x16 SIMD vectors.
///
/// This macro generates both `paint_u8` and `paint_f32` methods, converting between
/// formats as needed. Used for painters that work natively with high-precision f32 data.
macro_rules! f32x16_painter {
($($type_path:tt)+) => {
impl<S: Simd> crate::fine::Painter for $($type_path)+ {
fn paint_u8(&mut self, buf: &mut [u8]) {
use vello_common::fearless_simd::*;
use crate::fine::NumericVec;
self.simd.vectorize(#[inline(always)] || {
for chunk in buf.chunks_exact_mut(16) {
let next = self.next().unwrap();
let converted = u8x16::<S>::from_f32(next.simd, next);
converted.store_slice(chunk);
}
})
}
fn paint_f32(&mut self, buf: &mut [f32]) {
self.simd.vectorize(#[inline(always)] || {
for chunk in buf.chunks_exact_mut(16) {
let next = self.next().unwrap();
next.store_slice(chunk);
}
})
}
}
};
}
/// Implements the `Painter` trait for an iterator that produces u8x16 SIMD vectors.
///
/// This macro generates both `paint_u8` and `paint_f32` methods, converting between
/// formats as needed. Used for painters that work natively with low-precision u8 data.
macro_rules! u8x16_painter {
($($type_path:tt)+) => {
impl<S: Simd> crate::fine::Painter for $($type_path)+ {
fn paint_u8(&mut self, buf: &mut [u8]) {
self.simd.vectorize(#[inline(always)] || {
for chunk in buf.chunks_exact_mut(16) {
let next = self.next().unwrap();
next.store_slice(chunk);
}
})
}
fn paint_f32(&mut self, buf: &mut [f32]) {
use vello_common::fearless_simd::*;
use crate::fine::NumericVec;
self.simd.vectorize(#[inline(always)] || {
for chunk in buf.chunks_exact_mut(16) {
let next = self.next().unwrap();
let converted = f32x16::<S>::from_u8(next.simd, next);
converted.store_slice(chunk);
}
})
}
}
};
}
pub(crate) use f32x16_painter;
pub(crate) use u8x16_painter;
}