1use alloc::boxed::Box;
16use alloc::string::{String, ToString};
17use alloc::vec::Vec;
18
19use crate::caps::{Caps, CapsSet, PassthroughFields};
20use crate::format_element::{CapsConstraint, CapsPreferences};
21use crate::graph::{NodeId, NodeKind, ValidatedGraph};
22use crate::log::{self, LogLevel, Target, CAPS_CATEGORY};
23#[cfg(feature = "std")]
24use crate::runtime::passthrough::project_passthrough_derived;
25use crate::runtime::passthrough::{
26 couple_passthrough_derived, discover_passthrough, project_passthrough,
27};
28
29pub type LinkSolution = Vec<Caps>;
35
36#[derive(Debug, Clone, PartialEq)]
40pub struct CapsConflict {
41 pub upstream: CapsSet,
43 pub downstream: CapsSet,
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum NegotiationFailure {
50 EmptyLink {
53 upstream: usize,
54 downstream: usize,
55 conflict: Option<Box<CapsConflict>>,
58 },
59 Degenerate,
62 EndpointShapeMismatch { index: usize },
65 Unfixable { upstream: usize, downstream: usize },
68 Cyclic,
71 NoConsistentFixation,
77 MixedLegacyAndNative,
82}
83
84impl NegotiationFailure {
85 pub fn empty_link(upstream: usize, downstream: usize) -> Self {
88 NegotiationFailure::EmptyLink {
89 upstream,
90 downstream,
91 conflict: None,
92 }
93 }
94
95 pub fn empty_link_conflict(
97 upstream: usize,
98 downstream: usize,
99 up_set: CapsSet,
100 down_set: CapsSet,
101 ) -> Self {
102 NegotiationFailure::EmptyLink {
103 upstream,
104 downstream,
105 conflict: Some(Box::new(CapsConflict {
106 upstream: up_set,
107 downstream: down_set,
108 })),
109 }
110 }
111
112 pub fn conflict(&self) -> Option<&CapsConflict> {
114 match self {
115 NegotiationFailure::EmptyLink { conflict, .. } => conflict.as_deref(),
116 _ => None,
117 }
118 }
119}
120
121pub fn solve_linear<'a>(
127 constraints: &[&CapsConstraint<'a>],
128) -> Result<LinkSolution, NegotiationFailure> {
129 solve_linear_preferred(constraints, &[])
130}
131
132pub fn solve_linear_preferred<'a>(
138 constraints: &[&CapsConstraint<'a>],
139 preferences: &[Option<CapsPreferences>],
140) -> Result<LinkSolution, NegotiationFailure> {
141 if constraints.len() < 2 {
142 return Err(NegotiationFailure::Degenerate);
143 }
144
145 let any_legacy = constraints.iter().any(|c| is_legacy(c));
151 let any_native = constraints.iter().any(|c| !is_legacy(c));
152 if any_legacy && any_native {
153 return solve_mixed_cascade(constraints);
154 }
155 if any_legacy {
156 return solve_legacy_cascade(constraints);
157 }
158
159 let n = constraints.len();
160 let n_links = n - 1;
161
162 match constraints[0] {
164 CapsConstraint::Produces(_) => {}
165 _ => return Err(NegotiationFailure::EndpointShapeMismatch { index: 0 }),
166 }
167 match constraints[n - 1] {
168 CapsConstraint::Accepts(_) | CapsConstraint::AcceptsAny => {}
169 _ => return Err(NegotiationFailure::EndpointShapeMismatch { index: n - 1 }),
170 }
171
172 let mut links: Vec<Option<CapsSet>> = alloc::vec![None; n_links];
180
181 if let CapsConstraint::Produces(s) = constraints[0] {
183 links[0] = Some(s.clone());
184 }
185 if let CapsConstraint::Accepts(s) = constraints[n - 1] {
186 let li = n_links - 1;
187 links[li] = match links[li].take() {
188 Some(cur) => Some(cur.intersect(s)),
189 None => Some(s.clone()),
190 };
191 }
192
193 let max_iters = 8 * n_links + 4;
196 for _ in 0..max_iters {
197 let snapshot = links.clone();
198
199 for (i, c) in constraints.iter().enumerate() {
201 apply_constraint(i, c, &mut links, n_links)?;
202 }
203 for (i, c) in constraints.iter().enumerate().rev() {
206 apply_constraint(i, c, &mut links, n_links)?;
207 }
208
209 if links == snapshot {
210 break;
211 }
212 }
213
214 let mut domains = Vec::with_capacity(n_links);
216 for (li, slot) in links.iter().enumerate() {
217 let set = slot
218 .as_ref()
219 .ok_or(NegotiationFailure::empty_link(li, li + 1))?;
220 if set.is_empty() {
221 return Err(NegotiationFailure::empty_link(li, li + 1));
222 }
223 let candidates = fixated_candidates(set);
224 if candidates.is_empty() {
225 return Err(NegotiationFailure::Unfixable {
226 upstream: li,
227 downstream: li + 1,
228 });
229 }
230 domains.push(candidates);
231 }
232
233 let chain: Vec<ChainNode<'_, '_>> = constraints
237 .iter()
238 .enumerate()
239 .map(|(i, c)| ChainNode::new(c, preference_at(preferences, i)))
240 .collect();
241 if let Some(pick) = min_cost_chain(&chain, &domains) {
242 return Ok(pick
243 .iter()
244 .zip(&domains)
245 .map(|(&j, d)| d[j].clone())
246 .collect());
247 }
248 Ok(domains.into_iter().map(|mut d| d.remove(0)).collect())
249}
250
251fn is_legacy(c: &CapsConstraint<'_>) -> bool {
252 matches!(
253 c,
254 CapsConstraint::LegacySource(_)
255 | CapsConstraint::LegacyTransform { .. }
256 | CapsConstraint::LegacySink(_)
257 )
258}
259
260fn solve_legacy_cascade(
268 constraints: &[&CapsConstraint<'_>],
269) -> Result<LinkSolution, NegotiationFailure> {
270 let n = constraints.len();
271 let n_links = n - 1;
272
273 let mut current = match constraints[0] {
275 CapsConstraint::LegacySource(caps) => caps.clone(),
276 _ => return Err(NegotiationFailure::EndpointShapeMismatch { index: 0 }),
277 };
278 match constraints[n - 1] {
279 CapsConstraint::LegacySink(_) => {}
280 _ => return Err(NegotiationFailure::EndpointShapeMismatch { index: n - 1 }),
281 }
282
283 let mut links: Vec<Caps> = Vec::with_capacity(n_links);
284 for (i, c) in constraints
295 .iter()
296 .enumerate()
297 .skip(1)
298 .take(n.saturating_sub(2))
299 {
300 match c {
301 CapsConstraint::LegacyTransform {
302 intercept,
303 propose_output: _,
304 } => {
305 current =
306 intercept(¤t).map_err(|_| NegotiationFailure::empty_link(i - 1, i))?;
307 links.push(current.clone());
308 }
309 CapsConstraint::LegacySource(_) | CapsConstraint::LegacySink(_) => {
310 return Err(NegotiationFailure::EndpointShapeMismatch { index: i });
311 }
312 _ => return Err(NegotiationFailure::MixedLegacyAndNative),
313 }
314 }
315 if let CapsConstraint::LegacySink(intercept) = constraints[n - 1] {
317 current = intercept(¤t).map_err(|_| NegotiationFailure::empty_link(n - 2, n - 1))?;
318 links.push(current);
319 }
320
321 let fixed_last = links
329 .last()
330 .ok_or(NegotiationFailure::Degenerate)?
331 .fixate()
332 .map_err(|_| NegotiationFailure::Unfixable {
333 upstream: n - 2,
334 downstream: n - 1,
335 })?;
336 for slot in links.iter_mut() {
337 *slot = fixed_last.clone();
338 }
339
340 Ok(links)
341}
342
343fn solve_mixed_cascade(
358 constraints: &[&CapsConstraint<'_>],
359) -> Result<LinkSolution, NegotiationFailure> {
360 let n = constraints.len();
361 let n_links = n - 1;
362
363 let starts_with_source = matches!(
364 constraints[0],
365 CapsConstraint::Produces(_) | CapsConstraint::LegacySource(_)
366 );
367 let ends_with_sink = matches!(
368 constraints[n - 1],
369 CapsConstraint::Accepts(_) | CapsConstraint::LegacySink(_) | CapsConstraint::AcceptsAny
370 );
371 if !starts_with_source {
372 return Err(NegotiationFailure::EndpointShapeMismatch { index: 0 });
373 }
374 if !ends_with_sink {
375 return Err(NegotiationFailure::EndpointShapeMismatch { index: n - 1 });
376 }
377
378 let mut link_sets: Vec<CapsSet> = Vec::with_capacity(n_links);
380
381 let seed = match constraints[0] {
383 CapsConstraint::Produces(s) => s.clone(),
384 CapsConstraint::LegacySource(c) => CapsSet::one(c.clone()),
385 _ => unreachable!("checked above"),
386 };
387 link_sets.push(seed);
388
389 for i in 1..(n - 1) {
391 let upstream = link_sets[i - 1].clone();
392 let downstream = forward_propagate(constraints[i], &upstream, i)?;
393 link_sets.push(downstream);
394 }
395
396 let final_idx = n_links - 1;
398 let upstream = link_sets[final_idx].clone();
399 let narrowed = match constraints[n - 1] {
400 CapsConstraint::Accepts(s) => upstream.intersect(s),
401 CapsConstraint::AcceptsAny => upstream,
402 CapsConstraint::LegacySink(intercept) => {
403 let fixed = upstream.fixate().ok_or(NegotiationFailure::Unfixable {
404 upstream: n - 2,
405 downstream: n - 1,
406 })?;
407 let c = intercept(&fixed).map_err(|_| NegotiationFailure::empty_link(n - 2, n - 1))?;
408 CapsSet::one(c)
409 }
410 _ => unreachable!("checked above"),
411 };
412 if narrowed.is_empty() {
413 return Err(NegotiationFailure::empty_link(n - 2, n - 1));
414 }
415 link_sets[final_idx] = narrowed;
416
417 let mut out = Vec::with_capacity(n_links);
419 for (li, s) in link_sets.iter().enumerate() {
420 let fixed = s.fixate().ok_or(NegotiationFailure::Unfixable {
421 upstream: li,
422 downstream: li + 1,
423 })?;
424 out.push(fixed);
425 }
426 Ok(out)
427}
428
429fn forward_propagate(
430 c: &CapsConstraint<'_>,
431 upstream: &CapsSet,
432 i: usize,
433) -> Result<CapsSet, NegotiationFailure> {
434 match c {
435 CapsConstraint::Identity(s) => {
436 let r = upstream.intersect(s);
437 if r.is_empty() {
438 return Err(NegotiationFailure::empty_link(i - 1, i));
439 }
440 Ok(r)
441 }
442 CapsConstraint::Mapping(pairs) => {
443 let mut out = CapsSet::from_alternatives(Vec::new());
444 for (in_set, out_set) in pairs {
445 let in_match = upstream.intersect(in_set);
446 if !in_match.is_empty() {
447 out = out.union(out_set);
448 }
449 }
450 if out.is_empty() {
451 return Err(NegotiationFailure::empty_link(i - 1, i));
454 }
455 Ok(out)
456 }
457 CapsConstraint::DerivedOutput(f) => derived_forward(f.as_ref(), upstream, i),
458 CapsConstraint::DerivedFields(t) => derived_forward(&|c: &Caps| t.derive(c), upstream, i),
459 CapsConstraint::LegacyTransform {
460 intercept,
461 propose_output,
462 } => {
463 let fixed = upstream.fixate().ok_or(NegotiationFailure::Unfixable {
464 upstream: i - 1,
465 downstream: i,
466 })?;
467 let input = intercept(&fixed).map_err(|_| NegotiationFailure::empty_link(i - 1, i))?;
468 Ok(CapsSet::one(propose_output(&input)))
469 }
470 CapsConstraint::IdentityAny => {
471 Ok(upstream.clone())
473 }
474 CapsConstraint::Produces(_)
475 | CapsConstraint::Accepts(_)
476 | CapsConstraint::AcceptsAny
477 | CapsConstraint::LegacySource(_)
478 | CapsConstraint::LegacySink(_) => {
479 Err(NegotiationFailure::EndpointShapeMismatch { index: i })
480 }
481 }
482}
483
484fn derived_forward(
488 f: &dyn Fn(&Caps) -> CapsSet,
489 upstream: &CapsSet,
490 i: usize,
491) -> Result<CapsSet, NegotiationFailure> {
492 let fixed = upstream.fixate().ok_or(NegotiationFailure::Unfixable {
493 upstream: i - 1,
494 downstream: i,
495 })?;
496 let r = f(&fixed);
497 if r.is_empty() {
498 return Err(NegotiationFailure::empty_link(i, i + 1));
499 }
500 Ok(r)
501}
502
503#[derive(Debug, Clone, PartialEq)]
508pub(crate) enum ForwardResolve {
509 Fixed(Caps),
511 Defer,
516 Infeasible(NegotiationFailure),
520}
521
522#[cfg(all(test, feature = "std"))]
535pub(crate) fn downstream_feasibility(constraints: &[&CapsConstraint<'_>]) -> Vec<Option<CapsSet>> {
536 let n = constraints.len();
537 if n < 2 {
538 return Vec::new();
539 }
540 let n_links = n - 1;
541 let mut feas: Vec<Option<CapsSet>> = alloc::vec![None; n_links];
542 feas[n_links - 1] = match constraints[n - 1] {
545 CapsConstraint::Accepts(s) => Some(s.clone()),
546 _ => None,
547 };
548 for k in (0..n_links - 1).rev() {
553 feas[k] = backward_feasible(constraints[k + 1], feas[k + 1].as_ref(), None);
554 }
555 feas
556}
557
558#[cfg(feature = "std")]
564fn backward_feasible(
565 c: &CapsConstraint<'_>,
566 down: Option<&CapsSet>,
567 in_sample: Option<&Caps>,
568) -> Option<CapsSet> {
569 match c {
570 CapsConstraint::Identity(s) => Some(match down {
571 Some(d) => s.intersect(d),
572 None => s.clone(),
573 }),
574 CapsConstraint::IdentityAny => down.cloned(),
575 CapsConstraint::Mapping(pairs) => {
576 let mut acc = CapsSet::from_alternatives(Vec::new());
577 for (in_set, out_set) in pairs {
578 let out_ok = match down {
579 Some(d) => !out_set.intersect(d).is_empty(),
580 None => true,
581 };
582 if out_ok {
583 acc = acc.union(in_set);
584 }
585 }
586 Some(acc)
587 }
588 CapsConstraint::DerivedFields(t) => {
595 let d = down?;
596 let mask = t.passthrough();
597 let mut alts = Vec::with_capacity(d.alternatives().len());
598 for o in d.alternatives() {
599 alts.push(project_passthrough(o, mask)?);
600 }
601 Some(CapsSet::from_alternatives(alts))
602 }
603 CapsConstraint::DerivedOutput(f) => {
617 let (d, sample) = (down?, in_sample?);
618 let mask = discover_passthrough(f, sample);
619 if mask == PassthroughFields::NONE {
620 return None;
621 }
622 let mut alts = Vec::with_capacity(d.alternatives().len());
623 for o in d.alternatives() {
624 if let Some(c) = project_passthrough_derived(sample, o, mask) {
625 if !alts.contains(&c) {
626 alts.push(c);
627 }
628 }
629 }
630 (!alts.is_empty()).then(|| CapsSet::from_alternatives(alts))
631 }
632 _ => None,
635 }
636}
637
638pub(crate) fn resolve_forward_output(
658 constraint: &CapsConstraint<'_>,
659 input: &Caps,
660 downstream_feasible: Option<&CapsSet>,
661 prev_output: Option<&Caps>,
662) -> ForwardResolve {
663 let candidates = forward_propagate(constraint, &CapsSet::one(input.clone()), 1)
670 .ok()
671 .or_else(|| match constraint {
672 CapsConstraint::DerivedOutput(f) => Some(f(input)),
673 CapsConstraint::DerivedFields(t) => Some(t.derive(input)),
674 _ => None,
675 })
676 .filter(|c| !c.is_empty());
677 let Some(candidates) = candidates else {
678 return ForwardResolve::Defer;
679 };
680 let keep_shape = prev_output.and_then(|p| {
685 project_passthrough(p, PassthroughFields::NONE.with_format().with_channels())
686 });
687 fn tracks_input(survivor: &Caps, input: &Caps) -> bool {
695 match (survivor.dims(), input.dims()) {
696 (Some(s), Some(i)) => s == i,
697 _ => match (survivor, input) {
698 (
699 Caps::Audio {
700 channels: s_ch,
701 sample_rate: s_rate,
702 ..
703 },
704 Caps::Audio {
705 channels: i_ch,
706 sample_rate: i_rate,
707 ..
708 },
709 ) => s_rate == i_rate && s_ch == i_ch,
710 _ => false,
711 },
712 }
713 }
714 let fixate_kept_shape = |set: &CapsSet| -> Option<Caps> {
715 let shape = keep_shape.as_ref()?;
716 set.intersect(&CapsSet::one(shape.clone()))
717 .alternatives()
718 .iter()
719 .find(|c| tracks_input(c, input))
720 .cloned()
721 };
722 let derives_from_input = matches!(
725 constraint,
726 CapsConstraint::DerivedOutput(_) | CapsConstraint::DerivedFields(_)
727 );
728 let Some(d) = downstream_feasible else {
729 return match candidates.alternatives() {
730 [one] => match candidates.fixate() {
734 Some(c) => ForwardResolve::Fixed(c),
735 None if derives_from_input || tracks_input(one, input) => {
736 ForwardResolve::Fixed(one.clone())
737 }
738 None => ForwardResolve::Defer,
739 },
740 _ => match fixate_kept_shape(&candidates) {
744 Some(c) => ForwardResolve::Fixed(c),
745 None => ForwardResolve::Defer,
746 },
747 };
748 };
749 let narrowed = candidates.intersect(d);
750 if narrowed.is_empty() {
751 return ForwardResolve::Infeasible(NegotiationFailure::empty_link(0, 1));
752 }
753 match fixate_kept_shape(&narrowed)
754 .or_else(|| narrowed.fixate())
755 .or_else(|| match narrowed.alternatives() {
756 [one] => Some(one.clone()),
763 _ => None,
764 }) {
765 Some(c) => ForwardResolve::Fixed(c),
766 None => ForwardResolve::Defer,
767 }
768}
769
770fn apply_constraint(
771 i: usize,
772 c: &CapsConstraint<'_>,
773 links: &mut [Option<CapsSet>],
774 n_links: usize,
775) -> Result<(), NegotiationFailure> {
776 let in_idx = if i == 0 { None } else { Some(i - 1) };
777 let out_idx = if i == n_links { None } else { Some(i) };
778
779 match c {
780 CapsConstraint::Produces(s) => {
781 if let Some(idx) = out_idx {
782 narrow(links, idx, s, i, i + 1)?;
783 }
784 }
785 CapsConstraint::Accepts(s) => {
786 if let Some(idx) = in_idx {
787 narrow(links, idx, s, i - 1, i)?;
788 }
789 }
790 CapsConstraint::Identity(s) => {
791 if let Some(idx) = in_idx {
794 narrow(links, idx, s, i - 1, i)?;
795 }
796 if let Some(idx) = out_idx {
797 narrow(links, idx, s, i, i + 1)?;
798 }
799 if let (Some(ii), Some(oi)) = (in_idx, out_idx) {
801 let (a, b) = (links[ii].clone(), links[oi].clone());
802 if let (Some(a), Some(b)) = (a, b) {
803 let coupled = a.intersect(&b);
804 if coupled.is_empty() {
805 return Err(NegotiationFailure::empty_link(i - 1, i + 1));
806 }
807 links[ii] = Some(coupled.clone());
808 links[oi] = Some(coupled);
809 }
810 }
811 }
812 CapsConstraint::Mapping(pairs) => {
813 let (Some(ii), Some(oi)) = (in_idx, out_idx) else {
814 return Err(NegotiationFailure::EndpointShapeMismatch { index: i });
815 };
816 let mut new_in = CapsSet::from_alternatives(Vec::new());
818 let mut new_out = CapsSet::from_alternatives(Vec::new());
819 for (in_set, out_set) in pairs {
820 let in_match = match &links[ii] {
821 Some(cur) => cur.intersect(in_set),
822 None => in_set.clone(),
823 };
824 let out_match = match &links[oi] {
825 Some(cur) => cur.intersect(out_set),
826 None => out_set.clone(),
827 };
828 if !in_match.is_empty() && !out_match.is_empty() {
829 new_in = new_in.union(&in_match);
830 new_out = new_out.union(&out_match);
831 }
832 }
833 if new_in.is_empty() || new_out.is_empty() {
834 return Err(NegotiationFailure::empty_link(i - 1, i + 1));
835 }
836 links[ii] = Some(new_in);
837 links[oi] = Some(new_out);
838 }
839 CapsConstraint::DerivedOutput(f) => {
840 let (Some(ii), Some(oi)) = (in_idx, out_idx) else {
841 return Err(NegotiationFailure::EndpointShapeMismatch { index: i });
842 };
843 if let Some(input_set) = &links[ii] {
850 let derived = forward_derived_union(f.as_ref(), input_set);
851 if derived.is_empty() {
852 return Err(NegotiationFailure::empty_link(i, i + 1));
853 }
854 narrow(links, oi, &derived, i, i + 1)?;
855 }
856 if let (Some(in_set), Some(out_set)) = (links[ii].clone(), links[oi].clone()) {
862 match derived_backward(f.as_ref(), &in_set, &out_set) {
863 Ok(Some(narrowed)) => links[ii] = Some(narrowed),
864 Ok(None) => {}
865 Err(()) => return Err(NegotiationFailure::empty_link(i - 1, i)),
866 }
867 }
868 }
869 CapsConstraint::DerivedFields(t) => {
870 let (Some(ii), Some(oi)) = (in_idx, out_idx) else {
871 return Err(NegotiationFailure::EndpointShapeMismatch { index: i });
872 };
873 let derive = |c: &Caps| t.derive(c);
874 if let Some(input_set) = &links[ii] {
877 let derived = forward_derived_union(&derive, input_set);
878 if derived.is_empty() {
879 return Err(NegotiationFailure::empty_link(i, i + 1));
880 }
881 narrow(links, oi, &derived, i, i + 1)?;
882 }
883 if let (Some(in_set), Some(out_set)) = (links[ii].clone(), links[oi].clone()) {
886 match backward_field_narrow(&derive, t.passthrough(), &in_set, &out_set) {
887 Ok(Some(narrowed)) => links[ii] = Some(narrowed),
888 Ok(None) => {}
889 Err(()) => return Err(NegotiationFailure::empty_link(i - 1, i)),
890 }
891 }
892 }
893 CapsConstraint::AcceptsAny => {
894 }
899 CapsConstraint::IdentityAny => {
900 if let (Some(ii), Some(oi)) = (in_idx, out_idx) {
904 let (a, b) = (links[ii].clone(), links[oi].clone());
905 match (a, b) {
906 (Some(a), Some(b)) => {
907 let coupled = a.intersect(&b);
908 if coupled.is_empty() {
909 return Err(NegotiationFailure::empty_link(i - 1, i + 1));
910 }
911 links[ii] = Some(coupled.clone());
912 links[oi] = Some(coupled);
913 }
914 (Some(a), None) => links[oi] = Some(a),
915 (None, Some(b)) => links[ii] = Some(b),
916 (None, None) => {}
917 }
918 }
919 }
920 CapsConstraint::LegacySource(_)
921 | CapsConstraint::LegacyTransform { .. }
922 | CapsConstraint::LegacySink(_) => {
923 return Err(NegotiationFailure::MixedLegacyAndNative);
927 }
928 }
929 Ok(())
930}
931
932fn narrow(
933 links: &mut [Option<CapsSet>],
934 idx: usize,
935 contrib: &CapsSet,
936 upstream: usize,
937 downstream: usize,
938) -> Result<(), NegotiationFailure> {
939 let next = match &links[idx] {
940 Some(cur) => cur.intersect(contrib),
941 None => contrib.clone(),
942 };
943 if next.is_empty() {
944 return Err(NegotiationFailure::empty_link(upstream, downstream));
945 }
946 links[idx] = Some(next);
947 Ok(())
948}
949
950#[derive(Debug)]
955pub enum NodeConstraint<'a> {
956 Element(CapsConstraint<'a>),
959 Muxer {
971 inputs: Vec<CapsConstraint<'a>>,
972 output: CapsConstraint<'a>,
973 follows: Option<usize>,
974 },
975 Demux {
984 input: CapsConstraint<'a>,
985 ports: Vec<CapsConstraint<'a>>,
986 },
987}
988
989pub fn solve_graph<E>(
1001 graph: &ValidatedGraph<E>,
1002 constraints: &[NodeConstraint<'_>],
1003) -> Result<Vec<Caps>, NegotiationFailure> {
1004 solve_graph_labeled(graph, constraints, &|n| node_label_default(graph, n))
1007}
1008
1009pub fn solve_graph_labeled<E>(
1016 graph: &ValidatedGraph<E>,
1017 constraints: &[NodeConstraint<'_>],
1018 label: &dyn Fn(NodeId) -> String,
1019) -> Result<Vec<Caps>, NegotiationFailure> {
1020 solve_graph_preferred(graph, constraints, &[], label)
1021}
1022
1023pub fn solve_graph_preferred<E>(
1030 graph: &ValidatedGraph<E>,
1031 constraints: &[NodeConstraint<'_>],
1032 preferences: &[Option<CapsPreferences>],
1033 label: &dyn Fn(NodeId) -> String,
1034) -> Result<Vec<Caps>, NegotiationFailure> {
1035 let n = graph.node_count();
1036 if n < 2 || constraints.len() != n {
1037 return Err(NegotiationFailure::Degenerate);
1038 }
1039 let ne = graph.edge_count();
1040 let mut edges: Vec<Option<CapsSet>> = alloc::vec![None; ne];
1041
1042 let t = Target::category(CAPS_CATEGORY);
1043 let trace = log::enabled(CAPS_CATEGORY, LogLevel::Debug);
1044 if trace {
1045 crate::g2g_debug!(t, "negotiating {n} nodes, {ne} edges:");
1046 for (i, c) in constraints.iter().enumerate() {
1047 crate::g2g_debug!(t, " {} {}", label(NodeId(i as u32)), fmt_constraint(c));
1048 }
1049 }
1050
1051 let report = |f: &NegotiationFailure, edges: &[Option<CapsSet>]| match f {
1057 NegotiationFailure::EmptyLink {
1058 upstream,
1059 downstream,
1060 ..
1061 } => {
1062 let (up, down) = (NodeId(*upstream as u32), NodeId(*downstream as u32));
1063 crate::g2g_error!(
1064 t,
1065 "no caps overlap between {} and {}",
1066 label(up),
1067 label(down)
1068 );
1069 for (id, slot) in edges.iter().enumerate() {
1070 let e = graph.edge(id);
1071 if [e.src.node, e.dst.node]
1072 .iter()
1073 .any(|&x| x == up || x == down)
1074 {
1075 crate::g2g_error!(
1076 t,
1077 " {} -> {}: {}",
1078 label(e.src.node),
1079 label(e.dst.node),
1080 fmt_set_opt(slot)
1081 );
1082 }
1083 }
1084 }
1085 other => crate::g2g_error!(t, "negotiation failed: {other:?}"),
1086 };
1087
1088 let max_iters = 8 * ne + 4;
1090 for _ in 0..max_iters {
1091 let snapshot = edges.clone();
1092 for &node in graph.topo() {
1093 if let Err(f) = apply_node(graph, node, constraints, &mut edges) {
1094 report(&f, &edges);
1095 return Err(f);
1096 }
1097 }
1098 for &node in graph.topo().iter().rev() {
1099 if let Err(f) = apply_node(graph, node, constraints, &mut edges) {
1100 report(&f, &edges);
1101 return Err(f);
1102 }
1103 }
1104 if edges == snapshot {
1105 break;
1106 }
1107 }
1108
1109 let mut domains: Vec<Vec<Caps>> = Vec::with_capacity(ne);
1122 for (id, slot) in edges.iter().enumerate() {
1123 let (up, down) = edge_endpoints(graph, id);
1124 let set = match slot.as_ref() {
1125 Some(s) if !s.is_empty() => s,
1126 _ => {
1127 let f = NegotiationFailure::empty_link(up, down);
1128 report(&f, &edges);
1129 return Err(f);
1130 }
1131 };
1132 let doms = fixated_candidates(set);
1133 if doms.is_empty() {
1134 crate::g2g_error!(
1135 t,
1136 "{} -> {}: {} ✗ cannot fixate (still ambiguous after narrowing)",
1137 label(graph.edge(id).src.node),
1138 label(graph.edge(id).dst.node),
1139 fmt_set(set)
1140 );
1141 return Err(NegotiationFailure::Unfixable {
1142 upstream: up,
1143 downstream: down,
1144 });
1145 }
1146 domains.push(doms);
1147 }
1148
1149 let mut assign: Vec<Option<Caps>> = alloc::vec![None; ne];
1150 if let Some(chosen) = preferred_chain_assignment(graph, constraints, preferences, &domains) {
1151 assign = chosen;
1152 } else if !fixate_backtrack(graph, constraints, &domains, &mut assign, 0) {
1153 let f = NegotiationFailure::NoConsistentFixation;
1154 report(&f, &edges);
1155 return Err(f);
1156 }
1157 let out: Vec<Caps> = assign
1158 .into_iter()
1159 .map(|a| a.expect("every edge assigned"))
1160 .collect();
1161 if trace {
1162 for (id, c) in out.iter().enumerate() {
1163 let e = graph.edge(id);
1164 crate::g2g_debug!(
1165 t,
1166 "{} -> {}: {} ✓ -> {}",
1167 label(e.src.node),
1168 label(e.dst.node),
1169 fmt_set(edges[id].as_ref().expect("edge set present")),
1170 c.to_gst_string()
1171 );
1172 }
1173 }
1174 Ok(out)
1175}
1176
1177fn fixated_candidates(set: &CapsSet) -> Vec<Caps> {
1182 let mut out: Vec<Caps> = Vec::new();
1183 for alt in set.alternatives() {
1184 if let Ok(c) = alt.fixate() {
1185 if !out.contains(&c) {
1186 out.push(c);
1187 }
1188 }
1189 }
1190 out
1191}
1192
1193fn preference_at(
1195 preferences: &[Option<CapsPreferences>],
1196 index: usize,
1197) -> Option<&CapsPreferences> {
1198 preferences.get(index).and_then(Option::as_ref)
1199}
1200
1201struct ChainNode<'c, 'a> {
1204 constraint: &'c CapsConstraint<'a>,
1205 preferences: Option<&'c CapsPreferences>,
1206}
1207
1208impl<'c, 'a> ChainNode<'c, 'a> {
1209 fn new(constraint: &'c CapsConstraint<'a>, preferences: Option<&'c CapsPreferences>) -> Self {
1210 let preferences = preferences.filter(|p| !p.is_empty());
1211 Self {
1212 constraint,
1213 preferences,
1214 }
1215 }
1216
1217 fn cost(&self, input: Option<&Caps>, output: Option<&Caps>) -> u64 {
1222 match alternative_index(self.constraint, input, output) {
1223 Some(i) => self.preferences.map_or(i as u64, |p| p.cost(i) as u64),
1224 None => 0,
1225 }
1226 }
1227}
1228
1229fn alternative_index(
1233 c: &CapsConstraint<'_>,
1234 input: Option<&Caps>,
1235 output: Option<&Caps>,
1236) -> Option<usize> {
1237 let position = |set: &CapsSet, caps: &Caps| {
1238 set.alternatives()
1239 .iter()
1240 .position(|a| a.intersect(caps).is_ok())
1241 };
1242 match c {
1243 CapsConstraint::Produces(set) => position(set, output?),
1244 CapsConstraint::Accepts(set) => position(set, input?),
1245 CapsConstraint::Identity(set) => position(set, input.or(output)?),
1246 CapsConstraint::Mapping(pairs) => {
1247 let (input, output) = (input?, output?);
1248 pairs
1249 .iter()
1250 .position(|(i, o)| i.accepts(input) && o.accepts(output))
1251 }
1252 _ => None,
1253 }
1254}
1255
1256fn min_cost_chain(nodes: &[ChainNode<'_, '_>], domains: &[Vec<Caps>]) -> Option<Vec<usize>> {
1271 if domains.is_empty() || nodes.len() != domains.len() + 1 {
1272 return None;
1273 }
1274 if !nodes.iter().any(|n| n.preferences.is_some()) {
1275 return None;
1276 }
1277
1278 let mut best: Vec<Option<(u64, Vec<usize>)>> = domains[0]
1281 .iter()
1282 .enumerate()
1283 .map(|(j, caps)| Some((nodes[0].cost(None, Some(caps)), alloc::vec![j])))
1284 .collect();
1285
1286 for link in 1..domains.len() {
1287 let middle = &nodes[link];
1288 let mut next: Vec<Option<(u64, Vec<usize>)>> = alloc::vec![None; domains[link].len()];
1289 for (b, output) in domains[link].iter().enumerate() {
1290 for (a, input) in domains[link - 1].iter().enumerate() {
1291 let Some((so_far, path)) = best[a].as_ref() else {
1292 continue;
1293 };
1294 if !transform_pair_consistent(middle.constraint, input, output) {
1295 continue;
1296 }
1297 let total = so_far.saturating_add(middle.cost(Some(input), Some(output)));
1298 let mut candidate = path.clone();
1299 candidate.push(b);
1300 if is_better(next[b].as_ref(), total, &candidate) {
1301 next[b] = Some((total, candidate));
1302 }
1303 }
1304 }
1305 best = next;
1306 }
1307
1308 let last = nodes.last()?;
1309 let mut winner: Option<(u64, Vec<usize>)> = None;
1310 for (j, caps) in domains[domains.len() - 1].iter().enumerate() {
1311 let Some((so_far, path)) = best[j].as_ref() else {
1312 continue;
1313 };
1314 let total = so_far.saturating_add(last.cost(Some(caps), None));
1315 if is_better(winner.as_ref(), total, path) {
1316 winner = Some((total, path.clone()));
1317 }
1318 }
1319 winner.map(|(_, path)| path)
1320}
1321
1322fn is_better(current: Option<&(u64, Vec<usize>)>, cost: u64, path: &[usize]) -> bool {
1325 match current {
1326 None => true,
1327 Some((c, p)) => (cost, path) < (*c, p.as_slice()),
1328 }
1329}
1330
1331fn preferred_chain_assignment<E>(
1336 graph: &ValidatedGraph<E>,
1337 constraints: &[NodeConstraint<'_>],
1338 preferences: &[Option<CapsPreferences>],
1339 domains: &[Vec<Caps>],
1340) -> Option<Vec<Option<Caps>>> {
1341 if !preferences.iter().any(Option::is_some) {
1342 return None;
1343 }
1344 let (nodes, edges) = chain_order(graph, constraints)?;
1345 let chain: Vec<ChainNode<'_, '_>> = nodes
1346 .iter()
1347 .map(|&node| {
1348 let index = node.0 as usize;
1349 let NodeConstraint::Element(c) = &constraints[index] else {
1350 unreachable!("chain_order admits Element nodes only")
1351 };
1352 ChainNode::new(c, preference_at(preferences, index))
1353 })
1354 .collect();
1355 let chain_domains: Vec<Vec<Caps>> = edges.iter().map(|&e| domains[e].clone()).collect();
1356 let pick = min_cost_chain(&chain, &chain_domains)?;
1357
1358 let mut assign: Vec<Option<Caps>> = alloc::vec![None; domains.len()];
1359 for ((&edge, candidates), &j) in edges.iter().zip(&chain_domains).zip(&pick) {
1360 assign[edge] = Some(candidates[j].clone());
1361 }
1362 Some(assign)
1363}
1364
1365fn chain_order<E>(
1371 graph: &ValidatedGraph<E>,
1372 constraints: &[NodeConstraint<'_>],
1373) -> Option<(Vec<NodeId>, Vec<usize>)> {
1374 let n = graph.node_count();
1375 if n < 2 || graph.edge_count() + 1 != n {
1376 return None;
1377 }
1378 if !constraints
1379 .iter()
1380 .all(|c| matches!(c, NodeConstraint::Element(_)))
1381 {
1382 return None;
1383 }
1384 let mut head = None;
1385 for i in 0..n {
1386 let node = NodeId(i as u32);
1387 if graph.in_edges(node).len() > 1 || graph.out_edges(node).len() > 1 {
1388 return None;
1389 }
1390 if graph.in_edges(node).is_empty() {
1391 if head.is_some() {
1392 return None;
1393 }
1394 head = Some(node);
1395 }
1396 }
1397
1398 let mut nodes = Vec::with_capacity(n);
1399 let mut edges = Vec::with_capacity(n - 1);
1400 let mut current = head?;
1401 loop {
1402 nodes.push(current);
1403 match graph.out_edges(current).first() {
1404 Some(&edge) => {
1405 edges.push(edge);
1406 current = graph.edge(edge).dst.node;
1407 }
1408 None => break,
1409 }
1410 }
1411 (nodes.len() == n).then_some((nodes, edges))
1412}
1413
1414fn fixate_backtrack<E>(
1422 graph: &ValidatedGraph<E>,
1423 constraints: &[NodeConstraint<'_>],
1424 domains: &[Vec<Caps>],
1425 assign: &mut [Option<Caps>],
1426 edge: usize,
1427) -> bool {
1428 if edge == domains.len() {
1429 return true;
1430 }
1431 let e = graph.edge(edge);
1432 for cand in &domains[edge] {
1433 assign[edge] = Some(cand.clone());
1434 if node_consistent(graph, constraints, assign, e.src.node)
1435 && node_consistent(graph, constraints, assign, e.dst.node)
1436 && fixate_backtrack(graph, constraints, domains, assign, edge + 1)
1437 {
1438 return true;
1439 }
1440 }
1441 assign[edge] = None;
1442 false
1443}
1444
1445fn node_consistent<E>(
1454 graph: &ValidatedGraph<E>,
1455 constraints: &[NodeConstraint<'_>],
1456 assign: &[Option<Caps>],
1457 node: NodeId,
1458) -> bool {
1459 let in_e = graph.in_edges(node);
1460 let out_e = graph.out_edges(node);
1461 if in_e
1462 .iter()
1463 .chain(out_e.iter())
1464 .any(|&e| assign[e].is_none())
1465 {
1466 return true;
1467 }
1468 let get = |e: usize| assign[e].as_ref().expect("checked all assigned");
1469 match graph.kind(node) {
1470 NodeKind::Source
1472 | NodeKind::Sink
1473 | NodeKind::Muxer(_)
1474 | NodeKind::FaninSink(_)
1475 | NodeKind::FanoutSrc(_) => true,
1476 NodeKind::Tee(_) => {
1477 if matches!(&constraints[node.0 as usize], NodeConstraint::Demux { .. }) {
1481 true
1482 } else {
1483 let inp = get(in_e[0]);
1484 out_e.iter().all(|&o| get(o) == inp)
1485 }
1486 }
1487 NodeKind::Transform => match &constraints[node.0 as usize] {
1488 NodeConstraint::Element(c) => transform_pair_consistent(c, get(in_e[0]), get(out_e[0])),
1489 _ => true,
1490 },
1491 }
1492}
1493
1494fn transform_pair_consistent(c: &CapsConstraint<'_>, inp: &Caps, outp: &Caps) -> bool {
1497 match c {
1498 CapsConstraint::Identity(_) | CapsConstraint::IdentityAny => inp == outp,
1499 CapsConstraint::Mapping(pairs) => {
1500 pairs.iter().any(|(i, o)| i.accepts(inp) && o.accepts(outp))
1501 }
1502 CapsConstraint::DerivedOutput(f) => f(inp).accepts(outp),
1503 CapsConstraint::DerivedFields(t) => t.derive(inp).accepts(outp),
1504 _ => true,
1508 }
1509}
1510
1511fn node_label_default<E>(graph: &ValidatedGraph<E>, node: NodeId) -> String {
1514 alloc::format!("n{}:{}", node.0, kind_short(graph.kind(node)))
1515}
1516
1517fn kind_short(kind: NodeKind) -> &'static str {
1518 match kind {
1519 NodeKind::Source => "src",
1520 NodeKind::Transform => "xform",
1521 NodeKind::Sink => "sink",
1522 NodeKind::Tee(_) => "tee",
1523 NodeKind::Muxer(_) => "mux",
1524 NodeKind::FaninSink(_) => "fanin-sink",
1525 NodeKind::FanoutSrc(_) => "fanout-src",
1526 }
1527}
1528
1529fn fmt_set(set: &CapsSet) -> String {
1533 let alts = set.alternatives();
1534 if alts.is_empty() {
1535 return String::from("∅");
1536 }
1537 let mut parts: Vec<String> = Vec::new();
1538 for (i, a) in alts.iter().enumerate() {
1539 if i == 4 {
1540 parts.push(alloc::format!("(+{} more)", alts.len() - 4));
1541 break;
1542 }
1543 parts.push(a.to_gst_string());
1544 }
1545 parts.join(" | ")
1546}
1547
1548fn fmt_set_opt(slot: &Option<CapsSet>) -> String {
1549 match slot {
1550 Some(s) => fmt_set(s),
1551 None => String::from("(unconstrained)"),
1552 }
1553}
1554
1555fn fmt_constraint(nc: &NodeConstraint<'_>) -> String {
1557 match nc {
1558 NodeConstraint::Element(c) => fmt_caps_constraint(c),
1559 NodeConstraint::Muxer {
1560 inputs,
1561 output,
1562 follows,
1563 } => match follows {
1564 Some(pad) => alloc::format!("mux {} inputs -> follows input {pad}", inputs.len()),
1565 None => alloc::format!(
1566 "mux {} inputs -> {}",
1567 inputs.len(),
1568 fmt_caps_constraint(output)
1569 ),
1570 },
1571 NodeConstraint::Demux { ports, .. } => alloc::format!("demux -> {} ports", ports.len()),
1572 }
1573}
1574
1575fn fmt_caps_constraint(c: &CapsConstraint<'_>) -> String {
1576 match c {
1577 CapsConstraint::Produces(s) => alloc::format!("produces {}", fmt_set(s)),
1578 CapsConstraint::Accepts(s) => alloc::format!("accepts {}", fmt_set(s)),
1579 CapsConstraint::AcceptsAny => "accepts ANY".to_string(),
1580 CapsConstraint::Identity(s) => alloc::format!("identity {}", fmt_set(s)),
1581 CapsConstraint::IdentityAny => "identity ANY".to_string(),
1582 CapsConstraint::Mapping(pairs) => alloc::format!("maps {} pair(s)", pairs.len()),
1583 CapsConstraint::DerivedOutput(_) => "derives output".to_string(),
1584 CapsConstraint::DerivedFields(_) => "derives output (coupled)".to_string(),
1585 CapsConstraint::LegacySource(c) => alloc::format!("legacy source {}", c.to_gst_string()),
1586 CapsConstraint::LegacyTransform { .. } => "legacy transform".to_string(),
1587 CapsConstraint::LegacySink(_) => "legacy sink".to_string(),
1588 }
1589}
1590
1591#[cfg(feature = "std")]
1605pub(crate) fn graph_downstream_feasibility<E>(
1606 graph: &ValidatedGraph<E>,
1607 constraints: &[NodeConstraint<'_>],
1608 solution: &[Caps],
1609) -> Vec<Option<CapsSet>> {
1610 let ne = graph.edge_count();
1611 let mut feas: Vec<Option<CapsSet>> = alloc::vec![None; ne];
1612 for &node in graph.topo().iter().rev() {
1615 let idx = node.0 as usize;
1616 match graph.kind(node) {
1617 NodeKind::Source | NodeKind::FanoutSrc(_) => {}
1619 NodeKind::Sink => {
1620 let ie = graph.in_edges(node)[0];
1621 feas[ie] = match &constraints[idx] {
1622 NodeConstraint::Element(CapsConstraint::Accepts(s)) => Some(s.clone()),
1623 _ => None,
1624 };
1625 }
1626 NodeKind::Transform => {
1627 let ie = graph.in_edges(node)[0];
1628 let oe = graph.out_edges(node)[0];
1629 if let NodeConstraint::Element(c) = &constraints[idx] {
1630 feas[ie] = backward_feasible(c, feas[oe].as_ref(), solution.get(ie));
1633 }
1634 }
1635 NodeKind::Tee(_) => {
1636 if let NodeConstraint::Demux { input, .. } = &constraints[idx] {
1640 feas[graph.in_edges(node)[0]] = match input {
1641 CapsConstraint::Accepts(s) => Some(s.clone()),
1642 _ => None,
1643 };
1644 } else {
1645 let mut acc: Option<CapsSet> = None;
1646 for &oe in graph.out_edges(node) {
1647 if let Some(s) = feas[oe].as_ref() {
1648 acc = Some(match acc {
1649 Some(a) => a.intersect(s),
1650 None => s.clone(),
1651 });
1652 }
1653 }
1654 feas[graph.in_edges(node)[0]] = acc;
1655 }
1656 }
1657 NodeKind::Muxer(_) | NodeKind::FaninSink(_) => {
1661 if let NodeConstraint::Muxer { inputs, .. } = &constraints[idx] {
1662 for &ie in graph.in_edges(node) {
1663 let pad = graph.edge(ie).dst.index as usize;
1664 feas[ie] = match inputs.get(pad) {
1665 Some(CapsConstraint::Accepts(s)) => Some(s.clone()),
1666 _ => None,
1667 };
1668 }
1669 }
1670 }
1671 }
1672 }
1673 feas
1674}
1675
1676fn edge_endpoints<E>(graph: &ValidatedGraph<E>, edge_id: usize) -> (usize, usize) {
1678 let e = graph.edge(edge_id);
1679 (e.src.node.0 as usize, e.dst.node.0 as usize)
1680}
1681
1682fn apply_node<E>(
1683 graph: &ValidatedGraph<E>,
1684 node: NodeId,
1685 constraints: &[NodeConstraint<'_>],
1686 edges: &mut [Option<CapsSet>],
1687) -> Result<(), NegotiationFailure> {
1688 let kind = graph.kind(node);
1689 let in_e = graph.in_edges(node);
1690 let out_e = graph.out_edges(node);
1691 let idx = node.0 as usize;
1692 let nc = &constraints[idx];
1693 let shape_err = NegotiationFailure::EndpointShapeMismatch { index: idx };
1694 match kind {
1695 NodeKind::Source => match nc {
1696 NodeConstraint::Element(CapsConstraint::Produces(s)) => {
1697 narrow_edge(graph, edges, out_e[0], s, node)
1698 }
1699 NodeConstraint::Element(CapsConstraint::LegacySource(caps)) => {
1702 narrow_edge(graph, edges, out_e[0], &CapsSet::one(caps.clone()), node)
1703 }
1704 _ => Err(shape_err),
1705 },
1706 NodeKind::Sink => match nc {
1707 NodeConstraint::Element(CapsConstraint::Accepts(s)) => {
1708 narrow_edge(graph, edges, in_e[0], s, node)
1709 }
1710 NodeConstraint::Element(CapsConstraint::AcceptsAny)
1715 | NodeConstraint::Element(CapsConstraint::LegacySink(_)) => Ok(()),
1716 _ => Err(shape_err),
1717 },
1718 NodeKind::Transform => match nc {
1719 NodeConstraint::Element(c) => {
1720 apply_transform_node(graph, c, in_e[0], out_e[0], edges, node)
1721 }
1722 _ => Err(shape_err),
1723 },
1724 NodeKind::Tee(_) => match nc {
1725 NodeConstraint::Demux { input, ports } => {
1728 apply_demux_node(graph, node, in_e[0], out_e, input, ports, edges)
1729 }
1730 _ => apply_tee_node(graph, in_e[0], out_e, edges),
1731 },
1732 NodeKind::Muxer(_) => match nc {
1733 NodeConstraint::Muxer {
1734 inputs,
1735 output,
1736 follows,
1737 } => apply_muxer_node(graph, node, inputs, output, *follows, edges),
1738 _ => Err(shape_err),
1739 },
1740 NodeKind::FaninSink(_) => match nc {
1744 NodeConstraint::Muxer { inputs, .. } => narrow_muxer_inputs(graph, node, inputs, edges),
1745 _ => Err(shape_err),
1746 },
1747 NodeKind::FanoutSrc(_) => match nc {
1751 NodeConstraint::Demux { ports, .. } => {
1752 for &oe in out_e {
1753 let port = graph.edge(oe).src.index as usize;
1754 match ports.get(port) {
1755 Some(CapsConstraint::Produces(set)) => {
1756 narrow_edge(graph, edges, oe, set, node)?
1757 }
1758 Some(CapsConstraint::LegacySource(caps)) => {
1759 narrow_edge(graph, edges, oe, &CapsSet::one(caps.clone()), node)?
1760 }
1761 _ => return Err(shape_err),
1762 }
1763 }
1764 Ok(())
1765 }
1766 _ => Err(shape_err),
1767 },
1768 }
1769}
1770
1771fn narrow_edge<E>(
1775 graph: &ValidatedGraph<E>,
1776 edges: &mut [Option<CapsSet>],
1777 edge_id: usize,
1778 contrib: &CapsSet,
1779 by: NodeId,
1780) -> Result<(), NegotiationFailure> {
1781 let next = match &edges[edge_id] {
1782 Some(cur) => cur.intersect(contrib),
1783 None => contrib.clone(),
1784 };
1785 if next.is_empty() {
1786 let (up, down) = edge_endpoints(graph, edge_id);
1787 let other = edges[edge_id].clone().unwrap_or_else(empty_set);
1788 return Err(if graph.edge(edge_id).src.node == by {
1789 NegotiationFailure::empty_link_conflict(up, down, contrib.clone(), other)
1790 } else {
1791 NegotiationFailure::empty_link_conflict(up, down, other, contrib.clone())
1792 });
1793 }
1794 edges[edge_id] = Some(next);
1795 Ok(())
1796}
1797
1798fn empty_set() -> CapsSet {
1799 CapsSet::from_alternatives(Vec::new())
1800}
1801
1802fn couple_edges<E>(
1805 graph: &ValidatedGraph<E>,
1806 edges: &mut [Option<CapsSet>],
1807 a: usize,
1808 b: usize,
1809) -> Result<(), NegotiationFailure> {
1810 match (edges[a].clone(), edges[b].clone()) {
1811 (Some(sa), Some(sb)) => {
1812 let coupled = sa.intersect(&sb);
1813 if coupled.is_empty() {
1814 let up = edge_endpoints(graph, a).0;
1815 let down = edge_endpoints(graph, b).1;
1816 return Err(NegotiationFailure::empty_link_conflict(up, down, sa, sb));
1817 }
1818 edges[a] = Some(coupled.clone());
1819 edges[b] = Some(coupled);
1820 }
1821 (Some(sa), None) => edges[b] = Some(sa),
1822 (None, Some(sb)) => edges[a] = Some(sb),
1823 (None, None) => {}
1824 }
1825 Ok(())
1826}
1827
1828fn apply_transform_node<E>(
1831 graph: &ValidatedGraph<E>,
1832 c: &CapsConstraint<'_>,
1833 in_e: usize,
1834 out_e: usize,
1835 edges: &mut [Option<CapsSet>],
1836 node: NodeId,
1837) -> Result<(), NegotiationFailure> {
1838 match c {
1839 CapsConstraint::Identity(s) => {
1840 narrow_edge(graph, edges, in_e, s, node)?;
1841 narrow_edge(graph, edges, out_e, s, node)?;
1842 couple_edges(graph, edges, in_e, out_e)
1843 }
1844 CapsConstraint::IdentityAny => couple_edges(graph, edges, in_e, out_e),
1845 CapsConstraint::Mapping(pairs) => {
1846 let mut new_in = CapsSet::from_alternatives(Vec::new());
1847 let mut new_out = CapsSet::from_alternatives(Vec::new());
1848 for (in_set, out_set) in pairs {
1849 let in_match = match &edges[in_e] {
1850 Some(cur) => cur.intersect(in_set),
1851 None => in_set.clone(),
1852 };
1853 let out_match = match &edges[out_e] {
1854 Some(cur) => cur.intersect(out_set),
1855 None => out_set.clone(),
1856 };
1857 if !in_match.is_empty() && !out_match.is_empty() {
1858 new_in = new_in.union(&in_match);
1859 new_out = new_out.union(&out_match);
1860 }
1861 }
1862 if new_in.is_empty() || new_out.is_empty() {
1863 let up = edge_endpoints(graph, in_e).0;
1864 let down = edge_endpoints(graph, out_e).1;
1865 return Err(NegotiationFailure::empty_link_conflict(
1866 up,
1867 down,
1868 edges[in_e].clone().unwrap_or_else(empty_set),
1869 edges[out_e].clone().unwrap_or_else(empty_set),
1870 ));
1871 }
1872 edges[in_e] = Some(new_in);
1873 edges[out_e] = Some(new_out);
1874 Ok(())
1875 }
1876 CapsConstraint::DerivedOutput(f) => {
1877 if let Some(in_set) = edges[in_e].clone() {
1883 let derived = forward_derived_union(f.as_ref(), &in_set);
1884 if derived.is_empty() {
1885 let (up, down) = edge_endpoints(graph, out_e);
1886 return Err(NegotiationFailure::empty_link(up, down));
1887 }
1888 narrow_edge(graph, edges, out_e, &derived, node)?;
1889 }
1890 if let (Some(in_set), Some(out_set)) = (edges[in_e].clone(), edges[out_e].clone()) {
1893 match derived_backward(f.as_ref(), &in_set, &out_set) {
1894 Ok(Some(narrowed)) => edges[in_e] = Some(narrowed),
1895 Ok(None) => {}
1896 Err(()) => {
1897 let (up, down) = edge_endpoints(graph, in_e);
1898 return Err(NegotiationFailure::empty_link(up, down));
1899 }
1900 }
1901 }
1902 Ok(())
1903 }
1904 CapsConstraint::DerivedFields(t) => {
1905 let derive = |c: &Caps| t.derive(c);
1908 if let Some(in_set) = edges[in_e].clone() {
1909 let derived = forward_derived_union(&derive, &in_set);
1910 if derived.is_empty() {
1911 let (up, down) = edge_endpoints(graph, out_e);
1912 return Err(NegotiationFailure::empty_link(up, down));
1913 }
1914 narrow_edge(graph, edges, out_e, &derived, node)?;
1915 }
1916 if let (Some(in_set), Some(out_set)) = (edges[in_e].clone(), edges[out_e].clone()) {
1917 match backward_field_narrow(&derive, t.passthrough(), &in_set, &out_set) {
1918 Ok(Some(narrowed)) => edges[in_e] = Some(narrowed),
1919 Ok(None) => {}
1920 Err(()) => {
1921 let (up, down) = edge_endpoints(graph, in_e);
1922 return Err(NegotiationFailure::empty_link(up, down));
1923 }
1924 }
1925 }
1926 Ok(())
1927 }
1928 CapsConstraint::LegacyTransform { intercept, .. } => {
1932 if let Some(fixed_input) = edges[in_e].as_ref().and_then(fixed_single) {
1933 let out = intercept(&fixed_input).map_err(|_| {
1934 let (up, down) = edge_endpoints(graph, out_e);
1935 NegotiationFailure::empty_link(up, down)
1936 })?;
1937 return narrow_edge(graph, edges, out_e, &CapsSet::one(out), node);
1938 }
1939 Ok(())
1940 }
1941 _ => Err(NegotiationFailure::EndpointShapeMismatch {
1942 index: node.0 as usize,
1943 }),
1944 }
1945}
1946
1947fn fixed_single(set: &CapsSet) -> Option<Caps> {
1951 let fixed = set.fixate()?;
1952 (set.alternatives().len() == 1 && set.alternatives()[0] == fixed).then_some(fixed)
1953}
1954
1955fn forward_derived_union(f: &dyn Fn(&Caps) -> CapsSet, in_set: &CapsSet) -> CapsSet {
1961 in_set
1962 .alternatives()
1963 .iter()
1964 .fold(CapsSet::from_alternatives(Vec::new()), |acc, a| {
1965 acc.union(&f(a))
1966 })
1967}
1968
1969fn backward_filter_derived(
1981 f: &dyn Fn(&Caps) -> CapsSet,
1982 in_set: &CapsSet,
1983 out_set: &CapsSet,
1984) -> Result<Option<CapsSet>, ()> {
1985 if in_set.alternatives().len() <= 1 {
1986 return Ok(None);
1987 }
1988 let kept: Vec<Caps> = in_set
1989 .alternatives()
1990 .iter()
1991 .filter(|a| !f(a).intersect(out_set).is_empty())
1992 .cloned()
1993 .collect();
1994 if kept.is_empty() {
1995 return Err(());
1996 }
1997 if kept.len() == in_set.alternatives().len() {
1998 return Ok(None);
1999 }
2000 Ok(Some(CapsSet::from_alternatives(kept)))
2001}
2002
2003fn derived_backward(
2013 f: &dyn Fn(&Caps) -> CapsSet,
2014 in_set: &CapsSet,
2015 out_set: &CapsSet,
2016) -> Result<Option<CapsSet>, ()> {
2017 let mask = in_set
2018 .alternatives()
2019 .first()
2020 .map(|sample| discover_passthrough(f, sample))
2021 .unwrap_or(PassthroughFields::NONE);
2022 if mask == PassthroughFields::NONE {
2023 backward_filter_derived(f, in_set, out_set)
2024 } else {
2025 backward_field_narrow(f, mask, in_set, out_set)
2026 }
2027}
2028
2029fn backward_field_narrow(
2044 derive: &dyn Fn(&Caps) -> CapsSet,
2045 passthrough: PassthroughFields,
2046 in_set: &CapsSet,
2047 out_set: &CapsSet,
2048) -> Result<Option<CapsSet>, ()> {
2049 let mut kept: Vec<Caps> = Vec::new();
2050 let mut changed = false;
2051 for a in in_set.alternatives() {
2052 let reach = derive(a).intersect(out_set);
2053 if reach.is_empty() {
2054 changed = true; continue;
2056 }
2057 let mut any = false;
2062 for out_alt in reach.alternatives() {
2063 if let Some(c) = couple_passthrough_derived(a, out_alt, passthrough) {
2064 if &c != a {
2065 changed = true;
2066 }
2067 if !kept.contains(&c) {
2068 kept.push(c);
2069 }
2070 any = true;
2071 }
2072 }
2073 if !any {
2074 changed = true;
2076 }
2077 }
2078 if kept.is_empty() {
2079 return Err(());
2080 }
2081 if !changed {
2082 return Ok(None);
2083 }
2084 Ok(Some(CapsSet::from_alternatives(kept)))
2085}
2086
2087fn apply_tee_node<E>(
2090 graph: &ValidatedGraph<E>,
2091 in_e: usize,
2092 out_e: &[usize],
2093 edges: &mut [Option<CapsSet>],
2094) -> Result<(), NegotiationFailure> {
2095 let mut acc: Option<CapsSet> = edges[in_e].clone();
2096 for &oe in out_e {
2097 if let Some(s) = edges[oe].clone() {
2098 acc = Some(match acc {
2099 Some(a) => a.intersect(&s),
2100 None => s,
2101 });
2102 }
2103 }
2104 if let Some(coupled) = acc {
2105 if coupled.is_empty() {
2106 let (up, down) = edge_endpoints(graph, in_e);
2107 return Err(NegotiationFailure::empty_link(up, down));
2108 }
2109 edges[in_e] = Some(coupled.clone());
2110 for &oe in out_e {
2111 edges[oe] = Some(coupled.clone());
2112 }
2113 }
2114 Ok(())
2115}
2116
2117fn apply_demux_node<E>(
2123 graph: &ValidatedGraph<E>,
2124 node: NodeId,
2125 in_e: usize,
2126 out_e: &[usize],
2127 input: &CapsConstraint<'_>,
2128 ports: &[CapsConstraint<'_>],
2129 edges: &mut [Option<CapsSet>],
2130) -> Result<(), NegotiationFailure> {
2131 if let CapsConstraint::Accepts(s) = input {
2135 narrow_edge(graph, edges, in_e, s, node)?;
2136 }
2137 for &oe in out_e {
2138 let port = graph.edge(oe).src.index as usize;
2139 if let Some(CapsConstraint::Produces(s)) = ports.get(port) {
2140 narrow_edge(graph, edges, oe, s, node)?;
2141 }
2142 }
2143 Ok(())
2144}
2145
2146fn narrow_muxer_inputs<E>(
2152 graph: &ValidatedGraph<E>,
2153 node: NodeId,
2154 inputs: &[CapsConstraint<'_>],
2155 edges: &mut [Option<CapsSet>],
2156) -> Result<(), NegotiationFailure> {
2157 let shape_err = NegotiationFailure::EndpointShapeMismatch {
2158 index: node.0 as usize,
2159 };
2160 for &eid in graph.in_edges(node) {
2161 let pad = graph.edge(eid).dst.index as usize;
2162 match inputs.get(pad) {
2163 Some(CapsConstraint::Accepts(set)) => narrow_edge(graph, edges, eid, set, node)?,
2164 Some(CapsConstraint::AcceptsAny) | Some(CapsConstraint::LegacySink(_)) => {}
2165 _ => return Err(shape_err),
2166 }
2167 }
2168 Ok(())
2169}
2170
2171fn apply_muxer_node<E>(
2175 graph: &ValidatedGraph<E>,
2176 node: NodeId,
2177 inputs: &[CapsConstraint<'_>],
2178 output: &CapsConstraint<'_>,
2179 follows: Option<usize>,
2180 edges: &mut [Option<CapsSet>],
2181) -> Result<(), NegotiationFailure> {
2182 let idx = node.0 as usize;
2183 let shape_err = NegotiationFailure::EndpointShapeMismatch { index: idx };
2184 narrow_muxer_inputs(graph, node, inputs, edges)?;
2185 let out_edge = graph.out_edges(node)[0];
2186 if let Some(pad) = follows {
2191 let in_edge = graph
2192 .in_edges(node)
2193 .iter()
2194 .copied()
2195 .find(|&e| graph.edge(e).dst.index as usize == pad)
2196 .ok_or(shape_err)?;
2197 return couple_edges(graph, edges, in_edge, out_edge);
2198 }
2199 match output {
2200 CapsConstraint::Produces(set) => narrow_edge(graph, edges, out_edge, set, node),
2201 CapsConstraint::LegacySource(caps) => {
2203 narrow_edge(graph, edges, out_edge, &CapsSet::one(caps.clone()), node)
2204 }
2205 _ => Err(shape_err),
2206 }
2207}
2208
2209#[cfg(test)]
2210mod tests {
2211 use super::*;
2212 use crate::caps::{Dim, Rate, RawVideoFormat, VideoCodec};
2213 use crate::caps_transform::{CapsTransform, FieldTransform, RawVideoShape};
2214 use crate::runtime::passthrough::couple_passthrough;
2215 use alloc::boxed::Box;
2216 use alloc::vec;
2217
2218 fn video(fmt: RawVideoFormat, w: Dim, h: Dim, r: Rate) -> Caps {
2219 Caps::RawVideo {
2220 format: fmt,
2221 width: w,
2222 height: h,
2223 framerate: r,
2224 interlace: crate::Interlace::Any,
2225 }
2226 }
2227
2228 #[test]
2233 fn solve_linear_tensor_dtype_change_chain() {
2234 use crate::caps::{TensorDType, TensorLayout, TensorShape};
2235 let t = |d: TensorDType, s: TensorShape| Caps::Tensor {
2236 dtype: d,
2237 shape: s,
2238 layout: TensorLayout::Nchw,
2239 };
2240 let f32_in = t(TensorDType::F32, TensorShape::new([1, 3, 4, 4]));
2241 let u8_mid = t(TensorDType::U8, TensorShape::new([1, 3, 4, 4]));
2242 let logits = t(TensorDType::F32, TensorShape::new([1, 10]));
2243
2244 let src = CapsConstraint::Produces(CapsSet::one(f32_in.clone()));
2245 let quant = CapsConstraint::DerivedOutput(Box::new(|inp: &Caps| match inp {
2247 Caps::Tensor {
2248 dtype: TensorDType::F32,
2249 shape,
2250 layout,
2251 } => CapsSet::one(Caps::Tensor {
2252 dtype: TensorDType::U8,
2253 shape: *shape,
2254 layout: *layout,
2255 }),
2256 _ => CapsSet::from_alternatives(Vec::new()),
2257 }));
2258 let logits_c = logits.clone();
2260 let infer = CapsConstraint::DerivedOutput(Box::new(move |inp: &Caps| match inp {
2261 Caps::Tensor {
2262 dtype: TensorDType::U8,
2263 ..
2264 } => CapsSet::one(logits_c.clone()),
2265 _ => CapsSet::from_alternatives(Vec::new()),
2266 }));
2267 let sink = CapsConstraint::AcceptsAny;
2268
2269 let links = solve_linear(&[&src, &quant, &infer, &sink]).expect("tensor chain negotiates");
2270 assert_eq!(links[0], f32_in, "source link f32");
2271 assert_eq!(
2272 links[1], u8_mid,
2273 "quantize output is u8, not the source f32"
2274 );
2275 assert_eq!(links[2], logits, "inference output [1,10]");
2276 }
2277
2278 #[test]
2281 fn solve_graph_tensor_dtype_change_chain() {
2282 use crate::caps::{TensorDType, TensorLayout, TensorShape};
2283 use crate::graph::Graph;
2284 let t = |d: TensorDType, s: TensorShape| Caps::Tensor {
2285 dtype: d,
2286 shape: s,
2287 layout: TensorLayout::Nchw,
2288 };
2289 let f32_in = t(TensorDType::F32, TensorShape::new([1, 3, 4, 4]));
2290 let u8_mid = t(TensorDType::U8, TensorShape::new([1, 3, 4, 4]));
2291 let logits = t(TensorDType::F32, TensorShape::new([1, 10]));
2292 let logits_c = logits.clone();
2293 let cs: Vec<NodeConstraint> = vec![
2294 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(f32_in.clone()))),
2295 NodeConstraint::Element(CapsConstraint::DerivedOutput(Box::new(
2296 |inp: &Caps| match inp {
2297 Caps::Tensor {
2298 dtype: TensorDType::F32,
2299 shape,
2300 layout,
2301 } => CapsSet::one(Caps::Tensor {
2302 dtype: TensorDType::U8,
2303 shape: *shape,
2304 layout: *layout,
2305 }),
2306 _ => CapsSet::from_alternatives(Vec::new()),
2307 },
2308 ))),
2309 NodeConstraint::Element(CapsConstraint::DerivedOutput(Box::new(
2310 move |inp: &Caps| match inp {
2311 Caps::Tensor {
2312 dtype: TensorDType::U8,
2313 ..
2314 } => CapsSet::one(logits_c.clone()),
2315 _ => CapsSet::from_alternatives(Vec::new()),
2316 },
2317 ))),
2318 NodeConstraint::Element(CapsConstraint::AcceptsAny),
2319 ];
2320 let mut g: Graph<()> = Graph::new();
2321 let src = g.add_source(());
2322 let q = g.add_transform(());
2323 let inf = g.add_transform(());
2324 let sink = g.add_sink(());
2325 g.link(src, q).unwrap();
2326 g.link(q, inf).unwrap();
2327 g.link(inf, sink).unwrap();
2328 let v = g.finish().unwrap();
2329 let dag = solve_graph(&v, &cs).expect("tensor chain solves as a graph");
2330 assert_eq!(dag, vec![f32_in, u8_mid, logits]);
2331 }
2332
2333 fn fixed_video(fmt: RawVideoFormat, w: u32, h: u32, fps: u32) -> Caps {
2334 video(fmt, Dim::Fixed(w), Dim::Fixed(h), Rate::Fixed(fps << 16))
2335 }
2336
2337 fn compressed(codec: VideoCodec, w: Dim, h: Dim, r: Rate) -> Caps {
2338 Caps::CompressedVideo {
2339 codec,
2340 width: w,
2341 height: h,
2342 framerate: r,
2343 }
2344 }
2345
2346 fn fixed_compressed(codec: VideoCodec, w: u32, h: u32, fps: u32) -> Caps {
2347 compressed(codec, Dim::Fixed(w), Dim::Fixed(h), Rate::Fixed(fps << 16))
2348 }
2349
2350 #[test]
2351 fn solves_source_sink_minimal_chain() {
2352 let src = CapsConstraint::Produces(CapsSet::one(fixed_video(
2353 RawVideoFormat::Nv12,
2354 1280,
2355 720,
2356 30,
2357 )));
2358 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2359 RawVideoFormat::Nv12,
2360 Dim::Any,
2361 Dim::Any,
2362 Rate::Any,
2363 )));
2364 let links = solve_linear(&[&src, &sink]).unwrap();
2365 assert_eq!(
2366 links,
2367 vec![fixed_video(RawVideoFormat::Nv12, 1280, 720, 30)]
2368 );
2369 }
2370
2371 #[test]
2372 fn empty_link_when_formats_disjoint() {
2373 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2374 VideoCodec::H264,
2375 1280,
2376 720,
2377 30,
2378 )));
2379 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2380 RawVideoFormat::Nv12,
2381 Dim::Any,
2382 Dim::Any,
2383 Rate::Any,
2384 )));
2385 assert_eq!(
2386 solve_linear(&[&src, &sink]),
2387 Err(NegotiationFailure::empty_link(0, 1))
2388 );
2389 }
2390
2391 #[test]
2392 fn degenerate_when_fewer_than_two_elements() {
2393 let src =
2394 CapsConstraint::Produces(CapsSet::one(fixed_video(RawVideoFormat::Nv12, 1, 1, 1)));
2395 assert_eq!(solve_linear(&[&src]), Err(NegotiationFailure::Degenerate));
2396 assert_eq!(solve_linear(&[]), Err(NegotiationFailure::Degenerate));
2397 }
2398
2399 #[test]
2400 fn endpoint_shape_mismatch_rejected() {
2401 let id = CapsConstraint::Identity(CapsSet::one(fixed_video(RawVideoFormat::Nv12, 1, 1, 1)));
2402 let sink =
2403 CapsConstraint::Accepts(CapsSet::one(fixed_video(RawVideoFormat::Nv12, 1, 1, 1)));
2404 assert_eq!(
2405 solve_linear(&[&id, &sink]),
2406 Err(NegotiationFailure::EndpointShapeMismatch { index: 0 })
2407 );
2408 }
2409
2410 #[test]
2411 fn preference_tie_break_picks_self_first_alt() {
2412 let rgba = fixed_video(RawVideoFormat::Rgba8, 640, 480, 30);
2415 let h264 = fixed_compressed(VideoCodec::H264, 640, 480, 30);
2416 let src =
2417 CapsConstraint::Produces(CapsSet::from_alternatives(vec![rgba.clone(), h264.clone()]));
2418 let sink =
2419 CapsConstraint::Accepts(CapsSet::from_alternatives(vec![h264.clone(), rgba.clone()]));
2420 let links = solve_linear(&[&src, &sink]).unwrap();
2421 assert_eq!(links, vec![rgba]);
2424 }
2425
2426 #[test]
2427 fn identity_couples_input_and_output() {
2428 let src = CapsConstraint::Produces(CapsSet::one(fixed_video(
2429 RawVideoFormat::Nv12,
2430 1280,
2431 720,
2432 30,
2433 )));
2434 let id = CapsConstraint::Identity(CapsSet::one(video(
2435 RawVideoFormat::Nv12,
2436 Dim::Any,
2437 Dim::Any,
2438 Rate::Any,
2439 )));
2440 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2441 RawVideoFormat::Nv12,
2442 Dim::Any,
2443 Dim::Any,
2444 Rate::Any,
2445 )));
2446 let links = solve_linear(&[&src, &id, &sink]).unwrap();
2447 assert_eq!(
2448 links,
2449 vec![
2450 fixed_video(RawVideoFormat::Nv12, 1280, 720, 30),
2451 fixed_video(RawVideoFormat::Nv12, 1280, 720, 30),
2452 ]
2453 );
2454 }
2455
2456 #[test]
2457 fn identity_format_mismatch_returns_empty_link() {
2458 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2459 VideoCodec::H264,
2460 1280,
2461 720,
2462 30,
2463 )));
2464 let id = CapsConstraint::Identity(CapsSet::one(video(
2465 RawVideoFormat::Nv12,
2466 Dim::Any,
2467 Dim::Any,
2468 Rate::Any,
2469 )));
2470 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2471 RawVideoFormat::Nv12,
2472 Dim::Any,
2473 Dim::Any,
2474 Rate::Any,
2475 )));
2476 assert!(matches!(
2477 solve_linear(&[&src, &id, &sink]),
2478 Err(NegotiationFailure::EmptyLink { .. })
2479 ));
2480 }
2481
2482 #[test]
2483 fn derived_output_evaluated_after_input_fixates() {
2484 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2486 VideoCodec::H264,
2487 1920,
2488 1080,
2489 60,
2490 )));
2491 let dec = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
2492 Caps::CompressedVideo {
2493 width,
2494 height,
2495 framerate,
2496 ..
2497 } => CapsSet::one(Caps::RawVideo {
2498 format: RawVideoFormat::Nv12,
2499 width: width.clone(),
2500 height: height.clone(),
2501 framerate: framerate.clone(),
2502 interlace: crate::Interlace::Any,
2503 }),
2504 _ => CapsSet::from_alternatives(Vec::new()),
2505 }));
2506 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2507 RawVideoFormat::Nv12,
2508 Dim::Any,
2509 Dim::Any,
2510 Rate::Any,
2511 )));
2512 let links = solve_linear(&[&src, &dec, &sink]).unwrap();
2513 assert_eq!(
2514 links,
2515 vec![
2516 fixed_compressed(VideoCodec::H264, 1920, 1080, 60),
2517 fixed_video(RawVideoFormat::Nv12, 1920, 1080, 60),
2518 ]
2519 );
2520 }
2521
2522 #[test]
2523 fn derived_output_couples_downstream_geometry_pin_backward() {
2524 let src = CapsConstraint::Produces(CapsSet::one(compressed(
2531 VideoCodec::H264,
2532 Dim::Any,
2533 Dim::Any,
2534 Rate::Fixed(30 << 16),
2535 )));
2536 let dec = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
2537 Caps::CompressedVideo {
2538 width,
2539 height,
2540 framerate,
2541 ..
2542 } => CapsSet::one(Caps::RawVideo {
2543 format: RawVideoFormat::Nv12,
2544 width: width.clone(),
2545 height: height.clone(),
2546 framerate: framerate.clone(),
2547 interlace: crate::Interlace::Any,
2548 }),
2549 _ => CapsSet::from_alternatives(Vec::new()),
2550 }));
2551 let sink = CapsConstraint::Accepts(CapsSet::one(fixed_video(
2552 RawVideoFormat::Nv12,
2553 1280,
2554 720,
2555 30,
2556 )));
2557 let links = solve_linear(&[&src, &dec, &sink]).unwrap();
2558 assert_eq!(
2559 links,
2560 vec![
2561 fixed_compressed(VideoCodec::H264, 1280, 720, 30),
2562 fixed_video(RawVideoFormat::Nv12, 1280, 720, 30),
2563 ]
2564 );
2565 }
2566
2567 #[test]
2568 fn derived_output_fixed_output_imposes_no_backward_narrowing() {
2569 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2573 VideoCodec::H264,
2574 1920,
2575 1080,
2576 30,
2577 )));
2578 let dec = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
2579 Caps::CompressedVideo { .. } => {
2580 CapsSet::one(fixed_video(RawVideoFormat::Nv12, 640, 480, 30))
2581 }
2582 _ => CapsSet::from_alternatives(Vec::new()),
2583 }));
2584 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2585 RawVideoFormat::Nv12,
2586 Dim::Any,
2587 Dim::Any,
2588 Rate::Any,
2589 )));
2590 let links = solve_linear(&[&src, &dec, &sink]).unwrap();
2591 assert_eq!(
2592 links,
2593 vec![
2594 fixed_compressed(VideoCodec::H264, 1920, 1080, 30),
2595 fixed_video(RawVideoFormat::Nv12, 640, 480, 30),
2596 ]
2597 );
2598 }
2599
2600 #[test]
2601 fn mapping_picks_compatible_pair() {
2602 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2608 VideoCodec::H265,
2609 1280,
2610 720,
2611 30,
2612 )));
2613 let map = CapsConstraint::Mapping(vec![
2614 (
2615 CapsSet::one(compressed(VideoCodec::H264, Dim::Any, Dim::Any, Rate::Any)),
2616 CapsSet::one(fixed_video(RawVideoFormat::Nv12, 640, 480, 30)),
2617 ),
2618 (
2619 CapsSet::one(compressed(VideoCodec::H265, Dim::Any, Dim::Any, Rate::Any)),
2620 CapsSet::one(fixed_video(RawVideoFormat::Nv12, 1280, 720, 30)),
2621 ),
2622 ]);
2623 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2624 RawVideoFormat::Nv12,
2625 Dim::Any,
2626 Dim::Any,
2627 Rate::Any,
2628 )));
2629 let links = solve_linear(&[&src, &map, &sink]).unwrap();
2630 assert_eq!(links[0], fixed_compressed(VideoCodec::H265, 1280, 720, 30));
2631 assert_eq!(links[1], fixed_video(RawVideoFormat::Nv12, 1280, 720, 30));
2632 }
2633
2634 #[test]
2635 fn legacy_cascade_source_to_sink() {
2636 let src_caps = fixed_video(RawVideoFormat::Nv12, 1280, 720, 30);
2639 let src = CapsConstraint::LegacySource(src_caps.clone());
2640 let sink = CapsConstraint::LegacySink(Box::new(|upstream: &Caps| Ok(upstream.clone())));
2641 let links = solve_linear(&[&src, &sink]).unwrap();
2642 assert_eq!(links, vec![src_caps]);
2643 }
2644
2645 #[test]
2646 fn legacy_cascade_with_pass_through_transform() {
2647 let src_caps = fixed_video(RawVideoFormat::Nv12, 1920, 1080, 60);
2648 let src = CapsConstraint::LegacySource(src_caps.clone());
2649 let id = CapsConstraint::LegacyTransform {
2650 intercept: Box::new(|c: &Caps| Ok(c.clone())),
2651 propose_output: Box::new(|c: &Caps| c.clone()),
2652 };
2653 let sink = CapsConstraint::LegacySink(Box::new(|c: &Caps| Ok(c.clone())));
2654 let links = solve_linear(&[&src, &id, &sink]).unwrap();
2655 assert_eq!(links, vec![src_caps.clone(), src_caps]);
2656 }
2657
2658 #[test]
2659 fn legacy_cascade_with_boundary_transform() {
2660 let src_caps = fixed_compressed(VideoCodec::H264, 1280, 720, 30);
2662 let src = CapsConstraint::LegacySource(src_caps.clone());
2663 let dec = CapsConstraint::LegacyTransform {
2664 intercept: Box::new(|c: &Caps| Ok(c.clone())),
2665 propose_output: Box::new(|c: &Caps| match c {
2666 Caps::CompressedVideo {
2667 width,
2668 height,
2669 framerate,
2670 ..
2671 } => Caps::RawVideo {
2672 format: RawVideoFormat::Nv12,
2673 width: width.clone(),
2674 height: height.clone(),
2675 framerate: framerate.clone(),
2676 interlace: crate::Interlace::Any,
2677 },
2678 other => other.clone(),
2679 }),
2680 };
2681 let sink = CapsConstraint::LegacySink(Box::new(|c: &Caps| Ok(c.clone())));
2682 let links = solve_linear(&[&src, &dec, &sink]).unwrap();
2683 let h264 = fixed_compressed(VideoCodec::H264, 1280, 720, 30);
2693 assert_eq!(links, vec![h264.clone(), h264]);
2694 }
2695
2696 #[test]
2697 fn legacy_cascade_intercept_failure_returns_empty_link() {
2698 let src = CapsConstraint::LegacySource(fixed_compressed(VideoCodec::H264, 1280, 720, 30));
2699 let sink = CapsConstraint::LegacySink(Box::new(|_: &Caps| {
2700 Err(crate::error::G2gError::CapsMismatch)
2701 }));
2702 assert!(matches!(
2703 solve_linear(&[&src, &sink]),
2704 Err(NegotiationFailure::EmptyLink {
2705 upstream: 0,
2706 downstream: 1,
2707 ..
2708 })
2709 ));
2710 }
2711
2712 #[test]
2713 fn mixed_legacy_source_native_sink() {
2714 let caps = fixed_video(RawVideoFormat::Nv12, 1280, 720, 30);
2718 let src = CapsConstraint::LegacySource(caps.clone());
2719 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2720 RawVideoFormat::Nv12,
2721 Dim::Any,
2722 Dim::Any,
2723 Rate::Any,
2724 )));
2725 let links = solve_linear(&[&src, &sink]).unwrap();
2726 assert_eq!(links, vec![caps]);
2727 }
2728
2729 #[test]
2730 fn mixed_native_source_legacy_sink() {
2731 let caps = fixed_video(RawVideoFormat::Nv12, 640, 480, 30);
2733 let src = CapsConstraint::Produces(CapsSet::one(caps.clone()));
2734 let sink = CapsConstraint::LegacySink(Box::new(|c: &Caps| Ok(c.clone())));
2735 let links = solve_linear(&[&src, &sink]).unwrap();
2736 assert_eq!(links, vec![caps]);
2737 }
2738
2739 #[test]
2740 fn mixed_native_source_legacy_transform_native_sink() {
2741 let h264 = fixed_compressed(VideoCodec::H264, 1920, 1080, 60);
2745 let nv12 = fixed_video(RawVideoFormat::Nv12, 1920, 1080, 60);
2746 let src = CapsConstraint::Produces(CapsSet::one(h264));
2747 let dec = CapsConstraint::LegacyTransform {
2748 intercept: Box::new(|c: &Caps| Ok(c.clone())),
2749 propose_output: Box::new(|c: &Caps| match c {
2750 Caps::CompressedVideo {
2751 width,
2752 height,
2753 framerate,
2754 ..
2755 } => Caps::RawVideo {
2756 format: RawVideoFormat::Nv12,
2757 width: width.clone(),
2758 height: height.clone(),
2759 framerate: framerate.clone(),
2760 interlace: crate::Interlace::Any,
2761 },
2762 other => other.clone(),
2763 }),
2764 };
2765 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2766 RawVideoFormat::Nv12,
2767 Dim::Any,
2768 Dim::Any,
2769 Rate::Any,
2770 )));
2771 let links = solve_linear(&[&src, &dec, &sink]).unwrap();
2772 assert_eq!(
2773 links,
2774 vec![fixed_compressed(VideoCodec::H264, 1920, 1080, 60), nv12,]
2775 );
2776 }
2777
2778 #[test]
2779 fn mixed_chain_empty_link_when_sink_rejects() {
2780 let src = CapsConstraint::LegacySource(fixed_compressed(VideoCodec::H264, 1280, 720, 30));
2781 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2782 RawVideoFormat::Nv12,
2783 Dim::Any,
2784 Dim::Any,
2785 Rate::Any,
2786 )));
2787 assert!(matches!(
2788 solve_linear(&[&src, &sink]),
2789 Err(NegotiationFailure::EmptyLink { .. })
2790 ));
2791 }
2792
2793 #[test]
2794 fn accepts_any_native_chain_passes_source_caps_through() {
2795 let caps = fixed_video(RawVideoFormat::Nv12, 1280, 720, 30);
2796 let src = CapsConstraint::Produces(CapsSet::one(caps.clone()));
2797 let sink = CapsConstraint::AcceptsAny;
2798 let links = solve_linear(&[&src, &sink]).unwrap();
2799 assert_eq!(links, vec![caps]);
2800 }
2801
2802 #[test]
2803 fn accepts_any_mixed_chain_passes_legacy_source_through() {
2804 let caps = fixed_compressed(VideoCodec::H264, 1920, 1080, 60);
2807 let src = CapsConstraint::LegacySource(caps.clone());
2808 let sink = CapsConstraint::AcceptsAny;
2809 let links = solve_linear(&[&src, &sink]).unwrap();
2810 assert_eq!(links, vec![caps]);
2811 }
2812
2813 #[test]
2814 fn accepts_any_in_middle_position_is_silently_a_no_op() {
2815 let caps = fixed_video(RawVideoFormat::Nv12, 1, 1, 1);
2822 let src = CapsConstraint::Produces(CapsSet::one(caps.clone()));
2823 let mid = CapsConstraint::AcceptsAny;
2824 let sink = CapsConstraint::Accepts(CapsSet::one(caps.clone()));
2825 let links = solve_linear(&[&src, &mid, &sink]).unwrap();
2826 assert_eq!(links, vec![caps.clone(), caps]);
2827 }
2828
2829 #[test]
2830 fn identity_any_couples_native_links() {
2831 let caps = fixed_video(RawVideoFormat::Nv12, 1280, 720, 30);
2836 let src = CapsConstraint::Produces(CapsSet::one(caps.clone()));
2837 let mid = CapsConstraint::IdentityAny;
2838 let sink = CapsConstraint::AcceptsAny;
2839 let links = solve_linear(&[&src, &mid, &sink]).unwrap();
2840 assert_eq!(links, vec![caps.clone(), caps]);
2841 }
2842
2843 #[test]
2844 fn identity_any_in_mixed_chain_passes_legacy_source_through() {
2845 let caps = fixed_compressed(VideoCodec::H264, 1920, 1080, 60);
2846 let src = CapsConstraint::LegacySource(caps.clone());
2847 let mid = CapsConstraint::IdentityAny;
2848 let sink = CapsConstraint::AcceptsAny;
2849 let links = solve_linear(&[&src, &mid, &sink]).unwrap();
2850 assert_eq!(links, vec![caps.clone(), caps]);
2851 }
2852
2853 #[test]
2854 fn identity_any_endpoint_position_rejected_in_mixed() {
2855 let caps = fixed_video(RawVideoFormat::Nv12, 1, 1, 1);
2858 let bad_src = CapsConstraint::IdentityAny;
2859 let sink = CapsConstraint::Accepts(CapsSet::one(caps));
2860 assert!(matches!(
2861 solve_linear(&[&bad_src, &sink]),
2862 Err(NegotiationFailure::EndpointShapeMismatch { index: 0 })
2863 ));
2864 }
2865
2866 #[test]
2867 fn all_native_produces_to_accepts_any_passes_through() {
2868 let caps = fixed_video(RawVideoFormat::Rgba8, 1280, 720, 30);
2872 let src = CapsConstraint::Produces(CapsSet::one(caps.clone()));
2873 let sink = CapsConstraint::AcceptsAny;
2874 let links = solve_linear(&[&src, &sink]).unwrap();
2875 assert_eq!(links, vec![caps]);
2876 }
2877
2878 #[test]
2879 fn mapping_no_surviving_pair_returns_empty_link() {
2880 let src = CapsConstraint::Produces(CapsSet::one(fixed_compressed(
2881 VideoCodec::Av1,
2882 1280,
2883 720,
2884 30,
2885 )));
2886 let map = CapsConstraint::Mapping(vec![(
2887 CapsSet::one(compressed(VideoCodec::H264, Dim::Any, Dim::Any, Rate::Any)),
2888 CapsSet::one(video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any)),
2889 )]);
2890 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2891 RawVideoFormat::Nv12,
2892 Dim::Any,
2893 Dim::Any,
2894 Rate::Any,
2895 )));
2896 assert!(matches!(
2897 solve_linear(&[&src, &map, &sink]),
2898 Err(NegotiationFailure::EmptyLink { .. })
2899 ));
2900 }
2901
2902 #[cfg(feature = "std")]
2907 #[test]
2908 fn downstream_feasibility_is_source_independent() {
2909 let src =
2910 CapsConstraint::Produces(CapsSet::one(fixed_video(RawVideoFormat::Rgba8, 64, 64, 30)));
2911 let id = CapsConstraint::IdentityAny;
2912 let sink = CapsConstraint::Accepts(CapsSet::one(video(
2913 RawVideoFormat::Nv12,
2914 Dim::Any,
2915 Dim::Any,
2916 Rate::Any,
2917 )));
2918 let feas = downstream_feasibility(&[&src, &id, &sink]);
2919 assert_eq!(feas.len(), 2);
2922 assert!(feas[1].as_ref().unwrap().accepts(&video(
2923 RawVideoFormat::Nv12,
2924 Dim::Any,
2925 Dim::Any,
2926 Rate::Any,
2927 )));
2928 assert!(feas[0].as_ref().unwrap().accepts(&video(
2929 RawVideoFormat::Nv12,
2930 Dim::Any,
2931 Dim::Any,
2932 Rate::Any,
2933 )));
2934 assert!(!feas[0].as_ref().unwrap().accepts(&video(
2935 RawVideoFormat::Rgba8,
2936 Dim::Any,
2937 Dim::Any,
2938 Rate::Any,
2939 )));
2940 }
2941
2942 #[test]
2949 fn resolve_forward_output_steers_defers_and_rejects() {
2950 let conv = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| {
2952 let Caps::RawVideo {
2953 format,
2954 width,
2955 height,
2956 framerate,
2957 interlace: _,
2958 } = input
2959 else {
2960 return CapsSet::from_alternatives(vec![]);
2961 };
2962 CapsSet::from_alternatives(vec![
2963 video(*format, width.clone(), height.clone(), framerate.clone()),
2964 video(
2965 RawVideoFormat::Nv12,
2966 width.clone(),
2967 height.clone(),
2968 framerate.clone(),
2969 ),
2970 ])
2971 }));
2972 let i420 = fixed_video(RawVideoFormat::I420, 64, 64, 30);
2973 let nv12_set = CapsSet::one(video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any));
2974
2975 match resolve_forward_output(&conv, &i420, Some(&nv12_set), None) {
2977 ForwardResolve::Fixed(c) => {
2978 assert_eq!(
2979 c,
2980 video(
2981 RawVideoFormat::Nv12,
2982 Dim::Fixed(64),
2983 Dim::Fixed(64),
2984 Rate::Fixed(30 << 16)
2985 )
2986 );
2987 }
2988 other => panic!("expected Fixed(NV12), got {other:?}"),
2989 }
2990
2991 assert_eq!(
2994 resolve_forward_output(&conv, &i420, None, None),
2995 ForwardResolve::Defer
2996 );
2997
2998 let to_rgba = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
3004 Caps::RawVideo {
3005 width,
3006 height,
3007 framerate,
3008 ..
3009 } => CapsSet::one(video(
3010 RawVideoFormat::Rgba8,
3011 width.clone(),
3012 height.clone(),
3013 framerate.clone(),
3014 )),
3015 _ => CapsSet::from_alternatives(vec![]),
3016 }));
3017 let nv12_in = fixed_video(RawVideoFormat::Nv12, 64, 64, 30);
3018 match resolve_forward_output(&to_rgba, &nv12_in, None, None) {
3019 ForwardResolve::Fixed(c) => assert_eq!(
3020 c,
3021 video(
3022 RawVideoFormat::Rgba8,
3023 Dim::Fixed(64),
3024 Dim::Fixed(64),
3025 Rate::Fixed(30 << 16)
3026 )
3027 ),
3028 other => panic!("expected Fixed(RGBA8), got {other:?}"),
3029 }
3030
3031 let bgra_set = CapsSet::one(video(RawVideoFormat::Bgra8, Dim::Any, Dim::Any, Rate::Any));
3033 assert!(matches!(
3034 resolve_forward_output(&conv, &i420, Some(&bgra_set), None),
3035 ForwardResolve::Infeasible(NegotiationFailure::EmptyLink { .. })
3036 ));
3037 }
3038
3039 #[test]
3046 fn resolve_forward_output_forwards_an_unfixatable_single_output() {
3047 let scale = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
3050 Caps::RawVideo {
3051 format, framerate, ..
3052 } => CapsSet::one(video(
3053 *format,
3054 Dim::Fixed(640),
3055 Dim::Fixed(640),
3056 framerate.clone(),
3057 )),
3058 _ => CapsSet::from_alternatives(vec![]),
3059 }));
3060 let no_rate = video(
3063 RawVideoFormat::I420,
3064 Dim::Fixed(1280),
3065 Dim::Fixed(720),
3066 Rate::Any,
3067 );
3068 let downstream = CapsSet::one(video(
3069 RawVideoFormat::I420,
3070 Dim::Fixed(640),
3071 Dim::Fixed(640),
3072 Rate::Any,
3073 ));
3074 match resolve_forward_output(&scale, &no_rate, Some(&downstream), None) {
3075 ForwardResolve::Fixed(c) => assert_eq!(
3076 c,
3077 video(
3078 RawVideoFormat::I420,
3079 Dim::Fixed(640),
3080 Dim::Fixed(640),
3081 Rate::Any
3082 ),
3083 "the scaler's own output geometry, not its input's"
3084 ),
3085 other => panic!("expected Fixed(640x640), got {other:?}"),
3086 }
3087 }
3088
3089 #[test]
3094 fn resolve_forward_output_forwards_a_derived_single_output_without_a_snapshot() {
3095 let letterbox = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| match input {
3097 Caps::RawVideo {
3098 format,
3099 width,
3100 height: Dim::Fixed(h),
3101 framerate,
3102 ..
3103 } => CapsSet::one(video(
3104 *format,
3105 width.clone(),
3106 Dim::Fixed(h + 24),
3107 framerate.clone(),
3108 )),
3109 _ => CapsSet::from_alternatives(vec![]),
3110 }));
3111 let no_rate = video(
3112 RawVideoFormat::Rgba8,
3113 Dim::Fixed(640),
3114 Dim::Fixed(360),
3115 Rate::Any,
3116 );
3117 match resolve_forward_output(&letterbox, &no_rate, None, None) {
3118 ForwardResolve::Fixed(c) => assert_eq!(
3119 c,
3120 video(
3121 RawVideoFormat::Rgba8,
3122 Dim::Fixed(640),
3123 Dim::Fixed(384),
3124 Rate::Any
3125 ),
3126 "the boxer's own output geometry, not its input's"
3127 ),
3128 other => panic!("expected Fixed(640x384), got {other:?}"),
3129 }
3130 }
3131
3132 #[test]
3139 fn resolve_forward_output_keeps_previous_shape() {
3140 let conv = CapsConstraint::DerivedOutput(Box::new(|input: &Caps| {
3142 let Caps::RawVideo {
3143 format,
3144 width,
3145 height,
3146 framerate,
3147 interlace: _,
3148 } = input
3149 else {
3150 return CapsSet::from_alternatives(vec![]);
3151 };
3152 CapsSet::from_alternatives(vec![
3153 video(*format, width.clone(), height.clone(), framerate.clone()),
3154 video(
3155 RawVideoFormat::Nv12,
3156 width.clone(),
3157 height.clone(),
3158 framerate.clone(),
3159 ),
3160 ])
3161 }));
3162 let i420_big = fixed_video(RawVideoFormat::I420, 1920, 1080, 30);
3163
3164 let prev = fixed_video(RawVideoFormat::Nv12, 16, 16, 1);
3167 match resolve_forward_output(&conv, &i420_big, None, Some(&prev)) {
3168 ForwardResolve::Fixed(c) => assert_eq!(
3169 c,
3170 video(
3171 RawVideoFormat::Nv12,
3172 Dim::Fixed(1920),
3173 Dim::Fixed(1080),
3174 Rate::Fixed(30 << 16)
3175 )
3176 ),
3177 other => panic!("expected Fixed(NV12 at new dims), got {other:?}"),
3178 }
3179
3180 let prev_gone = fixed_video(RawVideoFormat::Bgra8, 16, 16, 1);
3182 assert_eq!(
3183 resolve_forward_output(&conv, &i420_big, None, Some(&prev_gone)),
3184 ForwardResolve::Defer
3185 );
3186
3187 let open_rate = CapsConstraint::DerivedOutput(Box::new(|_input: &Caps| {
3192 CapsSet::from_alternatives(vec![
3193 video(
3194 RawVideoFormat::Nv12,
3195 Dim::Fixed(1920),
3196 Dim::Fixed(1080),
3197 Rate::Any,
3198 ),
3199 video(
3200 RawVideoFormat::I420,
3201 Dim::Fixed(1920),
3202 Dim::Fixed(1080),
3203 Rate::Any,
3204 ),
3205 ])
3206 }));
3207 assert_eq!(
3208 resolve_forward_output(&open_rate, &i420_big, None, Some(&prev)),
3209 ForwardResolve::Defer
3210 );
3211
3212 let both = CapsSet::from_alternatives(vec![
3215 video(RawVideoFormat::I420, Dim::Any, Dim::Any, Rate::Any),
3216 video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any),
3217 ]);
3218 match resolve_forward_output(&conv, &i420_big, Some(&both), Some(&prev)) {
3219 ForwardResolve::Fixed(Caps::RawVideo { format, .. }) => {
3220 assert_eq!(format, RawVideoFormat::Nv12);
3221 }
3222 other => panic!("expected Fixed(NV12), got {other:?}"),
3223 }
3224 }
3225
3226 use crate::graph::Graph;
3227
3228 #[test]
3229 fn solve_graph_matches_solve_linear_on_a_chain() {
3230 let rgba = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
3232 let nv12 = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3233 let lin: Vec<CapsConstraint> = vec![
3234 CapsConstraint::Produces(CapsSet::one(rgba.clone())),
3235 CapsConstraint::DerivedOutput(Box::new({
3236 let nv12 = nv12.clone();
3237 move |_input: &Caps| CapsSet::one(nv12.clone())
3238 })),
3239 CapsConstraint::Accepts(CapsSet::one(nv12.clone())),
3240 ];
3241 let refs: Vec<&CapsConstraint> = lin.iter().collect();
3242 let linear = solve_linear(&refs).expect("linear chain solves");
3243
3244 let dag_cs: Vec<NodeConstraint> = vec![
3246 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(rgba.clone()))),
3247 NodeConstraint::Element(CapsConstraint::DerivedOutput(Box::new({
3248 let nv12 = nv12.clone();
3249 move |_input: &Caps| CapsSet::one(nv12.clone())
3250 }))),
3251 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12.clone()))),
3252 ];
3253 let mut g: Graph<()> = Graph::new();
3254 let src = g.add_source(());
3255 let tx = g.add_transform(());
3256 let sink = g.add_sink(());
3257 g.link(src, tx).unwrap();
3258 g.link(tx, sink).unwrap();
3259 let v = g.finish().unwrap();
3260 let dag = solve_graph(&v, &dag_cs).expect("same chain as a graph solves");
3261
3262 assert_eq!(
3263 dag, linear,
3264 "DAG solver matches the linear solver byte-for-byte"
3265 );
3266 assert_eq!(dag, vec![rgba, nv12]);
3267 }
3268
3269 #[test]
3270 fn solve_graph_empty_link_carries_both_sides_sets() {
3271 let rgba = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
3274 let nv12 = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3275 let cs: Vec<NodeConstraint> = vec![
3276 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(rgba.clone()))),
3277 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12.clone()))),
3278 ];
3279 let mut g: Graph<()> = Graph::new();
3280 let src = g.add_source(());
3281 let sink = g.add_sink(());
3282 g.link(src, sink).unwrap();
3283 let v = g.finish().unwrap();
3284
3285 let err = solve_graph(&v, &cs).expect_err("RGBA source vs NV12 sink cannot solve");
3286 assert!(matches!(
3287 err,
3288 NegotiationFailure::EmptyLink {
3289 upstream: 0,
3290 downstream: 1,
3291 ..
3292 }
3293 ));
3294 let c = err.conflict().expect("both candidate sets captured");
3295 assert_eq!(c.upstream, CapsSet::one(rgba));
3296 assert_eq!(c.downstream, CapsSet::one(nv12));
3297 }
3298
3299 #[test]
3300 fn solve_graph_tee_fanout_couples_branches() {
3301 let nv12_fixed = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3302 let nv12_any = video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any);
3303 let cs: Vec<NodeConstraint> = vec![
3305 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(nv12_fixed.clone()))),
3306 NodeConstraint::Element(CapsConstraint::IdentityAny), NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12_any.clone()))),
3308 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12_any))),
3309 ];
3310 let mut g: Graph<()> = Graph::new();
3311 let src = g.add_source(());
3312 let tee = g.add_tee(2);
3313 let a = g.add_sink(());
3314 let b = g.add_sink(());
3315 g.link(src, tee.input()).unwrap();
3316 g.link(tee.out(0), a).unwrap();
3317 g.link(tee.out(1), b).unwrap();
3318 let v = g.finish().unwrap();
3319
3320 let sol = solve_graph(&v, &cs).expect("tee fan-out solves");
3321 assert_eq!(sol.len(), 3, "three edges");
3322 assert!(
3323 sol.iter().all(|c| *c == nv12_fixed),
3324 "every branch carries the source caps"
3325 );
3326 }
3327
3328 #[test]
3329 fn solve_graph_rejects_incompatible_branch() {
3330 let nv12 = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3331 let nv12_any = video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any);
3332 let rgba_any = video(RawVideoFormat::Rgba8, Dim::Any, Dim::Any, Rate::Any);
3333 let cs: Vec<NodeConstraint> = vec![
3335 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(nv12))),
3336 NodeConstraint::Element(CapsConstraint::IdentityAny),
3337 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12_any))),
3338 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(rgba_any))),
3339 ];
3340 let mut g: Graph<()> = Graph::new();
3341 let src = g.add_source(());
3342 let tee = g.add_tee(2);
3343 let a = g.add_sink(());
3344 let b = g.add_sink(());
3345 g.link(src, tee.input()).unwrap();
3346 g.link(tee.out(0), a).unwrap();
3347 g.link(tee.out(1), b).unwrap();
3348 let v = g.finish().unwrap();
3349
3350 assert!(
3351 matches!(
3352 solve_graph(&v, &cs),
3353 Err(NegotiationFailure::EmptyLink { .. })
3354 ),
3355 "an incompatible branch fails the whole solve"
3356 );
3357 }
3358
3359 #[test]
3360 fn solve_graph_diamond_fixates_globally_consistent() {
3361 let v = fixed_compressed(VideoCodec::H264, 64, 48, 30);
3369 let w = fixed_compressed(VideoCodec::H265, 64, 48, 30);
3370 let a = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3371 let c = fixed_video(RawVideoFormat::I420, 64, 48, 30);
3372 let b = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
3373 let d = fixed_video(RawVideoFormat::I422, 64, 48, 30);
3374 let muxed = fixed_video(RawVideoFormat::I444, 64, 48, 30);
3375
3376 let cs: Vec<NodeConstraint> = vec![
3377 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::from_alternatives(vec![
3378 v.clone(),
3379 w.clone(),
3380 ]))),
3381 NodeConstraint::Element(CapsConstraint::IdentityAny), NodeConstraint::Element(CapsConstraint::Mapping(vec![
3383 (CapsSet::one(v.clone()), CapsSet::one(a.clone())),
3384 (CapsSet::one(w.clone()), CapsSet::one(c.clone())),
3385 ])),
3386 NodeConstraint::Element(CapsConstraint::Mapping(vec![
3387 (CapsSet::one(w.clone()), CapsSet::one(b.clone())),
3388 (CapsSet::one(v.clone()), CapsSet::one(d.clone())),
3389 ])),
3390 NodeConstraint::Muxer {
3391 inputs: vec![
3392 CapsConstraint::Accepts(CapsSet::from_alternatives(vec![a.clone(), c.clone()])),
3393 CapsConstraint::Accepts(CapsSet::from_alternatives(vec![b.clone(), d.clone()])),
3394 ],
3395 output: CapsConstraint::Produces(CapsSet::one(muxed)),
3396 follows: None,
3397 },
3398 NodeConstraint::Element(CapsConstraint::AcceptsAny),
3399 ];
3400 let mut g: Graph<()> = Graph::new();
3401 let src = g.add_source(());
3402 let tee = g.add_tee(2);
3403 let b1 = g.add_transform(());
3404 let b2 = g.add_transform(());
3405 let mux = g.add_muxer((), 2);
3406 let sink = g.add_sink(());
3407 g.link(src, tee.input()).unwrap();
3408 g.link(tee.out(0), b1).unwrap();
3409 g.link(tee.out(1), b2).unwrap();
3410 g.link(b1, mux.input(0)).unwrap();
3411 g.link(b2, mux.input(1)).unwrap();
3412 g.link(mux.output(), sink).unwrap();
3413 let vg = g.finish().unwrap();
3414
3415 let sol = solve_graph(&vg, &cs).expect("diamond has a satisfying assignment");
3416 assert_eq!(sol[1], sol[0], "tee broadcasts to branch 1");
3419 assert_eq!(sol[2], sol[0], "tee broadcasts to branch 2");
3420 let b1_pair = (sol[1].clone(), sol[3].clone());
3422 assert!(
3423 b1_pair == (v.clone(), a.clone()) || b1_pair == (w.clone(), c.clone()),
3424 "branch 1 fixated to a real mapping pair, got {b1_pair:?}"
3425 );
3426 let b2_pair = (sol[2].clone(), sol[4].clone());
3427 assert!(
3428 b2_pair == (w.clone(), b.clone()) || b2_pair == (v.clone(), d.clone()),
3429 "branch 2 fixated to a real mapping pair, got {b2_pair:?}"
3430 );
3431 }
3432
3433 #[test]
3434 fn solve_graph_muxer_fan_in_narrows_each_input() {
3435 let h264 = compressed(
3438 VideoCodec::H264,
3439 Dim::Fixed(64),
3440 Dim::Fixed(48),
3441 Rate::Fixed(30 << 16),
3442 );
3443 let h265 = compressed(
3444 VideoCodec::H265,
3445 Dim::Fixed(64),
3446 Dim::Fixed(48),
3447 Rate::Fixed(30 << 16),
3448 );
3449 let h264_any = compressed(VideoCodec::H264, Dim::Any, Dim::Any, Rate::Any);
3450 let h265_any = compressed(VideoCodec::H265, Dim::Any, Dim::Any, Rate::Any);
3451 let muxed = compressed(
3452 VideoCodec::H264,
3453 Dim::Fixed(64),
3454 Dim::Fixed(48),
3455 Rate::Fixed(30 << 16),
3456 );
3457
3458 let cs: Vec<NodeConstraint> = vec![
3459 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(h264.clone()))),
3460 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(h265.clone()))),
3461 NodeConstraint::Muxer {
3462 inputs: vec![
3463 CapsConstraint::Accepts(CapsSet::one(h264_any)),
3464 CapsConstraint::Accepts(CapsSet::one(h265_any)),
3465 ],
3466 output: CapsConstraint::Produces(CapsSet::one(muxed.clone())),
3467 follows: None,
3468 },
3469 NodeConstraint::Element(CapsConstraint::AcceptsAny),
3470 ];
3471 let mut g: Graph<()> = Graph::new();
3472 let s0 = g.add_source(());
3473 let s1 = g.add_source(());
3474 let mux = g.add_muxer((), 2);
3475 let sink = g.add_sink(());
3476 g.link(s0, mux.input(0)).unwrap();
3477 g.link(s1, mux.input(1)).unwrap();
3478 g.link(mux.output(), sink).unwrap();
3479 let v = g.finish().unwrap();
3480
3481 let sol = solve_graph(&v, &cs).expect("muxer fan-in solves");
3482 assert_eq!(
3484 sol,
3485 vec![h264, h265, muxed],
3486 "each input narrowed by its pad, output by produce"
3487 );
3488 }
3489
3490 #[test]
3491 fn solve_graph_muxer_follows_input_derives_output() {
3492 let rgba = fixed_video(RawVideoFormat::Rgba8, 320, 240, 30);
3496 let rgba_any = video(RawVideoFormat::Rgba8, Dim::Any, Dim::Any, Rate::Any);
3497 let text = Caps::Text {
3498 format: crate::caps::TextFormat::Utf8,
3499 };
3500
3501 let cs: Vec<NodeConstraint> = vec![
3502 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(rgba.clone()))),
3503 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(text.clone()))),
3504 NodeConstraint::Muxer {
3505 inputs: vec![
3506 CapsConstraint::Accepts(CapsSet::one(rgba_any)),
3507 CapsConstraint::Accepts(CapsSet::one(text.clone())),
3508 ],
3509 output: CapsConstraint::AcceptsAny,
3511 follows: Some(0),
3512 },
3513 NodeConstraint::Element(CapsConstraint::AcceptsAny),
3514 ];
3515 let mut g: Graph<()> = Graph::new();
3516 let video_src = g.add_source(());
3517 let text_src = g.add_source(());
3518 let mux = g.add_muxer((), 2);
3519 let sink = g.add_sink(());
3520 g.link(video_src, mux.input(0)).unwrap();
3521 g.link(text_src, mux.input(1)).unwrap();
3522 g.link(mux.output(), sink).unwrap();
3523 let v = g.finish().unwrap();
3524
3525 let sol = solve_graph(&v, &cs).expect("follows-input muxer solves");
3526 assert_eq!(
3528 sol[2], rgba,
3529 "output edge follows the video pad's negotiated caps"
3530 );
3531 assert_eq!(sol[0], rgba, "video pad edge unchanged");
3532 assert_eq!(sol[1], text, "text pad edge unchanged");
3533 }
3534
3535 #[test]
3536 fn solve_graph_muxer_wildcard_inputs_forward_source_caps() {
3537 let h264 = compressed(
3542 VideoCodec::H264,
3543 Dim::Fixed(64),
3544 Dim::Fixed(48),
3545 Rate::Fixed(30 << 16),
3546 );
3547 let aac = Caps::Audio {
3548 format: crate::caps::AudioFormat::Aac,
3549 channels: 2,
3550 sample_rate: 48_000,
3551 };
3552 let merged = compressed(
3553 VideoCodec::H264,
3554 Dim::Fixed(64),
3555 Dim::Fixed(48),
3556 Rate::Fixed(30 << 16),
3557 );
3558
3559 let cs: Vec<NodeConstraint> = vec![
3560 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(h264.clone()))),
3561 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(aac.clone()))),
3562 NodeConstraint::Muxer {
3563 inputs: vec![CapsConstraint::AcceptsAny, CapsConstraint::AcceptsAny],
3564 output: CapsConstraint::Produces(CapsSet::one(merged.clone())),
3565 follows: None,
3566 },
3567 NodeConstraint::Element(CapsConstraint::AcceptsAny),
3568 ];
3569 let mut g: Graph<()> = Graph::new();
3570 let s0 = g.add_source(());
3571 let s1 = g.add_source(());
3572 let mux = g.add_muxer((), 2);
3573 let sink = g.add_sink(());
3574 g.link(s0, mux.input(0)).unwrap();
3575 g.link(s1, mux.input(1)).unwrap();
3576 g.link(mux.output(), sink).unwrap();
3577 let v = g.finish().unwrap();
3578
3579 let sol = solve_graph(&v, &cs).expect("wildcard muxer solves");
3580 assert_eq!(sol, vec![h264, aac, merged]);
3582 }
3583
3584 #[test]
3585 fn solve_graph_accepts_legacy_bridge_constraints() {
3586 let h264 = fixed_compressed(VideoCodec::H264, 64, 48, 30);
3591 let cs: Vec<NodeConstraint> = vec![
3592 NodeConstraint::Element(CapsConstraint::LegacySource(h264.clone())),
3593 NodeConstraint::Element(CapsConstraint::LegacySource(h264.clone())),
3594 NodeConstraint::Muxer {
3595 inputs: vec![CapsConstraint::AcceptsAny, CapsConstraint::AcceptsAny],
3596 output: CapsConstraint::Produces(CapsSet::one(h264.clone())),
3597 follows: None,
3598 },
3599 NodeConstraint::Element(CapsConstraint::LegacySink(Box::new(|c: &Caps| {
3600 Ok(c.clone())
3601 }))),
3602 ];
3603 let mut g: Graph<()> = Graph::new();
3604 let s0 = g.add_source(());
3605 let s1 = g.add_source(());
3606 let mux = g.add_muxer((), 2);
3607 let sink = g.add_sink(());
3608 g.link(s0, mux.input(0)).unwrap();
3609 g.link(s1, mux.input(1)).unwrap();
3610 g.link(mux.output(), sink).unwrap();
3611 let v = g.finish().unwrap();
3612
3613 let sol = solve_graph(&v, &cs).expect("native muxer + legacy sink solves");
3614 assert_eq!(sol, vec![h264.clone(), h264.clone(), h264]);
3615 }
3616
3617 #[test]
3618 fn solve_graph_forwards_legacy_transform() {
3619 let rgba = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
3621 let nv12 = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3622 let cs: Vec<NodeConstraint> = vec![
3623 NodeConstraint::Element(CapsConstraint::LegacySource(rgba.clone())),
3624 NodeConstraint::Element(CapsConstraint::LegacyTransform {
3625 intercept: Box::new({
3626 let nv12 = nv12.clone();
3627 move |_in: &Caps| Ok(nv12.clone())
3628 }),
3629 propose_output: Box::new(|c: &Caps| c.clone()),
3630 }),
3631 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12.clone()))),
3632 ];
3633 let mut g: Graph<()> = Graph::new();
3634 let src = g.add_source(());
3635 let tx = g.add_transform(());
3636 let sink = g.add_sink(());
3637 g.link(src, tx).unwrap();
3638 g.link(tx, sink).unwrap();
3639 let v = g.finish().unwrap();
3640
3641 let sol = solve_graph(&v, &cs).expect("legacy transform forwards");
3642 assert_eq!(sol, vec![rgba, nv12]);
3643 }
3644
3645 #[cfg(feature = "std")]
3646 #[test]
3647 fn graph_feasibility_intersects_tee_branches() {
3648 let nv12_any = video(RawVideoFormat::Nv12, Dim::Any, Dim::Any, Rate::Any);
3651 let nv12_fixed = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3652 let cs: Vec<NodeConstraint> = vec![
3653 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(nv12_fixed.clone()))),
3654 NodeConstraint::Element(CapsConstraint::IdentityAny),
3655 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12_any))),
3656 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(nv12_fixed.clone()))),
3657 ];
3658 let mut g: Graph<()> = Graph::new();
3659 let src = g.add_source(());
3660 let tee = g.add_tee(2);
3661 let a = g.add_sink(());
3662 let b = g.add_sink(());
3663 g.link(src, tee.input()).unwrap();
3664 g.link(tee.out(0), a).unwrap();
3665 g.link(tee.out(1), b).unwrap();
3666 let v = g.finish().unwrap();
3667
3668 let sol = solve_graph(&v, &cs).expect("tee branches solve");
3669 let feas = graph_downstream_feasibility(&v, &cs, &sol);
3670 let tee_in = feas[0].as_ref().expect("tee input has feasibility");
3672 assert!(tee_in
3673 .intersect(&CapsSet::one(nv12_fixed.clone()))
3674 .fixate()
3675 .is_some());
3676 let off = fixed_video(RawVideoFormat::Nv12, 99, 99, 30);
3678 assert!(
3679 tee_in.intersect(&CapsSet::one(off)).is_empty(),
3680 "branch B pins 64x48"
3681 );
3682 }
3683
3684 #[cfg(feature = "std")]
3685 #[test]
3686 fn graph_feasibility_muxer_inputs_are_per_pad() {
3687 let h264_any = compressed(VideoCodec::H264, Dim::Any, Dim::Any, Rate::Any);
3691 let h265_any = compressed(VideoCodec::H265, Dim::Any, Dim::Any, Rate::Any);
3692 let cs: Vec<NodeConstraint> = vec![
3693 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(fixed_compressed(
3694 VideoCodec::H264,
3695 64,
3696 48,
3697 30,
3698 )))),
3699 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(fixed_compressed(
3700 VideoCodec::H265,
3701 64,
3702 48,
3703 30,
3704 )))),
3705 NodeConstraint::Muxer {
3706 inputs: vec![
3707 CapsConstraint::Accepts(CapsSet::one(h264_any.clone())),
3708 CapsConstraint::Accepts(CapsSet::one(h265_any.clone())),
3709 ],
3710 output: CapsConstraint::Produces(CapsSet::one(fixed_compressed(
3711 VideoCodec::H264,
3712 64,
3713 48,
3714 30,
3715 ))),
3716 follows: None,
3717 },
3718 NodeConstraint::Element(CapsConstraint::AcceptsAny),
3719 ];
3720 let mut g: Graph<()> = Graph::new();
3721 let s0 = g.add_source(());
3722 let s1 = g.add_source(());
3723 let mux = g.add_muxer((), 2);
3724 let sink = g.add_sink(());
3725 g.link(s0, mux.input(0)).unwrap();
3726 g.link(s1, mux.input(1)).unwrap();
3727 g.link(mux.output(), sink).unwrap();
3728 let v = g.finish().unwrap();
3729
3730 let sol = solve_graph(&v, &cs).expect("muxer graph solves");
3731 let feas = graph_downstream_feasibility(&v, &cs, &sol);
3732 assert_eq!(
3734 feas[0],
3735 Some(CapsSet::one(h264_any)),
3736 "pad 0 feasibility = its accept set"
3737 );
3738 assert_eq!(
3739 feas[1],
3740 Some(CapsSet::one(h265_any)),
3741 "pad 1 feasibility = its accept set"
3742 );
3743 assert_eq!(
3744 feas[2], None,
3745 "wildcard sink leaves the muxer output unconstrained"
3746 );
3747 }
3748
3749 #[cfg(feature = "std")]
3750 #[test]
3751 fn graph_feasibility_couples_pin_back_through_a_decoder() {
3752 let dec_closure = |input: &Caps| match input {
3759 Caps::CompressedVideo {
3760 width,
3761 height,
3762 framerate,
3763 ..
3764 } => CapsSet::one(Caps::RawVideo {
3765 format: RawVideoFormat::Nv12,
3766 width: width.clone(),
3767 height: height.clone(),
3768 framerate: framerate.clone(),
3769 interlace: crate::Interlace::Any,
3770 }),
3771 _ => CapsSet::from_alternatives(Vec::new()),
3772 };
3773 let cs: Vec<NodeConstraint> = vec![
3774 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(compressed(
3775 VideoCodec::H264,
3776 Dim::Any,
3777 Dim::Any,
3778 Rate::Fixed(30 << 16),
3779 )))),
3780 NodeConstraint::Element(CapsConstraint::DerivedOutput(Box::new(dec_closure))),
3781 NodeConstraint::Element(CapsConstraint::Accepts(CapsSet::one(fixed_video(
3782 RawVideoFormat::Nv12,
3783 1280,
3784 720,
3785 30,
3786 )))),
3787 ];
3788 let mut g: Graph<()> = Graph::new();
3789 let src = g.add_source(());
3790 let dec = g.add_transform(());
3791 let sink = g.add_sink(());
3792 g.link(src, dec).unwrap();
3793 g.link(dec, sink).unwrap();
3794 let v = g.finish().unwrap();
3795
3796 let sol = solve_graph(&v, &cs).expect("decoder graph solves");
3797 let feas = graph_downstream_feasibility(&v, &cs, &sol);
3798 let dec_in = feas[0]
3800 .as_ref()
3801 .expect("decoder input edge is now constrained");
3802 assert!(
3803 dec_in
3804 .intersect(&CapsSet::one(fixed_compressed(
3805 VideoCodec::H264,
3806 1280,
3807 720,
3808 30
3809 )))
3810 .fixate()
3811 .is_some(),
3812 "pinned 1280x720 couples back onto the H264 input edge",
3813 );
3814 let off = fixed_compressed(VideoCodec::H264, 640, 480, 30);
3815 assert!(
3816 dec_in.intersect(&CapsSet::one(off)).is_empty(),
3817 "off-geometry input is rejected by the snapshot"
3818 );
3819 }
3820
3821 fn scale_like<'a>() -> CapsConstraint<'a> {
3826 let range = Dim::Range { min: 1, max: 32768 };
3827 CapsConstraint::DerivedFields(CapsTransform::RawVideo {
3828 accept: Vec::new(),
3829 produce: Vec::new(),
3830 shapes: vec![
3831 RawVideoShape::PASSTHROUGH,
3832 RawVideoShape::PASSTHROUGH
3833 .with_width(FieldTransform::Fixed(range.clone()))
3834 .with_height(FieldTransform::Fixed(range)),
3835 ],
3836 })
3837 }
3838
3839 fn convert_like<'a>() -> CapsConstraint<'a> {
3842 CapsConstraint::DerivedFields(CapsTransform::RawVideo {
3843 accept: Vec::new(),
3844 produce: Vec::new(),
3845 shapes: vec![
3846 RawVideoShape::PASSTHROUGH
3847 .with_format(FieldTransform::Fixed(RawVideoFormat::Rgba8)),
3848 RawVideoShape::PASSTHROUGH.with_format(FieldTransform::Fixed(RawVideoFormat::Nv12)),
3849 ],
3850 })
3851 }
3852
3853 #[test]
3854 fn couple_passthrough_narrows_a_range_field_within_an_alternative() {
3855 let mask = PassthroughFields::NONE
3858 .with_width()
3859 .with_height()
3860 .with_framerate();
3861 let input = video(
3862 RawVideoFormat::Rgba8,
3863 Dim::Range { min: 1, max: 32768 },
3864 Dim::Range { min: 1, max: 32768 },
3865 Rate::Fixed(30 << 16),
3866 );
3867 let pin = fixed_video(RawVideoFormat::Nv12, 160, 120, 30);
3868 let coupled = couple_passthrough(&input, &pin, mask).unwrap();
3869 assert_eq!(
3870 coupled,
3871 fixed_video(RawVideoFormat::Rgba8, 160, 120, 30),
3872 "passthrough width/height/framerate pinned, retargeted format kept"
3873 );
3874 }
3875
3876 #[test]
3877 fn couple_passthrough_rejects_conflicting_passthrough_field() {
3878 let mask = PassthroughFields::NONE.with_format();
3880 let input = fixed_video(RawVideoFormat::Rgba8, 160, 120, 30);
3881 let pin = fixed_video(RawVideoFormat::Nv12, 160, 120, 30);
3882 assert_eq!(couple_passthrough(&input, &pin, mask), None);
3883 }
3884
3885 #[test]
3886 fn field_coupling_resolves_scale_then_convert() {
3887 let src = CapsConstraint::Produces(CapsSet::one(fixed_video(
3891 RawVideoFormat::Rgba8,
3892 320,
3893 240,
3894 30,
3895 )));
3896 let scale = scale_like();
3897 let convert = convert_like();
3898 let sink = CapsConstraint::Accepts(CapsSet::one(fixed_video(
3899 RawVideoFormat::Nv12,
3900 160,
3901 120,
3902 30,
3903 )));
3904 let links = solve_linear(&[&src, &scale, &convert, &sink]).unwrap();
3905 assert_eq!(
3906 links,
3907 vec![
3908 fixed_video(RawVideoFormat::Rgba8, 320, 240, 30),
3909 fixed_video(RawVideoFormat::Rgba8, 160, 120, 30),
3910 fixed_video(RawVideoFormat::Nv12, 160, 120, 30),
3911 ],
3912 "scaler reads 320x240, emits 160x120; convert changes only the format"
3913 );
3914 }
3915
3916 #[test]
3917 fn field_coupling_no_pin_stays_passthrough() {
3918 let src = CapsConstraint::Produces(CapsSet::one(fixed_video(
3921 RawVideoFormat::Rgba8,
3922 320,
3923 240,
3924 30,
3925 )));
3926 let scale = scale_like();
3927 let convert = convert_like();
3928 let sink = CapsConstraint::AcceptsAny;
3929 let links = solve_linear(&[&src, &scale, &convert, &sink]).unwrap();
3930 assert_eq!(
3931 links,
3932 vec![
3933 fixed_video(RawVideoFormat::Rgba8, 320, 240, 30),
3934 fixed_video(RawVideoFormat::Rgba8, 320, 240, 30),
3935 fixed_video(RawVideoFormat::Rgba8, 320, 240, 30),
3936 ],
3937 "passthrough is preferred and stable"
3938 );
3939 }
3940
3941 #[test]
3942 fn field_coupling_unsatisfiable_geometry_fails_loud() {
3943 let src = CapsConstraint::Produces(CapsSet::one(fixed_video(
3946 RawVideoFormat::Rgba8,
3947 320,
3948 240,
3949 30,
3950 )));
3951 let convert = convert_like();
3952 let sink = CapsConstraint::Accepts(CapsSet::one(fixed_video(
3953 RawVideoFormat::Nv12,
3954 160,
3955 120,
3956 30,
3957 )));
3958 assert!(
3959 solve_linear(&[&src, &convert, &sink]).is_err(),
3960 "geometry pin must fail loud"
3961 );
3962 }
3963
3964 #[test]
3967 fn explainer_formats_sets_constraints_and_labels() {
3968 let rgba = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
3969 let nv12 = fixed_video(RawVideoFormat::Nv12, 64, 48, 30);
3970
3971 let set = CapsSet::from_alternatives(vec![rgba.clone(), nv12.clone()]);
3973 let rendered = fmt_set(&set);
3974 assert!(rendered.contains("format=RGBA") && rendered.contains("format=NV12"));
3975 assert!(rendered.contains(" | "));
3976 assert_eq!(fmt_set(&CapsSet::from_alternatives(vec![])), "∅");
3977
3978 let wide = CapsSet::from_alternatives(
3980 (1..=6)
3981 .map(|w| fixed_video(RawVideoFormat::Rgba8, w * 16, 48, 30))
3982 .collect(),
3983 );
3984 assert!(fmt_set(&wide).contains("(+2 more)"), "{}", fmt_set(&wide));
3985
3986 assert!(
3988 fmt_caps_constraint(&CapsConstraint::Produces(CapsSet::one(rgba.clone())))
3989 .starts_with("produces ")
3990 );
3991 assert_eq!(
3992 fmt_caps_constraint(&CapsConstraint::AcceptsAny),
3993 "accepts ANY"
3994 );
3995 assert_eq!(
3996 fmt_caps_constraint(&CapsConstraint::DerivedOutput(Box::new(move |_: &Caps| {
3997 CapsSet::one(nv12.clone())
3998 }))),
3999 "derives output"
4000 );
4001 }
4002
4003 #[test]
4004 fn solve_graph_labeled_matches_default_and_uses_labels() {
4005 let rgba = fixed_video(RawVideoFormat::Rgba8, 64, 48, 30);
4008 let cs: Vec<NodeConstraint> = vec![
4009 NodeConstraint::Element(CapsConstraint::Produces(CapsSet::one(rgba.clone()))),
4010 NodeConstraint::Element(CapsConstraint::AcceptsAny),
4011 ];
4012 let mut g: Graph<()> = Graph::new();
4013 let src = g.add_source(());
4014 let sink = g.add_sink(());
4015 g.link(src, sink).unwrap();
4016 let v = g.finish().unwrap();
4017
4018 let default = solve_graph(&v, &cs).expect("solves");
4019 let labeled = solve_graph_labeled(&v, &cs, &|n| alloc::format!("node{}", n.0))
4020 .expect("solves with custom labels");
4021 assert_eq!(default, labeled);
4022 assert_eq!(labeled, vec![rgba]);
4023 }
4024}