| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Basic render operations. |
| |
| use alloc::vec; |
| use alloc::vec::Vec; |
| use vello_common::coarse::{MODE_HYBRID, Wide}; |
| use vello_common::encode::{EncodeExt, EncodedPaint}; |
| use vello_common::fearless_simd::Level; |
| use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, GlyphType, PreparedGlyph}; |
| use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke}; |
| use vello_common::mask::Mask; |
| use vello_common::paint::{Paint, PaintType}; |
| use vello_common::peniko::Font; |
| use vello_common::peniko::color::palette::css::BLACK; |
| use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; |
| use vello_common::recording::{PushLayerCommand, Recordable, Recording, RenderCommand}; |
| use vello_common::strip::Strip; |
| use vello_common::strip_generator::StripGenerator; |
| |
| /// Default tolerance for curve flattening |
| pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1; |
| |
| /// A render state which contains the style properties for path rendering and |
| /// the current transform. |
| #[derive(Debug)] |
| struct RenderState { |
| pub(crate) paint: PaintType, |
| pub(crate) paint_transform: Affine, |
| pub(crate) stroke: Stroke, |
| pub(crate) transform: Affine, |
| pub(crate) fill_rule: Fill, |
| pub(crate) blend_mode: BlendMode, |
| pub(crate) alphas: Vec<u8>, |
| } |
| |
| /// 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 { |
| pub(crate) width: u16, |
| pub(crate) height: u16, |
| pub(crate) wide: Wide<MODE_HYBRID>, |
| pub(crate) paint: PaintType, |
| pub(crate) paint_transform: Affine, |
| pub(crate) anti_alias: bool, |
| pub(crate) encoded_paints: Vec<EncodedPaint>, |
| paint_visible: bool, |
| pub(crate) stroke: Stroke, |
| pub(crate) transform: Affine, |
| pub(crate) fill_rule: Fill, |
| pub(crate) blend_mode: BlendMode, |
| pub(crate) strip_generator: StripGenerator, |
| } |
| |
| impl Scene { |
| /// Create a new render context with the given width and height in pixels. |
| pub fn new(width: u16, height: u16) -> Self { |
| let render_state = Self::default_render_state(); |
| Self { |
| width, |
| height, |
| wide: Wide::<MODE_HYBRID>::new(width, height), |
| anti_alias: true, |
| paint: render_state.paint, |
| paint_transform: render_state.paint_transform, |
| encoded_paints: vec![], |
| paint_visible: true, |
| stroke: render_state.stroke, |
| strip_generator: StripGenerator::new( |
| width, |
| height, |
| Level::try_detect().unwrap_or(Level::fallback()), |
| ), |
| transform: render_state.transform, |
| fill_rule: render_state.fill_rule, |
| blend_mode: render_state.blend_mode, |
| } |
| } |
| |
| /// Create default rendering state. |
| fn default_render_state() -> RenderState { |
| let transform = Affine::IDENTITY; |
| let fill_rule = Fill::NonZero; |
| let paint = BLACK.into(); |
| let paint_transform = Affine::IDENTITY; |
| let stroke = Stroke { |
| width: 1.0, |
| join: Join::Bevel, |
| start_cap: Cap::Butt, |
| end_cap: Cap::Butt, |
| ..Default::default() |
| }; |
| let blend_mode = BlendMode::new(Mix::Normal, Compose::SrcOver); |
| RenderState { |
| transform, |
| fill_rule, |
| paint, |
| paint_transform, |
| stroke, |
| blend_mode, |
| alphas: vec![], |
| } |
| } |
| |
| fn encode_current_paint(&mut self) -> Paint { |
| match self.paint.clone() { |
| PaintType::Solid(s) => s.into(), |
| PaintType::Gradient(_) => { |
| unimplemented!("Gradient not implemented") |
| } |
| PaintType::Image(i) => i.encode_into( |
| &mut self.encoded_paints, |
| self.transform * self.paint_transform, |
| ), |
| } |
| } |
| |
| /// Fill a path with the current paint and fill rule. |
| pub fn fill_path(&mut self, path: &BezPath) { |
| if !self.paint_visible { |
| return; |
| } |
| |
| let paint = self.encode_current_paint(); |
| self.fill_path_with(path, self.transform, self.fill_rule, paint, self.anti_alias); |
| } |
| |
| /// Build strips for a filled path with the given properties. |
| fn fill_path_with( |
| &mut self, |
| path: &BezPath, |
| transform: Affine, |
| fill_rule: Fill, |
| paint: Paint, |
| anti_alias: bool, |
| ) { |
| let wide = &mut self.wide; |
| let func = |strips| wide.generate(strips, fill_rule, paint, 0); |
| self.strip_generator |
| .generate_filled_path(path, fill_rule, transform, anti_alias, func); |
| } |
| |
| /// Stroke a path with the current paint and stroke settings. |
| pub fn stroke_path(&mut self, path: &BezPath) { |
| if !self.paint_visible { |
| return; |
| } |
| |
| let paint = self.encode_current_paint(); |
| self.stroke_path_with(path, self.transform, paint, self.anti_alias); |
| } |
| |
| /// Build strips for a stroked path with the given properties. |
| fn stroke_path_with( |
| &mut self, |
| path: &BezPath, |
| transform: Affine, |
| paint: Paint, |
| anti_alias: bool, |
| ) { |
| let wide = &mut self.wide; |
| let func = |strips| wide.generate(strips, Fill::NonZero, paint, 0); |
| self.strip_generator |
| .generate_stroked_path(path, &self.stroke, transform, anti_alias, func); |
| } |
| |
| /// Set whether to enable anti-aliasing. |
| pub fn set_anti_aliasing(&mut self, value: bool) { |
| self.anti_alias = value; |
| } |
| |
| /// Fill a rectangle with the current paint and fill rule. |
| pub fn fill_rect(&mut self, rect: &Rect) { |
| self.fill_path(&rect.to_path(DEFAULT_TOLERANCE)); |
| } |
| |
| /// 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)); |
| } |
| |
| /// Creates a builder for drawing a run of glyphs that have the same attributes. |
| pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> { |
| GlyphRunBuilder::new(font.clone(), self.transform, self) |
| } |
| |
| /// Push a new layer with the given properties. |
| /// |
| /// Only `clip_path` is supported for now. |
| pub fn push_layer( |
| &mut self, |
| clip_path: Option<&BezPath>, |
| blend_mode: Option<BlendMode>, |
| opacity: Option<f32>, |
| mask: Option<Mask>, |
| ) { |
| let clip = if let Some(c) = clip_path { |
| let mut strip_buf = &[][..]; |
| |
| self.strip_generator.generate_filled_path( |
| c, |
| self.fill_rule, |
| self.transform, |
| self.anti_alias, |
| |strips| strip_buf = strips, |
| ); |
| |
| Some((strip_buf, self.fill_rule)) |
| } else { |
| None |
| }; |
| |
| // Mask is unsupported. Blend is partially supported. |
| if mask.is_some() { |
| unimplemented!() |
| } |
| |
| self.wide.push_layer( |
| clip, |
| blend_mode.unwrap_or(BlendMode::new(Mix::Normal, Compose::SrcOver)), |
| None, |
| opacity.unwrap_or(1.), |
| 0, |
| ); |
| } |
| |
| /// Push a new clip layer. |
| pub fn push_clip_layer(&mut self, path: &BezPath) { |
| self.push_layer(Some(path), None, None, None); |
| } |
| |
| /// Pop the last pushed layer. |
| pub fn pop_layer(&mut self) { |
| self.wide.pop_layer(); |
| } |
| |
| /// Set the blend mode for subsequent rendering operations. |
| pub fn set_blend_mode(&mut self, blend_mode: BlendMode) { |
| self.blend_mode = blend_mode; |
| } |
| |
| /// Set the stroke settings for subsequent stroke operations. |
| pub fn set_stroke(&mut self, stroke: Stroke) { |
| self.stroke = 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.paint = paint.into(); |
| self.paint_visible = match &self.paint { |
| PaintType::Solid(color) => color.components[3] != 0.0, |
| _ => true, |
| }; |
| } |
| |
| /// 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.paint_transform = paint_transform; |
| } |
| |
| /// Reset the current paint transform. |
| pub fn reset_paint_transform(&mut self) { |
| self.paint_transform = Affine::IDENTITY; |
| } |
| |
| /// Set the fill rule for subsequent fill operations. |
| pub fn set_fill_rule(&mut self, fill_rule: Fill) { |
| self.fill_rule = fill_rule; |
| } |
| |
| /// Set the transform for subsequent rendering operations. |
| pub fn set_transform(&mut self, transform: Affine) { |
| self.transform = transform; |
| } |
| |
| /// Reset the transform to identity. |
| pub fn reset_transform(&mut self) { |
| self.transform = Affine::IDENTITY; |
| } |
| |
| /// Reset scene to default values. |
| pub fn reset(&mut self) { |
| self.wide.reset(); |
| self.strip_generator.reset(); |
| self.encoded_paints.clear(); |
| |
| let render_state = Self::default_render_state(); |
| self.transform = render_state.transform; |
| self.paint_transform = render_state.paint_transform; |
| self.fill_rule = render_state.fill_rule; |
| self.paint = render_state.paint; |
| self.stroke = render_state.stroke; |
| self.blend_mode = render_state.blend_mode; |
| } |
| |
| /// 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 |
| } |
| } |
| |
| impl GlyphRenderer for Scene { |
| fn fill_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) { |
| match prepared_glyph.glyph_type { |
| GlyphType::Outline(glyph) => { |
| let paint = self.encode_current_paint(); |
| self.fill_path_with( |
| glyph.path, |
| prepared_glyph.transform, |
| Fill::NonZero, |
| paint, |
| self.anti_alias, |
| ); |
| } |
| GlyphType::Bitmap(_) => {} |
| GlyphType::Colr(_) => {} |
| } |
| } |
| |
| fn stroke_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) { |
| match prepared_glyph.glyph_type { |
| GlyphType::Outline(glyph) => { |
| let paint = self.encode_current_paint(); |
| self.stroke_path_with(glyph.path, prepared_glyph.transform, paint, self.anti_alias); |
| } |
| GlyphType::Bitmap(_) => {} |
| GlyphType::Colr(_) => {} |
| } |
| } |
| } |
| |
| impl Recordable for Scene { |
| fn prepare_recording(&mut self, recording: &mut Recording) { |
| let buffers = recording.take_cached_strips(); |
| let (strips, alphas, strip_start_indices) = |
| self.generate_strips_from_commands(recording.commands(), buffers); |
| recording.set_cached_strips(strips, alphas, strip_start_indices); |
| } |
| |
| fn execute_recording(&mut self, recording: &Recording) { |
| let (cached_strips, cached_alphas) = recording.get_cached_strips(); |
| let adjusted_strips = self.prepare_cached_strips(cached_strips, cached_alphas); |
| |
| // Use pre-calculated strip start indices from when we generated the cache |
| let strip_start_indices = recording.get_strip_start_indices(); |
| let mut range_index = 0; |
| |
| // Replay commands in order, using cached strips for geometry |
| for command in recording.commands() { |
| match command { |
| RenderCommand::FillPath(_) |
| | RenderCommand::StrokePath(_) |
| | RenderCommand::FillRect(_) |
| | RenderCommand::StrokeRect(_) |
| | RenderCommand::FillOutlineGlyph(_) |
| | RenderCommand::StrokeOutlineGlyph(_) => { |
| self.process_geometry_command( |
| command, |
| strip_start_indices, |
| range_index, |
| &adjusted_strips, |
| ); |
| range_index += 1; |
| } |
| RenderCommand::SetPaint(paint) => { |
| self.set_paint(paint.clone()); |
| } |
| RenderCommand::SetPaintTransform(transform) => { |
| self.set_paint_transform(*transform); |
| } |
| RenderCommand::ResetPaintTransform => { |
| self.reset_paint_transform(); |
| } |
| RenderCommand::SetTransform(transform) => { |
| self.set_transform(*transform); |
| } |
| RenderCommand::SetFillRule(fill_rule) => { |
| self.set_fill_rule(*fill_rule); |
| } |
| RenderCommand::SetStroke(stroke) => { |
| self.set_stroke(stroke.clone()); |
| } |
| RenderCommand::PushLayer(PushLayerCommand { |
| clip_path, |
| blend_mode, |
| opacity, |
| mask, |
| }) => { |
| self.push_layer(clip_path.as_ref(), *blend_mode, *opacity, mask.clone()); |
| } |
| RenderCommand::PopLayer => { |
| self.pop_layer(); |
| } |
| } |
| } |
| } |
| } |
| |
| /// Recording management implementation. |
| impl Scene { |
| /// Generate strips from strip commands and capture ranges. |
| /// |
| /// Returns: |
| /// - `collected_strips`: The generated strips. |
| /// - `collected_alphas`: The generated alphas. |
| /// - `strip_start_indices`: The start indices of strips for each geometry command. |
| fn generate_strips_from_commands( |
| &mut self, |
| commands: &[RenderCommand], |
| buffers: (Vec<Strip>, Vec<u8>, Vec<usize>), |
| ) -> (Vec<Strip>, Vec<u8>, Vec<usize>) { |
| let (mut collected_strips, mut cached_alphas, mut strip_start_indices) = buffers; |
| collected_strips.clear(); |
| cached_alphas.clear(); |
| strip_start_indices.clear(); |
| |
| let saved_state = self.take_current_state(cached_alphas); |
| |
| for command in commands { |
| let start_index = collected_strips.len(); |
| |
| match command { |
| RenderCommand::FillPath(path) => { |
| self.generate_fill_strips(path, &mut collected_strips, self.transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::StrokePath(path) => { |
| self.generate_stroke_strips(path, &mut collected_strips, self.transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::FillRect(rect) => { |
| let path = rect.to_path(DEFAULT_TOLERANCE); |
| self.generate_fill_strips(&path, &mut collected_strips, self.transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::StrokeRect(rect) => { |
| let path = rect.to_path(DEFAULT_TOLERANCE); |
| self.generate_stroke_strips(&path, &mut collected_strips, self.transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::FillOutlineGlyph((path, transform)) => { |
| let glyph_transform = self.transform * *transform; |
| self.generate_fill_strips(path, &mut collected_strips, glyph_transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::StrokeOutlineGlyph((path, transform)) => { |
| let glyph_transform = self.transform * *transform; |
| self.generate_stroke_strips(path, &mut collected_strips, glyph_transform); |
| strip_start_indices.push(start_index); |
| } |
| RenderCommand::SetTransform(transform) => { |
| self.transform = *transform; |
| } |
| RenderCommand::SetFillRule(fill_rule) => { |
| self.fill_rule = *fill_rule; |
| } |
| RenderCommand::SetStroke(stroke) => { |
| self.stroke = stroke.clone(); |
| } |
| _ => {} |
| } |
| } |
| |
| let collected_alphas = self.strip_generator.take_alpha_buf(); |
| self.restore_state(saved_state); |
| |
| (collected_strips, collected_alphas, strip_start_indices) |
| } |
| |
| fn process_geometry_command( |
| &mut self, |
| command: &RenderCommand, |
| strip_start_indices: &[usize], |
| range_index: usize, |
| adjusted_strips: &[Strip], |
| ) { |
| assert!( |
| range_index < strip_start_indices.len(), |
| "Strip range index out of bounds: range_index={}, strip_start_indices.len()={}", |
| range_index, |
| strip_start_indices.len() |
| ); |
| let start = strip_start_indices[range_index]; |
| let end = strip_start_indices |
| .get(range_index + 1) |
| .copied() |
| .unwrap_or(adjusted_strips.len()); |
| let count = end - start; |
| assert!( |
| start < adjusted_strips.len() && count > 0, |
| "Invalid strip range: start={start}, end={end}, count={count}" |
| ); |
| let paint = self.encode_current_paint(); |
| let fill_rule = match command { |
| RenderCommand::FillPath(_) | RenderCommand::FillRect(_) => self.fill_rule, |
| RenderCommand::StrokePath(_) | RenderCommand::StrokeRect(_) => Fill::NonZero, |
| _ => Fill::NonZero, |
| }; |
| self.wide |
| .generate(&adjusted_strips[start..end], fill_rule, paint, 0); |
| } |
| |
| /// Prepare cached strips for rendering by adjusting alpha indices and extending alpha buffer. |
| #[expect( |
| clippy::cast_possible_truncation, |
| reason = "Alphas length conversion is safe in this case" |
| )] |
| fn prepare_cached_strips( |
| &mut self, |
| cached_strips: &[Strip], |
| cached_alphas: &[u8], |
| ) -> Vec<Strip> { |
| // Calculate offset for alpha indices based on current buffer size. |
| let alpha_offset = self.strip_generator.alpha_buf().len() as u32; |
| // Extend current alpha buffer with cached alphas. |
| self.strip_generator.extend_alpha_buf(cached_alphas); |
| // Create adjusted strips with corrected alpha indices |
| cached_strips |
| .iter() |
| .map(move |strip| { |
| let mut adjusted_strip = *strip; |
| adjusted_strip.alpha_idx += alpha_offset; |
| adjusted_strip |
| }) |
| .collect() |
| } |
| |
| /// Generate strips for a filled path. |
| fn generate_fill_strips(&mut self, path: &BezPath, strips: &mut Vec<Strip>, transform: Affine) { |
| self.strip_generator.generate_filled_path( |
| path, |
| self.fill_rule, |
| transform, |
| self.anti_alias, |
| |generated_strips| { |
| strips.extend_from_slice(generated_strips); |
| }, |
| ); |
| } |
| |
| /// Generate strips for a stroked path. |
| fn generate_stroke_strips( |
| &mut self, |
| path: &BezPath, |
| strips: &mut Vec<Strip>, |
| transform: Affine, |
| ) { |
| self.strip_generator.generate_stroked_path( |
| path, |
| &self.stroke, |
| transform, |
| self.anti_alias, |
| |generated_strips| { |
| strips.extend_from_slice(generated_strips); |
| }, |
| ); |
| } |
| |
| /// Save current rendering state. |
| fn take_current_state(&mut self, cached_alphas: Vec<u8>) -> RenderState { |
| RenderState { |
| paint: self.paint.clone(), |
| paint_transform: self.paint_transform, |
| transform: self.transform, |
| fill_rule: self.fill_rule, |
| blend_mode: self.blend_mode, |
| stroke: core::mem::take(&mut self.stroke), |
| alphas: self.strip_generator.replace_alpha_buf(cached_alphas), |
| } |
| } |
| |
| /// Restore rendering state. |
| fn restore_state(&mut self, state: RenderState) { |
| self.paint = state.paint; |
| self.paint_transform = state.paint_transform; |
| self.stroke = state.stroke; |
| self.transform = state.transform; |
| self.fill_rule = state.fill_rule; |
| self.blend_mode = state.blend_mode; |
| self.strip_generator.set_alpha_buf(state.alphas); |
| } |
| } |