Compare commits

..
6 changed files with 46 additions and 73 deletions
+19
View File
@@ -216,6 +216,9 @@ pub struct Decoder {
out: VecDeque<Output>,
/// pts of the temporal unit being decoded.
pts: i64,
/// Bytes of the repeated-frame (`show_existing_frame`) pictures queued by the current
/// [`Decoder::decode_pts`] call; each is copied out in full when it is returned.
chunk_output_bytes: u64,
}
/// Whether `pictures` frames of `width` x `height`, padded as `FrameBuf::new` pads them, fit in
@@ -298,6 +301,7 @@ impl Decoder {
memory_budget: deckcraft_bitstream::budget::DECODE_MEMORY_BUDGET,
out: VecDeque::new(),
pts: 0,
chunk_output_bytes: 0,
}
}
@@ -330,6 +334,7 @@ impl Decoder {
/// [`Decoder::decode`] with a timestamp that the pictures shown by this chunk carry.
pub fn decode_pts(&mut self, data: &[u8], pts: i64) -> Result<Vec<Picture>> {
self.pts = pts;
self.chunk_output_bytes = 0;
let parsed = self.parse(data);
// Hand out what is finished, then report a parse error.
let mut pics = Vec::new();
@@ -500,6 +505,20 @@ impl Decoder {
let idx = fh.frame_to_show_map_idx;
let slot = self.ref_frames[idx].clone().ok_or(Error::Invalid("show_existing_frame of an empty slot"))?;
let info = self.refs[idx].clone();
// Every repeat is copied out whole, so a compact header must not queue more
// pictures than half the decoder budget allows.
let shape = deckcraft_bitstream::budget::PictureShape {
width: u64::from(info.upscaled_width),
height: u64::from(info.frame_height),
shift_x: u32::from(seq.color.subsampling_x),
shift_y: u32::from(seq.color.subsampling_y),
chroma_planes: if seq.color.mono_chrome { 0 } else { 2 },
bytes_per_sample: 2,
};
self.chunk_output_bytes = self.chunk_output_bytes.saturating_add(shape.bytes().unwrap_or(u64::MAX));
if self.chunk_output_bytes > self.memory_budget / 2 {
return Err(Error::Invalid("repeated frame outputs exceed the decoder memory budget"));
}
let o = Output::Existing {
slot: slot.clone(),
seq: seq.clone(),
+3 -1
View File
@@ -404,7 +404,9 @@ impl Decoder {
chroma_planes: if chroma == 0 { 0 } else { 2 },
bytes_per_sample: 2,
};
let pictures = u64::from(sps.max_dec_pic_buffering) + 1 + self.max_in_flight as u64;
// Output frames waiting to be returned stay allocated after they leave the DPB.
let queued = self.out_queue.iter().filter(|o| !self.dpb.entries.iter().any(|e| Arc::ptr_eq(&e.frame, &o.frame))).count();
let pictures = u64::from(sps.max_dec_pic_buffering) + 1 + self.max_in_flight as u64 + queued as u64;
ensure!(
shape.fits(pictures, self.memory_budget),
"{}x{} pictures with {pictures} in the decoder exceed the memory budget",
+1 -3
View File
@@ -125,9 +125,7 @@ pub fn draw(ctx: &mut RenderContext, bevel: &Bevel, fill: Option<Rgba>, 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));
// 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));
let polys = crate::pathgrad::polygons(path);
if polys.is_empty() {
return;
}
+1 -15
View File
@@ -11,8 +11,6 @@ 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.
@@ -28,7 +26,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(), work: 0 };
let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new() };
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.
@@ -52,8 +50,6 @@ 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<bool>,
/// Fills plus gradient stops painted so far, against [`MAX_WORK`].
work: usize,
}
fn affine(t: &Transform) -> Affine {
@@ -178,16 +174,6 @@ 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);
+15 -53
View File
@@ -25,16 +25,6 @@ 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`.
@@ -124,31 +114,21 @@ fn shape_position(outline: &[Vec<Point>], c: Point, p: Point) -> f64 {
pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
let mut out: Vec<Vec<Point>> = Vec::new();
let mut cur: Vec<Point> = Vec::new();
let mut total = 0usize;
kurbo::flatten(path.iter(), 0.25, |el| {
total += 1;
if total > MAX_POINTS {
return;
}
match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push(std::mem::take(&mut cur));
}
cur = vec![p];
kurbo::flatten(path.iter(), 0.25, |el| match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push(std::mem::take(&mut cur));
}
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath => {
if let Some(f) = cur.first().copied() {
cur.push(f);
}
}
_ => {}
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);
}
@@ -166,7 +146,7 @@ pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
/// `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.
pub(crate) fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
let edges = |o: &[Vec<Point>]| o.iter().map(|p| p.len().saturating_sub(1)).sum::<usize>();
if edges(&outline) <= max_edges {
return outline;
@@ -253,17 +233,13 @@ 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 max_edges = edge_budget(pw, ph);
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 pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
simplify(polygons(path), pixel, max_edges)
} else {
Vec::new()
};
// An outline `polygons` refused (too many points) has no contours: skip the shape rather than
// paint it with the rectangle fallback's different gradient.
if kind == "shape" && outline.is_empty() {
return;
}
let lut = ramp(stops);
let a = alpha.clamp(0.0, 1.0);
let mut data = Vec::with_capacity((pw * ph) as usize);
@@ -396,20 +372,6 @@ mod tests {
assert!(t.elapsed() < std::time::Duration::from_secs(20), "{:?}", t.elapsed());
}
#[test]
fn a_shape_gradient_with_no_usable_outline_paints_nothing() {
// One polygon over the per-polygon cap is dropped by `polygons`.
let path = many_segment_square(26_000);
assert!(polygons(&path).is_empty());
let mut ctx = RenderContext::new(64, 64);
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (1.0, Rgba::rgb(255, 255, 255))];
fill(&mut ctx, "shape", CENTRE, &stops, Rect::new(0.0, 0.0, 64.0, 64.0), &path, Affine::IDENTITY, 1.0);
ctx.flush();
let mut px = vec![0u8; 64 * 64 * 4];
ctx.render(vello_cpu::PixmapMut::new(64, 64, &mut px).unwrap(), &mut vello_cpu::Resources::new());
assert!(px.chunks(4).all(|p| p[3] == 0), "nothing was painted");
}
#[test]
fn the_ramp_runs_through_the_stops() {
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (0.5, Rgba::rgb(128, 128, 128)), (1.0, Rgba::rgb(255, 255, 255))];
+7 -1
View File
@@ -166,11 +166,17 @@ impl Decoder {
if frames.len() > 1 {
self.stats.superframes += 1;
}
let queued = self.pending.len();
for f in frames {
if f.is_empty() {
continue;
}
self.decode_frame(f, pts)?;
if let Err(e) = self.decode_frame(f, pts) {
// A failed sample outputs nothing: drop what its earlier subframes queued so a
// later drain does not expand each repeat into its own picture.
self.pending.truncate(queued);
return Err(e);
}
}
Ok(self.drain(false))
}