| // 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()); | |
| } | |
| } |