Skip to main content

g2g_core/
format_element.rs

1//! M16 step 2 (DESIGN.md §4.13.1): negotiation-time element
2//! surface.
3//!
4//! `FormatElement` is the trait the future solver (M16 step 3) consumes;
5//! `CapsConstraint` is the per-element data it walks. Both coexist with
6//! the runtime `AsyncElement` surface during migration. The
7//! `AsyncElement` → `FormatElement` adapter (the "legacy" path the
8//! design plan calls for) lands together with the solver in step 3,
9//! because its exact shape is dictated by what the solver consumes.
10
11use alloc::boxed::Box;
12use alloc::vec::Vec;
13
14use crate::caps::{Caps, CapsSet};
15use crate::caps_transform::CapsTransform;
16use crate::element::{AsyncElement, ElementBound};
17use crate::error::G2gError;
18
19/// Boxed `intercept_caps`-style callback (input narrowing) for the legacy
20/// migration bridge. Deleted with the bridge.
21type InterceptFn<'a> = Box<dyn Fn(&Caps) -> Result<Caps, G2gError> + 'a>;
22/// Boxed `propose_output_caps`-style callback (forward derivation) for the
23/// legacy migration bridge. Deleted with the bridge.
24type ProposeFn<'a> = Box<dyn Fn(&Caps) -> Caps + 'a>;
25
26/// Per-element constraint surface read by the solver.
27///
28/// Replaces today's `intercept_caps` (the input-narrowing half) and the
29/// forward-half `propose_output_caps` hook with a single declarative
30/// shape. Element categories map cleanly:
31///
32/// | Today                                                | Tomorrow                          |
33/// | ---                                                  | ---                               |
34/// | Source `intercept_caps` returns its produced caps    | `Produces(set)`                   |
35/// | Sink `intercept_caps` narrows upstream               | `Accepts(set)`                    |
36/// | Identity transform                                   | `Identity(set)`                   |
37/// | Decoder reading dims from input SPS                  | `DerivedOutput(\|in\| out_set)`   |
38/// | Caps-driven transform (scale / convert / rate)       | `DerivedFields(transform)`        |
39/// | Pre-enumerated input/output pairs (codecs, scalers)  | `Mapping(pairs)`                  |
40pub enum CapsConstraint<'a> {
41    /// Sink shape: only the input side is constrained. Output is
42    /// unused (sink has no downstream).
43    Accepts(CapsSet),
44
45    /// Source shape: only the output side is constrained. Input is
46    /// unused.
47    Produces(CapsSet),
48
49    /// Pass-through transform: input == output, both drawn from this
50    /// set. Identity / probe / metering elements land here, as do
51    /// format converters that accept only one format.
52    Identity(CapsSet),
53
54    /// Format-changing transform with an explicit (input, output)
55    /// relation. The vector enumerates all legal pairs; the solver
56    /// picks one. Most decoders and encoders use this when their
57    /// output is determined by configuration rather than the input
58    /// data itself.
59    Mapping(Vec<(CapsSet, CapsSet)>),
60
61    /// Programmatic mapping: the output set is a function of the
62    /// already-narrowed input caps. Used when output depends on the
63    /// input in a way that can't be precomputed, e.g. a decoder
64    /// reading SPS to fix output dims. The solver calls this during
65    /// forward propagation, after the input link has been narrowed
66    /// but before the output link is solved.
67    DerivedOutput(Box<dyn Fn(&Caps) -> CapsSet + Send + Sync + 'a>),
68
69    /// Declarative form of [`DerivedOutput`](Self::DerivedOutput) (M837): the
70    /// forward derivation stated field by field as data the solver can inspect.
71    /// Because it knows which fields are passed through unchanged, it can
72    /// couple those *bidirectionally and per field*, so a downstream pin on a
73    /// passthrough field narrows the corresponding input field (`Range ∩ Fixed
74    /// = Fixed`) rather than only dropping whole input alternatives. That is
75    /// what lets a geometry pin flow back through a geometry-passthrough
76    /// transform (`videoscale ! videoconvert ! caps`). Used by the caps-driven
77    /// transforms (videoscale / videoconvert / videorate / audioconvert /
78    /// audioresample); decoders whose output isn't a per-field function of
79    /// their input stay on `DerivedOutput`.
80    DerivedFields(CapsTransform),
81
82    /// **Migration bridge.** Source whose
83    /// [`SourceLoop::intercept_caps`](crate::runtime::SourceLoop::intercept_caps)
84    /// has been evaluated to a single concrete `Caps`. Used by the
85    /// runner to wrap legacy elements until step 5 migrates them to
86    /// `Produces`. Deleted at the end of the migration.
87    LegacySource(Caps),
88
89    /// **Migration bridge.** Transform with the today's
90    /// `intercept_caps(upstream) -> Caps` + `propose_output_caps(input)
91    /// -> Caps` callbacks. Non-boundary transforms set
92    /// `propose_output` to clone the input. Deleted at the end of the
93    /// migration. The boxes are not `Send + Sync` because the solver
94    /// consumes them synchronously on the runner thread.
95    LegacyTransform {
96        intercept: InterceptFn<'a>,
97        propose_output: ProposeFn<'a>,
98    },
99
100    /// **Migration bridge.** Sink with today's
101    /// `intercept_caps(upstream) -> Caps` callback. Deleted at the end
102    /// of the migration.
103    LegacySink(InterceptFn<'a>),
104
105    /// Sink-shape wildcard: accepts whatever upstream produces, of any
106    /// media type, format, dims, or rate. Models debug / probe /
107    /// passthrough sinks (`FakeSink`, `syncsink`, `identity`-as-sink)
108    /// whose `intercept_caps` is `Ok(upstream.clone())`. The solver
109    /// treats this as a no-op narrowing on the link: the upstream's
110    /// produced caps flow through unchanged.
111    AcceptsAny,
112
113    /// Transform-shape wildcard: forwards whatever upstream produces.
114    /// Input == output, both unconstrained. Models pass-through
115    /// transforms like `IdentityTransform` (probe / tee / metering
116    /// without format constraints). The solver couples the input and
117    /// output links to be equal but doesn't narrow either by a set.
118    IdentityAny,
119}
120
121impl core::fmt::Debug for CapsConstraint<'_> {
122    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123        match self {
124            Self::Accepts(s) => f.debug_tuple("Accepts").field(s).finish(),
125            Self::Produces(s) => f.debug_tuple("Produces").field(s).finish(),
126            Self::Identity(s) => f.debug_tuple("Identity").field(s).finish(),
127            Self::Mapping(v) => f.debug_tuple("Mapping").field(v).finish(),
128            Self::DerivedOutput(_) => f.debug_tuple("DerivedOutput").field(&"<fn>").finish(),
129            Self::DerivedFields(t) => f.debug_tuple("DerivedFields").field(t).finish(),
130            Self::LegacySource(c) => f.debug_tuple("LegacySource").field(c).finish(),
131            Self::LegacyTransform { .. } => f
132                .debug_struct("LegacyTransform")
133                .field("intercept", &"<fn>")
134                .finish_non_exhaustive(),
135            Self::LegacySink(_) => f.debug_tuple("LegacySink").field(&"<fn>").finish(),
136            Self::AcceptsAny => f.write_str("AcceptsAny"),
137            Self::IdentityAny => f.write_str("IdentityAny"),
138        }
139    }
140}
141
142impl CapsConstraint<'_> {
143    /// ACCEPT_CAPS query (DESIGN.md §4.13.1): would this element accept a
144    /// link carrying `caps`? A pure check against the declared
145    /// constraint, with no runtime negotiation or back-and-forth.
146    ///
147    /// For sink / transform shapes this tests the *input* side; for the
148    /// source shape it tests the *produced* side. Wildcards accept
149    /// anything; the legacy bridges defer to their wrapped callbacks.
150    pub fn accepts(&self, caps: &Caps) -> bool {
151        match self {
152            Self::Accepts(set) | Self::Produces(set) | Self::Identity(set) => set.accepts(caps),
153            Self::Mapping(pairs) => pairs.iter().any(|(input, _)| input.accepts(caps)),
154            Self::DerivedOutput(f) => !f(caps).is_empty(),
155            Self::DerivedFields(t) => !t.derive(caps).is_empty(),
156            Self::AcceptsAny | Self::IdentityAny => true,
157            Self::LegacySource(produced) => produced.intersect(caps).is_ok(),
158            Self::LegacyTransform { intercept, .. } => intercept(caps).is_ok(),
159            Self::LegacySink(intercept) => intercept(caps).is_ok(),
160        }
161    }
162}
163
164/// What one element is willing to pay for each of its advertised
165/// alternatives: a cost per alternative, aligned index-for-index with the
166/// [`CapsSet`] its [`CapsConstraint`] advertises (with the pair list for
167/// [`Mapping`](CapsConstraint::Mapping)). Lower is more preferred. The solver
168/// picks, among the consistent assignments of a linear chain, the one whose
169/// per-element costs sum to the least.
170///
171/// An element that declares nothing keeps the implicit rule "cost =
172/// alternative index", which is the set's own order. Costs buy the two things
173/// that order cannot say: *equal* cost between alternatives, meaning this
174/// element does not care and a neighbour's preference should decide, and a
175/// large gap, meaning a fallback is much worse than the first choice so a
176/// neighbour's mild preference must not pull the chain onto it.
177#[derive(Clone, Debug, Default, PartialEq, Eq)]
178pub struct CapsPreferences {
179    costs: Vec<u32>,
180}
181
182impl CapsPreferences {
183    /// Declare one cost per advertised alternative, in the same order.
184    pub fn new(costs: Vec<u32>) -> Self {
185        Self { costs }
186    }
187
188    /// The implicit default stated explicitly: alternative `i` costs `i`.
189    pub fn by_order(count: usize) -> Self {
190        Self {
191            costs: (0..count as u32).collect(),
192        }
193    }
194
195    /// Every alternative equally acceptable, so neighbours decide.
196    pub fn indifferent(count: usize) -> Self {
197        Self {
198            costs: alloc::vec![0; count],
199        }
200    }
201
202    /// The declared costs, in alternative order.
203    pub fn costs(&self) -> &[u32] {
204        &self.costs
205    }
206
207    /// True when nothing is declared, which the solver reads the same as
208    /// no preferences at all.
209    pub fn is_empty(&self) -> bool {
210        self.costs.is_empty()
211    }
212
213    /// Cost of alternative `index`. An index past the declared costs falls
214    /// back to the implicit "cost = index", so a short list still describes
215    /// the alternatives it covers.
216    pub fn cost(&self, index: usize) -> u32 {
217        self.costs.get(index).copied().unwrap_or(index as u32)
218    }
219}
220
221/// Negotiation-time view of an element. The runtime side
222/// (`AsyncElement::process`) is unchanged; this trait carries the
223/// information the solver needs to assign caps to every link before
224/// `process` runs.
225pub trait FormatElement: ElementBound {
226    /// Declare the constraint this element imposes on its surrounding
227    /// links. Read by the solver during negotiation.
228    fn caps_constraint(&self) -> CapsConstraint<'_>;
229
230    /// Optional per-alternative costs for this element's advertised
231    /// constraint. Defaults to `None`, meaning the solver uses the
232    /// constraint's own preference order (cost = alternative index).
233    fn caps_preferences(&self) -> Option<CapsPreferences> {
234        None
235    }
236
237    /// Called by the runner once the solver has assigned caps to every
238    /// link. Boundary elements (decoders, encoders) receive distinct
239    /// input / output values; non-boundary elements receive equal
240    /// ones. Sources see `input = None`; sinks see `output = None`.
241    fn configure_link(
242        &mut self,
243        input: Option<&Caps>,
244        output: Option<&Caps>,
245    ) -> Result<(), G2gError>;
246}
247
248/// Bridge an `AsyncElement` transform into a `LegacyTransform`
249/// constraint. The returned constraint borrows `transform` for `'a`.
250/// Non-boundary transforms get `propose_output = clone(input)`;
251/// boundary transforms route to `AsyncElement::propose_output_caps`.
252/// Used by the runner during the migration window to feed legacy
253/// elements to the M16 solver.
254pub fn legacy_transform_constraint<'a, T: AsyncElement + ?Sized>(
255    transform: &'a T,
256) -> CapsConstraint<'a> {
257    CapsConstraint::LegacyTransform {
258        intercept: Box::new(move |upstream: &Caps| transform.intercept_caps(upstream)),
259        propose_output: Box::new(move |input: &Caps| transform.propose_output_caps(input)),
260    }
261}
262
263/// Bridge an `AsyncElement` sink into a `LegacySink` constraint. The
264/// returned constraint borrows `sink` for `'a`.
265pub fn legacy_sink_constraint<'a, S: AsyncElement + ?Sized>(sink: &'a S) -> CapsConstraint<'a> {
266    CapsConstraint::LegacySink(Box::new(move |upstream: &Caps| {
267        sink.intercept_caps(upstream)
268    }))
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::caps::{Dim, Rate, RawVideoFormat, VideoCodec};
275    use alloc::vec;
276
277    fn video(format: RawVideoFormat, w: Dim, h: Dim, r: Rate) -> Caps {
278        Caps::RawVideo {
279            format,
280            width: w,
281            height: h,
282            framerate: r,
283            interlace: crate::Interlace::Any,
284        }
285    }
286
287    fn compressed(codec: VideoCodec, w: Dim, h: Dim, r: Rate) -> Caps {
288        Caps::CompressedVideo {
289            codec,
290            width: w,
291            height: h,
292            framerate: r,
293        }
294    }
295
296    struct FakeSource;
297    impl FormatElement for FakeSource {
298        fn caps_constraint(&self) -> CapsConstraint<'_> {
299            CapsConstraint::Produces(CapsSet::one(compressed(
300                VideoCodec::H264,
301                Dim::Fixed(1920),
302                Dim::Fixed(1080),
303                Rate::Fixed(30 << 16),
304            )))
305        }
306        fn configure_link(
307            &mut self,
308            input: Option<&Caps>,
309            output: Option<&Caps>,
310        ) -> Result<(), G2gError> {
311            assert!(input.is_none() && output.is_some());
312            Ok(())
313        }
314    }
315
316    struct FakeSink;
317    impl FormatElement for FakeSink {
318        fn caps_constraint(&self) -> CapsConstraint<'_> {
319            CapsConstraint::Accepts(CapsSet::one(video(
320                RawVideoFormat::Nv12,
321                Dim::Any,
322                Dim::Any,
323                Rate::Any,
324            )))
325        }
326        fn configure_link(
327            &mut self,
328            input: Option<&Caps>,
329            output: Option<&Caps>,
330        ) -> Result<(), G2gError> {
331            assert!(input.is_some() && output.is_none());
332            Ok(())
333        }
334    }
335
336    struct FakeDecoder;
337    impl FormatElement for FakeDecoder {
338        fn caps_constraint(&self) -> CapsConstraint<'_> {
339            // Output dims are read from the input caps; framerate is preserved.
340            CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
341                Caps::CompressedVideo {
342                    width,
343                    height,
344                    framerate,
345                    ..
346                } => CapsSet::one(Caps::RawVideo {
347                    format: RawVideoFormat::Nv12,
348                    width: width.clone(),
349                    height: height.clone(),
350                    framerate: framerate.clone(),
351                    interlace: crate::Interlace::Any,
352                }),
353                _ => CapsSet::from_alternatives(Vec::new()),
354            }))
355        }
356        fn configure_link(
357            &mut self,
358            _input: Option<&Caps>,
359            _output: Option<&Caps>,
360        ) -> Result<(), G2gError> {
361            Ok(())
362        }
363    }
364
365    #[test]
366    fn source_constraint_is_produces() {
367        let s = FakeSource;
368        let c = s.caps_constraint();
369        match c {
370            CapsConstraint::Produces(set) => assert_eq!(set.alternatives().len(), 1),
371            _ => panic!("expected Produces"),
372        }
373    }
374
375    #[test]
376    fn sink_constraint_is_accepts() {
377        let s = FakeSink;
378        let c = s.caps_constraint();
379        match c {
380            CapsConstraint::Accepts(set) => assert!(!set.is_empty()),
381            _ => panic!("expected Accepts"),
382        }
383    }
384
385    #[test]
386    fn derived_output_is_function_of_input() {
387        let d = FakeDecoder;
388        let input = compressed(
389            VideoCodec::H264,
390            Dim::Fixed(1280),
391            Dim::Fixed(720),
392            Rate::Fixed(30 << 16),
393        );
394        let c = d.caps_constraint();
395        match c {
396            CapsConstraint::DerivedOutput(f) => {
397                let out = f(&input);
398                assert_eq!(
399                    out.alternatives(),
400                    &[video(
401                        RawVideoFormat::Nv12,
402                        Dim::Fixed(1280),
403                        Dim::Fixed(720),
404                        Rate::Fixed(30 << 16)
405                    )]
406                );
407            }
408            _ => panic!("expected DerivedOutput"),
409        }
410    }
411
412    #[test]
413    fn mapping_constraint_holds_pairs() {
414        struct FakeScaler;
415        impl FormatElement for FakeScaler {
416            fn caps_constraint(&self) -> CapsConstraint<'_> {
417                let in_a = CapsSet::one(video(
418                    RawVideoFormat::Nv12,
419                    Dim::Fixed(1920),
420                    Dim::Fixed(1080),
421                    Rate::Any,
422                ));
423                let out_a = CapsSet::one(video(
424                    RawVideoFormat::Nv12,
425                    Dim::Fixed(1280),
426                    Dim::Fixed(720),
427                    Rate::Any,
428                ));
429                let in_b = CapsSet::one(video(
430                    RawVideoFormat::I420,
431                    Dim::Fixed(1920),
432                    Dim::Fixed(1080),
433                    Rate::Any,
434                ));
435                let out_b = CapsSet::one(video(
436                    RawVideoFormat::I420,
437                    Dim::Fixed(1280),
438                    Dim::Fixed(720),
439                    Rate::Any,
440                ));
441                CapsConstraint::Mapping(vec![(in_a, out_a), (in_b, out_b)])
442            }
443            fn configure_link(
444                &mut self,
445                _input: Option<&Caps>,
446                _output: Option<&Caps>,
447            ) -> Result<(), G2gError> {
448                Ok(())
449            }
450        }
451        match FakeScaler.caps_constraint() {
452            CapsConstraint::Mapping(pairs) => assert_eq!(pairs.len(), 2),
453            other => panic!("expected Mapping, got {other:?}"),
454        }
455    }
456
457    #[test]
458    fn default_preferences_is_none() {
459        assert!(FakeSource.caps_preferences().is_none());
460        assert!(FakeSink.caps_preferences().is_none());
461    }
462
463    #[test]
464    fn accept_caps_query_checks_constraint_set() {
465        // ACCEPT_CAPS (DESIGN §7): pure check against the declared set.
466        let nv12_720 = video(
467            RawVideoFormat::Nv12,
468            Dim::Fixed(1280),
469            Dim::Fixed(720),
470            Rate::Any,
471        );
472        let h264_720 = compressed(
473            VideoCodec::H264,
474            Dim::Fixed(1280),
475            Dim::Fixed(720),
476            Rate::Any,
477        );
478
479        // Accepts(NV12/any) takes NV12, rejects H.264.
480        let sink = FakeSink.caps_constraint();
481        assert!(sink.accepts(&nv12_720));
482        assert!(!sink.accepts(&h264_720));
483
484        // Identity(NV12@1280x720) takes the exact caps, rejects a mismatch.
485        let id = CapsConstraint::Identity(CapsSet::one(nv12_720.clone()));
486        assert!(id.accepts(&nv12_720));
487        assert!(!id.accepts(&video(
488            RawVideoFormat::Nv12,
489            Dim::Fixed(1920),
490            Dim::Fixed(1080),
491            Rate::Any,
492        )));
493
494        // DerivedOutput keys on input validity: H.264 in, not NV12.
495        let dec = FakeDecoder.caps_constraint();
496        assert!(dec.accepts(&h264_720));
497
498        // Wildcards take anything.
499        assert!(CapsConstraint::AcceptsAny.accepts(&h264_720));
500        assert!(CapsConstraint::IdentityAny.accepts(&nv12_720));
501    }
502
503    #[test]
504    fn legacy_transform_bridge_wraps_async_element() {
505        use crate::element::{AsyncElement, ConfigureOutcome, OutputSink};
506        use crate::frame::PipelinePacket;
507        use core::future::{ready, Ready};
508
509        // Minimal AsyncElement impl that narrows width to 640 and
510        // declares itself a boundary that re-formats to NV12.
511        struct FakeXform;
512        impl AsyncElement for FakeXform {
513            type ProcessFuture<'a> = Ready<Result<(), G2gError>>;
514            fn intercept_caps(&self, upstream: &Caps) -> Result<Caps, G2gError> {
515                match upstream.dims() {
516                    Some((_w, height, framerate)) => Ok(Caps::CompressedVideo {
517                        codec: VideoCodec::H264,
518                        width: Dim::Fixed(640),
519                        height: height.clone(),
520                        framerate: framerate.clone(),
521                    }),
522                    None => Err(G2gError::CapsMismatch),
523                }
524            }
525            fn configure_pipeline(&mut self, _: &Caps) -> Result<ConfigureOutcome, G2gError> {
526                Ok(ConfigureOutcome::Accepted)
527            }
528            fn process<'a>(
529                &'a mut self,
530                _: PipelinePacket,
531                _: &'a mut dyn OutputSink,
532            ) -> Self::ProcessFuture<'a> {
533                ready(Ok(()))
534            }
535            fn is_format_boundary(&self) -> bool {
536                true
537            }
538            fn propose_output_caps(&self, input: &Caps) -> Caps {
539                match input {
540                    Caps::CompressedVideo {
541                        width,
542                        height,
543                        framerate,
544                        ..
545                    } => Caps::RawVideo {
546                        format: RawVideoFormat::Nv12,
547                        width: width.clone(),
548                        height: height.clone(),
549                        framerate: framerate.clone(),
550                        interlace: crate::Interlace::Any,
551                    },
552                    other => other.clone(),
553                }
554            }
555        }
556
557        let xf = FakeXform;
558        let c = legacy_transform_constraint(&xf);
559        let upstream = compressed(
560            VideoCodec::H264,
561            Dim::Fixed(1920),
562            Dim::Fixed(720),
563            Rate::Fixed(30 << 16),
564        );
565        match &c {
566            CapsConstraint::LegacyTransform {
567                intercept,
568                propose_output,
569            } => {
570                let narrowed = intercept(&upstream).unwrap();
571                assert_eq!(
572                    narrowed,
573                    compressed(
574                        VideoCodec::H264,
575                        Dim::Fixed(640),
576                        Dim::Fixed(720),
577                        Rate::Fixed(30 << 16)
578                    )
579                );
580                let out = propose_output(&narrowed);
581                assert_eq!(
582                    out,
583                    video(
584                        RawVideoFormat::Nv12,
585                        Dim::Fixed(640),
586                        Dim::Fixed(720),
587                        Rate::Fixed(30 << 16)
588                    )
589                );
590            }
591            _ => panic!("expected LegacyTransform"),
592        }
593    }
594
595    #[test]
596    fn configure_link_signature_works_for_all_shapes() {
597        let cap = video(
598            RawVideoFormat::Nv12,
599            Dim::Fixed(640),
600            Dim::Fixed(480),
601            Rate::Fixed(30 << 16),
602        );
603        FakeSource.configure_link(None, Some(&cap)).unwrap();
604        FakeSink.configure_link(Some(&cap), None).unwrap();
605        FakeDecoder.configure_link(Some(&cap), Some(&cap)).unwrap();
606    }
607}