Files
pdfcraft/vendor/vello_cpu/src/dispatch/single_threaded.rs
T
KenM76andClaude Opus 5.5 05630b5f15 fix(render): a gray drawn with the Color blend mode is no longer drawn black
vello_cpu 0.0.8's clip_color, shared by the nonseparable blend modes,
divided 0 by 0 when a gray's equal components were rounded just below 0
or above 1 after set_lum. The NaN drew the pixel black at the source's
alpha whatever the backdrop: nine gray levels under /BM /Color in an
empty isolated group (0.96 g among them) rendered (0, 0, 0), and two
more at half opacity came out wrong.

Vendor vello_cpu 0.0.8 (Apache-2.0 OR MIT) with a backport of
linebender/vello#2000, which clips only when the denominator is
positive. No vello_cpu release carries the fix yet; drop the copy when
one does.

Tests in it::nonseparable_gray run every gray level. The two Color
ramps fail with the unpatched blend.rs. The Luminosity ramp (black over
each opaque gray, upstream's other case) passes either way here: pages
use the u8 pipeline, and that case only failed in the f32 one; it stays
as a guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-28 14:45:32 -04:00

923 lines
32 KiB
Rust

// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::RenderMode;
use crate::dispatch::Dispatcher;
use crate::fine::{Fine, FineKernel};
use crate::kurbo::{Affine, BezPath, Rect, Stroke};
use crate::layer_manager::LayerManager;
use crate::peniko::{BlendMode, Fill};
use crate::region::Regions;
use vello_common::clip::ClipContext;
use vello_common::coarse::{Cmd, LayerKind, MODE_CPU, Wide, WideTilesBbox};
use vello_common::color::palette::css::TRANSPARENT;
use vello_common::encode::EncodedPaint;
use vello_common::fearless_simd::{Level, Simd};
use vello_common::filter_effects::Filter;
use vello_common::mask::Mask;
use vello_common::paint::{ImageResolver, Paint, PremulColor};
use vello_common::pixmap::Pixmap;
use vello_common::render_graph::{RenderGraph, RenderNodeKind};
use vello_common::strip_generator::{StripGenerator, StripStorage};
/// Single-threaded implementation of the rendering dispatcher.
///
/// This dispatcher handles the entire rendering pipeline on a single thread,
/// including path rasterization, layer composition, and filter effects.
/// It maintains the coarse tile grid (`Wide`), strip generation for paths,
/// and the render graph for managing layer dependencies and filter effects.
#[derive(Debug)]
pub(crate) struct SingleThreadedDispatcher {
/// Coarse tile grid containing rendering commands for each wide tile.
wide: Wide,
/// Clip context for managing non-isolated clipping.
clip_context: ClipContext,
/// Generator for converting paths into coverage strips.
strip_generator: StripGenerator,
/// Storage for alpha coverage data from strip generation.
strip_storage: StripStorage,
/// SIMD level for fearless SIMD dispatch.
level: Level,
/// Counter for generating unique layer IDs.
layer_id_next: u32,
/// Dependency graph tracking layer relationships and filter effects.
render_graph: RenderGraph,
}
impl SingleThreadedDispatcher {
/// Creates a new single-threaded dispatcher for the given dimensions.
///
/// # Arguments
/// * `width` - Width of the rendering surface in pixels.
/// * `height` - Height of the rendering surface in pixels.
/// * `level` - SIMD level to use for rasterization.
///
/// # Notes
/// The root layer (`layer_id` 0) is created immediately and must be node 0
/// in the render graph for proper rendering order.
pub(crate) fn new(width: u16, height: u16, level: Level) -> Self {
let wide = Wide::<MODE_CPU>::new(width, height);
let strip_generator = StripGenerator::new(width, height, level);
let clip_context = ClipContext::new();
let strip_storage = StripStorage::default();
let mut render_graph = RenderGraph::new();
// Create root node (layer_id 0) as the first node (will be node 0).
// This ensures the root layer is always rendered last in the execution order.
let wtile_bbox = WideTilesBbox::new(0, 0, wide.width_tiles(), wide.height_tiles());
let root_node = render_graph.add_node(RenderNodeKind::RootLayer {
layer_id: 0,
wtile_bbox,
});
assert_eq!(root_node, 0, "Root node must be node 0");
Self {
wide,
clip_context,
strip_generator,
strip_storage,
level,
layer_id_next: 0,
render_graph,
}
}
/// Rasterizes the scene using f32 precision (high quality).
///
/// This dispatches to the appropriate SIMD implementation based on the
/// configured level, using f32 for intermediate calculations.
#[cfg(feature = "f32_pipeline")]
fn rasterize_f32(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::F32Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
/// Rasterizes the scene using u8 precision (fast).
///
/// This dispatches to the appropriate SIMD implementation based on the
/// configured level, using u8 for intermediate calculations to maximize speed.
#[cfg(feature = "u8_pipeline")]
fn rasterize_u8(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::U8Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
/// Core rasterization dispatcher that chooses between simple and filter-aware paths.
///
/// # Type Parameters
/// * `S` - SIMD implementation to use.
/// * `F` - Fine rasterization kernel (determines precision).
///
/// If the scene contains filter effects, uses the filter-aware path which maintains
/// intermediate layer buffers. Otherwise, uses the simpler direct rasterization path.
fn rasterize_with<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut layer_manager = LayerManager::new();
if self.has_filters() {
// Use filter-aware path that maintains layer buffers for filter effects.
self.rasterize_with_filters::<S, F>(
simd,
buffer,
width,
height,
encoded_paints,
image_resolver,
&mut layer_manager,
);
} else {
// Use simple direct rasterization for scenes without filters.
self.rasterize_simple::<S, F>(
simd,
buffer,
width,
height,
encoded_paints,
image_resolver,
);
}
}
/// Rasterizes a scene with filter effects using dependency-ordered execution.
///
/// This processes the render graph in topological order, ensuring that filtered
/// layers are rendered into intermediate buffers before being composed. Each
/// filter layer is rendered to its own pixmap, the filter is applied, and then
/// the result is stored in the layer manager for use by dependent layers.
///
/// # Render Graph Execution
/// - `FilterLayer` nodes: Render to intermediate buffer, apply filter, store result.
/// - `RootLayer` node: Final composition to output buffer.
fn rasterize_with_filters<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
layer_manager: &mut LayerManager,
) {
let mut fine = Fine::<S, F>::new(simd);
// Process nodes in dependency order (filtered layers before their consumers).
for node_id in self.render_graph.execution_order() {
let node = &self.render_graph.nodes[node_id];
match &node.kind {
RenderNodeKind::FilterLayer {
layer_id,
filter,
wtile_bbox,
transform,
} => {
// Allocate intermediate buffer for this filtered layer.
let bbox_width = wtile_bbox.width_px();
let bbox_height = wtile_bbox.height_px();
let mut pixmap = Pixmap::new(bbox_width, bbox_height);
// TODO: Re-use this allocation by adding a .configure() or similar method
// to avoid allocating the internal Vec<Region> on every filtered layer.
let mut regions =
Regions::new(bbox_width, bbox_height, pixmap.data_as_u8_slice_mut());
// Render each tile in the layer's bounding box.
regions.update_regions(|region| {
// Convert region-local coords to global wtile coords.
let x = wtile_bbox.x0() + region.x;
let y = wtile_bbox.y0() + region.y;
self.process_layer_tile(
&mut fine,
x,
y,
*layer_id,
PremulColor::from_alpha_color(TRANSPARENT),
layer_manager,
encoded_paints,
image_resolver,
);
debug_assert_eq!(
fine.blend_buf.len(),
1,
"blend buffer should contain exactly one layer after tile processing"
);
fine.pack(region);
});
// Apply the filter effect to the completed layer.
fine.filter_layer(&mut pixmap, filter, layer_manager, *transform);
// Save the filtered pixmap to disk for debugging.
// #[cfg(all(debug_assertions, feature = "std", feature = "png"))]
// save_filtered_layer_debug(&pixmap, *layer_id);
// Store the filtered result for use by dependent layers.
layer_manager.register_layer(*layer_id, *wtile_bbox, pixmap);
}
RenderNodeKind::RootLayer {
layer_id,
wtile_bbox: _,
} => {
// Final composition directly to output buffer.
let mut regions = Regions::new(width, height, buffer);
regions.update_regions(|region| {
// Use the background color from the wide tile.
let bg = self.wide.get(region.x, region.y).bg;
self.process_layer_tile(
&mut fine,
region.x,
region.y,
*layer_id,
bg,
layer_manager,
encoded_paints,
image_resolver,
);
debug_assert_eq!(
fine.blend_buf.len(),
1,
"blend buffer should contain exactly one layer after tile processing"
);
fine.pack(region);
});
}
}
}
}
/// Processes all rendering commands for a single layer within a specific tile.
///
/// This handles the complex logic of composing filtered layers by:
/// 1. Running normal rendering commands in sequence.
/// 2. When encountering a filtered layer reference, compositing its pre-rendered
/// content from the layer manager.
/// 3. Skipping the filtered layer's internal commands (already rendered separately).
///
/// # Arguments
/// * `fine` - The fine rasterizer instance.
/// * `x`, `y` - Wide tile coordinates.
/// * `layer_id` - The layer being processed.
/// * `clear_color` - Initial color for the tile.
/// * `layer_manager` - Storage for filtered layer buffers.
/// * `encoded_paints` - Paint definitions for the scene.
/// * `image_resolver` - Resolver for looking up opaque image IDs.
fn process_layer_tile<S: Simd, F: FineKernel<S>>(
&self,
fine: &mut Fine<S, F>,
x: u16,
y: u16,
layer_id: u32,
clear_color: PremulColor,
layer_manager: &mut LayerManager,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let wtile = &self.wide.get(x, y);
fine.set_coords(x, y);
fine.clear(clear_color);
// Process all commands in this layer's render range.
// It can happen that the layer has no associated ranges in this wide tile in
// case they have been cleared by setting a new wide tile background, for example
// when filling a full-tile opaque solid color.
let Some(ranges) = wtile.layer_cmd_ranges.get(&layer_id) else {
return;
};
let mut cmd_idx = ranges.render_range.start;
while cmd_idx < ranges.render_range.end {
let cmd: &Cmd = &wtile.cmds[cmd_idx];
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
// Special handling for filtered layer composition.
// Filtered layers have already been rendered and stored in layer_manager.
// Here we composite them into the current buffer, with special handling for clipping.
if let Cmd::PushBuf(LayerKind::Filtered(child_layer_id), _) = cmd {
// Unlike above, the unwrap is safe here because as long as the filtered layer
// is referenced in the wide tile, it must have associated layer ranges.
let filtered_ranges = wtile.layer_cmd_ranges.get(child_layer_id).unwrap();
// Check what comes after the filtered layer push to determine clipping state
match wtile.cmds.get(cmd_idx + 1) {
// Zero-clip region: tile is completely outside the clip path.
// The layer was already rendered for filtering, but we skip compositing
// since this tile is entirely clipped out.
// (PushZeroClip only appears for clipped filter layers)
// See https://github.com/linebender/vello/pull/1541/ for why we
// add the ID check.
Some(Cmd::PushZeroClip(id)) if *id == *child_layer_id => {
// If we have a zero-clip, it means that the whole layer should not be drawn.
// Therefore, we want to skip to the very end so that only `PopBuf` will
// be run. Therefore, we jump to `filtered_ranges.full_range.end - 1`.
cmd_idx = filtered_ranges.full_range.end - 1;
continue;
}
// Partial clip: push the clip buffer, then composite the filtered layer
Some(Cmd::PushBuf(LayerKind::Clip(id), _)) if *id == *child_layer_id => {
fine.run_cmd(
&wtile.cmds[cmd_idx + 1],
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
cmd_idx += 1;
if let Some(mut region) =
layer_manager.layer_tile_region_mut(*child_layer_id, x, y)
{
fine.unpack(&mut region);
}
}
// No clip or fully inside clip: composite the filtered layer directly
_ => {
if let Some(mut region) =
layer_manager.layer_tile_region_mut(*child_layer_id, x, y)
{
fine.unpack(&mut region);
}
}
}
// Skip past the filtered layer's internal commands, as they were already
// rendered when the FilterLayer node was processed earlier.
cmd_idx = filtered_ranges.render_range.end.max(cmd_idx + 1);
} else {
cmd_idx += 1;
}
}
}
/// Simple rasterization path for scenes without filter effects.
///
/// This directly processes each tile's commands without maintaining intermediate
/// layer buffers. All rendering happens in a single pass directly to the output buffer.
/// This is more efficient than the filter-aware path when no filters are present.
fn rasterize_simple<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut regions = Regions::new(width, height, buffer);
let mut fine = Fine::<S, F>::new(simd);
regions.update_regions(|region| {
let x = region.x;
let y = region.y;
let wtile = self.wide.get(x, y);
fine.set_coords(x, y);
// Clear to background and process all commands in order.
fine.clear(wtile.bg);
for cmd in &wtile.cmds {
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
}
fine.pack(region);
});
}
/// Returns true if the scene contains any filter effects.
fn has_filters(&self) -> bool {
self.render_graph.has_filters()
}
/// Composites at an offset using f32 precision (high quality).
#[cfg(feature = "f32_pipeline")]
fn composite_at_offset_f32(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::F32Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.composite_at_offset_with::<_, F32Kernel>(
simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver
));
}
/// Composites at an offset using u8 precision (fast).
#[cfg(feature = "u8_pipeline")]
fn composite_at_offset_u8(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::U8Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.composite_at_offset_with::<_, U8Kernel>(
simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver
));
}
/// Core implementation for compositing at an offset.
///
/// Composites tiles sequentially, writing directly to the destination buffer
/// at the specified offset.
fn composite_at_offset_with<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut regions = Regions::new_at_offset(
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
buffer,
);
let mut fine = Fine::<S, F>::new(simd);
regions.update_regions(|region| {
let x = region.x;
let y = region.y;
let wtile = self.wide.get(x, y);
fine.set_coords(x, y);
// Unpack existing pixel data from the region instead of clearing,
// so that rendering composites onto the existing pixmap contents.
fine.unpack(region);
for cmd in &wtile.cmds {
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
}
fine.pack(region);
});
}
}
impl Dispatcher for SingleThreadedDispatcher {
fn wide(&self) -> &Wide {
&self.wide
}
fn fill_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Convert path to coverage strips.
self.strip_generator.generate_filled_path(
path,
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn stroke_path(
&mut self,
path: &BezPath,
stroke: &Stroke,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Convert stroked path to coverage strips.
self.strip_generator.generate_stroked_path(
path,
stroke,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn fill_rect_fast(
&mut self,
rect: &Rect,
paint: Paint,
blend_mode: BlendMode,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Generate strips directly for the rectangle (bypasses path processing).
self.strip_generator.generate_filled_rect_fast(
rect,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
fill_rule: Fill,
transform: Affine,
blend_mode: BlendMode,
opacity: f32,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
// Allocate a new unique layer ID.
self.layer_id_next += 1;
// Generate clip coverage if a clip path is provided.
let clip = if let Some(c) = clip_path {
self.strip_generator.generate_filled_path(
c,
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
Some(self.strip_storage.strips.as_slice())
} else {
None
};
// Push the layer onto the coarse tile stack and update render graph.
self.wide.push_layer(
self.layer_id_next,
clip,
blend_mode,
mask,
opacity,
filter,
transform,
&mut self.render_graph,
0,
);
}
fn pop_layer(&mut self) {
// Pop the current layer and update render graph.
self.wide.pop_layer(&mut self.render_graph);
}
fn reset(&mut self) {
// Clear all rendering state to prepare for a new scene.
self.wide.reset();
self.clip_context.reset();
self.strip_generator.reset();
self.strip_storage.clear();
self.render_graph.clear();
self.layer_id_next = 0;
// Recreate root node as node 0 (required for proper execution order).
let root_node = self.render_graph.add_node(RenderNodeKind::RootLayer {
layer_id: 0,
wtile_bbox: WideTilesBbox::new(0, 0, self.wide.width_tiles(), self.wide.height_tiles()),
});
debug_assert_eq!(root_node, 0, "Root node must be node 0");
// Reset layer ID counter.
self.layer_id_next = 0;
}
fn flush(&mut self, _encoded_paints: &[EncodedPaint]) {
// No-op for single-threaded dispatcher (no work queue to flush).
}
fn rasterize(
&self,
buffer: &mut [u8],
render_mode: RenderMode,
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
// If only the u8 pipeline is enabled, then use it
#[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
{
let _ = render_mode;
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
// If only the f32 pipeline is enabled, then use it
#[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
{
let _ = render_mode;
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
// If both pipelines are enabled, select precision based on render mode parameter.
#[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
match render_mode {
RenderMode::OptimizeSpeed => {
// Use u8 precision for faster rendering.
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
RenderMode::OptimizeQuality => {
// Use f32 precision for higher quality.
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
}
#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
{
// This case never gets hit because there is a compile_error in the root.
// But have this code disables some warnings and makes the compile error easier to read
let _ = (
buffer,
render_mode,
width,
height,
encoded_paints,
image_resolver,
);
}
}
fn composite_at_offset(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
render_mode: RenderMode,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
#[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
{
let _ = render_mode;
self.composite_at_offset_u8(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
#[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
{
let _ = render_mode;
self.composite_at_offset_f32(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
#[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
match render_mode {
RenderMode::OptimizeSpeed => {
self.composite_at_offset_u8(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
RenderMode::OptimizeQuality => {
self.composite_at_offset_f32(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
}
#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
{
let _ = (
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
render_mode,
encoded_paints,
image_resolver,
);
}
}
fn push_clip_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
aliasing_threshold: Option<u8>,
) {
self.clip_context.push_clip(
path,
&mut self.strip_generator,
fill_rule,
transform,
aliasing_threshold,
);
}
fn pop_clip_path(&mut self) {
self.clip_context.pop_clip();
}
}
/// Saves a filtered pixmap to disk for debugging purposes.
/// Only available in debug builds with `std` and `png` features enabled.
#[allow(
dead_code,
reason = "useful debug utility, can be enabled by uncommenting the call site"
)]
#[cfg(all(debug_assertions, feature = "std", feature = "png"))]
fn save_filtered_layer_debug(pixmap: &Pixmap, layer_id: u32) {
use std::path::PathBuf;
let diffs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/diffs");
let _ = std::fs::create_dir_all(&diffs_path);
let filename = diffs_path.join(alloc::format!("filtered_layer_{}.png", layer_id));
if let Ok(png_data) = pixmap.clone().into_png() {
let _ = std::fs::write(&filename, &png_data);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kurbo::Rect;
use vello_common::color::palette::css::BLUE;
use vello_common::kurbo::Shape;
use vello_common::paint::PremulColor;
/// Verifies that `reset()` properly clears all internal buffers and state.
///
/// This is important to ensure that a dispatcher can be reused for multiple
/// rendering passes without accumulating stale data from previous frames.
#[test]
fn buffers_cleared_on_reset() {
let mut dispatcher = SingleThreadedDispatcher::new(100, 100, Level::new());
// Render a simple shape to populate internal buffers.
dispatcher.fill_path(
&Rect::new(0.0, 0.0, 50.0, 50.0).to_path(0.1),
Fill::NonZero,
Affine::IDENTITY,
Paint::Solid(PremulColor::from_alpha_color(BLUE)),
BlendMode::default(),
None,
None,
&[],
);
// Ensure there is data to clear.
assert!(!dispatcher.strip_storage.alphas.is_empty());
assert!(!dispatcher.wide.get(0, 0).cmds.is_empty());
dispatcher.reset();
// Verify all buffers are cleared.
assert!(dispatcher.strip_storage.alphas.is_empty());
assert!(dispatcher.wide.get(0, 0).cmds.is_empty());
assert_eq!(dispatcher.layer_id_next, 0);
}
}