//! Encoding of a guiduck fragment tree into a `vello::Scene`. //! //! One encoder, one definition of how a fragment's display list becomes scene //! commands. A per-fragment scene cache lived here once, serving unchanged //! self-contained fragments through `Scene::append`; it was removed after it //! produced pixels that disagreed with encoding in place, in a way that never //! reproduced outside a running application. What it saved was the smaller //! half of the win — the decisive one is [`VelloBackend`] skipping the whole //! encode when nothing in the store changed, which is untouched. A cache can //! come back, designed from a clean start and checked against this encoder. //! //! [`VelloBackend`]: crate::VelloBackend use guiduck_scene::geom::Affine; use guiduck_scene::paint::Fill; use guiduck_scene::{DisplayItem, FragmentId, FragmentStore, RenderError, ScopeTracker, Shape}; /// Nesting deeper than this is treated as a cycle in the fragment graph. const MAX_DEPTH: usize = 256; /// Resolves a `Child` display item while encoding: receives the target /// scene, the child fragment, its composed transform, and its depth. type ChildResolver<'a> = dyn FnMut(&mut vello::Scene, FragmentId, Affine, usize) -> Result<(), RenderError> + 'a; /// Encode the fragment tree rooted at `root` into `scene`, with `root_transform` /// applied to everything (typically the logical-to-physical scale). /// /// The scene is not reset; callers reset it per frame so that encoding can /// later compose multiple trees if needed. pub fn encode_fragment_tree( scene: &mut vello::Scene, root: FragmentId, store: &FragmentStore, root_transform: Affine, ) -> Result<(), RenderError> { encode_plain(scene, root, store, root_transform, 0) } fn encode_plain( scene: &mut vello::Scene, id: FragmentId, store: &FragmentStore, base: Affine, depth: usize, ) -> Result<(), RenderError> { encode_fragment(scene, id, store, base, depth, &mut |scene, child, t, d| { encode_plain(scene, child, store, t, d) }) } /// Encode one fragment's display list, delegating `Child` items to `child`. /// This is the single definition of how display items become scene commands. fn encode_fragment( scene: &mut vello::Scene, id: FragmentId, store: &FragmentStore, base: Affine, depth: usize, child: &mut ChildResolver<'_>, ) -> Result<(), RenderError> { if depth > MAX_DEPTH { return Err(RenderError::ExcessiveDepth(id)); } let fragment = store.get(id).ok_or(RenderError::MissingFragment(id))?; // Transforms nest by composition, so PushTransform saves the current one; // clips and layers both live on vello's layer stack. The shared tracker // rejects pops of the wrong kind, which would otherwise silently pop the // wrong vello scope. let mut transform = base; let mut transform_stack: Vec = Vec::new(); let mut scopes = ScopeTracker::default(); for item in &fragment.items { if !scopes.apply(item) { return Err(RenderError::UnbalancedFragment(id)); } match item { DisplayItem::Fill { shape, brush, rule } => { fill_shape(scene, *rule, transform, brush, shape); } DisplayItem::Stroke { shape, brush, style, } => match shape { Shape::Rect(rect) => scene.stroke(style, transform, brush, None, rect), Shape::RoundedRect(rrect) => scene.stroke(style, transform, brush, None, rrect), Shape::Path(path) => scene.stroke(style, transform, brush, None, path), }, DisplayItem::GlyphRun(run) => { scene .draw_glyphs(&run.font) .font_size(run.size) .transform(transform) .normalized_coords(&run.normalized_coords) .hint(run.hint) .brush(&run.brush) .draw( Fill::NonZero, run.glyphs.iter().map(|g| vello::Glyph { id: g.id, x: g.x, y: g.y, }), ); } DisplayItem::Image { image, dest } => { let (w, h) = (image.image.width, image.image.height); if w == 0 || h == 0 { continue; } let fit = Affine::translate((dest.x0, dest.y0)) * Affine::scale_non_uniform(dest.width() / w as f64, dest.height() / h as f64); scene.draw_image(image, transform * fit); } DisplayItem::PushClip(shape) => { push_clip_shape(scene, transform, shape); } DisplayItem::PushLayer { alpha, blend, bounds, } => match bounds { Shape::Rect(rect) => { scene.push_layer(Fill::NonZero, *blend, *alpha, transform, rect); } Shape::RoundedRect(rrect) => { scene.push_layer(Fill::NonZero, *blend, *alpha, transform, rrect); } Shape::Path(path) => { scene.push_layer(Fill::NonZero, *blend, *alpha, transform, path); } }, DisplayItem::PopClip | DisplayItem::PopLayer => { scene.pop_layer(); } DisplayItem::PushTransform(t) => { transform_stack.push(transform); transform *= *t; } DisplayItem::PopTransform => { transform = transform_stack .pop() .ok_or(RenderError::UnbalancedFragment(id))?; } DisplayItem::Child { transform: placement, fragment, } => { child(scene, *fragment, transform * *placement, depth + 1)?; } } } if !scopes.is_closed() || !transform_stack.is_empty() { return Err(RenderError::UnbalancedFragment(id)); } Ok(()) } fn fill_shape( scene: &mut vello::Scene, style: Fill, transform: Affine, brush: &guiduck_scene::paint::Brush, shape: &Shape, ) { match shape { Shape::Rect(rect) => scene.fill(style, transform, brush, None, rect), Shape::RoundedRect(rrect) => scene.fill(style, transform, brush, None, rrect), Shape::Path(path) => scene.fill(style, transform, brush, None, path), } } fn push_clip_shape(scene: &mut vello::Scene, transform: Affine, shape: &Shape) { match shape { Shape::Rect(rect) => scene.push_clip_layer(Fill::NonZero, transform, rect), Shape::RoundedRect(rrect) => scene.push_clip_layer(Fill::NonZero, transform, rrect), Shape::Path(path) => scene.push_clip_layer(Fill::NonZero, transform, path), } }