Skip to main content

g2g_python/
format.rs

1//! The shared pixel-format contract between g2g and the gst-python-ml backend.
2//!
3//! gst-python-ml's `tasks/frame_format.py` and its `FrameIO.read_frame(s)`
4//! speak GStreamer-style format strings (`"RGBA"`, `"NV12"`, ...). g2g speaks
5//! [`RawVideoFormat`]. Caps negotiation (which format the link carries) and the
6//! per-frame call (which `fmt` string Python is handed) must agree, so the
7//! mapping lives in one place here rather than being re-derived on each side.
8//!
9//! Only the formats g2g currently models are mapped; an unmapped string from
10//! the Python side is a negotiation error, not a silent guess.
11
12use g2g_core::RawVideoFormat;
13
14/// The GStreamer-style format string for a g2g raw-video format, as the
15/// gst-python-ml `FrameIO` / `frame_format` code expects it.
16pub fn format_to_py(fmt: RawVideoFormat) -> &'static str {
17    match fmt {
18        RawVideoFormat::Rgba8 => "RGBA",
19        RawVideoFormat::Bgra8 => "BGRA",
20        RawVideoFormat::Rgb8 => "RGB",
21        RawVideoFormat::Nv12 => "NV12",
22        RawVideoFormat::I420 => "I420",
23        RawVideoFormat::Yuyv => "YUY2",
24        RawVideoFormat::I420p10 => "I420_10LE",
25        RawVideoFormat::I420p12 => "I420_12LE",
26        RawVideoFormat::I422 => "Y42B",
27        RawVideoFormat::I422p10 => "I422_10LE",
28        RawVideoFormat::I422p12 => "I422_12LE",
29        RawVideoFormat::I444 => "Y444",
30        RawVideoFormat::I444p10 => "Y444_10LE",
31        RawVideoFormat::I444p12 => "Y444_12LE",
32        RawVideoFormat::P010 => "P010_10LE",
33        // A g2g format this binding does not model (or one added since): return
34        // a marker the gst-python-ml `FrameIO` will reject, rather than guess.
35        _ => "UNKNOWN",
36    }
37}
38
39/// Parse a gst-python-ml format string back into a g2g [`RawVideoFormat`].
40/// Returns `None` for a format g2g does not model. `YUYV` is accepted as an
41/// alias for `YUY2` (V4L2 vs GStreamer spelling of the same packed 4:2:2).
42pub fn format_from_py(s: &str) -> Option<RawVideoFormat> {
43    Some(match s {
44        "RGBA" => RawVideoFormat::Rgba8,
45        "BGRA" => RawVideoFormat::Bgra8,
46        "RGB" => RawVideoFormat::Rgb8,
47        "NV12" => RawVideoFormat::Nv12,
48        "I420" => RawVideoFormat::I420,
49        "YUY2" | "YUYV" => RawVideoFormat::Yuyv,
50        "I420_10LE" => RawVideoFormat::I420p10,
51        "I420_12LE" => RawVideoFormat::I420p12,
52        "Y42B" => RawVideoFormat::I422,
53        "I422_10LE" => RawVideoFormat::I422p10,
54        "I422_12LE" => RawVideoFormat::I422p12,
55        "Y444" => RawVideoFormat::I444,
56        "Y444_10LE" => RawVideoFormat::I444p10,
57        "Y444_12LE" => RawVideoFormat::I444p12,
58        "P010_10LE" => RawVideoFormat::P010,
59        _ => return None,
60    })
61}
62
63/// Bytes one `width` x `height` frame of `fmt` occupies, for allocating a blank
64/// source buffer. Packed formats are exact; the fully-planar YUV family derives
65/// its size from the format's own subsampling and sample depth.
66pub fn frame_bytes(fmt: RawVideoFormat, width: u32, height: u32) -> usize {
67    // Fully-planar YUV (I420/I422/I444 at 8/10/12-bit): Y plus two subsampled
68    // chroma planes at this depth's sample size.
69    if let Some((hs, vs)) = fmt.chroma_shift() {
70        let (w, h) = (width as usize, height as usize);
71        let (cw, ch) = (w.div_ceil(1 << hs), h.div_ceil(1 << vs));
72        return (w * h + 2 * cw * ch) * fmt.bytes_per_sample();
73    }
74    let (w, h) = (width as usize, height as usize);
75    match fmt {
76        RawVideoFormat::Rgba8 | RawVideoFormat::Bgra8 => w * h * 4,
77        // Packed RGB: three bytes per pixel, no alpha.
78        RawVideoFormat::Rgb8 => w * h * 3,
79        RawVideoFormat::Yuyv => w * h * 2,
80        RawVideoFormat::Nv12 => w * h * 3 / 2,
81        // Semi-planar 10-bit: NV12's sample counts at 2 bytes each.
82        RawVideoFormat::P010 => w * h * 3,
83        // The fully-planar formats are handled above via `chroma_shift`.
84        RawVideoFormat::I420
85        | RawVideoFormat::I420p10
86        | RawVideoFormat::I420p12
87        | RawVideoFormat::I422
88        | RawVideoFormat::I422p10
89        | RawVideoFormat::I422p12
90        | RawVideoFormat::I444
91        | RawVideoFormat::I444p10
92        | RawVideoFormat::I444p12 => unreachable!("planar YUV handled by chroma_shift"),
93        // A packed format not modeled here (or one added since): fail loud
94        // rather than mis-size a buffer.
95        _ => unreachable!("unmodeled packed RawVideoFormat: {fmt:?}"),
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn round_trips_every_modeled_format() {
105        for fmt in [
106            RawVideoFormat::Rgba8,
107            RawVideoFormat::Bgra8,
108            RawVideoFormat::Nv12,
109            RawVideoFormat::I420,
110            RawVideoFormat::Yuyv,
111            RawVideoFormat::I420p10,
112            RawVideoFormat::I420p12,
113            RawVideoFormat::I422,
114            RawVideoFormat::I422p10,
115            RawVideoFormat::I422p12,
116            RawVideoFormat::I444,
117            RawVideoFormat::I444p10,
118            RawVideoFormat::I444p12,
119        ] {
120            assert_eq!(format_from_py(format_to_py(fmt)), Some(fmt));
121        }
122    }
123
124    #[test]
125    fn frame_bytes_match_planar_geometry() {
126        // 4x4: I420 8-bit = 16 + 2*4 = 24; 10-bit doubles to 48.
127        assert_eq!(frame_bytes(RawVideoFormat::I420, 4, 4), 24);
128        assert_eq!(frame_bytes(RawVideoFormat::I420p10, 4, 4), 48);
129        // 4:2:2 keeps full height: 16 + 2*(2*4) = 32 (8-bit), 64 (12-bit).
130        assert_eq!(frame_bytes(RawVideoFormat::I422, 4, 4), 32);
131        assert_eq!(frame_bytes(RawVideoFormat::I422p12, 4, 4), 64);
132        // 4:4:4 full chroma: 16 + 2*16 = 48 (8-bit), 96 (10-bit).
133        assert_eq!(frame_bytes(RawVideoFormat::I444, 4, 4), 48);
134        assert_eq!(frame_bytes(RawVideoFormat::I444p10, 4, 4), 96);
135    }
136
137    #[test]
138    fn yuyv_is_an_alias_for_yuy2() {
139        assert_eq!(format_from_py("YUYV"), Some(RawVideoFormat::Yuyv));
140        assert_eq!(format_from_py("YUY2"), Some(RawVideoFormat::Yuyv));
141    }
142
143    #[test]
144    fn unmodeled_format_is_none() {
145        assert_eq!(format_from_py("GRAY8"), None);
146    }
147
148    #[test]
149    fn frame_bytes_per_format() {
150        assert_eq!(frame_bytes(RawVideoFormat::Rgba8, 4, 2), 32);
151        assert_eq!(frame_bytes(RawVideoFormat::Yuyv, 4, 2), 16);
152        assert_eq!(frame_bytes(RawVideoFormat::Nv12, 4, 2), 12);
153    }
154}