video: configure bounded decoder frame limits

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 01:10:32 +00:00
1 parent 6fa8323991
commit 4cca9be2ab
6 files changed
+123 -7

No files matched your search

+2 -1
View File
@@ -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
+12 -1
View File
@@ -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();
+31 -2
View File
@@ -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();
+7
View File
@@ -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.
+68 -2
View File
@@ -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];
+3 -1
View File
@@ -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};