From f1cd1b2499bfc825c18e3500ac3662e21d705f48 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 18:35:00 +0000 Subject: [PATCH] Render: bound bevel edge work, shape-gradient flattening and colour-glyph fills --- crates/render/src/bevel.rs | 4 ++- crates/render/src/colr.rs | 16 +++++++++++- crates/render/src/pathgrad.rs | 47 ++++++++++++++++++++++++----------- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/crates/render/src/bevel.rs b/crates/render/src/bevel.rs index 854d3c775..e3b8d4469 100644 --- a/crates/render/src/bevel.rs +++ b/crates/render/src/bevel.rs @@ -125,7 +125,9 @@ pub fn draw(ctx: &mut RenderContext, bevel: &Bevel, fill: Option, path: &B let longest = bounds.width().max(bounds.height()) * k; let s = (MAX_SIDE / longest).min(1.0) * k; let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 2048), ((bounds.height() * s).ceil() as u32).clamp(1, 2048)); - let polys = crate::pathgrad::polygons(path); + // Each pixel tests every edge, so a dense outline is simplified to the raster's edge budget. + let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph)); + let polys = crate::pathgrad::simplify(crate::pathgrad::polygons(path), pixel, crate::pathgrad::edge_budget(pw, ph)); if polys.is_empty() { return; } diff --git a/crates/render/src/colr.rs b/crates/render/src/colr.rs index 97294aa81..28c09757c 100644 --- a/crates/render/src/colr.rs +++ b/crates/render/src/colr.rs @@ -11,6 +11,8 @@ use vello_cpu::{RenderContext, peniko}; /// Nesting depth of clips and layers past which a glyph opens no more (hostile fonts). const MAX_DEPTH: usize = 64; +/// Fills and gradient stops one colour glyph may paint; the rest of a hostile glyph is skipped. +const MAX_WORK: usize = 100_000; /// Paint `gid` of `face` as a colour glyph, its origin mapped by `at` (y down, points) at `scale` /// points per font unit. `fg` is the text colour (palette index 0xFFFF) and `alpha` fades it all. @@ -26,7 +28,7 @@ pub fn paint(ctx: &mut RenderContext, face: &FontFace, gid: u32, at: Affine, sca .unwrap_or_default(); // Font units are y up; the slide is y down. let base = at * Affine::scale(scale) * Affine::scale_non_uniform(1.0, -1.0); - let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new() }; + let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new(), work: 0 }; p.ctx.push_layer(None, None, Some(alpha.clamp(0.0, 1.0) as f32), None, None); let ok = glyph.paint(face.location(), &mut p).is_ok(); // A malformed glyph may leave layers open: close them all. @@ -50,6 +52,8 @@ struct Painter<'a> { /// Clips and composite groups pushed by the glyph, innermost last: true when a layer was /// opened on the context for it (false past `MAX_DEPTH`, so its pop is skipped too). opened: Vec, + /// Fills plus gradient stops painted so far, against [`MAX_WORK`]. + work: usize, } fn affine(t: &Transform) -> Affine { @@ -174,6 +178,16 @@ impl ColorPainter for Painter<'_> { } fn fill(&mut self, brush: Brush<'_>) { + let stops = match &brush { + Brush::Solid { .. } => 0, + Brush::LinearGradient { color_stops, .. } | Brush::RadialGradient { color_stops, .. } | Brush::SweepGradient { color_stops, .. } => { + color_stops.len() + } + }; + self.work = self.work.saturating_add(1 + stops); + if self.work > MAX_WORK { + return; + } match brush { Brush::Solid { palette_index, alpha } => { let c = self.color(palette_index, alpha); diff --git a/crates/render/src/pathgrad.rs b/crates/render/src/pathgrad.rs index 168d9e0a3..e23ebaeb2 100644 --- a/crates/render/src/pathgrad.rs +++ b/crates/render/src/pathgrad.rs @@ -25,6 +25,16 @@ const MAX_EDGE_WORK: u64 = 64_000_000; /// Fewest edges a simplified polygon keeps, so a coarse outline still has a shape. const MIN_POLY_EDGES: usize = 8; +/// Most points `polygons` keeps from one path; a path flattening past this (a huge finite curve) is +/// not drawn rather than held in memory first. +const MAX_POINTS: usize = 2_000_000; + +/// The edge budget for a `pw` x `ph` raster where each pixel tests every edge. +pub(crate) fn edge_budget(pw: u32, ph: u32) -> usize { + let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1); + usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES) +} + /// Colour position (0 at the focus, 1 at the edge) of `p` for path `kind` in `bounds` with the /// focus rect `focus` (fractions inset from left, top, right, bottom), `outline` being the /// shape's flattened polygon(s) for `shape`. @@ -114,21 +124,31 @@ fn shape_position(outline: &[Vec], c: Point, p: Point) -> f64 { pub fn polygons(path: &BezPath) -> Vec> { let mut out: Vec> = Vec::new(); let mut cur: Vec = Vec::new(); - kurbo::flatten(path.iter(), 0.25, |el| match el { - PathEl::MoveTo(p) => { - if cur.len() > 1 { - out.push(std::mem::take(&mut cur)); - } - cur = vec![p]; + let mut total = 0usize; + kurbo::flatten(path.iter(), 0.25, |el| { + total += 1; + if total > MAX_POINTS { + return; } - PathEl::LineTo(p) => cur.push(p), - PathEl::ClosePath => { - if let Some(f) = cur.first().copied() { - cur.push(f); + match el { + PathEl::MoveTo(p) => { + if cur.len() > 1 { + out.push(std::mem::take(&mut cur)); + } + cur = vec![p]; } + PathEl::LineTo(p) => cur.push(p), + PathEl::ClosePath => { + if let Some(f) = cur.first().copied() { + cur.push(f); + } + } + _ => {} } - _ => {} }); + if total > MAX_POINTS { + return Vec::new(); + } if cur.len() > 1 { out.push(cur); } @@ -146,7 +166,7 @@ pub fn polygons(path: &BezPath) -> Vec> { /// `outline` cut down to at most about `max_edges` edges in total: first by dropping points /// closer than `tol` to the last kept one (detail finer than a raster pixel), then, if still too /// many, by keeping every k-th point. Each polygon stays closed. Returns it unchanged when it fits. -fn simplify(outline: Vec>, tol: f64, max_edges: usize) -> Vec> { +pub(crate) fn simplify(outline: Vec>, tol: f64, max_edges: usize) -> Vec> { let edges = |o: &[Vec]| o.iter().map(|p| p.len().saturating_sub(1)).sum::(); if edges(&outline) <= max_edges { return outline; @@ -233,8 +253,7 @@ pub fn fill(ctx: &mut RenderContext, kind: &str, focus: [f64; 4], stops: &[(f64, let s = (MAX_SIDE / longest).min(1.0) * k; let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 4096), ((bounds.height() * s).ceil() as u32).clamp(1, 4096)); let outline = if kind == "shape" { - let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1); - let max_edges = usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES); + let max_edges = edge_budget(pw, ph); let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph)); simplify(polygons(path), pixel, max_edges) } else {