blob: 038dbdab53ab64062f8985766621752a7c72041c [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Basic render operations.
use crate::RenderMode;
use crate::dispatch::Dispatcher;
#[cfg(feature = "multithreading")]
use crate::dispatch::multi_threaded::MultiThreadedDispatcher;
use crate::dispatch::single_threaded::SingleThreadedDispatcher;
use crate::kurbo::{PathEl, Point};
use alloc::boxed::Box;
#[cfg(feature = "text")]
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use vello_common::blurred_rounded_rect::BlurredRoundedRectangle;
use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke};
use vello_common::mask::Mask;
#[cfg(feature = "text")]
use vello_common::paint::ImageSource;
use vello_common::paint::{Paint, PaintType};
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Compose, Fill, Mix};
use vello_common::pixmap::Pixmap;
use vello_common::recording::{PushLayerCommand, Recordable, Recording, RenderCommand};
use vello_common::strip::Strip;
use vello_common::strip_generator::StripGenerator;
#[cfg(feature = "text")]
use vello_common::{
color::{AlphaColor, Srgb},
colr::{ColrPainter, ColrRenderer},
glyph::{GlyphRenderer, GlyphRunBuilder, GlyphType, PreparedGlyph},
};
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1;
/// A render context.
#[derive(Debug)]
pub struct RenderContext {
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) paint: PaintType,
pub(crate) paint_transform: Affine,
pub(crate) stroke: Stroke,
pub(crate) transform: Affine,
pub(crate) fill_rule: Fill,
pub(crate) temp_path: BezPath,
// TODO: Consider taking a configurable threshold instead of just a boolean value here.
pub(crate) anti_alias: bool,
pub(crate) encoded_paints: Vec<EncodedPaint>,
#[cfg_attr(
not(feature = "text"),
allow(dead_code, reason = "used when the `text` feature is enabled")
)]
pub(crate) render_settings: RenderSettings,
dispatcher: Box<dyn Dispatcher>,
}
/// Settings to apply to the render context.
#[derive(Copy, Clone, Debug)]
pub struct RenderSettings {
/// The SIMD level that should be used for rendering operations.
pub level: Level,
/// The number of worker threads that should be used for rendering. Only has an effect
/// if the `multithreading` feature is active.
pub num_threads: u16,
/// Whether to prioritize speed or quality when rendering.
///
/// For most cases (especially for real-time rendering), it is highly recommended to set
/// this to `OptimizeSpeed`. If accuracy is a more significant concern (for example for visual
/// regression testing), then you can set this to `OptimizeQuality`.
///
/// Currently, the only difference this makes is that when choosing `OptimizeSpeed`, rasterization
/// will happen using u8/u16, while `OptimizeQuality` will use a f32-based pipeline.
pub render_mode: RenderMode,
}
impl Default for RenderSettings {
fn default() -> Self {
Self {
level: Level::new(),
#[cfg(feature = "multithreading")]
num_threads: std::thread::available_parallelism()
.unwrap()
.get()
.saturating_sub(1) as u16,
#[cfg(not(feature = "multithreading"))]
num_threads: 0,
render_mode: RenderMode::OptimizeSpeed,
}
}
}
impl RenderContext {
/// Create a new render context with the given width and height in pixels.
pub fn new(width: u16, height: u16) -> Self {
Self::new_with(width, height, RenderSettings::default())
}
/// Create a new render context with specific settings.
pub fn new_with(width: u16, height: u16, settings: RenderSettings) -> Self {
#[cfg(feature = "multithreading")]
let dispatcher: Box<dyn Dispatcher> = if settings.num_threads == 0 {
Box::new(SingleThreadedDispatcher::new(width, height, settings.level))
} else {
Box::new(MultiThreadedDispatcher::new(
width,
height,
settings.num_threads,
settings.level,
))
};
#[cfg(not(feature = "multithreading"))]
let dispatcher: Box<dyn Dispatcher> =
{ Box::new(SingleThreadedDispatcher::new(width, height, settings.level)) };
let transform = Affine::IDENTITY;
let fill_rule = Fill::NonZero;
let paint = BLACK.into();
let paint_transform = Affine::IDENTITY;
let stroke = Stroke {
width: 1.0,
join: Join::Bevel,
start_cap: Cap::Butt,
end_cap: Cap::Butt,
..Default::default()
};
let encoded_paints = vec![];
let temp_path = BezPath::new();
let anti_alias = true;
Self {
width,
height,
dispatcher,
transform,
anti_alias,
paint,
render_settings: settings,
paint_transform,
fill_rule,
stroke,
temp_path,
encoded_paints,
}
}
fn encode_current_paint(&mut self) -> Paint {
match self.paint.clone() {
PaintType::Solid(s) => s.into(),
PaintType::Gradient(g) => {
// TODO: Add caching?
g.encode_into(
&mut self.encoded_paints,
self.transform * self.paint_transform,
)
}
PaintType::Image(i) => i.encode_into(
&mut self.encoded_paints,
self.transform * self.paint_transform,
),
}
}
/// Fill a path.
pub fn fill_path(&mut self, path: &BezPath) {
let paint = self.encode_current_paint();
self.dispatcher
.fill_path(path, self.fill_rule, self.transform, paint, self.anti_alias);
}
/// Stroke a path.
pub fn stroke_path(&mut self, path: &BezPath) {
let paint = self.encode_current_paint();
self.dispatcher
.stroke_path(path, &self.stroke, self.transform, paint, self.anti_alias);
}
/// Fill a rectangle.
pub fn fill_rect(&mut self, rect: &Rect) {
// Don't use `rect.to_path` here, because it will perform a new allocation, which
// profiling showed can become a bottleneck for many small rectangles.
// TODO: Generalize this so that for example `blurred_rectangle` and other places
// can also profit from this.
self.temp_path.truncate(0);
self.temp_path
.push(PathEl::MoveTo(Point::new(rect.x0, rect.y0)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x1, rect.y0)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x1, rect.y1)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x0, rect.y1)));
self.temp_path.push(PathEl::ClosePath);
let paint = self.encode_current_paint();
self.dispatcher.fill_path(
&self.temp_path,
self.fill_rule,
self.transform,
paint,
self.anti_alias,
);
}
/// Fill a blurred rectangle with the given radius and standard deviation.
///
/// Note that this only works properly if the current paint is set to a solid color.
/// If not, it will fall back to using black as the fill color.
pub fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
let color = match self.paint {
PaintType::Solid(s) => s,
// Fallback to black when attempting to blur a rectangle with an image/gradient paint
_ => BLACK,
};
let blurred_rect = BlurredRoundedRectangle {
rect: *rect,
color,
radius,
std_dev,
};
// The actual rectangle we paint needs to be larger so that the blurring effect
// is not cut off.
// The impulse response of a gaussian filter is infinite.
// For performance reason we cut off the filter at some extent where the response is close to zero.
let kernel_size = 2.5 * std_dev;
let inflated_rect = rect.inflate(f64::from(kernel_size), f64::from(kernel_size));
let transform = self.transform * self.paint_transform;
let paint = blurred_rect.encode_into(&mut self.encoded_paints, transform);
self.dispatcher.fill_path(
&inflated_rect.to_path(0.1),
Fill::NonZero,
self.transform,
paint,
self.anti_alias,
);
}
/// Stroke a rectangle.
pub fn stroke_rect(&mut self, rect: &Rect) {
self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE));
}
/// Creates a builder for drawing a run of glyphs that have the same attributes.
#[cfg(feature = "text")]
pub fn glyph_run(&mut self, font: &crate::peniko::Font) -> GlyphRunBuilder<'_, Self> {
GlyphRunBuilder::new(font.clone(), self.transform, self)
}
/// Push a new layer with the given properties.
///
/// Note that the mask, if provided, needs to have the same size as the render context. Otherwise,
/// it will be ignored. In addition to that, the mask will not be affected by the current
/// transformation matrix in place.
pub fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
blend_mode: Option<BlendMode>,
opacity: Option<f32>,
mask: Option<Mask>,
) {
let mask = mask.and_then(|m| {
if m.width() != self.width || m.height() != self.height {
None
} else {
Some(m)
}
});
let blend_mode = blend_mode.unwrap_or(BlendMode::new(Mix::Normal, Compose::SrcOver));
let opacity = opacity.unwrap_or(1.0);
self.dispatcher.push_layer(
clip_path,
self.fill_rule,
self.transform,
blend_mode,
opacity,
self.anti_alias,
mask,
);
}
/// Push a new clip layer.
pub fn push_clip_layer(&mut self, path: &BezPath) {
self.push_layer(Some(path), None, None, None);
}
/// Push a new blend layer.
pub fn push_blend_layer(&mut self, blend_mode: BlendMode) {
self.push_layer(None, Some(blend_mode), None, None);
}
/// Push a new opacity layer.
pub fn push_opacity_layer(&mut self, opacity: f32) {
self.push_layer(None, None, Some(opacity), None);
}
/// Set whether to enable anti-aliasing.
pub fn set_anti_aliasing(&mut self, value: bool) {
self.anti_alias = value;
}
/// Push a new mask layer.
///
/// Note that the mask, if provided, needs to have the same size as the render context. Otherwise,
/// it will be ignored. In addition to that, the mask will not be affected by the current
/// transformation matrix in place.
pub fn push_mask_layer(&mut self, mask: Mask) {
self.push_layer(None, None, None, Some(mask));
}
/// Pop the last-pushed layer.
pub fn pop_layer(&mut self) {
self.dispatcher.pop_layer();
}
/// Set the current stroke.
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = stroke;
}
/// Get the current stroke
pub fn stroke(&self) -> &Stroke {
&self.stroke
}
/// Set the current paint.
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.paint = paint.into();
}
/// Get the current paint.
pub fn paint(&self) -> &PaintType {
&self.paint
}
/// Set the current paint transform.
///
/// The paint transform is applied to the paint after the transform of the geometry the paint
/// is drawn in, i.e., the paint transform is applied after the global transform. This allows
/// transforming the paint independently from the drawn geometry.
pub fn set_paint_transform(&mut self, paint_transform: Affine) {
self.paint_transform = paint_transform;
}
/// Get the current paint transform.
pub fn paint_transform(&self) -> &Affine {
&self.paint_transform
}
/// Reset the current paint transform.
pub fn reset_paint_transform(&mut self) {
self.paint_transform = Affine::IDENTITY;
}
/// Set the current fill rule.
pub fn set_fill_rule(&mut self, fill_rule: Fill) {
self.fill_rule = fill_rule;
}
/// Get the current fill rule.
pub fn fill_rule(&self) -> &Fill {
&self.fill_rule
}
/// Set the current transform.
pub fn set_transform(&mut self, transform: Affine) {
self.transform = transform;
}
/// Get the current transform.
pub fn transform(&self) -> &Affine {
&self.transform
}
/// Reset the current transform.
pub fn reset_transform(&mut self) {
self.transform = Affine::IDENTITY;
}
/// Reset the render context.
pub fn reset(&mut self) {
self.dispatcher.reset();
self.encoded_paints.clear();
self.reset_transform();
self.reset_paint_transform();
}
/// Flush any pending operations.
///
/// This is a no-op when using the single-threaded render mode, and can be ignored.
/// For multi-threaded rendering, you _have_ to call this before rasterizing, otherwise
/// the program will panic.
pub fn flush(&mut self) {
self.dispatcher.flush();
}
/// Render the current context into a buffer.
/// The buffer is expected to be in premultiplied RGBA8 format with length `width * height * 4`
pub fn render_to_buffer(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
render_mode: RenderMode,
) {
// TODO: Maybe we should move those checks into the dispatcher.
let wide = self.dispatcher.wide();
assert!(!wide.has_layers(), "some layers haven't been popped yet");
assert_eq!(
buffer.len(),
(width as usize) * (height as usize) * 4,
"provided width ({}) and height ({}) do not match buffer size ({})",
width,
height,
buffer.len(),
);
self.dispatcher
.rasterize(buffer, render_mode, width, height, &self.encoded_paints);
}
/// Render the current context into a pixmap.
pub fn render_to_pixmap(&self, pixmap: &mut Pixmap) {
let width = pixmap.width();
let height = pixmap.height();
self.render_to_buffer(
pixmap.data_as_u8_slice_mut(),
width,
height,
self.render_settings.render_mode,
);
}
/// Return the width of the pixmap.
pub fn width(&self) -> u16 {
self.width
}
/// Return the height of the pixmap.
pub fn height(&self) -> u16 {
self.height
}
/// Return the render settings used by the `RenderContext`.
pub fn render_settings(&self) -> &RenderSettings {
&self.render_settings
}
}
#[cfg(feature = "text")]
impl GlyphRenderer for RenderContext {
fn fill_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) {
match prepared_glyph.glyph_type {
GlyphType::Outline(glyph) => {
let paint = self.encode_current_paint();
self.dispatcher.fill_path(
glyph.path,
Fill::NonZero,
prepared_glyph.transform,
paint,
self.anti_alias,
);
}
GlyphType::Bitmap(glyph) => {
// We need to change the state of the render context
// to render the bitmap, but don't want to pollute the context,
// so simulate a `save` and `restore` operation.
let old_transform = self.transform;
let old_paint = self.paint.clone();
// If we scale down by a large factor, fall back to cubic scaling.
let quality = if prepared_glyph.transform.as_coeffs()[0] < 0.5
|| prepared_glyph.transform.as_coeffs()[3] < 0.5
{
crate::peniko::ImageQuality::High
} else {
crate::peniko::ImageQuality::Medium
};
let image = vello_common::paint::Image {
source: ImageSource::Pixmap(Arc::new(glyph.pixmap)),
x_extend: crate::peniko::Extend::Pad,
y_extend: crate::peniko::Extend::Pad,
quality,
};
self.set_paint(image);
self.set_transform(prepared_glyph.transform);
self.fill_rect(&glyph.area);
// Restore the state.
self.set_paint(old_paint);
self.transform = old_transform;
}
GlyphType::Colr(glyph) => {
// Same as for bitmap glyphs, save the state and restore it later on.
let old_transform = self.transform;
let old_paint = self.paint.clone();
let context_color = match old_paint {
PaintType::Solid(s) => s,
_ => BLACK,
};
let area = glyph.area;
let glyph_pixmap = {
let settings = RenderSettings {
level: self.render_settings.level,
render_mode: self.render_settings.render_mode,
num_threads: 0,
};
let mut ctx = Self::new_with(glyph.pix_width, glyph.pix_height, settings);
let mut pix = Pixmap::new(glyph.pix_width, glyph.pix_height);
let mut colr_painter = ColrPainter::new(glyph, context_color, &mut ctx);
colr_painter.paint();
// Technically not necessary since we always render single-threaded, but just
// to be safe.
ctx.flush();
ctx.render_to_pixmap(&mut pix);
pix
};
let image = vello_common::paint::Image {
source: ImageSource::Pixmap(Arc::new(glyph_pixmap)),
x_extend: crate::peniko::Extend::Pad,
y_extend: crate::peniko::Extend::Pad,
// Since the pixmap will already have the correct size, no need to
// use a different image quality here.
quality: crate::peniko::ImageQuality::Low,
};
self.set_paint(image);
self.set_transform(prepared_glyph.transform);
self.fill_rect(&area);
// Restore the state.
self.set_paint(old_paint);
self.transform = old_transform;
}
}
}
fn stroke_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) {
match prepared_glyph.glyph_type {
GlyphType::Outline(glyph) => {
let paint = self.encode_current_paint();
self.dispatcher.stroke_path(
glyph.path,
&self.stroke,
prepared_glyph.transform,
paint,
self.anti_alias,
);
}
GlyphType::Bitmap(_) | GlyphType::Colr(_) => {
// The definitions of COLR and bitmap glyphs can't meaningfully support being stroked.
// (COLR's imaging model only has fills)
self.fill_glyph(prepared_glyph);
}
}
}
}
#[cfg(feature = "text")]
impl ColrRenderer for RenderContext {
fn push_clip_layer(&mut self, clip: &BezPath) {
Self::push_clip_layer(self, clip);
}
fn push_blend_layer(&mut self, blend_mode: BlendMode) {
Self::push_blend_layer(self, blend_mode);
}
fn fill_solid(&mut self, color: AlphaColor<Srgb>) {
self.set_paint(color);
self.fill_rect(&Rect::new(
0.0,
0.0,
f64::from(self.width),
f64::from(self.height),
));
}
fn fill_gradient(&mut self, gradient: crate::peniko::Gradient) {
self.set_paint(gradient);
self.fill_rect(&Rect::new(
0.0,
0.0,
f64::from(self.width),
f64::from(self.height),
));
}
fn set_paint_transform(&mut self, affine: Affine) {
Self::set_paint_transform(self, affine);
}
fn pop_layer(&mut self) {
Self::pop_layer(self);
}
}
impl Recordable for RenderContext {
fn prepare_recording(&mut self, recording: &mut Recording) {
let buffers = recording.take_cached_strips();
let (strips, alphas, strip_start_indices) =
self.generate_strips_from_commands(recording.commands(), buffers);
recording.set_cached_strips(strips, alphas, strip_start_indices);
}
fn execute_recording(&mut self, recording: &Recording) {
let (cached_strips, cached_alphas) = recording.get_cached_strips();
let adjusted_strips = self.prepare_cached_strips(cached_strips, cached_alphas);
// Use pre-calculated strip start indices from when we generated the cache.
let strip_start_indices = recording.get_strip_start_indices();
let mut range_index = 0;
// Replay commands in order, using cached strips for geometry.
for command in recording.commands() {
match command {
RenderCommand::FillPath(_)
| RenderCommand::StrokePath(_)
| RenderCommand::FillRect(_)
| RenderCommand::StrokeRect(_) => {
self.process_geometry_command(
command,
strip_start_indices,
range_index,
&adjusted_strips,
);
range_index += 1;
}
#[cfg(feature = "text")]
RenderCommand::FillOutlineGlyph(_) | RenderCommand::StrokeOutlineGlyph(_) => {
self.process_geometry_command(
command,
strip_start_indices,
range_index,
&adjusted_strips,
);
range_index += 1;
}
RenderCommand::SetPaint(paint) => {
self.set_paint(paint.clone());
}
RenderCommand::SetPaintTransform(transform) => {
self.set_paint_transform(*transform);
}
RenderCommand::ResetPaintTransform => {
self.reset_paint_transform();
}
RenderCommand::SetTransform(transform) => {
self.set_transform(*transform);
}
RenderCommand::SetFillRule(fill_rule) => {
self.set_fill_rule(*fill_rule);
}
RenderCommand::SetStroke(stroke) => {
self.set_stroke(stroke.clone());
}
RenderCommand::PushLayer(PushLayerCommand {
clip_path,
blend_mode,
opacity,
mask,
}) => {
self.push_layer(clip_path.as_ref(), *blend_mode, *opacity, mask.clone());
}
RenderCommand::PopLayer => {
self.pop_layer();
}
}
}
}
}
/// Saved state for recording operations.
#[derive(Debug)]
struct RenderState {
transform: Affine,
fill_rule: Fill,
stroke: Stroke,
paint: PaintType,
paint_transform: Affine,
alphas: Vec<u8>,
}
/// Recording management implementation.
impl RenderContext {
/// Generate strips from strip commands and capture ranges.
///
/// Returns:
/// - `collected_strips`: The generated strips.
/// - `collected_alphas`: The generated alphas.
/// - `strip_start_indices`: The start indices of strips for each geometry command.
fn generate_strips_from_commands(
&mut self,
commands: &[RenderCommand],
buffers: (Vec<Strip>, Vec<u8>, Vec<usize>),
) -> (Vec<Strip>, Vec<u8>, Vec<usize>) {
let (mut collected_strips, mut cached_alphas, mut strip_start_indices) = buffers;
collected_strips.clear();
cached_alphas.clear();
strip_start_indices.clear();
let saved_state = self.take_current_state(cached_alphas);
let mut strip_generator =
StripGenerator::new(self.width, self.height, self.render_settings.level);
for command in commands {
let start_index = collected_strips.len();
match command {
RenderCommand::FillPath(path) => {
self.generate_fill_strips(
path,
&mut collected_strips,
self.transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
RenderCommand::StrokePath(path) => {
self.generate_stroke_strips(
path,
&mut collected_strips,
self.transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
RenderCommand::FillRect(rect) => {
let path = rect.to_path(DEFAULT_TOLERANCE);
self.generate_fill_strips(
&path,
&mut collected_strips,
self.transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
RenderCommand::StrokeRect(rect) => {
let path = rect.to_path(DEFAULT_TOLERANCE);
self.generate_stroke_strips(
&path,
&mut collected_strips,
self.transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
#[cfg(feature = "text")]
RenderCommand::FillOutlineGlyph((path, transform)) => {
let glyph_transform = self.transform * *transform;
self.generate_fill_strips(
path,
&mut collected_strips,
glyph_transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
#[cfg(feature = "text")]
RenderCommand::StrokeOutlineGlyph((path, transform)) => {
let glyph_transform = self.transform * *transform;
self.generate_stroke_strips(
path,
&mut collected_strips,
glyph_transform,
&mut strip_generator,
);
strip_start_indices.push(start_index);
}
RenderCommand::SetTransform(transform) => {
self.transform = *transform;
}
RenderCommand::SetFillRule(fill_rule) => {
self.fill_rule = *fill_rule;
}
RenderCommand::SetStroke(stroke) => {
self.stroke = stroke.clone();
}
_ => {}
}
}
let collected_alphas = strip_generator.take_alpha_buf();
self.restore_state(saved_state);
(collected_strips, collected_alphas, strip_start_indices)
}
}
/// Recording management implementation.
impl RenderContext {
fn process_geometry_command(
&mut self,
command: &RenderCommand,
strip_start_indices: &[usize],
range_index: usize,
adjusted_strips: &[Strip],
) {
assert!(
range_index < strip_start_indices.len(),
"Strip range index out of bounds"
);
let start = strip_start_indices[range_index];
let end = strip_start_indices
.get(range_index + 1)
.copied()
.unwrap_or(adjusted_strips.len());
let count = end - start;
assert!(
start < adjusted_strips.len() && count > 0,
"Invalid strip range"
);
let paint = self.encode_current_paint();
let fill_rule = match command {
RenderCommand::FillPath(_) | RenderCommand::FillRect(_) => self.fill_rule,
RenderCommand::StrokePath(_) | RenderCommand::StrokeRect(_) => Fill::NonZero,
_ => Fill::NonZero,
};
self.dispatcher
.wide_mut()
.generate(&adjusted_strips[start..end], fill_rule, paint, 0);
}
/// Prepare cached strips for rendering by adjusting indices.
fn prepare_cached_strips(
&mut self,
cached_strips: &[Strip],
cached_alphas: &[u8],
) -> Vec<Strip> {
// Calculate offset for alpha indices based on current dispatcher's alpha buffer size.
let alpha_offset = self.dispatcher.alpha_buf().len() as u32;
// Extend the dispatcher's alpha buffer with cached alphas.
self.dispatcher.extend_alpha_buf(cached_alphas);
// Create adjusted strips with corrected alpha indices.
cached_strips
.iter()
.map(move |strip| {
let mut adjusted_strip = *strip;
adjusted_strip.alpha_idx += alpha_offset;
adjusted_strip
})
.collect()
}
/// Generate strips for a filled path.
fn generate_fill_strips(
&mut self,
path: &BezPath,
strips: &mut Vec<Strip>,
transform: Affine,
strip_generator: &mut StripGenerator,
) {
strip_generator.generate_filled_path(
path,
self.fill_rule,
transform,
self.anti_alias,
|generated_strips| {
strips.extend_from_slice(generated_strips);
},
);
}
/// Generate strips for a stroked path.
fn generate_stroke_strips(
&mut self,
path: &BezPath,
strips: &mut Vec<Strip>,
transform: Affine,
strip_generator: &mut StripGenerator,
) {
strip_generator.generate_stroked_path(
path,
&self.stroke,
transform,
self.anti_alias,
|generated_strips| {
strips.extend_from_slice(generated_strips);
},
);
}
/// Save the current rendering state.
fn take_current_state(&mut self, alphas: Vec<u8>) -> RenderState {
RenderState {
paint: self.paint.clone(),
paint_transform: self.paint_transform,
transform: self.transform,
fill_rule: self.fill_rule,
stroke: core::mem::take(&mut self.stroke),
alphas: self.dispatcher.replace_alpha_buf(alphas),
}
}
/// Restore the saved rendering state.
fn restore_state(&mut self, state: RenderState) {
self.transform = state.transform;
self.fill_rule = state.fill_rule;
self.stroke = state.stroke;
self.paint = state.paint;
self.paint_transform = state.paint_transform;
self.dispatcher.set_alpha_buf(state.alphas);
}
}
#[cfg(test)]
mod tests {
use crate::RenderContext;
use vello_common::kurbo::{Rect, Shape};
use vello_common::tile::Tile;
#[test]
fn clip_overflow() {
let mut ctx = RenderContext::new(100, 100);
for _ in 0..(usize::from(u16::MAX) + 1).div_ceil(usize::from(Tile::HEIGHT * Tile::WIDTH)) {
ctx.fill_rect(&Rect::new(0.0, 0.0, 1.0, 1.0));
}
ctx.push_clip_layer(&Rect::new(20.0, 20.0, 180.0, 180.0).to_path(0.1));
ctx.pop_layer();
ctx.flush();
}
#[cfg(feature = "multithreading")]
#[test]
fn multithreaded_crash_after_reset() {
use crate::{Level, RenderMode, RenderSettings};
use vello_common::pixmap::Pixmap;
let mut pixmap = Pixmap::new(200, 200);
let settings = RenderSettings {
level: Level::new(),
num_threads: 1,
render_mode: RenderMode::OptimizeQuality,
};
let mut ctx = RenderContext::new_with(200, 200, settings);
ctx.reset();
ctx.fill_path(&Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1));
ctx.flush();
ctx.render_to_pixmap(&mut pixmap);
ctx.flush();
ctx.render_to_pixmap(&mut pixmap);
}
}