blob: 352e6c6ee822f085b5ed56021f83015084f0ea22 [file] [edit]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Basic render operations.
#[cfg(feature = "text")]
use crate::Resources;
#[cfg(feature = "text")]
use crate::text::GlyphRunBuilder;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
use core::ops::Range;
use vello_common::blurred_rounded_rect::BlurredRoundedRectangle;
use vello_common::clip::PathDataRef;
use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::filter::FilterData;
use vello_common::filter_effects::Filter;
use vello_common::geometry::{RectU16, SizeU16};
use vello_common::kurbo::{Affine, BezPath, Rect, Shape, Stroke};
use vello_common::mask::Mask;
use vello_common::multi_atlas::AtlasConfig;
use vello_common::paint::{Paint, PaintType, Tint};
#[cfg(feature = "text")]
use vello_common::peniko::FontData;
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Fill};
use vello_common::record::{CommandRecorder, Drawable, LayerClip, LayerProps, PoppedLayer};
use vello_common::render_state::RenderState;
use vello_common::strip::Strip;
use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage};
use vello_common::transforms::{RootTransforms, Transforms};
use vello_common::util::{RectExt, is_axis_aligned, strip_bbox};
use vello_common::viewport::ViewportState;
/// Default tolerance for curve flattening
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1;
/// Path or rectangle draw retained by the scene recorder.
#[derive(Debug)]
pub(crate) enum RecordedDraw {
/// Path represented by generated strips.
Path(RecordedPath),
/// Rectangle retained for the direct GPU rectangle path.
Rect(RecordedRect),
}
/// Recorded path strips and their paint.
#[derive(Debug)]
pub(crate) struct RecordedPath {
/// Range selecting the path's strips from scene strip storage.
pub(crate) strips: Range<usize>,
/// Paint applied to the path.
pub(crate) paint: Paint,
}
/// Recorded rectangle and its paint.
#[derive(Debug)]
pub(crate) struct RecordedRect {
/// Rectangle in scene coordinates.
pub(crate) rect: Rect,
/// Paint applied to the rectangle.
pub(crate) paint: Paint,
}
impl RecordedDraw {
fn new_path(strips: Range<usize>, paint: Paint) -> Self {
Self::Path(RecordedPath { strips, paint })
}
fn new_rect(rect: Rect, paint: Paint) -> Self {
Self::Rect(RecordedRect { rect, paint })
}
}
impl Drawable for RecordedDraw {
#[expect(
clippy::cast_possible_truncation,
reason = "recorded fast rectangles are clipped to the u16 viewport"
)]
fn bbox(&self, strips: &[Strip]) -> Option<RectU16> {
match self {
// TODO: The bbox trait should take a reference to _all_ strips, and then we select
// the correct strips here. Otherwise, callers of this method always have to make sure
// to slice the strips correctly themselves.
// TODO: We also should just compute the bbox during strip generation instead of
// recomputing it here.
Self::Path(_) => strip_bbox(strips),
Self::Rect(rect) => {
let rect = rect.rect;
(!rect.is_zero_area()).then(|| {
RectU16::new(
rect.x0.floor() as u16,
rect.y0.floor() as u16,
rect.x1.ceil() as u16,
rect.y1.ceil() as u16,
)
.snap_to_tile_coordinates()
})
}
}
}
fn blend_mode(&self) -> Option<&BlendMode> {
None
}
}
/// 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,
/// Configuration for GPU memory used while rendering.
pub memory_settings: MemorySettings,
}
/// Settings controlling usage of GPU memory.
#[derive(Copy, Clone, Debug, Default)]
pub struct MemorySettings {
/// Configuration for the atlas holding uploaded images.
pub image_atlas_config: AtlasConfig,
/// Configuration for intermediate layer and scratch textures.
pub layers_config: LayersConfig,
}
/// Configuration for temporary textures used to render layers and effects.
///
/// Layers, blend modes, and filters need temporary GPU textures before their results can be
/// composited into the main scene. This configuration allows you to specify some limits to prevent
/// excessive allocations for complex or adversarial scenes.
///
/// In general, Vello GPU has the following priorities in that specific order:
/// - Render as many scenes as possible successfully.
/// - Use as little memory as possible.
/// - Be as performant as possible.
///
/// Vello GPU will by default prefer minimizing memory usage, even if at the cost of worse
/// performance. However, since the exact tradeoff is very application-dependent,
/// this configuration still allows you to tune the exact parameters such that better performance
/// is accepted at the cost of larger memory consumption.
#[derive(Copy, Clone, Debug)]
pub struct LayersConfig {
/// Maximum number of intermediate textures that may be allocated.
///
/// `None` permits growth up to device limits. A finite value allows you to impose a hard limit,
/// but might result in certain scenes being rejected. The exact texture requirements for
/// different types of scenes are considered an implementation detail, but in general:
///
/// - `0` for scenes without layers, including layers introduced by COLR glyphs.
/// - `1` for scenes with only single-child layers and no blend modes / filters.
/// - `4` for scenes with only single-child layers with potentially blend modes
/// or simple filters.
///
/// Any other type of scene with complex layer nesting might need more intermediate textures,
/// depending on the exact contents.
pub max_textures: Option<usize>,
/// Minimum width and height of each allocated intermediate texture.
///
/// By default, textures only grow to fit the largest required layer,
/// up to [`Self::max_texture_size`], and do not currently shrink between frames. By increasing
/// the minimum texture size, you can force Vello GPU to more aggressively batch layer draws,
/// at the cost of larger memory consumption.
///
/// Desktop GPUs may benefit from a larger minimum when rendering scenes with many layers. On
/// mobile GPUs, experiments have shown that keeping intermediate textures smaller is
/// more important for both memory usage and performance, even if it requires more render passes.
/// Therefore, you should in general avoid making this value too large. The default of
/// 512 by 512 is intended as a conservative starting point. Setting this to 1024 by 1024 is a
/// reasonable choice, too. Higher values are only recommended if benchmarks have proven
/// that your target workload profits from this and you don't plan on targeting mobile devices.
pub min_texture_size: SizeU16,
/// Maximum width and height of each allocated intermediate texture.
///
/// This should be at least the size of the main scene (and potentially more to account for
/// padding of large filters), unless you explicitly want to reject too large layers to limit
/// peak memory consumption.
///
/// Regardless of what this value is set to, device limits will always be respected.
pub max_texture_size: SizeU16,
}
impl Default for LayersConfig {
fn default() -> Self {
Self {
max_textures: None,
min_texture_size: SizeU16::new(512),
max_texture_size: SizeU16::new(4096),
}
}
}
impl Default for RenderSettings {
fn default() -> Self {
Self {
level: Level::try_detect().unwrap_or(Level::baseline()),
memory_settings: MemorySettings::default(),
}
}
}
/// A render context for hybrid CPU/GPU rendering.
///
/// This context maintains the state for path rendering and manages the rendering
/// pipeline from paths to strips that can be rendered by the GPU.
#[derive(Debug)]
pub struct Scene {
/// Width of the rendering surface in pixels.
pub(crate) width: u16,
/// Height of the rendering surface in pixels.
pub(crate) height: u16,
/// Viewport-dependent path and clip generation state.
viewport_state: ViewportState,
pub(crate) render_state: RenderState,
/// Root transform stack.
root_transforms: RootTransforms,
pub(crate) aliasing_threshold: Option<u8>,
/// Storage for encoded non-solid paint data.
pub(crate) encoded_paints: Vec<EncodedPaint>,
/// Whether the current paint is visible (e.g., alpha > 0).
paint_visible: bool,
/// Storage for generated strips and alpha values.
pub(crate) strip_storage: RefCell<StripStorage>,
/// Current filter effect applied to individual draw operations.
filter: Option<Filter>,
/// The command recorder.
pub(crate) recorder: CommandRecorder<RecordedDraw>,
}
impl Scene {
/// 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,
Level::try_detect().unwrap_or(Level::baseline()),
)
}
/// Create a new render context with a specific SIMD level.
pub fn new_with(width: u16, height: u16, level: Level) -> Self {
Self {
width,
height,
viewport_state: ViewportState::new(width, height, level),
render_state: RenderState::default(),
root_transforms: RootTransforms::default(),
aliasing_threshold: None,
encoded_paints: vec![],
paint_visible: true,
strip_storage: RefCell::new(StripStorage::new(GenerationMode::Append)),
filter: None,
recorder: CommandRecorder::new(width, height),
}
}
fn active_rect(&self) -> Rect {
Rect::new(
0.0,
0.0,
f64::from(self.viewport_state.width()),
f64::from(self.viewport_state.height()),
)
}
fn transforms(&self) -> &Transforms {
&self.render_state.transforms
}
fn transforms_mut(&mut self) -> &mut Transforms {
&mut self.render_state.transforms
}
fn effective_path_transform(&self) -> Affine {
self.root_transforms
.effective_path_transform(self.transforms())
}
fn effective_paint_transform(&self) -> Affine {
self.root_transforms
.effective_paint_transform(self.transforms())
}
/// Encode the current paint into a `Paint` that can be used for rendering.
///
/// For solid colors, this is a simple conversion. For gradients and images,
/// this encodes the paint data into the `encoded_paints` buffer and returns
/// a `Paint` that references that data. The combined transform (geometry + paint)
/// is applied during encoding.
fn encode_current_paint(&mut self) -> Paint {
// Note: In vello_cpu, during fine rasterization we apply a 0.5 offset to the location
// to account for the fact that we want to sample the pixel center instead of the top-left
// corner. For vello_gpu, we don't need this, because the GPU itself already applies
// this shift automatically.
let transform = self.effective_paint_transform();
match self.render_state.paint.clone() {
PaintType::Solid(s) => s.into(),
PaintType::Gradient(g) => g.encode_into(&mut self.encoded_paints, transform, None),
PaintType::Image(i) => {
i.encode_into(&mut self.encoded_paints, transform, self.render_state.tint)
}
}
}
/// Fill a path with the current paint and fill rule.
pub fn fill_path(&mut self, path: &BezPath) {
if !self.paint_visible {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let paint = ctx.encode_current_paint();
ctx.fill_path_with(
path,
ctx.effective_path_transform(),
ctx.render_state.fill_rule,
paint,
ctx.aliasing_threshold,
);
});
}
/// Build strips for a filled path with the given properties and record the draw.
fn fill_path_with(
&mut self,
path: &BezPath,
transform: Affine,
fill_rule: Fill,
paint: Paint,
aliasing_threshold: Option<u8>,
) {
self.record_generated_path(paint, |strip_generator, strip_storage, clip_path| {
strip_generator.generate_filled_path(
path,
fill_rule,
transform,
aliasing_threshold,
strip_storage,
clip_path,
);
});
}
/// Push a new clip path to the clip stack.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/vello_cpu/examples)
/// example for how this method differs from `push_clip_layer`.
pub fn push_clip_path(&mut self, path: &BezPath) {
let transform = self.transforms().clip_path_transform();
self.viewport_state.push_clip(
path,
self.render_state.fill_rule,
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.viewport_state.pop_clip();
}
/// Stroke a path with the current paint and stroke settings.
pub fn stroke_path(&mut self, path: &BezPath) {
if !self.paint_visible {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let paint = ctx.encode_current_paint();
ctx.stroke_path_with(
path,
ctx.effective_path_transform(),
paint,
ctx.aliasing_threshold,
);
});
}
/// Build strips for a stroked path with the given properties and record the draw.
fn stroke_path_with(
&mut self,
path: &BezPath,
transform: Affine,
paint: Paint,
aliasing_threshold: Option<u8>,
) {
let stroke = self.render_state.stroke.clone();
self.record_generated_path(paint, |strip_generator, strip_storage, clip_path| {
strip_generator.generate_stroked_path(
path,
&stroke,
transform,
aliasing_threshold,
strip_storage,
clip_path,
);
});
}
/// 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;
}
/// Fill a rectangle with the current paint and fill rule.
pub fn fill_rect(&mut self, rect: &Rect) {
if !self.paint_visible || rect.is_zero_area() {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let paint = ctx.encode_current_paint();
if let Some(bounds) = ctx.fast_rect_bounds(rect) {
ctx.recorder
.push_draw(RecordedDraw::new_rect(bounds, paint), &[]);
return;
}
let transform = ctx.effective_path_transform();
if is_axis_aligned(&transform) && ctx.aliasing_threshold.is_none() {
let transformed_rect = transform.transform_rect_bbox(*rect);
ctx.record_generated_path(paint, |strip_generator, strip_storage, clip_path| {
strip_generator.generate_filled_rect_fast(
&transformed_rect,
strip_storage,
clip_path,
);
});
} else {
// TODO: Use a temporary storage for rect paths, like in `vello_cpu`.
ctx.fill_path_with(
&rect.to_path(DEFAULT_TOLERANCE),
transform,
ctx.render_state.fill_rule,
paint,
ctx.aliasing_threshold,
);
}
});
}
fn record_generated_path<F>(&mut self, paint: Paint, generate: F)
where
F: FnOnce(&mut StripGenerator, &mut StripStorage, Option<PathDataRef<'_>>),
{
let strips = {
let mut strip_storage = self.strip_storage.borrow_mut();
let strip_start = strip_storage.strips.len();
self.viewport_state
.with_generator_and_clip(|strip_generator, clip_path| {
generate(strip_generator, &mut strip_storage, clip_path);
});
strip_start..strip_storage.strips.len()
};
if strips.is_empty() {
return;
}
let draw = RecordedDraw::new_path(strips.clone(), paint);
let strip_storage = self.strip_storage.borrow();
self.recorder.push_draw(draw, &strip_storage.strips[strips]);
}
#[inline]
fn can_emit_fast_strips(&self) -> bool {
self.viewport_state.clip().is_none() && self.aliasing_threshold.is_none()
}
fn fast_rect_bounds(&self, rect: &Rect) -> Option<Rect> {
if !self.can_emit_fast_strips() {
return None;
}
// TODO: Either bail out or properly implement the case where `aliasing_threshold` is set.
// We can't handle skewed rectangles.
// TODO: Maybe support rotated rectangles (https://github.com/linebender/vello/pull/1482#discussion_r2881223621)
let transform = self.effective_path_transform();
if !is_axis_aligned(&transform) {
return None;
}
let transformed_rect = transform
.transform_rect_bbox(*rect)
.intersect(self.active_rect());
// Can't handle mirrored or zero-sized rectangles.
if transformed_rect.is_zero_area() {
return None;
}
Some(transformed_rect)
}
/// Stroke a rectangle with the current paint and stroke settings.
pub fn stroke_rect(&mut self, rect: &Rect) {
self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE));
}
/// Fill a blurred rectangle with the given corner radius and standard deviation.
///
/// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted: the
/// paint is fully opaque outside the blurred rectangle and fades to transparent inside it. This
/// can be used to implement inset box shadows.
///
/// This operation uses the current transform and paint transform. Like Vello CPU, it only
/// uses solid paints; non-solid paints fall back to black.
pub fn fill_blurred_rounded_rect(
&mut self,
rect: &Rect,
radius: f32,
std_dev: f32,
invert: bool,
) {
if !self.paint_visible {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let rect = rect.abs();
let color = match ctx.render_state.paint {
PaintType::Solid(s) => s,
_ => BLACK,
};
let blurred_rect = BlurredRoundedRectangle {
rect,
color,
radius,
std_dev,
invert,
};
let kernel_size = 2.5 * std_dev;
let inflated_rect = rect.inflate(f64::from(kernel_size), f64::from(kernel_size));
let transform = ctx.effective_paint_transform();
let paint = blurred_rect.encode_into(&mut ctx.encoded_paints, transform, None);
if let Some(bounds) = ctx.fast_rect_bounds(&inflated_rect) {
ctx.recorder
.push_draw(RecordedDraw::new_rect(bounds, paint), &[]);
return;
}
let path_transform = ctx.effective_path_transform();
if is_axis_aligned(&path_transform) && ctx.aliasing_threshold.is_none() {
let transformed_rect = path_transform.transform_rect_bbox(inflated_rect);
ctx.record_generated_path(paint, |strip_generator, strip_storage, clip_path| {
strip_generator.generate_filled_rect_fast(
&transformed_rect,
strip_storage,
clip_path,
);
});
} else {
ctx.fill_path_with(
&inflated_rect.to_path(DEFAULT_TOLERANCE),
path_transform,
Fill::NonZero,
paint,
ctx.aliasing_threshold,
);
}
});
}
/// 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: &FontData,
) -> GlyphRunBuilder<'a> {
glifo::GlyphRunBuilder::new(
font.clone(),
self.transforms().scene_transform(),
*self.transforms().paint_transform(),
crate::text::HybridGlyphRunBackend {
scene: self,
resources,
atlas_cache_enabled: false,
},
)
}
/// Push a new layer with the given properties.
///
/// # Panics
///
/// Panics if `mask` is provided because mask layers are not yet supported.
pub fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
blend_mode: Option<BlendMode>,
opacity: Option<f32>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
if mask.is_some() {
unimplemented!("mask layers are currently not supported");
}
let blend_mode = blend_mode.unwrap_or_default();
let layer_transform = self.effective_path_transform();
let filter_data = filter.map(|filter| FilterData::new(filter, layer_transform));
let relative_root_transform =
filter_data
.as_ref()
.map_or(Affine::IDENTITY, |filter_data| {
let (shift_x, shift_y) = filter_data.source_shift();
Affine::translate((f64::from(shift_x), f64::from(shift_y)))
});
self.root_transforms.push_root(relative_root_transform);
if let Some(filter_plan) = &filter_data {
self.viewport_state.push_root_viewport(filter_plan);
}
let clip_path = clip_path.map(|path| {
let mut strip_storage = self.strip_storage.borrow_mut();
let strip_start = strip_storage.strips.len();
self.viewport_state
.with_generator_and_clip(|strip_generator, existing_clip| {
strip_generator.generate_filled_path(
path,
self.render_state.fill_rule,
layer_transform,
self.aliasing_threshold,
&mut strip_storage,
existing_clip,
);
let strip_range = strip_start..strip_storage.strips.len();
LayerClip {
bbox: strip_bbox(&strip_storage.strips[strip_range.clone()])
.unwrap_or(RectU16::ZERO),
strip_range,
thread_idx: 0,
}
})
});
self.recorder.push_layer(
LayerProps {
blend_mode,
opacity: opacity.unwrap_or(1.0),
mask: None,
clip_path,
},
filter_data,
);
}
/// Push a new clip layer.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/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.
///
/// # Panics
///
/// This method currently always panics since masks are not supported yet.
pub fn push_mask_layer(&mut self, mask: Mask) {
self.push_layer(None, None, None, Some(mask), None);
}
/// Push a new filter layer.
pub fn push_filter_layer(&mut self, filter: Filter) {
self.push_layer(None, None, None, None, Some(filter));
}
/// Pop the last pushed layer.
pub fn pop_layer(&mut self) {
if self.recorder.pop_layer() == PoppedLayer::Filter {
self.viewport_state.pop_root_viewport();
}
self.root_transforms.pop_root();
}
/// Set the blend mode for subsequent rendering operations.
///
/// # Panics
///
/// Panics if `blend_mode` is destructive, which is currently not supported for
/// non-isolated blends. You need to use clip layers instead if you need that behavior.
pub fn set_blend_mode(&mut self, blend_mode: BlendMode) {
assert!(
!blend_mode.is_destructive(),
"destructive blend modes are currently not supported"
);
self.render_state.blend_mode = blend_mode;
}
/// Set the stroke settings for subsequent stroke operations.
pub fn set_stroke(&mut self, stroke: Stroke) {
self.render_state.stroke = stroke;
}
/// Get the current stroke.
pub fn stroke(&self) -> &Stroke {
&self.render_state.stroke
}
/// Get a mutable reference to the current stroke.
#[cfg(feature = "text")]
pub(crate) fn stroke_mut(&mut self) -> &mut Stroke {
&mut self.render_state.stroke
}
/// Set the paint for subsequent rendering operations.
// TODO: This API is not final. Supporting images from a pixmap is explicitly out of scope.
// Instead images should be passed via a backend-agnostic opaque id, and be hydrated at
// render time into a texture usable by the renderer backend.
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.render_state.paint = paint.into();
self.set_paint_visible();
}
fn set_paint_visible(&mut self) {
self.paint_visible = match &self.render_state.paint {
PaintType::Solid(color) => color.components[3] != 0.0,
_ => true,
};
}
/// Set the tint for subsequent image paint operations.
pub fn set_tint(&mut self, tint: Option<Tint>) {
self.render_state.tint = tint;
}
/// Clear the tint, so subsequent image paints are drawn without tinting.
pub fn reset_tint(&mut self) {
self.render_state.tint = None;
}
/// Get the current paint.
pub fn paint(&self) -> &PaintType {
&self.render_state.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.transforms_mut().set_paint_transform(paint_transform);
}
/// Reset the current paint transform.
pub fn reset_paint_transform(&mut self) {
self.transforms_mut().reset_paint_transform();
}
/// Set the fill rule for subsequent fill operations.
pub fn set_fill_rule(&mut self, fill_rule: Fill) {
self.render_state.fill_rule = fill_rule;
}
/// Set the transform for subsequent rendering operations.
pub fn set_transform(&mut self, transform: Affine) {
self.transforms_mut().set_transform(transform);
}
/// Reset the transform to identity.
pub fn reset_transform(&mut self) {
self.transforms_mut().reset_transform();
}
/// Apply filter to the current paint (affects next drawn element).
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;
}
fn with_optional_filter_or_blend_layer<F, T>(&mut self, f: F) -> T
where
F: FnOnce(&mut Self) -> T,
{
let blend_mode = self.render_state.blend_mode;
let blend_mode = (blend_mode != BlendMode::default()).then_some(blend_mode);
let filter = self.filter.clone();
if blend_mode.is_some() || filter.is_some() {
self.push_layer(None, blend_mode, None, None, filter);
let result = f(self);
self.pop_layer();
result
} else {
f(self)
}
}
/// Reset the scene and update its size.
pub fn reset_and_resize(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
self.reset();
}
/// Reset scene to default values.
pub fn reset(&mut self) {
self.viewport_state.reset(self.width, self.height);
{
let mut ss = self.strip_storage.borrow_mut();
ss.clear();
}
self.encoded_paints.clear();
self.root_transforms.reset();
self.render_state.reset();
self.set_paint_visible();
self.recorder.reset(self.width, self.height);
self.filter = None;
}
/// Get the width of the render context.
pub fn width(&self) -> u16 {
self.width
}
/// Get the height of the render context.
pub fn height(&self) -> u16 {
self.height
}
/// Take current rendering state and reset the existing state to its default.
pub fn take_current_state(&mut self) -> RenderState {
let state = core::mem::take(&mut self.render_state);
self.set_paint_visible();
state
}
/// Save a copy of the current rendering state.
pub fn save_current_state(&mut self) -> RenderState {
self.render_state.clone()
}
/// Restore rendering state.
pub fn restore_state(&mut self, state: RenderState) {
self.render_state = state;
self.set_paint_visible();
}
}
#[cfg(test)]
mod tests {
use super::{RecordedDraw, Scene};
#[cfg(feature = "text")]
use crate::resources::Resources;
#[cfg(feature = "text")]
use alloc::sync::Arc;
#[cfg(feature = "text")]
use glifo::Glyph;
use vello_common::TextureId;
use vello_common::geometry::RectU16;
use vello_common::kurbo::{BezPath, Rect};
use vello_common::paint::{Image, ImageSource, Paint, PremulColor};
use vello_common::peniko::ImageSampler;
use vello_common::peniko::color::palette::css::{BLUE, TRANSPARENT};
#[cfg(feature = "text")]
use vello_common::peniko::{Blob, FontData};
use vello_common::record::Drawable;
#[test]
fn recorded_rect_bbox_rounds_fractional_outward() {
let draw = RecordedDraw::new_rect(
Rect::new(0.0, 0.0, 4.5, 4.5),
Paint::Solid(PremulColor::from_alpha_color(BLUE)),
);
assert_eq!(draw.bbox(&[]), Some(RectU16::new(0, 0, 8, 8)));
}
#[test]
fn reset_restores_default_paint_visibility() {
let mut scene = Scene::new(100, 100);
scene.set_paint(TRANSPARENT);
scene.reset();
scene.fill_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
assert_eq!(scene.recorder.draws.len(), 1);
}
#[test]
fn reset_and_resize_updates_scene_size() {
let mut scene = Scene::new(8, 4);
scene.reset_and_resize(4, 8);
assert_eq!(scene.width(), 4);
assert_eq!(scene.height(), 8);
assert_eq!(scene.recorder.scene_size.width(), 4);
assert_eq!(scene.recorder.scene_size.height(), 8);
scene.set_paint(BLUE);
scene.fill_rect(&Rect::new(0.0, 0.0, 8.0, 8.0));
let RecordedDraw::Rect(rect) = &scene.recorder.draws[0] else {
panic!("expected a recorded rectangle");
};
assert_eq!(rect.rect, Rect::new(0.0, 0.0, 4.0, 8.0));
}
#[test]
fn empty_generated_path_is_not_recorded() {
let mut scene = Scene::new(100, 100);
scene.fill_path(&BezPath::new());
assert!(scene.strip_storage.borrow().strips.is_empty());
assert!(scene.recorder.draws.is_empty());
assert!(scene.recorder.nodes.is_empty());
}
#[test]
fn zero_area_external_texture_rect_is_a_noop() {
let mut scene = Scene::new(100, 100);
scene.set_paint(Image {
image: ImageSource::external_texture(TextureId(7), RectU16::new(0, 0, 8, 8), false),
sampler: ImageSampler::default(),
});
scene.fill_rect(&Rect::new(10.0, 10.0, 10.0, 20.0));
assert!(scene.encoded_paints.is_empty());
assert!(scene.strip_storage.borrow().strips.is_empty());
assert!(scene.recorder.draws.is_empty());
assert!(scene.recorder.nodes.is_empty());
}
#[cfg(feature = "text")]
#[test]
fn glyph_atlas_resources_are_lazy() {
const ROBOTO_FONT: &[u8] = include_bytes!("../../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 scene = Scene::new(200, 200);
let mut resources = Resources::new(vello_common::multi_atlas::AtlasConfig::default());
let mut triangle = BezPath::new();
triangle.move_to((10.0, 10.0));
triangle.line_to((90.0, 50.0));
triangle.line_to((10.0, 90.0));
triangle.close_path();
scene.fill_rect(&Rect::new(10.0, 10.0, 50.0, 50.0));
scene.fill_path(&triangle);
scene
.glyph_run(&mut resources, &font)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_none());
scene
.glyph_run(&mut resources, &font)
.atlas_cache(true)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_some());
}
}