mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-09 11:59:36 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca95a18d3 |
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,9 +12,6 @@ use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHand
|
||||
|
||||
use crate::icons::{self, Icon};
|
||||
|
||||
/// Video shapes that get a decoder at once; more play their sound without a picture.
|
||||
const MAX_VIDEO_FEEDS: usize = 16;
|
||||
|
||||
/// Who started a voice: the editor's control bar stops when you leave the slide, the slide show
|
||||
/// manages its own.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -230,21 +227,12 @@ impl MediaHost {
|
||||
audio: clip.volume > 0.0,
|
||||
};
|
||||
let source_changed = self.owners.get(&id).is_some_and(|o| o.source != media_key);
|
||||
// A shape whose feed could not be had (the cap, a decode error) tries again on each play.
|
||||
let feedless = self.videos.get(&id).is_some_and(|v| v.feed.is_none());
|
||||
if clip.video && (source_changed || feedless || !self.videos.contains_key(&id)) {
|
||||
// One decoder and frame queue per video shape: past the cap a shape plays its sound only.
|
||||
let live = self.videos.iter().filter(|(vid, v)| **vid != id && v.feed.is_some()).count();
|
||||
let feed = if live >= MAX_VIDEO_FEEDS {
|
||||
log::info!("video {id}: more than {MAX_VIDEO_FEEDS} videos playing; showing no picture");
|
||||
None
|
||||
} else {
|
||||
match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
}
|
||||
if clip.video && (source_changed || !self.videos.contains_key(&id)) {
|
||||
let feed = match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
self.videos.insert(id, Video { feed, frame: None, tex: None });
|
||||
|
||||
@@ -196,40 +196,18 @@ pub(super) fn part_textures(ctx: &egui::Context, doc: &Presentation, from: Optio
|
||||
]
|
||||
}
|
||||
|
||||
/// Width of a slide-show grid thumbnail, in pixels.
|
||||
const THUMB_WIDTH: f64 = 320.0;
|
||||
/// Tallest grid thumbnail, so an extreme portrait slide cannot ask for a huge raster.
|
||||
const THUMB_MAX_HEIGHT: f64 = 2048.0;
|
||||
|
||||
/// The scale and pixel size of a grid thumbnail of a `width` x `height` slide.
|
||||
fn thumb_size(width: f64, height: f64) -> (f64, f64, f64) {
|
||||
let (sw, sh) = (width.max(1.0), height.max(1.0));
|
||||
let scale = (THUMB_WIDTH / sw).min(THUMB_MAX_HEIGHT / sh);
|
||||
(scale, (sw * scale).round().max(1.0), (sh * scale).round().max(1.0))
|
||||
}
|
||||
|
||||
pub(super) fn render_thumb(ctx: &egui::Context, show: &mut Show, doc: &Presentation, i: usize) -> Option<TextureHandle> {
|
||||
if show.thumbs.len() >= GRID_CAP {
|
||||
return None;
|
||||
}
|
||||
// 320 wide, unless the slide is so tall that this would pass the height limit: fit that box.
|
||||
let (scale, w, h) = thumb_size(doc.slide_size.width, doc.slide_size.height);
|
||||
let img = deckcraft_render::render_slide(doc, i, &RenderOpts { scale, size: Some((w as u32, h as u32)), ..Default::default() });
|
||||
let w = 320.0_f64;
|
||||
let h = (w * doc.slide_size.height / doc.slide_size.width.max(1.0)).round().max(1.0);
|
||||
let img = deckcraft_render::render_slide(
|
||||
doc,
|
||||
i,
|
||||
&RenderOpts { scale: w / doc.slide_size.width.max(1.0), size: Some((w as u32, h as u32)), ..Default::default() },
|
||||
);
|
||||
let tex = crate::textures::upload(ctx, format!("show-grid-{i}"), crate::textures::to_color_image(&img, false));
|
||||
show.thumbs.push((i, tex.clone()));
|
||||
Some(tex)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod thumb_size_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ordinary_slides_are_320_wide_and_extreme_ones_stay_in_the_height_limit() {
|
||||
let (scale, w, h) = thumb_size(960.0, 540.0);
|
||||
assert_eq!((w, h), (320.0, 180.0));
|
||||
assert!((scale - 320.0 / 960.0).abs() < 1e-12);
|
||||
let (_, w, h) = thumb_size(1.0, 1.0e9);
|
||||
assert!(h <= THUMB_MAX_HEIGHT && w >= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user