Files
pdfcraft/vendor/vello_cpu/src/render.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

918 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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::render_state::RenderState;
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();
}
}
/// 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()
}
}
#[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());
}
}