Skip to main content

moq_video/
size.rs

1use crate::{Error, Rate};
2
3/// A frame resolution in pixels.
4///
5/// Names the pair that [`decode::Config::scale_hint`](crate::decode::Config::scale_hint)
6/// and [`Frame::resize`](crate::Frame::resize) both take, so
7/// width and height can't be swapped at a call site.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9pub struct Size {
10	/// Width in pixels.
11	pub width: u32,
12	/// Height in pixels.
13	pub height: u32,
14}
15
16impl Size {
17	/// A size of `width` x `height` pixels.
18	pub fn new(width: u32, height: u32) -> Self {
19		Self { width, height }
20	}
21
22	/// Total pixels. Can't overflow: the widest `u32` square still fits a `u64`.
23	pub fn pixels(&self) -> u64 {
24		self.width as u64 * self.height as u64
25	}
26
27	pub(crate) fn byte_len(&self, bytes_per_pixel: usize, what: &str) -> Result<usize, Error> {
28		usize::try_from(self.pixels())
29			.ok()
30			.and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
31			.ok_or_else(|| Error::Codec(anyhow::anyhow!("{what} {self}: byte length is too large to represent")))
32	}
33
34	/// Reject anything the I420 pipeline can't represent.
35	///
36	/// I420 chroma is subsampled 2x2, so every stage (encode, decode, resize)
37	/// needs even, non-zero dimensions. Checking here keeps the rule in one place
38	/// instead of re-deriving it at each boundary.
39	pub(crate) fn validate(&self, what: &str) -> Result<(), Error> {
40		self.validate_nonzero(what)?;
41		if !self.width.is_multiple_of(2) || !self.height.is_multiple_of(2) {
42			return Err(Error::Codec(anyhow::anyhow!("{what} {self}: dimensions must be even")));
43		}
44		Ok(())
45	}
46
47	/// Reject a size whose derived quantities cannot be computed at all.
48	///
49	/// Dimensions arrive as a pair of `u32`, so their product reaches ~1.8e19
50	/// before anything else looks at them. That overflows the two things every
51	/// encoder derives from a size: the default bitrate (pixels x framerate) and
52	/// a packed frame's byte count (up to 4 bytes per pixel for RGBA). Both would
53	/// panic, which for a binding is an aborted host process rather than an
54	/// error return, so refuse the config here instead.
55	///
56	/// This is arithmetic, not policy: it rejects only what cannot be represented,
57	/// leaving "no encoder handles a frame that large" to the backend.
58	pub(crate) fn validate_encodable(&self, what: &str, framerate: Rate) -> Result<(), Error> {
59		let pixels = self.pixels();
60		let representable = u128::from(pixels)
61			.checked_mul(u128::from(framerate.numerator()))
62			.is_some_and(|value| value / u128::from(framerate.denominator()) <= u128::from(u64::MAX))
63			&& usize::try_from(pixels).is_ok_and(|pixels| pixels.checked_mul(4).is_some());
64
65		if !representable {
66			return Err(Error::Codec(anyhow::anyhow!(
67				"{what} {self} at {framerate}fps: dimensions too large to represent"
68			)));
69		}
70		Ok(())
71	}
72
73	/// The half of [`Size::validate`] that is not about chroma, for the surfaces
74	/// that hold RGB and so tolerate odd dimensions (a render target sized to a
75	/// window).
76	pub(crate) fn validate_nonzero(&self, what: &str) -> Result<(), Error> {
77		if self.width == 0 || self.height == 0 {
78			return Err(Error::Codec(anyhow::anyhow!(
79				"{what} {self}: dimensions must be non-zero"
80			)));
81		}
82		Ok(())
83	}
84}
85
86impl std::fmt::Display for Size {
87	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88		write!(f, "{}x{}", self.width, self.height)
89	}
90}
91
92impl From<(u32, u32)> for Size {
93	fn from((width, height): (u32, u32)) -> Self {
94		Self::new(width, height)
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use super::*;
101
102	#[test]
103	fn validate_rejects_odd_and_zero() {
104		assert!(Size::new(320, 240).validate("frame").is_ok());
105		assert!(Size::new(0, 240).validate("frame").is_err());
106		assert!(Size::new(320, 0).validate("frame").is_err());
107		assert!(Size::new(321, 240).validate("frame").is_err());
108		assert!(Size::new(320, 241).validate("frame").is_err());
109	}
110
111	/// Regression: `u32` dimensions can reach a pixel count whose derived
112	/// quantities overflow. A binding hands these straight through, and a panic
113	/// there aborts the host process, so the size has to be refused first.
114	#[test]
115	fn validate_encodable_rejects_unrepresentable_sizes() {
116		// A frame nobody can encode, but whose arithmetic still fits: the backend
117		// decides, not us.
118		let rate = Rate::new(30, 1).unwrap();
119		assert!(Size::new(65534, 65534).validate_encodable("frame", rate).is_ok());
120
121		// pixels x framerate overflows u64.
122		assert!(
123			Size::new(u32::MAX - 1, u32::MAX - 1)
124				.validate_encodable("frame", rate)
125				.is_err()
126		);
127		// ...and it is the product that matters, not either side alone.
128		assert!(Size::new(u32::MAX - 1, 2).validate_encodable("frame", rate).is_ok());
129		let extreme = Rate::new(crate::rate::MAX_FRAMES_PER_SECOND, 1).unwrap();
130		assert!(
131			Size::new(u32::MAX - 1, u32::MAX - 1)
132				.validate_encodable("frame", extreme)
133				.is_err()
134		);
135	}
136
137	#[test]
138	fn display_reads_as_a_resolution() {
139		assert_eq!(Size::new(1920, 1080).to_string(), "1920x1080");
140	}
141}