Compare commits

..
4 changed files with 48 additions and 36 deletions
-19
View File
@@ -216,9 +216,6 @@ 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
@@ -301,7 +298,6 @@ impl Decoder {
memory_budget: deckcraft_bitstream::budget::DECODE_MEMORY_BUDGET,
out: VecDeque::new(),
pts: 0,
chunk_output_bytes: 0,
}
}
@@ -334,7 +330,6 @@ 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();
@@ -505,20 +500,6 @@ 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(),
+1 -3
View File
@@ -404,9 +404,7 @@ impl Decoder {
chroma_planes: if chroma == 0 { 0 } else { 2 },
bytes_per_sample: 2,
};
// 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;
let pictures = u64::from(sps.max_dec_pic_buffering) + 1 + self.max_in_flight as u64;
ensure!(
shape.fits(pictures, self.memory_budget),
"{}x{} pictures with {pictures} in the decoder exceed the memory budget",
+46 -7
View File
@@ -15,6 +15,10 @@ use crate::xml::{A, El, W};
const DEG: f64 = 60_000.0;
/// Distinct path coordinate spaces whose guides are evaluated for one custom geometry; paths in
/// further spaces are left out.
const MAX_SPACES: usize = 16;
/// Guide variables for one shape.
pub struct Guides {
vars: HashMap<String, f64>,
@@ -151,14 +155,26 @@ fn pt(g: &Guides, e: &El) -> (f64, f64) {
pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec<CustomPath> {
let mut out = vec![];
let Some(pl) = cg.child("pathLst") else { return out };
let mut spaces: HashMap<(u64, u64), Guides> = HashMap::new();
// The cap on cached spaces saves guide evaluation: a geometry without guides has none to save.
let guided = ["avLst", "gdLst"].iter().any(|n| cg.child(n).is_some_and(|l| l.elements().next().is_some()));
for p in pl.children_named("path").take(1000) {
let pw = p.f64("w").unwrap_or(0.0);
let ph = p.f64("h").unwrap_or(0.0);
// Guides are evaluated in the path's own space when it has one.
let (gw, gh) = if pw > 0.0 || ph > 0.0 { (pw, ph) } else { (w_emu, h_emu) };
let mut g = Guides::new(gw, gh);
g.eval_list(cg.child("avLst"));
g.eval_list(cg.child("gdLst"));
// The guide lists are evaluated once per coordinate space, not once per path.
let key = (gw.to_bits(), gh.to_bits());
if !spaces.contains_key(&key) {
if guided && spaces.len() >= MAX_SPACES {
continue;
}
let mut g = Guides::new(gw, gh);
g.eval_list(cg.child("avLst"));
g.eval_list(cg.child("gdLst"));
spaces.insert(key, g);
}
let Some(g) = spaces.get(&key) else { continue };
let mut d = String::new();
let mut cur = (0.0, 0.0);
let mut start = (0.0, 0.0);
@@ -166,7 +182,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec<CustomPath> {
match c.local() {
"moveTo" => {
if let Some(q) = c.child("pt") {
cur = pt(&g, q);
cur = pt(g, q);
start = cur;
d.push_str("M ");
num(&mut d, cur.0);
@@ -177,7 +193,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec<CustomPath> {
}
"lnTo" => {
if let Some(q) = c.child("pt") {
cur = pt(&g, q);
cur = pt(g, q);
d.push_str("L ");
num(&mut d, cur.0);
d.push(' ');
@@ -186,7 +202,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec<CustomPath> {
}
}
"cubicBezTo" => {
let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(&g, q)).collect();
let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(g, q)).collect();
if let [a, b, e] = pts.as_slice() {
d.push_str("C ");
for (x, y) in [a, b, e] {
@@ -199,7 +215,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec<CustomPath> {
}
}
"quadBezTo" => {
let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(&g, q)).collect();
let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(g, q)).collect();
if let [a, e] = pts.as_slice() {
d.push_str("Q ");
for (x, y) in [a, e] {
@@ -470,6 +486,29 @@ mod tests {
assert_eq!(paths[1].d, "M 1 1 Q 2 2 3 1");
}
#[test]
fn paths_share_guides_and_distinct_spaces_are_capped() {
let path = |w: usize| format!(r#"<a:path w="{w}" h="10"><a:moveTo><a:pt x="mid" y="1"/></a:moveTo></a:path>"#);
let paths: String = (1..=40).map(|i| path(10 * i)).collect();
let x = format!(
r#"<a:custGeom xmlns:a="x"><a:gdLst><a:gd name="mid" fmla="*/ w 1 2"/></a:gdLst><a:pathLst>{paths}{}</a:pathLst></a:custGeom>"#,
path(10)
);
let d = parse(x.as_bytes()).unwrap();
let out = read_cust_geom(&d.root, 1.0, 1.0);
assert_eq!(out.len(), MAX_SPACES + 1, "16 spaces plus the repeat of the first");
assert!(out[0].d.starts_with("M 5 1"), "{}", out[0].d);
assert!(out[MAX_SPACES].d.starts_with("M 5 1"), "{}", out[MAX_SPACES].d);
}
#[test]
fn guide_free_geometries_are_not_capped_by_space() {
let paths: String = (1..=40).map(|i| format!(r#"<a:path w="{}" h="10"><a:moveTo><a:pt x="1" y="1"/></a:moveTo></a:path>"#, 10 * i)).collect();
let x = format!(r#"<a:custGeom xmlns:a="x"><a:avLst/><a:gdLst/><a:pathLst>{paths}</a:pathLst></a:custGeom>"#);
let d = parse(x.as_bytes()).unwrap();
assert_eq!(read_cust_geom(&d.root, 1.0, 1.0).len(), 40);
}
#[test]
fn write_then_read_path() {
let p = CustomPath { w: 100.0, h: 50.0, d: "M 0 0 L 100 0 C 100 10 90 50 50 50 Q 0 50 0 0 Z".into(), fill: FillMode::Norm, stroke: true };
+1 -7
View File
@@ -166,17 +166,11 @@ impl Decoder {
if frames.len() > 1 {
self.stats.superframes += 1;
}
let queued = self.pending.len();
for f in frames {
if f.is_empty() {
continue;
}
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);
}
self.decode_frame(f, pts)?;
}
Ok(self.drain(false))
}