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),
}
}