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}