Skip to main content

g2g_core/
caps_transform.rs

1//! M837: declarative forward derivation for caps transforms
2//! (DESIGN.md ยง4.13.1).
3//!
4//! A caps-driven transform (videoscale / videoconvert / videorate /
5//! audioconvert / audioresample) derives its output caps field by field from
6//! its input: some fields pass through, some are retargeted to a value the
7//! element's properties or the downstream pin choose. [`CapsTransform`] states
8//! that relation as data instead of a closure, so the solver can read it: the
9//! backward-coupling mask ([`CapsTransform::passthrough`]) is the set of fields
10//! every output alternative derives with [`FieldTransform::Identity`], which
11//! makes a mask that disagrees with the derivation unrepresentable.
12
13use alloc::vec::Vec;
14
15use crate::caps::{AudioFormat, Caps, CapsSet, Dim, PassthroughFields, Rate, RawVideoFormat};
16
17/// How one output caps field is derived from the corresponding input field.
18///
19/// `Fixed` carries a whole caps value, so it also expresses a *ranged* retarget
20/// (`Dim::Range`, `Rate::Any`, `ANY_SAMPLE_RATE`): the alternative a downstream
21/// capsfilter then pins. A scale-by-rational variant (`width * num / den`) was
22/// considered and left out: no element derives a field that way, so the
23/// vocabulary stays at these two until one does.
24#[derive(Clone, Debug, PartialEq)]
25pub enum FieldTransform<T> {
26    /// Output field == input field. The declaration that makes the field a
27    /// passthrough, so the solver couples it backward.
28    Identity,
29    /// Output field is this value, whatever the input carries.
30    Fixed(T),
31}
32
33impl<T: Clone> FieldTransform<T> {
34    /// Derive the output field from `input`.
35    pub fn apply(&self, input: &T) -> T {
36        match self {
37            Self::Identity => input.clone(),
38            Self::Fixed(v) => v.clone(),
39        }
40    }
41}
42
43impl<T> FieldTransform<T> {
44    pub fn is_identity(&self) -> bool {
45        matches!(self, Self::Identity)
46    }
47}
48
49/// One output alternative of a raw-video [`CapsTransform`].
50#[derive(Clone, Debug, PartialEq)]
51pub struct RawVideoShape {
52    pub format: FieldTransform<RawVideoFormat>,
53    pub width: FieldTransform<Dim>,
54    pub height: FieldTransform<Dim>,
55    pub framerate: FieldTransform<Rate>,
56}
57
58impl RawVideoShape {
59    /// Every field passed through. Retarget from here with the `with_*`
60    /// setters: `RawVideoShape::PASSTHROUGH.with_width(FieldTransform::Fixed(w))`.
61    pub const PASSTHROUGH: Self = Self {
62        format: FieldTransform::Identity,
63        width: FieldTransform::Identity,
64        height: FieldTransform::Identity,
65        framerate: FieldTransform::Identity,
66    };
67
68    pub fn with_format(mut self, t: FieldTransform<RawVideoFormat>) -> Self {
69        self.format = t;
70        self
71    }
72    pub fn with_width(mut self, t: FieldTransform<Dim>) -> Self {
73        self.width = t;
74        self
75    }
76    pub fn with_height(mut self, t: FieldTransform<Dim>) -> Self {
77        self.height = t;
78        self
79    }
80    pub fn with_framerate(mut self, t: FieldTransform<Rate>) -> Self {
81        self.framerate = t;
82        self
83    }
84}
85
86/// One output alternative of an audio [`CapsTransform`].
87#[derive(Clone, Debug, PartialEq)]
88pub struct AudioShape {
89    pub format: FieldTransform<AudioFormat>,
90    pub channels: FieldTransform<u8>,
91    pub sample_rate: FieldTransform<u32>,
92}
93
94impl AudioShape {
95    /// Every field passed through; retarget with the `with_*` setters.
96    pub const PASSTHROUGH: Self = Self {
97        format: FieldTransform::Identity,
98        channels: FieldTransform::Identity,
99        sample_rate: FieldTransform::Identity,
100    };
101
102    pub fn with_format(mut self, t: FieldTransform<AudioFormat>) -> Self {
103        self.format = t;
104        self
105    }
106    pub fn with_channels(mut self, t: FieldTransform<u8>) -> Self {
107        self.channels = t;
108        self
109    }
110    pub fn with_sample_rate(mut self, t: FieldTransform<u32>) -> Self {
111        self.sample_rate = t;
112        self
113    }
114}
115
116/// Declarative forward derivation for a transform element, read by the solver
117/// through [`CapsConstraint::DerivedFields`](crate::format_element::CapsConstraint).
118///
119/// `shapes` are the output alternatives in preference order (first preferred);
120/// duplicates are dropped, so a shape that coincides with the passthrough on a
121/// given input costs nothing. `accept` gates the input format (empty = every
122/// format of that media kind) and `produce` gates the derived output format
123/// (empty = unrestricted), which is what keeps an input-only format out of the
124/// derived set. An input the transform doesn't accept, or no surviving shape,
125/// derives the empty set, which the solver reads as an unsatisfiable link.
126#[derive(Clone, Debug, PartialEq)]
127pub enum CapsTransform {
128    RawVideo {
129        accept: Vec<RawVideoFormat>,
130        produce: Vec<RawVideoFormat>,
131        shapes: Vec<RawVideoShape>,
132    },
133    Audio {
134        accept: Vec<AudioFormat>,
135        produce: Vec<AudioFormat>,
136        shapes: Vec<AudioShape>,
137    },
138}
139
140impl CapsTransform {
141    /// Forward derivation: the ordered output alternatives for `input`.
142    pub fn derive(&self, input: &Caps) -> CapsSet {
143        let mut alts: Vec<Caps> = Vec::new();
144        match (self, input) {
145            (
146                Self::RawVideo {
147                    accept,
148                    produce,
149                    shapes,
150                },
151                Caps::RawVideo {
152                    format,
153                    width,
154                    height,
155                    framerate,
156                    interlace,
157                },
158            ) => {
159                if !accept.is_empty() && !accept.contains(format) {
160                    return CapsSet::from_alternatives(Vec::new());
161                }
162                for s in shapes {
163                    let f = s.format.apply(format);
164                    if !produce.is_empty() && !produce.contains(&f) {
165                        continue;
166                    }
167                    push_unique(
168                        &mut alts,
169                        Caps::RawVideo {
170                            format: f,
171                            width: s.width.apply(width),
172                            height: s.height.apply(height),
173                            framerate: s.framerate.apply(framerate),
174                            // A format/geometry reshape leaves scan structure alone.
175                            interlace: *interlace,
176                        },
177                    );
178                }
179            }
180            (
181                Self::Audio {
182                    accept,
183                    produce,
184                    shapes,
185                },
186                Caps::Audio {
187                    format,
188                    channels,
189                    sample_rate,
190                },
191            ) => {
192                if !accept.is_empty() && !accept.contains(format) {
193                    return CapsSet::from_alternatives(Vec::new());
194                }
195                for s in shapes {
196                    let f = s.format.apply(format);
197                    if !produce.is_empty() && !produce.contains(&f) {
198                        continue;
199                    }
200                    push_unique(
201                        &mut alts,
202                        Caps::Audio {
203                            format: f,
204                            channels: s.channels.apply(channels),
205                            sample_rate: s.sample_rate.apply(sample_rate),
206                        },
207                    );
208                }
209            }
210            _ => {}
211        }
212        CapsSet::from_alternatives(alts)
213    }
214
215    /// Which fields the solver may couple backward: those every output shape
216    /// derives with [`FieldTransform::Identity`]. A field one alternative
217    /// retargets is not coupled, whichever alternative the solve picks. No
218    /// shapes means nothing derives, so nothing couples.
219    pub fn passthrough(&self) -> PassthroughFields {
220        match self {
221            Self::RawVideo { shapes, .. } if !shapes.is_empty() => PassthroughFields {
222                format: shapes.iter().all(|s| s.format.is_identity()),
223                width: shapes.iter().all(|s| s.width.is_identity()),
224                height: shapes.iter().all(|s| s.height.is_identity()),
225                framerate: shapes.iter().all(|s| s.framerate.is_identity()),
226                channels: false,
227                sample_rate: false,
228            },
229            Self::Audio { shapes, .. } if !shapes.is_empty() => PassthroughFields {
230                format: shapes.iter().all(|s| s.format.is_identity()),
231                width: false,
232                height: false,
233                framerate: false,
234                channels: shapes.iter().all(|s| s.channels.is_identity()),
235                sample_rate: shapes.iter().all(|s| s.sample_rate.is_identity()),
236            },
237            _ => PassthroughFields::NONE,
238        }
239    }
240}
241
242fn push_unique(alts: &mut Vec<Caps>, c: Caps) {
243    if !alts.contains(&c) {
244        alts.push(c);
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::caps::ANY_SAMPLE_RATE;
252    use alloc::vec;
253
254    fn raw(format: RawVideoFormat, w: u32, h: u32, fps: u32) -> Caps {
255        Caps::RawVideo {
256            format,
257            width: Dim::Fixed(w),
258            height: Dim::Fixed(h),
259            framerate: Rate::Fixed(fps << 16),
260            interlace: crate::Interlace::Any,
261        }
262    }
263
264    fn pcm(format: AudioFormat, channels: u8, sample_rate: u32) -> Caps {
265        Caps::Audio {
266            format,
267            channels,
268            sample_rate,
269        }
270    }
271
272    fn video(accept: &[RawVideoFormat], shapes: Vec<RawVideoShape>) -> CapsTransform {
273        CapsTransform::RawVideo {
274            accept: accept.to_vec(),
275            produce: Vec::new(),
276            shapes,
277        }
278    }
279
280    #[test]
281    fn identity_shape_derives_the_input_and_couples_every_field() {
282        let t = video(&[RawVideoFormat::Nv12], vec![RawVideoShape::PASSTHROUGH]);
283        let inp = raw(RawVideoFormat::Nv12, 320, 240, 30);
284        assert_eq!(t.derive(&inp).alternatives(), core::slice::from_ref(&inp));
285        let pt = t.passthrough();
286        assert!(pt.format && pt.width && pt.height && pt.framerate);
287    }
288
289    #[test]
290    fn fixed_shape_retargets_the_field_and_drops_it_from_the_mask() {
291        let t = video(
292            &[RawVideoFormat::Nv12],
293            vec![RawVideoShape::PASSTHROUGH
294                .with_width(FieldTransform::Fixed(Dim::Fixed(64)))
295                .with_height(FieldTransform::Fixed(Dim::Fixed(32)))],
296        );
297        assert_eq!(
298            t.derive(&raw(RawVideoFormat::Nv12, 320, 240, 30))
299                .alternatives(),
300            &[raw(RawVideoFormat::Nv12, 64, 32, 30)]
301        );
302        let pt = t.passthrough();
303        assert!(
304            pt.format && pt.framerate,
305            "format + rate still pass through"
306        );
307        assert!(
308            !pt.width && !pt.height,
309            "retargeted geometry is not coupled"
310        );
311    }
312
313    #[test]
314    fn unaccepted_input_derives_nothing() {
315        let t = video(&[RawVideoFormat::Nv12], vec![RawVideoShape::PASSTHROUGH]);
316        assert!(t
317            .derive(&raw(RawVideoFormat::Rgba8, 320, 240, 30))
318            .is_empty());
319        // Wrong media kind entirely.
320        assert!(t.derive(&pcm(AudioFormat::PcmS16Le, 2, 48_000)).is_empty());
321        // An empty accept list takes any format of the kind.
322        let any = video(&[], vec![RawVideoShape::PASSTHROUGH]);
323        assert!(!any
324            .derive(&raw(RawVideoFormat::Rgba8, 320, 240, 30))
325            .is_empty());
326    }
327
328    #[test]
329    fn produce_gate_drops_an_input_only_passthrough() {
330        // Yuyv in, never out: the passthrough alternative is filtered, so the
331        // derived set is the producible format only.
332        let t = CapsTransform::RawVideo {
333            accept: vec![RawVideoFormat::Yuyv, RawVideoFormat::Nv12],
334            produce: vec![RawVideoFormat::Nv12],
335            shapes: vec![
336                RawVideoShape::PASSTHROUGH,
337                RawVideoShape::PASSTHROUGH.with_format(FieldTransform::Fixed(RawVideoFormat::Nv12)),
338            ],
339        };
340        assert_eq!(
341            t.derive(&raw(RawVideoFormat::Yuyv, 320, 240, 30))
342                .alternatives(),
343            &[raw(RawVideoFormat::Nv12, 320, 240, 30)]
344        );
345        // An Nv12 input hits the passthrough first, and the duplicate retarget
346        // collapses into it.
347        assert_eq!(
348            t.derive(&raw(RawVideoFormat::Nv12, 320, 240, 30))
349                .alternatives(),
350            &[raw(RawVideoFormat::Nv12, 320, 240, 30)]
351        );
352    }
353
354    #[test]
355    fn audio_shapes_derive_channels_and_rate() {
356        let t = CapsTransform::Audio {
357            accept: vec![AudioFormat::PcmS16Le],
358            produce: Vec::new(),
359            shapes: vec![
360                AudioShape::PASSTHROUGH,
361                AudioShape::PASSTHROUGH.with_sample_rate(FieldTransform::Fixed(ANY_SAMPLE_RATE)),
362            ],
363        };
364        assert_eq!(
365            t.derive(&pcm(AudioFormat::PcmS16Le, 2, 44_100))
366                .alternatives(),
367            &[
368                pcm(AudioFormat::PcmS16Le, 2, 44_100),
369                pcm(AudioFormat::PcmS16Le, 2, ANY_SAMPLE_RATE)
370            ]
371        );
372        let pt = t.passthrough();
373        assert!(pt.format && pt.channels);
374        assert!(!pt.sample_rate, "one alternative retargets the rate");
375    }
376
377    #[test]
378    fn no_shapes_derives_nothing_and_couples_nothing() {
379        // How an element declares an invalid configuration (videorate with a
380        // non-positive target): the solve fails loud instead of fixating.
381        let t = video(&[RawVideoFormat::Nv12], Vec::new());
382        assert!(t
383            .derive(&raw(RawVideoFormat::Nv12, 320, 240, 30))
384            .is_empty());
385        assert_eq!(t.passthrough(), PassthroughFields::NONE);
386    }
387}