Skip to main content

vtome/
frame.rs

1//! One decoded picture, and the pool that stops decoding from allocating.
2//!
3//! Frames stay in whatever the decoder produced — usually planar YUV with the
4//! chroma at half resolution — all the way to the shader. Converting to RGBA on
5//! the CPU would triple the bytes crossing the bus and burn a core doing what
6//! the GPU does for free; see [`crate::color`].
7
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::color::ColorSpace;
12use crate::error::{Error, Result};
13
14/// How the samples are laid out.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum PixelFormat {
18    /// Planar Y, U, V with chroma at half width and half height. What almost
19    /// every consumer video decodes to.
20    I420,
21    /// Planar, chroma at half width and full height.
22    I422,
23    /// Planar, chroma at full resolution.
24    I444,
25    /// Y plane then interleaved UV, half width and half height. What the
26    /// hardware decoders hand back.
27    Nv12,
28    /// NV12 at 10 bits, each sample in the top bits of a 16-bit word.
29    P010,
30    /// Packed 8-bit RGBA. Still images, and anything already converted.
31    Rgba8,
32    /// Packed 8-bit BGRA, which is what several platform surfaces want.
33    Bgra8,
34}
35
36impl PixelFormat {
37    /// How many separately-addressed planes there are.
38    pub fn plane_count(self) -> usize {
39        match self {
40            PixelFormat::I420 | PixelFormat::I422 | PixelFormat::I444 => 3,
41            PixelFormat::Nv12 | PixelFormat::P010 => 2,
42            PixelFormat::Rgba8 | PixelFormat::Bgra8 => 1,
43        }
44    }
45
46    /// Bits per sample, per component.
47    pub fn bit_depth(self) -> u32 {
48        match self {
49            PixelFormat::P010 => 10,
50            _ => 8,
51        }
52    }
53
54    /// Bytes each sample occupies, which is not the same as its bit depth: a
55    /// 10-bit sample is stored in two bytes.
56    pub fn bytes_per_sample(self) -> usize {
57        match self {
58            PixelFormat::P010 => 2,
59            _ => 1,
60        }
61    }
62
63    /// Whether the planes are luma and chroma rather than colour.
64    pub fn is_yuv(self) -> bool {
65        !matches!(self, PixelFormat::Rgba8 | PixelFormat::Bgra8)
66    }
67
68    /// How far chroma is subsampled, as right-shifts of the luma dimensions.
69    ///
70    /// `(1, 1)` for 4:2:0 — half width, half height. `(0, 0)` for anything with
71    /// full-resolution colour.
72    pub fn chroma_shift(self) -> (u32, u32) {
73        match self {
74            PixelFormat::I420 | PixelFormat::Nv12 | PixelFormat::P010 => (1, 1),
75            PixelFormat::I422 => (1, 0),
76            _ => (0, 0),
77        }
78    }
79
80    /// The size of one plane in samples, for a picture of this size.
81    ///
82    /// Chroma dimensions round *up*: a 1921-pixel-wide 4:2:0 picture has 961
83    /// chroma columns, not 960, and rounding down loses the last one.
84    pub fn plane_dimensions(self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
85        if plane >= self.plane_count() {
86            return None;
87        }
88
89        // Plane 0 is always full resolution; on packed formats it is the only
90        // one, and its "width" counts pixels rather than samples.
91        if plane == 0 {
92            return Some((width, height));
93        }
94
95        let (h_shift, v_shift) = self.chroma_shift();
96        let chroma_width = width.div_ceil(1 << h_shift);
97        let chroma_height = height.div_ceil(1 << v_shift);
98
99        Some((chroma_width, chroma_height))
100    }
101
102    /// Samples per pixel within one plane: 4 for packed RGBA, 2 for NV12's
103    /// interleaved chroma, 1 for a planar plane.
104    pub fn samples_per_pixel(self, plane: usize) -> usize {
105        match (self, plane) {
106            (PixelFormat::Rgba8 | PixelFormat::Bgra8, _) => 4,
107            (PixelFormat::Nv12 | PixelFormat::P010, 1) => 2,
108            _ => 1,
109        }
110    }
111
112    /// The tightest stride for a plane — no padding at all.
113    pub fn tight_stride(self, plane: usize, width: u32) -> usize {
114        let (plane_width, _) = self.plane_dimensions(plane, width, 1).unwrap_or((width, 1));
115
116        plane_width as usize * self.samples_per_pixel(plane) * self.bytes_per_sample()
117    }
118}
119
120/// Where one plane sits in a frame's buffer.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub struct Plane {
123    /// Byte offset from the start of the frame's data.
124    pub offset: usize,
125    /// Bytes between the start of one row and the start of the next.
126    ///
127    /// Decoders align rows for their own reasons, so this is almost never the
128    /// row's width in bytes. Treating the two as the same is the classic way to
129    /// get a picture that shears diagonally.
130    pub stride: usize,
131}
132
133/// A decoded picture.
134///
135/// The data is behind an [`Arc`] so a frame can be handed to a renderer, an
136/// encoder, and a callback at once without copying — none of them mutate it.
137#[derive(Clone, Debug)]
138pub struct Frame {
139    width: u32,
140    height: u32,
141    format: PixelFormat,
142    color: ColorSpace,
143    pts: Duration,
144    planes: Vec<Plane>,
145    data: Arc<Vec<u8>>,
146}
147
148impl Frame {
149    /// A frame over `data`, with the planes laid out end to end and no padding.
150    ///
151    /// # Errors
152    ///
153    /// [`Error::BadFrame`] if `data` is not the size that layout implies, or
154    /// either dimension is zero.
155    pub fn packed(
156        width: u32,
157        height: u32,
158        format: PixelFormat,
159        color: ColorSpace,
160        pts: Duration,
161        data: Vec<u8>,
162    ) -> Result<Self> {
163        let (planes, needed) = packed_layout(format, width, height)?;
164
165        if data.len() < needed {
166            return Err(Error::BadFrame {
167                reason: format!(
168                    "{width}×{height} {format:?} needs {needed} bytes, got {}",
169                    data.len()
170                ),
171            });
172        }
173
174        Ok(Frame {
175            width,
176            height,
177            format,
178            color,
179            pts,
180            planes,
181            data: Arc::new(data),
182        })
183    }
184
185    /// A frame whose planes are where the decoder put them.
186    ///
187    /// # Errors
188    ///
189    /// [`Error::BadFrame`] if the plane count is wrong for the format, or any
190    /// plane's last row runs past the end of `data`.
191    pub fn with_planes(
192        width: u32,
193        height: u32,
194        format: PixelFormat,
195        color: ColorSpace,
196        pts: Duration,
197        planes: Vec<Plane>,
198        data: Vec<u8>,
199    ) -> Result<Self> {
200        if width == 0 || height == 0 {
201            return Err(Error::BadFrame {
202                reason: format!("a {width}×{height} picture has no pixels"),
203            });
204        }
205
206        if planes.len() != format.plane_count() {
207            return Err(Error::BadFrame {
208                reason: format!(
209                    "{format:?} has {} planes, {} given",
210                    format.plane_count(),
211                    planes.len()
212                ),
213            });
214        }
215
216        for (index, plane) in planes.iter().enumerate() {
217            let (plane_width, plane_height) = format
218                .plane_dimensions(index, width, height)
219                .expect("plane count was checked");
220
221            let row_bytes =
222                plane_width as usize * format.samples_per_pixel(index) * format.bytes_per_sample();
223
224            if plane.stride < row_bytes {
225                return Err(Error::BadFrame {
226                    reason: format!(
227                        "plane {index}: stride {} is shorter than a {row_bytes}-byte row",
228                        plane.stride
229                    ),
230                });
231            }
232
233            // The last row needs `row_bytes`, not a whole stride — a decoder is
234            // entitled to end the buffer at the end of the picture.
235            let span = plane
236                .stride
237                .checked_mul(plane_height.saturating_sub(1) as usize)
238                .and_then(|full| full.checked_add(row_bytes))
239                .and_then(|span| span.checked_add(plane.offset))
240                .ok_or_else(|| Error::BadFrame {
241                    reason: format!("plane {index}: the layout overflows a usize"),
242                })?;
243
244            if span > data.len() {
245                return Err(Error::BadFrame {
246                    reason: format!(
247                        "plane {index} runs to byte {span} of a {}-byte buffer",
248                        data.len()
249                    ),
250                });
251            }
252        }
253
254        Ok(Frame {
255            width,
256            height,
257            format,
258            color,
259            pts,
260            planes,
261            data: Arc::new(data),
262        })
263    }
264
265    /// Width in pixels.
266    pub fn width(&self) -> u32 {
267        self.width
268    }
269
270    /// Height in pixels.
271    pub fn height(&self) -> u32 {
272        self.height
273    }
274
275    /// The sample layout.
276    pub fn format(&self) -> PixelFormat {
277        self.format
278    }
279
280    /// What the samples mean.
281    pub fn color(&self) -> ColorSpace {
282        self.color
283    }
284
285    /// When this picture is due, from the start of the stream.
286    pub fn pts(&self) -> Duration {
287        self.pts
288    }
289
290    /// Retimes the frame, for a player that offsets a stream against a master
291    /// clock. Cheap: the data is shared, not copied.
292    pub fn with_pts(&self, pts: Duration) -> Self {
293        Frame {
294            pts,
295            data: Arc::clone(&self.data),
296            planes: self.planes.clone(),
297            ..*self
298        }
299    }
300
301    /// Where each plane is.
302    pub fn planes(&self) -> &[Plane] {
303        &self.planes
304    }
305
306    /// One plane's bytes, from its offset to the end of its last row.
307    pub fn plane_data(&self, plane: usize) -> Option<&[u8]> {
308        let descriptor = self.planes.get(plane)?;
309        let (plane_width, plane_height) =
310            self.format
311                .plane_dimensions(plane, self.width, self.height)?;
312
313        let row_bytes = plane_width as usize
314            * self.format.samples_per_pixel(plane)
315            * self.format.bytes_per_sample();
316        let span = descriptor.stride * (plane_height as usize - 1) + row_bytes;
317
318        self.data.get(descriptor.offset..descriptor.offset + span)
319    }
320
321    /// One row of one plane, without the padding a stride may add.
322    pub fn row(&self, plane: usize, row: u32) -> Option<&[u8]> {
323        let descriptor = self.planes.get(plane)?;
324        let (plane_width, plane_height) =
325            self.format
326                .plane_dimensions(plane, self.width, self.height)?;
327
328        if row >= plane_height {
329            return None;
330        }
331
332        let row_bytes = plane_width as usize
333            * self.format.samples_per_pixel(plane)
334            * self.format.bytes_per_sample();
335        let start = descriptor.offset + descriptor.stride * row as usize;
336
337        self.data.get(start..start + row_bytes)
338    }
339
340    /// The whole buffer, planes and padding alike.
341    pub fn data(&self) -> &[u8] {
342        &self.data
343    }
344
345    /// How many bytes this frame holds, for accounting against a memory
346    /// ceiling.
347    pub fn byte_len(&self) -> usize {
348        self.data.len()
349    }
350}
351
352/// Plane offsets and total size for a tightly packed frame.
353fn packed_layout(format: PixelFormat, width: u32, height: u32) -> Result<(Vec<Plane>, usize)> {
354    if width == 0 || height == 0 {
355        return Err(Error::BadFrame {
356            reason: format!("a {width}×{height} picture has no pixels"),
357        });
358    }
359
360    let mut planes = Vec::with_capacity(format.plane_count());
361    let mut offset = 0_usize;
362
363    for index in 0..format.plane_count() {
364        let (plane_width, plane_height) = format
365            .plane_dimensions(index, width, height)
366            .expect("index is below the plane count");
367
368        let stride =
369            plane_width as usize * format.samples_per_pixel(index) * format.bytes_per_sample();
370
371        planes.push(Plane { offset, stride });
372
373        offset = stride
374            .checked_mul(plane_height as usize)
375            .and_then(|size| size.checked_add(offset))
376            .ok_or_else(|| Error::BadFrame {
377                reason: format!("{width}×{height} {format:?} does not fit in memory"),
378            })?;
379    }
380
381    Ok((planes, offset))
382}
383
384/// The number of bytes a tightly packed frame of this description needs.
385///
386/// For sizing a pool, or refusing a resolution before allocating for it.
387pub fn packed_size(format: PixelFormat, width: u32, height: u32) -> Result<usize> {
388    packed_layout(format, width, height).map(|(_, size)| size)
389}
390
391/// Buffers handed back for reuse, so steady-state playback allocates nothing.
392///
393/// Decoding 24 frames a second means 24 allocations a second of several
394/// megabytes each; at 4K that is enough to keep an allocator busy and enough to
395/// fragment one. Frames come back here when the renderer is done with them.
396#[derive(Debug, Default)]
397pub struct FramePool {
398    buffers: Vec<Vec<u8>>,
399    capacity: usize,
400    reused: u64,
401    allocated: u64,
402}
403
404impl FramePool {
405    /// A pool holding at most `capacity` buffers.
406    ///
407    /// Bounded on purpose: an unbounded pool is a memory leak that happens to
408    /// be spelled differently.
409    pub fn new(capacity: usize) -> Self {
410        FramePool {
411            buffers: Vec::with_capacity(capacity),
412            capacity,
413            reused: 0,
414            allocated: 0,
415        }
416    }
417
418    /// A buffer of at least `size` bytes, cleared to that length.
419    pub fn take(&mut self, size: usize) -> Vec<u8> {
420        // Any buffer big enough will do; taking the first avoids a scan that
421        // would cost more than the allocation it saves.
422        if let Some(index) = self
423            .buffers
424            .iter()
425            .position(|buffer| buffer.capacity() >= size)
426        {
427            let mut buffer = self.buffers.swap_remove(index);
428            buffer.clear();
429            buffer.resize(size, 0);
430            self.reused += 1;
431
432            return buffer;
433        }
434
435        self.allocated += 1;
436
437        vec![0; size]
438    }
439
440    /// Offers a buffer back. Dropped if the pool is full.
441    pub fn give(&mut self, buffer: Vec<u8>) {
442        if self.buffers.len() < self.capacity {
443            self.buffers.push(buffer);
444        }
445    }
446
447    /// Takes a frame's buffer back if nothing else still holds it.
448    ///
449    /// A frame the renderer is still reading is left alone — which is the point
450    /// of the [`Arc`]: reuse is an optimisation, never a race.
451    pub fn reclaim(&mut self, frame: Frame) {
452        if let Ok(buffer) = Arc::try_unwrap(frame.data) {
453            self.give(buffer);
454        }
455    }
456
457    /// How many `take` calls were answered without allocating, and how many
458    /// were not. Steady-state playback should stop adding to the second.
459    pub fn statistics(&self) -> (u64, u64) {
460        (self.reused, self.allocated)
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    fn frame_of(format: PixelFormat, width: u32, height: u32) -> Frame {
469        let size = packed_size(format, width, height).unwrap();
470
471        Frame::packed(
472            width,
473            height,
474            format,
475            ColorSpace::default(),
476            Duration::ZERO,
477            vec![0; size],
478        )
479        .unwrap()
480    }
481
482    #[test]
483    fn i420_is_one_and_a_half_bytes_a_pixel() {
484        let size = packed_size(PixelFormat::I420, 1920, 1080).unwrap();
485
486        assert_eq!(size, 1920 * 1080 * 3 / 2);
487    }
488
489    /// The off-by-one that eats the right-hand column: 4:2:0 chroma for an odd
490    /// width has to round up.
491    #[test]
492    fn odd_dimensions_round_chroma_up() {
493        let format = PixelFormat::I420;
494
495        assert_eq!(format.plane_dimensions(1, 1921, 1081), Some((961, 541)));
496
497        let size = packed_size(format, 1921, 1081).unwrap();
498        assert_eq!(size, 1921 * 1081 + 2 * (961 * 541));
499    }
500
501    #[test]
502    fn nv12_has_two_planes_and_interleaved_chroma() {
503        assert_eq!(PixelFormat::Nv12.plane_count(), 2);
504        assert_eq!(PixelFormat::Nv12.samples_per_pixel(1), 2);
505
506        let size = packed_size(PixelFormat::Nv12, 640, 480).unwrap();
507        assert_eq!(size, 640 * 480 * 3 / 2);
508    }
509
510    #[test]
511    fn p010_takes_two_bytes_a_sample() {
512        assert_eq!(PixelFormat::P010.bit_depth(), 10);
513        assert_eq!(
514            packed_size(PixelFormat::P010, 640, 480).unwrap(),
515            640 * 480 * 3
516        );
517    }
518
519    #[test]
520    fn a_frame_too_small_for_its_dimensions_is_refused() {
521        let error = Frame::packed(
522            1920,
523            1080,
524            PixelFormat::I420,
525            ColorSpace::default(),
526            Duration::ZERO,
527            vec![0; 100],
528        );
529
530        assert!(matches!(error, Err(Error::BadFrame { .. })));
531    }
532
533    #[test]
534    fn a_zero_sized_picture_is_refused() {
535        assert!(matches!(
536            packed_size(PixelFormat::I420, 0, 1080),
537            Err(Error::BadFrame { .. })
538        ));
539    }
540
541    /// Strides are what decoders actually hand back, and the last row is
542    /// allowed to stop at the end of the picture rather than the end of a
543    /// stride.
544    #[test]
545    fn padded_strides_are_accepted_and_the_last_row_need_not_be_padded() {
546        let width = 100_u32;
547        let height = 10_u32;
548        let stride = 128_usize;
549
550        let luma = stride * (height as usize - 1) + width as usize;
551        let chroma_rows = height.div_ceil(2) as usize;
552        let chroma = stride * (chroma_rows - 1) + width.div_ceil(2) as usize;
553
554        let planes = vec![
555            Plane { offset: 0, stride },
556            Plane {
557                offset: luma,
558                stride,
559            },
560            Plane {
561                offset: luma + chroma,
562                stride,
563            },
564        ];
565
566        let total = luma + 2 * chroma;
567
568        let frame = Frame::with_planes(
569            width,
570            height,
571            PixelFormat::I420,
572            ColorSpace::default(),
573            Duration::ZERO,
574            planes,
575            vec![0; total],
576        )
577        .unwrap();
578
579        assert_eq!(frame.row(0, 9).unwrap().len(), width as usize);
580        assert!(frame.row(0, 10).is_none(), "there is no eleventh row");
581    }
582
583    #[test]
584    fn a_stride_shorter_than_a_row_is_refused() {
585        let planes = vec![
586            Plane {
587                offset: 0,
588                stride: 10,
589            },
590            Plane {
591                offset: 0,
592                stride: 10,
593            },
594            Plane {
595                offset: 0,
596                stride: 10,
597            },
598        ];
599
600        let error = Frame::with_planes(
601            100,
602            10,
603            PixelFormat::I420,
604            ColorSpace::default(),
605            Duration::ZERO,
606            planes,
607            vec![0; 10_000],
608        );
609
610        let Err(Error::BadFrame { reason }) = error else {
611            panic!("a 10-byte stride cannot hold a 100-byte row");
612        };
613
614        assert!(reason.contains("stride"), "{reason}");
615    }
616
617    /// The hostile case: a header claiming a stride that walks off the end of
618    /// the buffer. Caught here rather than in a decoder's memcpy.
619    #[test]
620    fn a_plane_that_runs_past_the_buffer_is_refused() {
621        let planes = vec![
622            Plane {
623                offset: 0,
624                stride: 1024,
625            },
626            Plane {
627                offset: 0,
628                stride: 1024,
629            },
630            Plane {
631                offset: 0,
632                stride: 1024,
633            },
634        ];
635
636        let error = Frame::with_planes(
637            100,
638            100,
639            PixelFormat::I420,
640            ColorSpace::default(),
641            Duration::ZERO,
642            planes,
643            vec![0; 1000],
644        );
645
646        assert!(matches!(error, Err(Error::BadFrame { .. })));
647    }
648
649    #[test]
650    fn retiming_shares_the_pixels() {
651        let frame = frame_of(PixelFormat::I420, 64, 64);
652        let moved = frame.with_pts(Duration::from_millis(500));
653
654        assert_eq!(moved.pts(), Duration::from_millis(500));
655        assert_eq!(frame.pts(), Duration::ZERO);
656        assert!(
657            Arc::ptr_eq(&frame.data, &moved.data),
658            "retiming copied the picture"
659        );
660    }
661
662    #[test]
663    fn the_pool_stops_allocating_once_it_is_warm() {
664        let mut pool = FramePool::new(4);
665        let size = packed_size(PixelFormat::I420, 320, 240).unwrap();
666
667        for _ in 0..10 {
668            let buffer = pool.take(size);
669            pool.give(buffer);
670        }
671
672        let (reused, allocated) = pool.statistics();
673        assert_eq!(allocated, 1, "one allocation should have served all ten");
674        assert_eq!(reused, 9);
675    }
676
677    #[test]
678    fn the_pool_will_not_take_a_buffer_still_being_read() {
679        let mut pool = FramePool::new(4);
680        let frame = frame_of(PixelFormat::I420, 64, 64);
681        let still_held = frame.clone();
682
683        pool.reclaim(frame);
684        assert_eq!(pool.buffers.len(), 0, "that buffer is still in use");
685
686        drop(still_held);
687    }
688}