blob: 488e46837f3dfec2caea63626c20e7bdc20b186d [file]
// 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 = "text")]
use crate::text::{GlyphAtlasResources, GlyphRunBuilder};
#[cfg(feature = "text")]
use glifo::GlyphPrepCache;
#[cfg(feature = "multithreading")]
use crate::dispatch::multi_threaded::MultiThreadedDispatcher;
use crate::dispatch::single_threaded::SingleThreadedDispatcher;
use crate::kurbo::{PathEl, Point};
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use hashbrown::HashMap;
use vello_common::blurred_rounded_rect::BlurredRoundedRectangle;
use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::{Affine, BezPath, Rect, Stroke};
use vello_common::mask::Mask;
use vello_common::paint::{ImageId, ImageResolver, Paint, PaintType, Tint};
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Fill};
use vello_common::pixmap::Pixmap;
use vello_common::recording::{
PushLayerCommand, Recordable, Recorder, Recording, RenderCommand, RenderState,
};
use vello_common::strip::Strip;
use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage};
use vello_common::util::is_axis_aligned;
#[cfg(feature = "text")]
pub(crate) const DEFAULT_GLYPH_ATLAS_SIZE: u16 = 4096;
// Why do we need this? The reason is that the way uploaded images work in Vello Hybrid
// is different from how they work in Vello CPU.
//
// In Vello Hybrid, all images, regardless of whether they are user-uploaded
// images or cached glyphs, are stored in an image atlas at a certain location. An image ID then
// uniquely resolves to an atlas page index + a location on that page. Whenever we want to
// cache a new glyph, we simply allocate a location in the image atlas and then return the image
// ID associated with that location.
//
// On Vello CPU, it works differently: An image ID is associated with a complete pixmap.
// If a user uploads an image, instead of blitting it into a bigger image atlas, we just
// store the user-provided pixmap and associate an image ID with the whole pixmap. However,
// for glyph caching to work we need the same semantics as in Vello Hybrid. Therefore, we
// use a marker to determine whether an image ID refers to a normal uploaded image or a cached
// glyph and apply special handling based on that.
//
// All IDs < than this value are reserved for normal images, all IDs >= this value are
// reserved for atlas pages.
pub(crate) const ATLAS_IMAGE_ID_BASE: u32 = u32::MAX / 2;
/// Persistent resources required by Vello CPU for rendering.
#[derive(Debug, Default)]
pub struct Resources {
pub(crate) image_registry: ImageRegistry,
#[cfg(feature = "text")]
pub(crate) glyph_prep_cache: GlyphPrepCache,
// Will be initialized lazily on first use.
#[cfg(feature = "text")]
pub(crate) glyph_resources: Option<GlyphAtlasResources>,
}
impl Resources {
/// Create a new set of renderer resources.
pub fn new() -> Self {
Self::default()
}
pub(crate) fn before_render(&mut self) {
#[cfg(feature = "text")]
self.prepare_glyph_cache();
}
pub(crate) fn after_render(&mut self) {
#[cfg(feature = "text")]
self.maintain_glyph_cache();
}
}
/// A render context for CPU-based 2D graphics rendering.
///
/// This is the main entry point for drawing operations. It maintains the current
/// rendering state (transforms, paint, stroke, etc.) and dispatches drawing commands
/// to the underlying rasterization engine.
#[derive(Debug)]
pub struct RenderContext {
/// Width of the render target in pixels.
pub(crate) width: u16,
/// Height of the render target in pixels.
pub(crate) height: u16,
/// The current rendering state.
pub(crate) state: RenderState,
/// The current mask in place.
pub(crate) mask: Option<Mask>,
/// Temporary path buffer to avoid repeated allocations.
pub(crate) temp_path: BezPath,
/// Optional threshold for aliasing.
pub(crate) aliasing_threshold: Option<u8>,
pub(crate) encoded_paints: Vec<EncodedPaint>,
pub(crate) filter: Option<Filter>,
#[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::try_detect().unwrap_or(Level::baseline()),
#[cfg(feature = "multithreading")]
num_threads: (std::thread::available_parallelism()
.unwrap()
.get()
.saturating_sub(1) as u16)
.min(8),
#[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 encoded_paints = vec![];
let temp_path = BezPath::new();
let aliasing_threshold = None;
Self {
width,
height,
dispatcher,
state: RenderState::default(),
aliasing_threshold,
render_settings: settings,
mask: None,
temp_path,
encoded_paints,
filter: None,
}
}
fn encode_current_paint(&mut self) -> Paint {
match self.state.paint.clone() {
PaintType::Solid(s) => s.into(),
PaintType::Gradient(g) => {
// TODO: Add caching?
g.encode_into(
&mut self.encoded_paints,
self.state.transform * self.state.paint_transform,
None,
)
}
PaintType::Image(i) => i.encode_into(
&mut self.encoded_paints,
self.state.transform * self.state.paint_transform,
self.state.tint,
),
}
}
/// Fill a path.
pub fn fill_path(&mut self, path: &BezPath) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
ctx.dispatcher.fill_path(
path,
ctx.state.fill_rule,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
/// Stroke a path.
pub fn stroke_path(&mut self, path: &BezPath) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
ctx.dispatcher.stroke_path(
path,
&ctx.state.stroke,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
/// Fill a rectangle.
pub fn fill_rect(&mut self, rect: &Rect) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
// Fast path: Use optimized rect filling if we have no skew in the path transform
// and anti-aliasing is enabled.
// TODO: Maybe also support no anti-aliasing in the fast path
if is_axis_aligned(&ctx.state.transform) && ctx.aliasing_threshold.is_none() {
// Transform the rect to screen coordinates.
let transformed_rect = ctx.state.transform.transform_rect_bbox(*rect);
ctx.dispatcher.fill_rect_fast(
&transformed_rect,
paint,
ctx.state.blend_mode,
ctx.mask.clone(),
&ctx.encoded_paints,
);
} else {
// Fall back to path-based rendering for rotated/skewed transforms.
ctx.rect_to_temp_path(rect);
ctx.dispatcher.fill_path(
&ctx.temp_path,
ctx.state.fill_rule,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
}
});
}
/// Stroke a rectangle.
pub fn stroke_rect(&mut self, rect: &Rect) {
self.with_optional_filter(|ctx| {
ctx.rect_to_temp_path(rect);
let paint = ctx.encode_current_paint();
ctx.dispatcher.stroke_path(
&ctx.temp_path,
&ctx.state.stroke,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
fn rect_to_temp_path(&mut self, rect: &Rect) {
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);
}
/// Fill a blurred rectangle with the given corner 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 rect = rect.abs();
let color = match self.state.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,
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.state.transform * self.state.paint_transform;
self.rect_to_temp_path(&inflated_rect);
let paint = blurred_rect.encode_into(&mut self.encoded_paints, transform, None);
self.dispatcher.fill_path(
&self.temp_path,
Fill::NonZero,
self.state.transform,
paint,
self.state.blend_mode,
self.aliasing_threshold,
self.mask.clone(),
&self.encoded_paints,
);
}
/// Creates a builder for drawing a run of glyphs that have the same attributes.
#[cfg(feature = "text")]
pub fn glyph_run<'a>(
&'a mut self,
resources: &'a mut Resources,
font: &crate::peniko::FontData,
) -> GlyphRunBuilder<'a> {
glifo::GlyphRunBuilder::new(
font.clone(),
self.state.transform,
crate::text::CpuGlyphRunBackend {
ctx: self,
resources,
atlas_cache_enabled: false,
},
)
}
/// 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>,
filter: Option<Filter>,
) {
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_default();
let opacity = opacity.unwrap_or(1.0);
self.dispatcher.push_layer(
clip_path,
self.state.fill_rule,
self.state.transform,
blend_mode,
opacity,
self.aliasing_threshold,
mask,
filter,
);
}
/// Push a new clip layer.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
/// example for how this method differs from `push_clip_path`.
pub fn push_clip_layer(&mut self, path: &BezPath) {
self.push_layer(Some(path), None, 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, None);
}
/// Push a new opacity layer.
pub fn push_opacity_layer(&mut self, opacity: f32) {
self.push_layer(None, None, Some(opacity), None, None);
}
/// Push a new mask layer. The mask needs to have the same dimensions as the
/// render context. The mask will not be affected by the current transform
/// in place.
///
/// See the explanation in the [masking](https://github.com/linebender/vello/tree/main/sparse_strips/masking/examples)
/// example for how this method differs from `set_mask`.
pub fn push_mask_layer(&mut self, mask: Mask) {
self.push_layer(None, None, None, Some(mask), None);
}
/// Push a filter layer that affects all subsequent drawing operations.
///
/// WARNING: Note that filters are currently incomplete and experimental. In
/// particular, they will lead to a panic when used in combination with
/// multi-threaded rendering.
pub fn push_filter_layer(&mut self, filter: Filter) {
self.push_layer(None, None, None, None, Some(filter));
}
/// Set the aliasing threshold.
///
/// If set to `None` (which is the recommended option in nearly all cases),
/// anti-aliasing will be applied.
///
/// If instead set to some value, then a pixel will be fully painted if
/// the coverage is bigger than the threshold (between 0 and 255), otherwise
/// it will not be painted at all.
///
/// Note that there is no performance benefit to disabling anti-aliasing and
/// this functionality is simply provided for compatibility.
pub fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>) {
self.aliasing_threshold = aliasing_threshold;
}
/// 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.state.stroke = stroke;
}
/// Get the current stroke.
pub fn stroke(&self) -> &Stroke {
&self.state.stroke
}
/// Get a mutable reference to the current stroke.
#[cfg(feature = "text")]
pub(crate) fn stroke_mut(&mut self) -> &mut Stroke {
&mut self.state.stroke
}
/// Set the current paint.
///
/// If the paint is an image with `ImageSource::OpaqueId`, it will be
/// resolved to the corresponding pixmap at rasterization time.
/// Make sure to register images with [`Resources::register_image`] first.
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.state.paint = paint.into();
}
/// Get the current paint.
pub fn paint(&self) -> &PaintType {
&self.state.paint
}
/// Set the tint for subsequent image paint operations.
pub fn set_tint(&mut self, tint: Option<Tint>) {
self.state.tint = tint;
}
/// Clear the tint, so subsequent image paints are drawn without tinting.
pub fn reset_tint(&mut self) {
self.state.tint = None;
}
/// Set the blend mode that should be used when drawing objects.
pub fn set_blend_mode(&mut self, blend_mode: BlendMode) {
self.state.blend_mode = blend_mode;
}
/// Get the currently active blend mode.
pub fn blend_mode(&self) -> BlendMode {
self.state.blend_mode
}
/// 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.state.paint_transform = paint_transform;
}
/// Get the current paint transform.
pub fn paint_transform(&self) -> &Affine {
&self.state.paint_transform
}
/// Reset the current paint transform.
pub fn reset_paint_transform(&mut self) {
self.state.paint_transform = Affine::IDENTITY;
}
/// Set the current fill rule.
pub fn set_fill_rule(&mut self, fill_rule: Fill) {
self.state.fill_rule = fill_rule;
}
/// Set the mask to use for path-painting operations. The mask needs to
/// have the same dimensions as the render context. The mask will not be
/// affected by the current transform in place.
///
/// See the explanation in the [masking](https://github.com/linebender/vello/tree/main/sparse_strips/masking/examples)
/// example for how this method differs from `push_mask_layer`.
pub fn set_mask(&mut self, mask: Mask) {
self.mask = Some(mask);
}
/// Reset the mask that is used for path-painting operations.
pub fn reset_mask(&mut self) {
self.mask = None;
}
/// Get the current fill rule.
pub fn fill_rule(&self) -> &Fill {
&self.state.fill_rule
}
/// Set the current transform.
pub fn set_transform(&mut self, transform: Affine) {
self.state.transform = transform;
}
/// Get the current transform.
pub fn transform(&self) -> &Affine {
&self.state.transform
}
/// Reset the current transform.
pub fn reset_transform(&mut self) {
self.state.transform = Affine::IDENTITY;
}
/// Apply filter to the current paint (affects next drawn elements).
///
/// This sets a filter that will be applied to the next drawn element.
/// To apply a filter to multiple elements, use `push_filter_layer` instead.
pub fn set_filter_effect(&mut self, filter: Filter) {
self.filter = Some(filter);
}
/// Reset the current filter effect.
pub fn reset_filter_effect(&mut self) {
self.filter = None;
}
/// Reset the render context.
pub fn reset(&mut self) {
self.dispatcher.reset();
self.encoded_paints.clear();
self.mask = None;
self.state.reset();
}
/// Push a new clip path to the clip stack.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
/// example for how this method differs from `push_clip_layer`.
pub fn push_clip_path(&mut self, path: &BezPath) {
self.dispatcher.push_clip_path(
path,
self.state.fill_rule,
self.state.transform,
self.aliasing_threshold,
);
}
/// Pop a clip path from the clip stack.
///
/// Note that unlike `push_clip_layer`, it is permissible to have pending
/// pushed clip paths before finishing the rendering operation.
pub fn pop_clip_path(&mut self) {
self.dispatcher.pop_clip_path();
}
/// 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(&self.encoded_paints);
}
/// 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,
resources: &mut Resources,
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(),
);
resources.before_render();
self.dispatcher.rasterize(
buffer,
render_mode,
width,
height,
&self.encoded_paints,
&resources.image_registry,
);
// TODO: We need to figure something out here API-wise. At the moment, the user can
// theoretically rasterize the same `RenderContext` multiple times without resetting in-between.
// However, if glyph caching is enabled, this method call could now evict that were previously
// assumed to exist in `RenderContext`, meaning that if the user rasterizes the same `RenderContext`
// again without resetting it, some of the cached glyphs might be stale and not exist anymore.
resources.after_render();
}
/// Render the current context into a pixmap.
pub fn render_to_pixmap(&self, resources: &mut Resources, pixmap: &mut Pixmap) {
let width = pixmap.width();
let height = pixmap.height();
self.render_to_buffer(
resources,
pixmap.data_as_u8_slice_mut(),
width,
height,
self.render_settings.render_mode,
);
}
/// Composite the current context into a region of a pixmap.
///
/// The context's content (sized `self.width × self.height`) is composited
/// directly to the destination pixmap starting at `(dst_x, dst_y)`.
/// If the region extends beyond the pixmap bounds, it is clipped.
///
/// Unlike [`render_to_pixmap`](Self::render_to_pixmap), this method composites on top of
/// existing pixmap content rather than clearing it first, allowing multiple
/// renders to accumulate.
///
/// This is useful for rendering individual elements (like glyphs) into
/// a spritesheet at specific coordinates.
///
/// # Panics
///
/// This method is only supported with the single-threaded dispatcher and will
/// **panic** if called on a `RenderContext` using the multi-threaded dispatcher.
pub fn composite_to_pixmap_at_offset(
&self,
resources: &Resources,
pixmap: &mut Pixmap,
dst_x: u16,
dst_y: u16,
) {
let dst_buffer_width = pixmap.width();
let dst_buffer_height = pixmap.height();
self.dispatcher.composite_at_offset(
pixmap.data_as_u8_slice_mut(),
self.width,
self.height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
self.render_settings.render_mode,
&self.encoded_paints,
&resources.image_registry,
);
}
/// 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
}
/// Execute a drawing operation, optionally wrapping it in a filter layer.
fn with_optional_filter<F>(&mut self, mut f: F)
where
F: FnMut(&mut Self),
{
if let Some(filter) = self.filter.clone() {
self.push_filter_layer(filter);
f(self);
self.pop_layer();
} else {
f(self);
}
}
/// Take current rendering state and reset the existing state to its default.
pub fn take_current_state(&mut self) -> RenderState {
core::mem::take(&mut self.state)
}
/// Save a copy of the current rendering state.
pub fn save_current_state(&mut self) -> RenderState {
self.state.clone()
}
/// Restore rendering state.
pub fn restore_state(&mut self, state: RenderState) {
self.state = state;
}
}
/// Image registry implementation.
impl Resources {
/// Register a pixmap in the image registry and return its [`ImageId`].
pub fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId {
self.image_registry.register(pixmap)
}
/// Remove an image from the registry.
pub fn destroy_image(&mut self, id: ImageId) -> bool {
self.image_registry.destroy(id)
}
/// Resolve an `ImageId` to its pixmap data.
pub fn resolve_image(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.image_registry.resolve(id)
}
/// Clear the image registry.
pub fn clear_images(&mut self) {
self.image_registry.clear();
}
}
impl Recordable for RenderContext {
fn record<F>(&mut self, recording: &mut Recording, f: F)
where
F: FnOnce(&mut Recorder<'_>),
{
let mut recorder = Recorder::new(recording, self.state.transform);
f(&mut recorder);
}
fn prepare_recording(&mut self, recording: &mut Recording) {
let buffers = recording.take_cached_strips();
let (strip_storage, strip_start_indices) =
self.generate_strips_from_commands(recording.commands(), buffers);
recording.set_cached_strips(strip_storage, 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(
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::SetTint(tint) => {
self.set_tint(*tint);
}
RenderCommand::SetFilterEffect(filter) => {
self.set_filter_effect(filter.clone());
}
RenderCommand::ResetFilterEffect => {
self.reset_filter_effect();
}
RenderCommand::PushLayer(PushLayerCommand {
clip_path,
blend_mode,
opacity,
mask,
filter,
}) => {
self.push_layer(
clip_path.as_ref(),
*blend_mode,
*opacity,
mask.clone(),
filter.clone(),
);
}
RenderCommand::PopLayer => {
self.pop_layer();
}
}
}
}
}
/// Registry that maps opaque [`ImageId`]s to [`Pixmap`] data.
///
/// Used by [`RenderContext`] to resolve `ImageSource::OpaqueId` at rasterization time.
#[derive(Debug, Default)]
pub(crate) struct ImageRegistry {
images: HashMap<u32, Arc<Pixmap>>,
next_id: u32,
}
impl ImageRegistry {
fn register(&mut self, pixmap: Arc<Pixmap>) -> ImageId {
let id = self.next_id;
assert!(
id < ATLAS_IMAGE_ID_BASE,
"image registry exhausted non-atlas image IDs"
);
self.next_id += 1;
self.images.insert(id, pixmap);
ImageId::new(id)
}
#[cfg(feature = "text")]
pub(crate) fn register_atlas_page(&mut self, page_index: u32, pixmap: Arc<Pixmap>) {
self.images.insert(
ImageId::new(ATLAS_IMAGE_ID_BASE + page_index).as_u32(),
pixmap,
);
}
pub(crate) fn destroy(&mut self, id: ImageId) -> bool {
self.images.remove(&id.as_u32()).is_some()
}
#[cfg(feature = "text")]
pub(crate) fn destroy_atlas_page(&mut self, page_index: u32) -> bool {
self.destroy(ImageId::new(ATLAS_IMAGE_ID_BASE + page_index))
}
fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.images.get(&id.as_u32()).cloned()
}
fn clear(&mut self) {
self.images.clear();
self.next_id = 0;
}
}
impl ImageResolver for ImageRegistry {
fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.images.get(&id.as_u32()).cloned()
}
}
/// 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: (StripStorage, Vec<usize>),
) -> (StripStorage, Vec<usize>) {
let (mut strip_storage, mut strip_start_indices) = buffers;
strip_storage.clear();
strip_storage.set_generation_mode(GenerationMode::Append);
strip_start_indices.clear();
let saved_state = self.take_current_state();
let mut strip_generator =
StripGenerator::new(self.width, self.height, self.render_settings.level);
for command in commands {
let start_index = strip_storage.strips.len();
match command {
RenderCommand::FillPath(path) => {
strip_generator.generate_filled_path(
path,
self.state.fill_rule,
self.state.transform,
self.aliasing_threshold,
&mut strip_storage,
self.dispatcher.current_clip_path(),
);
strip_start_indices.push(start_index);
}
RenderCommand::StrokePath(path) => {
strip_generator.generate_stroked_path(
path,
&self.state.stroke,
self.state.transform,
self.aliasing_threshold,
&mut strip_storage,
self.dispatcher.current_clip_path(),
);
strip_start_indices.push(start_index);
}
RenderCommand::FillRect(rect) => {
self.rect_to_temp_path(rect);
strip_generator.generate_filled_path(
&self.temp_path,
self.state.fill_rule,
self.state.transform,
self.aliasing_threshold,
&mut strip_storage,
self.dispatcher.current_clip_path(),
);
strip_start_indices.push(start_index);
}
RenderCommand::StrokeRect(rect) => {
self.rect_to_temp_path(rect);
strip_generator.generate_stroked_path(
&self.temp_path,
&self.state.stroke,
self.state.transform,
self.aliasing_threshold,
&mut strip_storage,
self.dispatcher.current_clip_path(),
);
strip_start_indices.push(start_index);
}
RenderCommand::SetTransform(transform) => {
self.state.transform = *transform;
}
RenderCommand::SetFillRule(fill_rule) => {
self.state.fill_rule = *fill_rule;
}
RenderCommand::SetStroke(stroke) => {
self.state.stroke = stroke.clone();
}
_ => {}
}
}
self.restore_state(saved_state);
(strip_storage, strip_start_indices)
}
}
/// Recording management implementation.
impl RenderContext {
fn process_geometry_command(
&mut self,
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;
if count == 0 {
// There are no strips to generate.
return;
}
assert!(
start < adjusted_strips.len() && count > 0,
"Invalid strip range"
);
let paint = self.encode_current_paint();
self.dispatcher.generate_wide_cmd(
&adjusted_strips[start..end],
paint,
self.state.blend_mode,
&self.encoded_paints,
);
}
/// 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 = {
let storage = self.dispatcher.strip_storage_mut();
let offset = storage.alphas.len() as u32;
// Extend the dispatcher's alpha buffer with cached alphas.
storage.alphas.extend(cached_alphas);
offset
};
// Create adjusted strips with corrected alpha indices.
cached_strips
.iter()
.map(move |strip| {
let mut adjusted_strip = *strip;
adjusted_strip.set_alpha_idx(adjusted_strip.alpha_idx() + alpha_offset);
adjusted_strip
})
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::RenderContext;
#[cfg(feature = "text")]
use crate::peniko::{Blob, FontData};
#[cfg(feature = "text")]
use alloc::sync::Arc;
#[cfg(feature = "text")]
use glifo::Glyph;
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::try_detect().unwrap_or(Level::baseline()),
num_threads: 1,
render_mode: RenderMode::OptimizeQuality,
};
let mut resources = crate::Resources::new();
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 resources, &mut pixmap);
ctx.flush();
ctx.render_to_pixmap(&mut resources, &mut pixmap);
}
#[cfg(feature = "text")]
#[test]
fn glyph_atlas_resources_are_lazy() {
const ROBOTO_FONT: &[u8] =
include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf");
let font = FontData::new(Blob::new(Arc::new(ROBOTO_FONT)), 0);
let glyphs = [Glyph {
id: 1,
x: 0.0,
y: 0.0,
}];
let mut resources = crate::Resources::new();
let mut ctx = RenderContext::new(100, 100);
ctx.fill_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
ctx.fill_path(&Rect::new(10.0, 10.0, 20.0, 20.0).to_path(0.1));
ctx.glyph_run(&mut resources, &font)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_none());
ctx.glyph_run(&mut resources, &font)
.atlas_cache(true)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_some());
}
}