mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
video: configure bounded decoder frame limits
This commit is contained in:
1 parent
6fa8323991
commit
4cca9be2ab
6 files changed
+123
-7
No files matched your search
@@ -99,7 +99,8 @@ A picture is refused when the pictures the decoder would hold do not fit
|
||||
1 GiB on 32-bit targets): the DPB (the larger of its slots at the new size and what it holds
|
||||
now), the picture being decoded, and what is in flight or waiting in the output queue. Pictures
|
||||
already held count at their own size, and a buffer shared by the DPB and the queue counts once.
|
||||
The per-picture size cap applies as well.
|
||||
The default per-picture luma cap is 2^25 samples; applications with a different bounded frame
|
||||
policy can raise it with `Decoder::set_max_picture_luma_samples`.
|
||||
|
||||
`configure_avcc` checks every entry of the record before it handles any: the first list holds
|
||||
SPS NAL units and the second PPS NAL units. A record that carries a picture slice is an error and
|
||||
|
||||
@@ -115,6 +115,8 @@ pub struct Decoder {
|
||||
/// Bytes the picture store may take ([`crate::budget`]).
|
||||
memory_budget: u64,
|
||||
threads: usize,
|
||||
/// Maximum reconstructed luma samples in one picture.
|
||||
max_luma_samples: u64,
|
||||
/// Draft mode: skip deblocking of non-reference pictures ([`Decoder::set_draft`]).
|
||||
draft: bool,
|
||||
}
|
||||
@@ -179,6 +181,7 @@ impl Decoder {
|
||||
max_in_flight: threads.max(1) + 2,
|
||||
memory_budget: crate::budget::DECODE_MEMORY_BUDGET,
|
||||
threads: workers,
|
||||
max_luma_samples: super::params::MAX_LUMA_SAMPLES,
|
||||
draft: false,
|
||||
}
|
||||
}
|
||||
@@ -391,7 +394,7 @@ impl Decoder {
|
||||
// Before the header fields that depend on the SPS: they are validated for the streams
|
||||
// we decode (a 10-bit slice may have a QP below 0), so a stream we cannot decode must
|
||||
// fail as unsupported here, not as invalid there (#626).
|
||||
sps.check_supported()?;
|
||||
sps.check_supported_with_limit(self.max_luma_samples)?;
|
||||
found = Some((pps.clone(), sps.clone()));
|
||||
Ok((&**pps, &**sps))
|
||||
})?;
|
||||
@@ -468,6 +471,14 @@ impl Decoder {
|
||||
self.memory_budget = bytes;
|
||||
}
|
||||
|
||||
/// Set the maximum luma sample count allowed in a single decoded picture.
|
||||
///
|
||||
/// The default is 2^25 samples. This is a per-picture geometry guard; the separate
|
||||
/// memory budget controls aggregate decoded picture storage.
|
||||
pub fn set_max_picture_luma_samples(&mut self, samples: u64) {
|
||||
self.max_luma_samples = samples;
|
||||
}
|
||||
|
||||
/// Refuse an SPS whose pictures would not fit the memory budget, before anything is allocated.
|
||||
fn check_budget(&self, sps: &Sps) -> Result<()> {
|
||||
let chroma = sps.chroma_array_type();
|
||||
|
||||
@@ -6,7 +6,7 @@ use craft_media::bitstream::BitReader;
|
||||
|
||||
/// Maximum supported reconstructed luma area. 8K UHD fits comfortably while
|
||||
/// hostile SPS dimensions cannot allocate full pictures and decoder work state.
|
||||
const MAX_LUMA_SAMPLES: u64 = 1 << 25;
|
||||
pub(super) const MAX_LUMA_SAMPLES: u64 = 1 << 25;
|
||||
|
||||
/// Scaling matrices after applying fall-back rules, stored in raster order (weightScale).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -527,7 +527,11 @@ impl Sps {
|
||||
}
|
||||
|
||||
pub fn check_supported(&self) -> Result<()> {
|
||||
if u64::from(self.width()).saturating_mul(u64::from(self.height())) > MAX_LUMA_SAMPLES {
|
||||
self.check_supported_with_limit(MAX_LUMA_SAMPLES)
|
||||
}
|
||||
|
||||
pub(super) fn check_supported_with_limit(&self, max_luma_samples: u64) -> Result<()> {
|
||||
if u64::from(self.width()).saturating_mul(u64::from(self.height())) > max_luma_samples {
|
||||
return unsupported("picture dimensions exceed the decoder memory limit");
|
||||
}
|
||||
if !self.frame_mbs_only {
|
||||
@@ -744,6 +748,31 @@ mod tests {
|
||||
assert_eq!(sps.pic_order_cnt_type, 2);
|
||||
assert_eq!(sps.crop_rect(), (0, 0, 176, 144));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_can_raise_single_picture_sample_limit() {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_bits(66, 8);
|
||||
w.write_bits(0xC0, 8);
|
||||
w.write_bits(30, 8);
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_ue(2);
|
||||
w.write_ue(1);
|
||||
w.write_bit(false);
|
||||
w.write_ue(511); // 8192 luma samples wide
|
||||
w.write_ue(511); // 8192 luma samples high
|
||||
w.write_bit(true);
|
||||
w.write_bit(true);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.rbsp_trailing();
|
||||
|
||||
let sps = Sps::parse(&w.finish()).unwrap();
|
||||
assert_eq!((sps.width(), sps.height()), (8192, 8192));
|
||||
assert!(sps.check_supported().is_err());
|
||||
assert!(sps.check_supported_with_limit(1 << 26).is_ok());
|
||||
}
|
||||
/// Baseline SPS with the given `ue(v)` overrides for the fields hostile streams can max out.
|
||||
fn hostile_sps(profile: u32, bit_depth: u32, log2_frame_num: u32, w: u32, h: u32, crop: u32) -> Vec<u8> {
|
||||
let mut bw = BitWriter::new();
|
||||
|
||||
@@ -16,6 +16,8 @@ use craft_video::prores::*;
|
||||
let frame: Frame = decode_frame(&sample)?; // 10-bit 4:2:2 / 12-bit 4:4:4
|
||||
let f10 = decode_frame_with(&sample, &DecodeOptions { bit_depth: Some(10), threads: true })?;
|
||||
decode_frame_into(&sample, &DecodeOptions::default(), &mut frame_buf)?; // reuses planes
|
||||
let limits = DecodeLimits { max_pixels: 8192 * 8192, max_frame_bytes: 512 * 1024 * 1024 };
|
||||
let bounded = decode_frame_with_limits(&sample, &DecodeOptions::default(), &limits)?;
|
||||
let hdr: FrameHeader = probe(&sample)?; // header only
|
||||
|
||||
// Encode (progressive).
|
||||
@@ -28,6 +30,11 @@ alpha: Option<Vec<u16>>, interlace: Interlace, color: ColorInfo { primaries, tra
|
||||
aspect_ratio, frame_rate_code }` holds planar samples with no row padding (`chroma_width()` is
|
||||
`ceil(width/2)` for 4:2:2). The colour codes are H.273 code points straight from the frame header.
|
||||
|
||||
`DecodeLimits` bounds both frame area and aggregate decoded plane storage before allocation.
|
||||
`decode_frame_with` uses an aggregate limit of 300 MiB and does not impose an additional area
|
||||
limit; use `decode_frame_with_limits` when the application needs a stricter pixel policy or a
|
||||
different finite memory ceiling.
|
||||
|
||||
`Profile` = `Proxy` (`apco`), `Lt` (`apcs`), `Standard` (`apcn`), `Hq` (`apch`), `P4444` (`ap4h`),
|
||||
`P4444Xq` (`ap4x`). The frame itself doesn't say which profile it is (only the container fourcc
|
||||
does), so the decoder doesn't need to know.
|
||||
|
||||
@@ -11,6 +11,21 @@ use crate::prores::{AlphaType, ChromaFormat, Error, Frame, Interlace, Result};
|
||||
/// Maximum aggregate decoded plane storage for one ProRes frame (300 MiB; 8K ProRes 4444 with alpha is ~265 MiB).
|
||||
const MAX_FRAME_BYTES: usize = 300 * 1024 * 1024;
|
||||
|
||||
/// Resource limits for decoding one ProRes frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeLimits {
|
||||
/// Maximum frame width × height. Checked before any plane allocation.
|
||||
pub max_pixels: u64,
|
||||
/// Maximum aggregate output plane storage in bytes.
|
||||
pub max_frame_bytes: usize,
|
||||
}
|
||||
|
||||
impl Default for DecodeLimits {
|
||||
fn default() -> Self {
|
||||
Self { max_pixels: u64::MAX, max_frame_bytes: MAX_FRAME_BYTES }
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoder options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeOptions {
|
||||
@@ -105,15 +120,28 @@ impl Scratch {
|
||||
|
||||
/// Decode one ProRes frame.
|
||||
pub fn decode_frame_with(data: &[u8], opts: &DecodeOptions) -> Result<Frame> {
|
||||
decode_frame_with_limits(data, opts, &DecodeLimits::default())
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame with explicit geometry and aggregate-memory limits.
|
||||
pub fn decode_frame_with_limits(data: &[u8], opts: &DecodeOptions, limits: &DecodeLimits) -> Result<Frame> {
|
||||
let mut f = Frame::new(0, 0, ChromaFormat::Yuv422, 10, false)?;
|
||||
decode_frame_into(data, opts, &mut f)?;
|
||||
decode_frame_into_with_limits(data, opts, limits, &mut f)?;
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame into `out`, reusing its plane allocations (the fastest way to decode
|
||||
/// a sequence). On error the contents of `out` are unspecified.
|
||||
pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) -> Result<()> {
|
||||
decode_frame_into_with_limits(data, opts, &DecodeLimits::default(), out)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame into `out` under explicit resource limits.
|
||||
pub fn decode_frame_into_with_limits(data: &[u8], opts: &DecodeOptions, limits: &DecodeLimits, out: &mut Frame) -> Result<()> {
|
||||
let (hdr, mut off, frame_size) = FrameHeader::parse(data)?;
|
||||
if u64::from(hdr.width) * u64::from(hdr.height) > limits.max_pixels {
|
||||
return Err(Error::Unsupported("frame size"));
|
||||
}
|
||||
let data = &data[..frame_size];
|
||||
let depth = opts.bit_depth.unwrap_or(match hdr.chroma {
|
||||
ChromaFormat::Yuv422 => 10,
|
||||
@@ -131,7 +159,7 @@ pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) ->
|
||||
.checked_add(chroma.checked_mul(2).ok_or(Error::Invalid("chroma plane count overflows"))?)
|
||||
.and_then(|n| if hdr.alpha == AlphaType::None { Some(n) } else { n.checked_add(luma) })
|
||||
.ok_or(Error::Invalid("frame plane count overflows"))?;
|
||||
if samples.checked_mul(std::mem::size_of::<u16>()).is_none_or(|n| n > MAX_FRAME_BYTES) {
|
||||
if samples.checked_mul(std::mem::size_of::<u16>()).is_none_or(|n| n > limits.max_frame_bytes) {
|
||||
return Err(Error::Invalid("frame dimensions exceed the decoder memory limit"));
|
||||
}
|
||||
let interlaced = hdr.interlace != Interlace::Progressive;
|
||||
@@ -564,6 +592,44 @@ mod alpha_decode_tests {
|
||||
assert_eq!(slice_quantiser(224).unwrap(), qscale(224));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_limits_preserve_large_4444_geometry_before_allocating() {
|
||||
let hdr = FrameHeader {
|
||||
header_size: 20,
|
||||
bitstream_version: 1,
|
||||
encoder_id: *b"test",
|
||||
width: 8192,
|
||||
height: 8192,
|
||||
chroma: ChromaFormat::Yuv444,
|
||||
interlace: Interlace::Progressive,
|
||||
aspect_ratio: 0,
|
||||
frame_rate_code: 0,
|
||||
color: crate::prores::ColorInfo::default(),
|
||||
alpha: AlphaType::Bits16,
|
||||
qmat_luma: [4; 64],
|
||||
qmat_chroma: [4; 64],
|
||||
luma_matrix_present: false,
|
||||
chroma_matrix_present: false,
|
||||
};
|
||||
let header = hdr.write();
|
||||
let frame_size = 8 + header.len();
|
||||
let mut data = Vec::with_capacity(frame_size);
|
||||
data.extend_from_slice(&(frame_size as u32).to_be_bytes());
|
||||
data.extend_from_slice(&crate::prores::header::FRAME_ID);
|
||||
data.extend_from_slice(&header);
|
||||
|
||||
let defaults = DecodeLimits { max_pixels: 8192 * 8192, ..DecodeLimits::default() };
|
||||
assert!(matches!(
|
||||
decode_frame_with_limits(&data, &DecodeOptions::default(), &defaults),
|
||||
Err(Error::Invalid("frame dimensions exceed the decoder memory limit"))
|
||||
));
|
||||
|
||||
let soundcraft = DecodeLimits { max_pixels: 8192 * 8192, max_frame_bytes: 512 * 1024 * 1024 };
|
||||
assert!(matches!(decode_frame_with_limits(&data, &DecodeOptions::default(), &soundcraft), Err(Error::Truncated)));
|
||||
let too_few_pixels = DecodeLimits { max_pixels: 8192 * 8192 - 1, ..soundcraft };
|
||||
assert!(matches!(decode_frame_with_limits(&data, &DecodeOptions::default(), &too_few_pixels), Err(Error::Unsupported("frame size"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_decoder_requires_complete_bounded_runs() {
|
||||
let mut out = [0u16; 1];
|
||||
|
||||
@@ -18,7 +18,9 @@ mod encode;
|
||||
pub mod header;
|
||||
mod tables;
|
||||
|
||||
pub use decode::{DecodeOptions, decode_frame, decode_frame_into, decode_frame_with, probe};
|
||||
pub use decode::{
|
||||
DecodeLimits, DecodeOptions, decode_frame, decode_frame_into, decode_frame_into_with_limits, decode_frame_with, decode_frame_with_limits, probe,
|
||||
};
|
||||
pub use encode::{Encoder, EncoderConfig};
|
||||
pub use header::{FrameHeader, PictureHeader};
|
||||
|
||||
|
||||
Reference in new issue
Block a user