mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 17:34:06 -06:00
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
923 lines
32 KiB
Rust
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);
|
|
}
|
|
}
|