blob: c8965c3973c19f06bdf6d609d63c3cfa5ae3a3b3 [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::{ClipRef, ClipShape};
use vello_common::encode::{EncodeExt, EncodedPaint, invert_paint_transform};
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, PremulColor, Tint};
#[cfg(feature = "text")]
use vello_common::peniko::FontData;
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Fill, GradientKind, ImageQuality};
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, into_fast_path_rect, strip_bbox};
use vello_common::viewport::ViewportState;
#[cfg(feature = "wgpu")]
use crate::sparse_strips::{AddPath, PathSummary, StripInput, summarize_path_tiles};
#[cfg(feature = "wgpu")]
use vello_common::clip::PathDataRef;
/// 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),
/// Path whose strips the GPU produces at render time (see [`Scene::set_gpu_strips`]).
#[cfg(feature = "wgpu")]
GpuPath(RecordedGpuPath),
/// Rectangle retained for the direct GPU rectangle path.
Rect(RecordedRect),
}
/// A path recorded for GPU strip generation.
#[cfg(feature = "wgpu")]
#[derive(Debug)]
pub(crate) struct RecordedGpuPath {
/// Index of the path in the scene's [`GpuStrips`] input; its strip range is known once
/// the GPU strips are resolved.
pub(crate) path: u32,
/// Bounding box of the path's strips, derived from its tiles.
pub(crate) bbox: RectU16,
/// Paint applied to the path.
pub(crate) paint: Paint,
}
/// 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),
#[cfg(feature = "wgpu")]
Self::GpuPath(path) => (!path.bbox.is_empty()).then_some(path.bbox),
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(Clone, Debug)]
pub struct RenderSettings {
pub(crate) level: Level,
pub(crate) memory_settings: MemorySettings,
#[cfg(feature = "wgpu")]
pub(crate) pipeline_cache: Option<wgpu::PipelineCache>,
}
impl RenderSettings {
/// Sets the SIMD level used for rendering operations.
///
/// By default, the level is detected at runtime, falling back to [`Level::baseline`].
#[must_use]
pub fn with_level(mut self, level: Level) -> Self {
self.level = level;
self
}
/// Returns the SIMD level used for rendering operations.
pub fn level(&self) -> Level {
self.level
}
/// Sets the configuration for GPU memory used while rendering.
#[must_use]
pub fn with_memory_settings(mut self, memory_settings: MemorySettings) -> Self {
self.memory_settings = memory_settings;
self
}
/// Sets the cache used when creating the wgpu render pipelines.
///
/// A cache can substantially reduce startup times, especially on low-tier devices.
/// Requires [`wgpu::Features::PIPELINE_CACHE`] on the device.
/// By default, no cache is used.
#[cfg(feature = "wgpu")]
#[must_use]
pub fn with_pipeline_cache(mut self, pipeline_cache: wgpu::PipelineCache) -> Self {
self.pipeline_cache = Some(pipeline_cache);
self
}
}
/// 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(),
#[cfg(feature = "wgpu")]
pipeline_cache: None,
}
}
}
/// A render context for 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>,
/// 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>,
/// SIMD level.
#[cfg(feature = "wgpu")]
level: Level,
/// State of GPU strip generation, when enabled.
#[cfg(feature = "wgpu")]
pub(crate) gpu_strips: Option<RefCell<GpuStrips>>,
}
/// Paths whose strips are generated on the GPU, and the result once resolved.
///
/// Experimental: see [`Scene::set_gpu_strips`].
#[cfg(feature = "wgpu")]
#[derive(Debug)]
pub(crate) struct GpuStrips {
/// The sorted tiles and lines of every recorded GPU path.
input: StripInput,
/// Per GPU path, the clip snapshot to intersect with, if the path was drawn under a clip.
clips: Vec<Option<usize>>,
/// Copies of the clip paths active while GPU paths were recorded.
snapshots: Vec<ClipSnapshot>,
/// Per GPU path, its range in the scene's strip storage, once resolved.
pub(crate) ranges: Vec<Range<usize>>,
/// Whether the GPU strips of this recording have been produced.
resolved: bool,
/// The GPU buffers of the resolved recording; the renderer copies their alphas into the
/// alpha texture.
pub(crate) buffers: Option<crate::sparse_strips::gpu::SparseStripBuffers>,
/// Byte offset in the scene's alphas at which the GPU alphas start (a multiple of the alpha
/// texture row size).
pub(crate) alpha_base: usize,
/// Per GPU path, what its strips will be (exact counts and bounding box).
summaries: Vec<PathSummary>,
/// Per GPU path, whether it is drawn directly from GPU-built instances (root level, solid
/// paint, no clip) rather than through read-back strips.
direct: Vec<bool>,
/// Per GPU path, its `(instance base, instance count)` when direct, computed at resolve.
pub(crate) instance_spans: Vec<Option<[u32; 2]>>,
/// How many bytes of the scene's alphas the renderer must upload: up to the GPU alpha
/// region when nothing follows it, since the GPU writes that region itself.
pub(crate) alpha_upload_len: Option<usize>,
}
/// The strips of a clip path at the time a GPU path was recorded under it.
#[cfg(feature = "wgpu")]
#[derive(Debug)]
struct ClipSnapshot {
key: (u64, usize),
strips: Vec<Strip>,
alphas: Vec<u8>,
bbox: RectU16,
}
#[cfg(feature = "wgpu")]
impl GpuStrips {
fn new(width: u16, height: u16) -> Self {
Self {
input: StripInput::new(width, height),
clips: Vec::new(),
snapshots: Vec::new(),
ranges: Vec::new(),
resolved: false,
buffers: None,
alpha_base: 0,
summaries: Vec::new(),
direct: Vec::new(),
instance_spans: Vec::new(),
alpha_upload_len: None,
}
}
/// Whether any path needs its strips on the CPU (clipped, non-solid paint, or in a layer).
fn needs_readback(&self) -> bool {
self.direct.iter().any(|direct| !direct)
}
fn reset(&mut self, width: u16, height: u16) {
self.input.clear();
self.input.width = width;
self.input.height = height;
self.clips.clear();
self.snapshots.clear();
self.ranges.clear();
self.resolved = false;
// `buffers` is kept and reused by the next resolve.
self.alpha_base = 0;
self.alpha_upload_len = None;
self.summaries.clear();
self.direct.clear();
self.instance_spans.clear();
}
/// Index of the snapshot of the current clip, taking it if this clip has not been seen.
fn snapshot(&mut self, key: (u64, usize), clip: PathDataRef<'_>) -> usize {
if let Some(last) = self.snapshots.last()
&& last.key == key
{
return self.snapshots.len() - 1;
}
self.snapshots.push(ClipSnapshot {
key,
strips: clip.strips.to_vec(),
alphas: clip.alphas.to_vec(),
bbox: clip.bbox,
});
self.snapshots.len() - 1
}
}
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![],
strip_storage: RefCell::new(StripStorage::new(GenerationMode::Append)),
filter: None,
recorder: CommandRecorder::new(width, height),
#[cfg(feature = "wgpu")]
level,
#[cfg(feature = "wgpu")]
gpu_strips: None,
}
}
/// Generate the strips of filled and stroked paths on the GPU instead of the CPU.
///
/// Experimental. Flattening, tiling and sorting stay on the CPU; the GPU produces the strips
/// and alphas, which are currently read back before scheduling so that the rest of the
/// renderer is unchanged. Must be set before recording; [`Self::reset`] keeps the setting.
/// Only the wgpu backend produces GPU strips.
#[cfg(feature = "wgpu")]
pub fn set_gpu_strips(&mut self, enabled: bool) {
self.gpu_strips = enabled.then(|| RefCell::new(GpuStrips::new(self.width, self.height)));
}
/// Whether GPU strip generation is enabled.
#[cfg(feature = "wgpu")]
pub fn gpu_strips_enabled(&self) -> bool {
self.gpu_strips.is_some()
}
/// Record a path for GPU strip generation, or on the CPU when the GPU cannot represent it.
///
/// `prepare` flattens, tiles and sorts the path with the given cull bounding box.
#[cfg(feature = "wgpu")]
#[expect(
clippy::cast_possible_truncation,
reason = "the GPU input indexes paths with u32"
)]
fn record_gpu_path(
&mut self,
paint: Paint,
fill_rule: Fill,
aliasing_threshold: Option<u8>,
prepare: impl FnOnce(&mut StripGenerator, Option<RectU16>),
) {
enum Outcome {
None,
Cpu(Range<usize>),
Gpu(u32, RectU16),
}
let gpu_cell = self.gpu_strips.as_ref().expect("GPU strips are enabled");
let mut gpu = gpu_cell.borrow_mut();
let clip_key = self.viewport_state.clip_key();
let (width, height) = (self.width, self.height);
let strip_storage = &self.strip_storage;
// Only unclipped, solid-paint paths are drawn straight from GPU buffers.
let direct_paint = matches!(paint, Paint::Solid(_));
let outcome = self
.viewport_state
.with_generator_and_clip(|generator, clip| {
prepare(generator, clip.map(|clip| clip.path.bbox));
// Paths inside filter root viewports are tiled against a padded viewport that
// the GPU batch does not know about; render those on the CPU.
let added = if generator.width() == width && generator.height() == height {
gpu.input.add_path(
generator.tiles(),
generator.lines(),
fill_rule,
aliasing_threshold,
)
} else {
AddPath::CpuFallback
};
match added {
AddPath::Empty => Outcome::None,
AddPath::CpuFallback => {
let mut strip_storage = strip_storage.borrow_mut();
let start = strip_storage.strips.len();
generator.render_prepared(
fill_rule,
aliasing_threshold,
&mut strip_storage,
clip.map(|clip| clip.path),
);
Outcome::Cpu(start..strip_storage.strips.len())
}
AddPath::Added => {
let summary = summarize_path_tiles(
generator.tiles(),
generator.lines(),
fill_rule,
width,
height,
);
let mut bbox = summary.bbox;
let snapshot = clip.map(|clip| {
// The intersection's bbox is at most the path's within the clip's.
bbox = bbox.intersect(clip.path.bbox);
gpu.snapshot(clip_key, clip.path)
});
gpu.direct.push(direct_paint && snapshot.is_none());
gpu.summaries.push(summary);
gpu.clips.push(snapshot);
Outcome::Gpu(gpu.clips.len() as u32 - 1, bbox)
}
}
});
drop(gpu);
match outcome {
Outcome::None => {}
Outcome::Cpu(strips) => {
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]);
}
Outcome::Gpu(path, bbox) => {
let draw = RecordedDraw::GpuPath(RecordedGpuPath { path, bbox, paint });
self.recorder.push_draw(draw, &[]);
}
}
}
/// Produce the strips of all recorded GPU paths and append them to the strip storage.
///
/// Submits the GPU passes. When some path needs its strips on the CPU (clipped, non-solid
/// paint, or in a layer), waits for them and reads the strips back; the alphas stay on the GPU
/// and the renderer copies them into the alpha texture, at `alpha_row_bytes`-aligned rows
/// after the CPU alphas (the scene's alpha buffer is padded to reserve that region). Paths
/// recorded under a clip are intersected with the clip's strips on the CPU, which needs
/// their alphas read back too. Idempotent per recording.
#[cfg(feature = "wgpu")]
#[expect(
clippy::cast_possible_truncation,
reason = "alpha indices are u32 in strips"
)]
pub(crate) fn resolve_gpu_strips(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pipelines: &crate::sparse_strips::gpu::SparseStripPipelines,
alpha_row_bytes: u32,
) {
use crate::sparse_strips::TILE_PIXELS;
let Some(gpu_cell) = &self.gpu_strips else {
return;
};
let mut gpu = gpu_cell.borrow_mut();
if gpu.resolved {
return;
}
gpu.resolved = true;
let gpu = &mut *gpu;
gpu.ranges.clear();
if gpu.input.entries.is_empty() {
return;
}
let mut storage = self.strip_storage.borrow_mut();
let row_bytes = alpha_row_bytes.max(TILE_PIXELS) as usize;
// Reserve the GPU alpha region: row-aligned, sized to the block bound.
let alpha_base = storage.alphas.len().next_multiple_of(row_bytes);
storage.alphas.resize(alpha_base, 0);
// Direct paths get contiguous instance slots in path order; the scheduler draws each
// span with a plain instanced draw.
gpu.instance_spans.clear();
let mut n_instances = 0_u32;
for (summary, direct) in gpu.summaries.iter().zip(&gpu.direct) {
if *direct {
gpu.instance_spans
.push(Some([n_instances, summary.instances]));
n_instances += summary.instances;
} else {
gpu.instance_spans.push(None);
}
}
// The summaries give exact totals, so the buffers are sized to them.
let (total_strips, total_blocks) = gpu.summaries.iter().fold((0, 0), |(s, b), summary| {
(s + summary.strips, b + summary.blocks)
});
gpu.input.exact_counts = Some((total_strips, total_blocks));
let buffers = pipelines.prepare_buffers(
device,
queue,
gpu.buffers.take(),
&gpu.input,
(alpha_base / TILE_PIXELS as usize) as u32,
alpha_row_bytes,
n_instances,
);
storage
.alphas
.resize(alpha_base + buffers.alpha_rows as usize * row_bytes, 0);
gpu.alpha_base = alpha_base;
gpu.alpha_upload_len = Some(alpha_base);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("sparse strips"),
});
pipelines.encode_timed(&mut encoder, &buffers, None, gpu.needs_readback());
if !gpu.needs_readback() {
// Everything is drawn from GPU buffers: submit the passes now so that the GPU
// produces the strips while the CPU schedules the frame, and never wait for them.
queue.submit([encoder.finish()]);
gpu.buffers = Some(buffers);
gpu.ranges.resize(gpu.input.paths.len(), 0..0);
return;
}
let needs_alphas = gpu.clips.iter().any(Option::is_some);
let out =
crate::sparse_strips::gpu::read_back(device, queue, encoder, &buffers, needs_alphas);
let buffers_alpha_bytes = buffers.alpha_rows as usize * row_bytes;
gpu.buffers = Some(buffers);
for (path, summary) in out.paths.iter().zip(&gpu.summaries) {
debug_assert_eq!(
(
path.strip_end - path.strip_start,
path.block_end - path.block_start
),
(summary.strips, summary.blocks),
"CPU path summary matches the GPU allocation"
);
}
let unclipped_bbox = RectU16::new(0, 0, u16::MAX, u16::MAX);
let alpha_base_u32 = alpha_base as u32;
for (path, clip) in out.paths.iter().zip(&gpu.clips) {
let range = path.strip_start as usize..path.strip_end as usize;
let start = storage.strips.len();
match clip {
None => storage.strips.extend_from_slice(&out.strips[range]),
Some(snapshot) => {
// The intersection reads alphas relative to the path's own buffer, so
// present the GPU strips with their alpha indices rebased to it.
let rebased: Vec<Strip> = out.strips[range]
.iter()
.map(|strip| {
let mut strip = *strip;
strip.set_alpha_idx(strip.alpha_idx() - alpha_base_u32);
strip
})
.collect();
let snapshot = &gpu.snapshots[*snapshot];
vello_common::clip::intersect(
self.level,
PathDataRef {
strips: &snapshot.strips,
alphas: &snapshot.alphas,
bbox: snapshot.bbox,
},
PathDataRef {
strips: &rebased,
alphas: &out.alphas,
bbox: unclipped_bbox,
},
&mut storage,
);
}
}
gpu.ranges.push(start..storage.strips.len());
}
if storage.alphas.len() > alpha_base + buffers_alpha_bytes {
// Intersections appended alphas after the GPU region; upload everything.
gpu.alpha_upload_len = None;
}
}
/// How many bytes of the scene's alphas the renderer must upload (the rest is written by
/// the GPU), or `None` for all of them.
#[cfg(feature = "wgpu")]
pub(crate) fn gpu_alpha_upload_len(&self) -> Option<usize> {
self.gpu_strips
.as_ref()
.and_then(|gpu| gpu.borrow().alpha_upload_len)
}
/// The GPU alpha copy the renderer must perform after uploading the CPU alphas: the
/// buffer, the first alpha texture row to write, and the number of rows.
#[cfg(feature = "wgpu")]
#[expect(clippy::cast_possible_truncation, reason = "texture rows are u32")]
pub(crate) fn gpu_alpha_copy(
&self,
alpha_row_bytes: u32,
) -> Option<(core::cell::Ref<'_, GpuStrips>, u32, u32)> {
let gpu = self.gpu_strips.as_ref()?.borrow();
if !gpu.resolved || gpu.input.entries.is_empty() {
return None;
}
gpu.buffers.as_ref()?;
let row = (gpu.alpha_base / alpha_row_bytes.max(1) as usize) as u32;
let rows = gpu.buffers.as_ref().map_or(0, |b| b.alpha_rows);
Some((gpu, row, rows))
}
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.
///
/// Returns `None` if the current paint cannot cover any pixels (see [`Self::paint_has_area`]).
fn encode_current_paint(&mut self) -> Option<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 {
PaintType::Solid(s) => Some(Paint::Solid(PremulColor::from_alpha_color(*s))),
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)
}
}
}
/// Whether the effective paint transform maps paint space onto a non-zero area.
///
/// A singular transform collapses images, gradients and blurred rectangles onto a line or a
/// point, so drawing with it must not touch any pixel. Checked before pushing a blend or
/// filter layer so that a skipped draw does not leave an empty layer behind.
fn paint_transform_has_area(&self) -> bool {
invert_paint_transform(self.effective_paint_transform()).is_some()
}
/// Whether drawing with the current paint can produce any pixels.
fn paint_has_area(&self) -> bool {
matches!(self.render_state.paint, PaintType::Solid(_)) || self.paint_transform_has_area()
}
/// Fill a path with the current paint and fill rule.
pub fn fill_path(&mut self, path: &BezPath) {
if !self.paint_has_area() {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let Some(paint) = ctx.encode_current_paint() else {
return;
};
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>,
) {
#[cfg(feature = "wgpu")]
if self.gpu_strips.is_some() {
self.record_gpu_path(
paint,
fill_rule,
aliasing_threshold,
|generator, cull_bbox| {
generator.prepare_filled_path(path, transform, cull_bbox);
},
);
return;
}
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.map(|clip| 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(
path,
self.render_state.fill_rule,
transform,
self.aliasing_threshold,
);
}
/// Push a rectangular clip path.
pub fn push_clip_rect(&mut self, rect: &Rect) {
let transform = self.transforms().clip_path_transform();
if let Some(rect) = into_fast_path_rect(*rect, &transform, self.aliasing_threshold) {
self.viewport_state.push_clip_rect(&rect);
} else {
self.push_clip_path(&rect.to_path(DEFAULT_TOLERANCE));
}
}
/// Pop the most recently pushed clip path or clip rectangle from the clip stack.
///
/// Note that unlike `push_clip_layer`, it is permissible to have pending
/// pushed clips before finishing the rendering operation.
pub fn pop_clip(&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_has_area() {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let Some(paint) = ctx.encode_current_paint() else {
return;
};
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();
#[cfg(feature = "wgpu")]
if self.gpu_strips.is_some() {
self.record_gpu_path(
paint,
Fill::NonZero,
aliasing_threshold,
|generator, cull_bbox| {
generator.prepare_stroked_path(path, &stroke, transform, cull_bbox);
},
);
return;
}
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.map(|clip| 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_has_area() || rect.is_zero_area() {
return;
}
self.with_optional_filter_or_blend_layer(|ctx| {
let Some(paint) = ctx.encode_current_paint() else {
return;
};
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 let Some(transformed_rect) =
into_fast_path_rect(*rect, &transform, ctx.aliasing_threshold)
{
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<ClipRef<'_>>),
{
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]);
}
fn fast_rect_bounds(&self, rect: &Rect) -> Option<Rect> {
// 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();
let mut transformed_rect = into_fast_path_rect(*rect, &transform, self.aliasing_threshold)?
.intersect(self.active_rect());
if let Some(clip) = self.viewport_state.clip() {
let ClipShape::AxisAlignedRect(clip_rect) = clip.shape else {
return None;
};
transformed_rect = transformed_rect.intersect(clip_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.
#[track_caller]
pub fn fill_blurred_rounded_rect(
&mut self,
rect: &Rect,
radius: f32,
std_dev: f32,
invert: bool,
) {
if !cfg!(feature = "blurred_rounded_rect") {
panic!("blurred rounded rectangles require the `blurred_rounded_rect` feature");
}
if !self.paint_transform_has_area() {
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 Some(paint) = blurred_rect.encode_into(&mut ctx.encoded_paints, transform, None)
else {
return;
};
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 let Some(transformed_rect) =
into_fast_path_rect(inflated_rect, &path_transform, ctx.aliasing_threshold)
{
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::GpuGlyphRunBackend {
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.map(|clip| clip.path),
);
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.
#[track_caller]
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
let paint = paint.into();
assert_paint_supported(&paint);
self.render_state.paint = paint;
}
/// 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.aliasing_threshold = None;
self.recorder.reset(self.width, self.height);
self.filter = None;
#[cfg(feature = "wgpu")]
if let Some(gpu) = &self.gpu_strips {
gpu.borrow_mut().reset(self.width, self.height);
}
}
/// 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 {
core::mem::take(&mut self.render_state)
}
/// Save a copy of the current rendering state.
pub fn save_current_state(&mut self) -> RenderState {
self.render_state.clone()
}
/// Restore rendering state.
#[track_caller]
pub fn restore_state(&mut self, state: RenderState) {
assert_paint_supported(&state.paint);
self.render_state = state;
}
}
#[cfg(feature = "probe")]
impl vello_common::probe::ProbeRenderer for Scene {
fn set_transform(&mut self, transform: Affine) {
Self::set_transform(self, transform);
}
fn set_paint(&mut self, paint: PaintType) {
Self::set_paint(self, paint);
}
fn fill_path(&mut self, path: &BezPath) {
Self::fill_path(self, path);
}
fn fill_rect(&mut self, rect: &Rect) {
Self::fill_rect(self, rect);
}
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) {
Self::fill_blurred_rounded_rect(self, rect, radius, std_dev, invert);
}
fn push_layer(&mut self, blend_mode: Option<BlendMode>, opacity: Option<f32>) {
Self::push_layer(self, None, blend_mode, opacity, None, None);
}
fn push_filter_layer(&mut self, filter: Filter) {
Self::push_filter_layer(self, filter);
}
fn pop_layer(&mut self) {
Self::pop_layer(self);
}
fn set_paint_transform(&mut self, paint_transform: Affine) {
Self::set_paint_transform(self, paint_transform);
}
fn reset_paint_transform(&mut self) {
Self::reset_paint_transform(self);
}
}
#[track_caller]
fn assert_paint_supported(paint: &PaintType) {
match paint {
PaintType::Solid(_) => {}
PaintType::Gradient(gradient) => assert!(
cfg!(feature = "gradient_sweep") || !matches!(gradient.kind, GradientKind::Sweep(_)),
"sweep gradients require the `gradient_sweep` feature"
),
PaintType::Image(image) => assert!(
cfg!(feature = "image_bicubic") || image.sampler.quality != ImageQuality::High,
"bicubic image sampling requires the `image_bicubic` feature"
),
}
}
#[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;
#[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 rectangular_clip_keeps_direct_rect_draw_path() {
let mut scene = Scene::new(100, 100);
scene.push_clip_rect(&Rect::new(10.25, 5.5, 80.75, 90.0));
scene.push_clip_rect(&Rect::new(20.5, 0.0, 70.25, 60.75));
scene.fill_rect(&Rect::new(0.0, 20.25, 50.5, 80.0));
let RecordedDraw::Rect(rect) = &scene.recorder.draws[0] else {
panic!("expected a directly recorded rectangle");
};
assert_eq!(rect.rect, Rect::new(20.5, 20.25, 50.5, 60.75));
assert!(scene.strip_storage.borrow().strips.is_empty());
}
#[test]
fn reset_restores_default_aliasing_threshold() {
let mut scene = Scene::new(4, 4);
scene.set_aliasing_threshold(Some(128));
scene.reset();
assert_eq!(scene.aliasing_threshold, None);
}
#[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())
.unwrap();
assert!(resources.glyph_resources.is_none());
scene
.glyph_run(&mut resources, &font)
.atlas_cache(true)
.fill_glyphs(glyphs.into_iter())
.unwrap();
assert!(resources.glyph_resources.is_some());
}
}