encode.rs raw

//! 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<Affine> = 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),
    }
}