1use crate::blocks::LiveStream;
92use crate::declare::{AlignFrame, Dense, Features, Plan};
93use crate::densewrap::{make_ba_backend, make_match_backend, make_sift_backend, make_sparse_backend, parse_profile};
94use crate::events::{Command, Event, EventKind, Frame, ZoneRange};
95use crate::intrinsics::{IntrinsicsPrior, DEFAULT_DIST_SIGMA, DEFAULT_FOCAL_SIGMA};
96use crate::live::DisplayFeed;
97use crate::pipeline::{Pipeline, QueuePolicy, Summary};
98use crate::post::LiveOptions;
99use crate::session::{FrameSet, Input, Session, SessionConfig};
100use crate::sinks::{self, DefaultSinks, SinkOptions, SinkSet};
101use cumulus3d_core::io::ply::PointCloud;
102use cumulus3d_core::io::GpsRecord;
103use std::collections::{BTreeMap, HashMap, VecDeque};
104use std::fmt;
105use std::path::{Path, PathBuf};
106use std::sync::mpsc::Receiver;
107use std::sync::{Arc, Condvar, Mutex, MutexGuard};
108use std::thread::JoinHandle;
109use std::time::SystemTime;
110
111#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
117pub struct FrameKey {
118 pub source_id: String,
120 pub sequence: u64,
122 pub revision: u32,
124}
125
126impl FrameKey {
127 pub fn new(source_id: impl Into<String>, sequence: u64, revision: u32) -> Self {
129 Self { source_id: source_id.into(), sequence, revision }
130 }
131}
132
133impl fmt::Display for FrameKey {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{}#{}r{}", self.source_id, self.sequence, self.revision)
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum EncodedFormat {
142 Jpeg,
144 Png,
146}
147
148impl EncodedFormat {
149 fn extension(self) -> &'static str {
150 match self {
151 EncodedFormat::Jpeg => "jpg",
152 EncodedFormat::Png => "png",
153 }
154 }
155}
156
157#[derive(Clone)]
159pub enum FramePayload {
160 File {
162 relative_name: String,
164 },
165 Rgb8 {
167 width: u32,
169 height: u32,
171 pixels: Arc<[u8]>,
173 },
174 Encoded {
176 bytes: Arc<[u8]>,
178 format: EncodedFormat,
180 },
181}
182
183impl fmt::Debug for FramePayload {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 match self {
186 FramePayload::File { relative_name } => f.debug_struct("File").field("relative_name", relative_name).finish(),
187 FramePayload::Rgb8 { width, height, .. } => f.debug_struct("Rgb8").field("width", width).field("height", height).finish(),
188 FramePayload::Encoded { bytes, format } => {
189 f.debug_struct("Encoded").field("bytes", &bytes.len()).field("format", format).finish()
190 }
191 }
192 }
193}
194
195#[derive(Clone, Debug)]
197pub struct LiveImage {
198 pub camera: String,
200 pub payload: FramePayload,
202}
203
204#[derive(Clone, Debug)]
206pub struct LiveFrameSet {
207 pub key: FrameKey,
209 pub timestamp: SystemTime,
211 pub images: Vec<LiveImage>,
213 pub gps: Vec<GpsRecord>,
216}
217
218impl LiveFrameSet {
219 pub fn new(key: FrameKey) -> Self {
221 Self { key, timestamp: SystemTime::now(), images: Vec::new(), gps: Vec::new() }
222 }
223 pub fn image(mut self, camera: impl Into<String>, payload: FramePayload) -> Self {
225 self.images.push(LiveImage { camera: camera.into(), payload });
226 self
227 }
228 pub fn file(self, camera: impl Into<String>, relative_name: impl Into<String>) -> Self {
230 self.image(camera, FramePayload::File { relative_name: relative_name.into() })
231 }
232 pub fn rgb8(self, camera: impl Into<String>, width: u32, height: u32, pixels: impl Into<Arc<[u8]>>) -> Self {
234 self.image(camera, FramePayload::Rgb8 { width, height, pixels: pixels.into() })
235 }
236 pub fn encoded(self, camera: impl Into<String>, bytes: impl Into<Arc<[u8]>>, format: EncodedFormat) -> Self {
238 self.image(camera, FramePayload::Encoded { bytes: bytes.into(), format })
239 }
240 pub fn gps_fix(mut self, camera: impl Into<String>, lat: f64, lon: f64, alt: f64) -> Self {
242 self.gps.push(GpsRecord { name: camera.into(), lat, lon, alt });
243 self
244 }
245 pub fn at(mut self, t: SystemTime) -> Self {
247 self.timestamp = t;
248 self
249 }
250}
251
252#[derive(Clone, Debug)]
254pub enum FrameState {
255 Ready(LiveFrameSet),
257 Pending {
259 key: FrameKey,
261 timestamp: SystemTime,
263 missing_cameras: Vec<String>,
265 },
266 End,
268}
269
270impl FrameState {
271 pub fn ready(set: LiveFrameSet) -> Self {
273 FrameState::Ready(set)
274 }
275 pub fn pending<S: Into<String>>(key: FrameKey, missing_cameras: impl IntoIterator<Item = S>) -> Self {
277 FrameState::Pending { key, timestamp: SystemTime::now(), missing_cameras: missing_cameras.into_iter().map(Into::into).collect() }
278 }
279}
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
287pub enum ReconcilePolicy {
288 #[default]
290 AppendOnly,
291 ReplayWindow {
295 positions: usize,
297 },
298 InvalidateAffectedZones,
303 Strict,
305}
306
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub enum Backpressure {
310 Unbounded,
312 Block {
314 max_pending: usize,
316 },
317 KeepLatest {
320 max_pending: usize,
322 },
323}
324
325impl Default for Backpressure {
326 fn default() -> Self {
327 Backpressure::Block { max_pending: 8 }
328 }
329}
330
331#[derive(Clone, Debug, PartialEq, Eq)]
333pub enum IgnoreReason {
334 StaleRevision {
336 accepted: u32,
338 },
339 CorrectionNotApplied,
341 Late,
343 OutsideReplayWindow {
345 position: usize,
347 },
348 ForeignSource {
350 expected: String,
352 },
353 Closed,
355}
356
357impl fmt::Display for IgnoreReason {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 match self {
360 IgnoreReason::StaleRevision { accepted } => write!(f, "stale revision (accepted revision {accepted})"),
361 IgnoreReason::CorrectionNotApplied => write!(f, "correction not applied by this policy"),
362 IgnoreReason::Late => write!(f, "late arrival not inserted by this policy"),
363 IgnoreReason::OutsideReplayWindow { position } => write!(f, "outside replay window (position {position})"),
364 IgnoreReason::ForeignSource { expected } => write!(f, "foreign source (node source is {expected})"),
365 IgnoreReason::Closed => write!(f, "node closed"),
366 }
367 }
368}
369
370#[derive(Clone, Debug, PartialEq, Eq)]
372pub enum Outcome {
373 Appended {
375 position: usize,
377 skipped: u64,
379 },
380 Duplicate {
382 position: usize,
384 },
385 Waiting {
387 missing: Vec<String>,
389 },
390 Replayed {
392 from: usize,
394 positions: usize,
396 },
397 Invalidated {
399 position: usize,
401 zones: Vec<usize>,
403 },
404 Ignored(IgnoreReason),
406 Ended,
408}
409
410#[derive(Clone, Debug, PartialEq, Eq)]
412pub enum ComposeError {
413 Config(Vec<String>),
415 InvalidFrameSet(String),
417 Rejected(String),
419 Io(String),
421 Session(String),
423 UnknownNode(String),
425 DuplicateNode(String),
427 Closed,
429}
430
431impl fmt::Display for ComposeError {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 match self {
434 ComposeError::Config(v) => write!(f, "invalid node configuration: {}", v.join("; ")),
435 ComposeError::InvalidFrameSet(m) => write!(f, "invalid frame set: {m}"),
436 ComposeError::Rejected(m) => write!(f, "rejected: {m}"),
437 ComposeError::Io(m) => write!(f, "i/o: {m}"),
438 ComposeError::Session(m) => write!(f, "session failed: {m}"),
439 ComposeError::UnknownNode(id) => write!(f, "no node {id:?} on this host"),
440 ComposeError::DuplicateNode(id) => write!(f, "node {id:?} already exists on this host"),
441 ComposeError::Closed => write!(f, "node already closed"),
442 }
443 }
444}
445
446impl std::error::Error for ComposeError {}
447
448#[derive(Clone, Debug, Default, PartialEq, Eq)]
450pub struct ComposeStats {
451 pub declared: u64,
453 pub appended: u64,
455 pub duplicates: u64,
457 pub waiting: u64,
459 pub skipped_sequences: u64,
461 pub replays: u64,
463 pub replayed_positions: u64,
465 pub invalidations: u64,
467 pub ignored: u64,
469}
470
471pub struct Closed {
473 pub session: Session,
475 pub summary: Summary,
477 pub stats: ComposeStats,
479}
480
481impl fmt::Debug for Closed {
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 f.debug_struct("Closed")
484 .field("positions", &self.session.positions())
485 .field("summary", &self.summary)
486 .field("stats", &self.stats)
487 .finish()
488 }
489}
490
491#[derive(Clone, Default)]
497pub struct CompositionHost {
498 nodes: Arc<Mutex<BTreeMap<String, ReconNode>>>,
499}
500
501impl fmt::Debug for CompositionHost {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 f.debug_struct("CompositionHost").field("nodes", &self.ids()).finish()
504 }
505}
506
507impl CompositionHost {
508 pub fn new() -> Self {
510 Self::default()
511 }
512 pub fn node(&self, id: &str) -> Option<ReconNode> {
514 lock(&self.nodes).get(id).cloned()
515 }
516 pub fn ids(&self) -> Vec<String> {
518 lock(&self.nodes).keys().cloned().collect()
519 }
520 pub fn compose(&self, id: &str, state: FrameState) -> Result<Outcome, ComposeError> {
522 self.node(id).ok_or_else(|| ComposeError::UnknownNode(id.to_string()))?.compose(state)
523 }
524 pub fn close(&self, id: &str) -> Result<Closed, ComposeError> {
526 let node = lock(&self.nodes).remove(id).ok_or_else(|| ComposeError::UnknownNode(id.to_string()))?;
527 node.close_inner()
528 }
529 pub fn close_all(&self) -> Vec<(String, Result<Closed, ComposeError>)> {
531 let nodes = std::mem::take(&mut *lock(&self.nodes));
532 nodes.into_iter().map(|(id, n)| (id, n.close_inner())).collect()
533 }
534 fn register(&self, node: &ReconNode) -> Result<(), ComposeError> {
535 let mut m = lock(&self.nodes);
536 if m.contains_key(&node.id) {
537 return Err(ComposeError::DuplicateNode(node.id.clone()));
538 }
539 m.insert(node.id.clone(), node.clone());
540 Ok(())
541 }
542}
543
544pub fn reconstruct(host: &CompositionHost, id: &str, state: FrameState) -> Result<Outcome, ComposeError> {
547 host.compose(id, state)
548}
549
550type Attach = Box<dyn FnOnce(Pipeline<Session>) -> Pipeline<Session> + Send>;
555
556pub struct NodeBuilder {
561 id: String,
562 plan: Plan,
563 policy: ReconcilePolicy,
564 backpressure: Backpressure,
565 source: Option<String>,
566 spool: PathBuf,
567 output: Option<(PathBuf, SinkOptions)>,
568 attach: Vec<Attach>,
569 frames: bool,
570 deltas: Option<LiveOptions>,
571 stream_origin: Option<[f64; 3]>,
573 stream_only: bool,
575 prior: Option<IntrinsicsPrior>,
577 prior_error: Option<String>,
579}
580
581impl fmt::Debug for NodeBuilder {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 f.debug_struct("NodeBuilder").field("id", &self.id).field("policy", &self.policy).field("hooks", &self.attach.len()).finish()
584 }
585}
586
587impl ReconNode {
588 pub fn declare() -> NodeBuilder {
590 let mut plan = Plan::default();
591 plan.dense.enabled = false;
592 plan.gps = None;
593 NodeBuilder {
594 id: "default".to_string(),
595 plan,
596 policy: ReconcilePolicy::default(),
597 backpressure: Backpressure::default(),
598 source: None,
599 spool: PathBuf::from(".live"),
600 output: None,
601 attach: Vec::new(),
602 frames: false,
603 deltas: None,
604 stream_origin: None,
605 stream_only: false,
606 prior: None,
607 prior_error: None,
608 }
609 }
610}
611
612impl NodeBuilder {
613 pub fn id(mut self, id: impl Into<String>) -> Self {
615 self.id = id.into();
616 self
617 }
618 pub fn plan(mut self, plan: Plan) -> Self {
621 self.plan = plan;
622 self
623 }
624 pub fn images(mut self, dir: impl Into<PathBuf>) -> Self {
626 self.plan.input.images = dir.into();
627 self
628 }
629 pub fn cameras<S: Into<String>>(mut self, cams: impl IntoIterator<Item = S>) -> Self {
631 self.plan.input.cameras = cams.into_iter().map(Into::into).collect();
632 self
633 }
634 pub fn zones(mut self, span: usize, overlap: usize) -> Self {
636 self.plan.zones.span = span;
637 self.plan.zones.overlap = overlap;
638 self
639 }
640 pub fn align(mut self, frame: AlignFrame) -> Self {
642 self.plan.align.frame = frame;
643 self
644 }
645 pub fn fixed_enu_origin(mut self, on: bool) -> Self {
647 self.plan.align.fixed_origin = on;
648 self
649 }
650 pub fn features(mut self, f: Features) -> Self {
652 self.plan.features = f;
653 self
654 }
655 pub fn match_backend(mut self, name: &str) -> Self {
657 self.plan.matching.backend = name.to_string();
658 self
659 }
660 pub fn gpu(mut self) -> Self {
662 self.plan.features.backend = "cuda".into();
663 self.plan.matching.backend = "cuda".into();
664 self.plan.sparse.backend = "cuda".into();
665 self.plan.dense.backend = "cuda".into();
666 self.plan.sparse.ba_backend = "cuda".into();
667 self.plan.dense.undistort_backend = "cuda".into();
668 self
669 }
670 pub fn stable_display(mut self, on: bool) -> Self {
675 self.plan.sparse.stable_display = on;
676 self
677 }
678 pub fn display_deltas(mut self, opts: LiveOptions) -> Self {
685 self.deltas = Some(opts);
686 self
687 }
688 pub fn incremental_triangulation(mut self, on: bool) -> Self {
690 self.plan.sparse.incremental_triangulation = on;
691 self
692 }
693 pub fn settle(mut self, tol: f64, max_zone: usize) -> Self {
696 self.plan.sparse.settle_tol = tol;
697 self.plan.sparse.settle_max_zone = max_zone;
698 self
699 }
700 pub fn live_stride(mut self, stride: usize) -> Self {
702 self.plan.sinks.live_stride = stride;
703 self
704 }
705 pub fn stream_origin(mut self, lat: f64, lon: f64, alt: f64) -> Self {
708 self.stream_origin = Some([lat, lon, alt]);
709 self
710 }
711 pub fn stream_only(mut self, on: bool) -> Self {
714 self.stream_only = on;
715 self
716 }
717 pub fn dense(mut self, d: Dense) -> Self {
719 self.plan.dense = Dense { enabled: true, ..d };
720 self
721 }
722 pub fn no_dense(mut self) -> Self {
724 self.plan.dense.enabled = false;
725 self
726 }
727 pub fn seed(mut self, seed: u64) -> Self {
729 self.plan.seed = Some(seed);
730 self
731 }
732 pub fn reconcile(mut self, p: ReconcilePolicy) -> Self {
734 self.policy = p;
735 self
736 }
737 pub fn backpressure(mut self, b: Backpressure) -> Self {
739 self.backpressure = b;
740 self
741 }
742 pub fn source(mut self, id: impl Into<String>) -> Self {
744 self.source = Some(id.into());
745 self
746 }
747 pub fn spool(mut self, rel: impl Into<PathBuf>) -> Self {
749 self.spool = rel.into();
750 self
751 }
752 pub fn output(mut self, dir: impl Into<PathBuf>, opts: SinkOptions) -> Self {
755 self.output = Some((dir.into(), opts));
756 self
757 }
758 fn hook(mut self, f: impl FnOnce(Pipeline<Session>) -> Pipeline<Session> + Send + 'static) -> Self {
759 self.attach.push(Box::new(f));
760 self
761 }
762 pub fn on<F>(mut self, kind: EventKind, f: F) -> Self
764 where
765 F: FnMut(&Event) + Send + 'static,
766 {
767 self.frames |= kind == EventKind::FrameDecoded;
768 self.hook(move |p| p.on(kind, f))
769 }
770 pub fn on_event<F>(self, f: F) -> Self
772 where
773 F: FnMut(&Event) + Send + 'static,
774 {
775 self.hook(move |p| p.on_any(f))
776 }
777 pub fn on_frame<F>(mut self, f: F) -> Self
779 where
780 F: FnMut(&Arc<Frame>) + Send + 'static,
781 {
782 self.frames = true;
783 self.hook(move |p| p.on_frame(f))
784 }
785 pub fn on_zone_preview<F>(self, f: F) -> Self
787 where
788 F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
789 {
790 self.hook(move |p| p.on_zone_preview(f))
791 }
792 pub fn on_zone_refined<F>(self, f: F) -> Self
794 where
795 F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
796 {
797 self.hook(move |p| p.on_zone_refined(f))
798 }
799 pub fn on_message<F>(self, f: F) -> Self
801 where
802 F: FnMut(EventKind, &str) + Send + 'static,
803 {
804 self.hook(move |p| p.on_message(f))
805 }
806 pub fn event_policy(self, kind: EventKind, p: QueuePolicy) -> Self {
808 self.hook(move |pl| pl.policy(kind, p))
809 }
810 pub fn sync_hooks(self, on: bool) -> Self {
812 self.hook(move |pl| pl.sync(on))
813 }
814
815 pub fn intrinsics_prior(mut self, prior: IntrinsicsPrior) -> Self {
828 self.prior = Some(prior);
829 self.prior_error = None;
830 self
831 }
832
833 pub fn intrinsics_prior_json(mut self, path: impl AsRef<Path>) -> Self {
837 match IntrinsicsPrior::load(path, DEFAULT_FOCAL_SIGMA, DEFAULT_DIST_SIGMA) {
838 Ok(p) => {
839 self.prior = Some(p);
840 self.prior_error = None;
841 }
842 Err(e) => self.prior_error = Some(e),
843 }
844 self
845 }
846
847 pub fn build(self, host: &CompositionHost) -> Result<ReconNode, ComposeError> {
850 let NodeBuilder {
851 id,
852 plan,
853 policy,
854 backpressure,
855 source,
856 spool,
857 output,
858 attach,
859 frames,
860 deltas,
861 stream_origin,
862 stream_only,
863 prior,
864 prior_error,
865 } = self;
866 let mut pr = Vec::new();
867 let cams = &plan.input.cameras;
868 if cams.is_empty() {
869 pr.push("cameras: empty".to_string());
870 }
871 let mut seen = std::collections::BTreeSet::new();
872 for c in cams {
873 if c.is_empty() || c.contains(['/', '\\']) {
874 pr.push(format!("cameras: invalid name {c:?}"));
875 } else if !seen.insert(c.as_str()) {
876 pr.push(format!("cameras: duplicate {c}"));
877 }
878 }
879 let z = plan.zones;
880 if z.span == 0 {
881 pr.push("zones.span: must be at least 1".into());
882 } else if z.overlap >= z.span {
883 pr.push(format!("zones.overlap: must be smaller than the span ({}), got {}", z.span, z.overlap));
884 }
885 if !plan.input.images.is_dir() {
886 pr.push(format!("images: folder not found: {}", plan.input.images.display()));
887 }
888 if spool.is_absolute() || spool.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
889 pr.push(format!("spool: must be a relative path inside the image folder, got {}", spool.display()));
890 }
891 if plan.sinks.live_stride == 0 {
892 pr.push("live_stride: must be at least 1 (1 = keep all)".into());
893 }
894 if plan.sparse.settle_tol.is_nan() || plan.sparse.settle_tol < 0.0 {
895 pr.push(format!("settle tolerance: must be >= 0, got {}", plan.sparse.settle_tol));
896 }
897 if let ReconcilePolicy::ReplayWindow { positions: 0 } = policy {
898 pr.push("reconcile: replay window must be at least 1 position".into());
899 }
900 match backpressure {
901 Backpressure::Block { max_pending: 0 } | Backpressure::KeepLatest { max_pending: 0 } => {
902 pr.push("backpressure: max_pending must be at least 1".into())
903 }
904 _ => {}
905 }
906 if let Some(e) = prior_error {
908 pr.push(format!("intrinsics_prior: {e}"));
909 }
910 let prior = match prior {
911 Some(p) => Some(p),
912 None => plan.sparse.intrinsics_prior.as_ref().and_then(|f| {
913 IntrinsicsPrior::load(f, plan.sparse.intrinsics_prior_sigma, plan.sparse.intrinsics_prior_dist_sigma)
914 .map_err(|e| pr.push(format!("sparse.intrinsics_prior: {e}")))
915 .ok()
916 }),
917 };
918 if let Some(p) = &prior {
919 match p.validate() {
920 Err(e) => pr.push(format!("intrinsics_prior: {e}")),
921 Ok(()) => {
922 for n in p.named.keys().filter(|n| !cams.contains(n)) {
923 pr.push(format!("intrinsics_prior: camera {n:?} is not one of the node's cameras"));
924 }
925 }
926 }
927 }
928 let sift = make_sift_backend(&plan.features.backend).map_err(|e| pr.push(format!("features.backend: {e}"))).ok();
929 let matcher = make_match_backend(&plan.matching.backend).map_err(|e| pr.push(format!("matching.backend: {e}"))).ok();
930 let ba_backend = make_ba_backend(&plan.sparse.ba_backend).map_err(|e| pr.push(format!("sparse.ba_backend: {e}"))).ok();
931 let sparse = make_sparse_backend(&plan.sparse.backend).map_err(|e| pr.push(format!("sparse.backend: {e}"))).ok();
932 if plan.dense.enabled {
933 if let Err(e) = parse_profile(&plan.dense.profile) {
934 pr.push(format!("dense.profile: {e}"));
935 }
936 }
937 let dense = if plan.dense.enabled { plan.dense.config().map_err(|e| pr.push(format!("dense: {e}"))).ok() } else { None };
938 if !pr.is_empty() {
939 return Err(ComposeError::Config(pr));
940 }
941 if host.node(&id).is_some() {
942 return Err(ComposeError::DuplicateNode(id));
943 }
944
945 let mut sc = SessionConfig::new(plan.input.images.clone());
946 sc.span = z.span;
947 sc.overlap = z.overlap;
948 sc.total_positions = None;
949 sc.incremental_triangulation = plan.sparse.incremental_triangulation;
950 sc.fixed_enu_origin = plan.align.fixed_origin;
951 sc.seed = plan.seed;
952 sc.extraction.sift.max_num_features = plan.features.max_num_features;
953 sc.extraction.reader.max_image_size = plan.features.max_image_size;
954 sc.sift = sift.expect("checked");
955 sc.matcher = Arc::from(matcher.expect("checked"));
956 sc.ba_backend = ba_backend.expect("checked");
957 sc.sparse = sparse.expect("checked");
958 sc.dense = dense;
959 sc.history = match policy {
960 ReconcilePolicy::ReplayWindow { positions } => positions + 1,
961 _ => 0,
962 };
963 sc.decode_frames = frames;
964 sc.stable_display = plan.sparse.stable_display;
965 sc.settle_tol = plan.sparse.settle_tol;
966 sc.settle_max_zone = plan.sparse.settle_max_zone;
967 sc.intrinsics_prior = prior.map(Arc::new);
968 if deltas.as_ref().is_some_and(|o| o.stride == 0) {
969 return Err(ComposeError::Config(vec!["display_deltas: stride must be at least 1".into()]));
970 }
971
972 let origin = Arc::new(Mutex::new(None));
973 let mut pl = Pipeline::new(Session::new(sc));
974 if let Some(opts) = deltas {
975 let mut feed = DisplayFeed::new(opts).with_origin(Arc::clone(&origin));
976 pl = pl.derive(move |e| feed.handle(e).into_iter().collect());
977 }
978 if let Some((dir, opts)) = &output {
979 let io = |e: std::io::Error| ComposeError::Io(format!("{}: {e}", dir.display()));
980 pl = if plan.sparse.stable_display {
981 let set = SinkSet {
983 models: opts.save_models && !stream_only,
984 zone_ply: !stream_only,
985 live_files: !stream_only,
986 live_stride: plan.sinks.live_stride,
987 live_stream: Some(LiveStream { enu: stream_origin.is_some(), origin_gps: stream_origin }),
988 ..SinkSet::all()
989 };
990 pl.on_any(DefaultSinks::with_set(dir, opts.echo, set).map_err(io)?.into_hook())
991 } else {
992 sinks::attach(pl, dir, opts).map_err(io)?
993 };
994 }
995 for a in attach {
996 pl = a(pl);
997 }
998 let core = Core {
999 cameras: plan.input.cameras.clone(),
1000 image_root: plan.input.images.clone(),
1001 spool,
1002 policy,
1003 source,
1004 accepted: BTreeMap::new(),
1005 order: Vec::new(),
1006 recent: VecDeque::new(),
1007 waiting: BTreeMap::new(),
1008 stats: ComposeStats::default(),
1009 pipeline: Some(pl),
1010 ended: None,
1011 origin,
1012 };
1013 let node = ReconNode { id, backpressure, core: Arc::new(Mutex::new(core)) };
1014 host.register(&node)?;
1015 Ok(node)
1016 }
1017}
1018
1019#[derive(Clone)]
1026pub struct ReconNode {
1027 id: String,
1028 backpressure: Backpressure,
1029 core: Arc<Mutex<Core>>,
1030}
1031
1032impl fmt::Debug for ReconNode {
1033 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034 f.debug_struct("ReconNode").field("id", &self.id).finish()
1035 }
1036}
1037
1038#[derive(Clone, Copy, Debug)]
1039struct Accepted {
1040 revision: u32,
1041 position: usize,
1042}
1043
1044struct Core {
1045 cameras: Vec<String>,
1046 image_root: PathBuf,
1047 spool: PathBuf,
1048 policy: ReconcilePolicy,
1049 source: Option<String>,
1050 accepted: BTreeMap<u64, Accepted>,
1052 order: Vec<u64>,
1054 recent: VecDeque<FrameSet>,
1056 waiting: BTreeMap<u64, Vec<String>>,
1058 stats: ComposeStats,
1059 pipeline: Option<Pipeline<Session>>,
1060 ended: Option<Closed>,
1062 origin: Arc<Mutex<Option<[f64; 3]>>>,
1064}
1065
1066impl ReconNode {
1067 pub fn id(&self) -> &str {
1069 &self.id
1070 }
1071
1072 pub fn compose(&self, state: FrameState) -> Result<Outcome, ComposeError> {
1074 lock(&self.core).compose(state)
1075 }
1076
1077 pub fn compose_all(&self, states: impl IntoIterator<Item = FrameState>) -> Result<Vec<Outcome>, ComposeError> {
1079 states.into_iter().map(|s| self.compose(s)).collect()
1080 }
1081
1082 pub fn poll(&self) -> usize {
1084 lock(&self.core).pipeline.as_mut().map_or(0, |p| p.poll())
1085 }
1086
1087 pub fn subscribe(&self) -> Option<Receiver<Event>> {
1089 lock(&self.core).pipeline.as_ref().map(|p| p.subscribe())
1090 }
1091
1092 pub fn stats(&self) -> ComposeStats {
1094 lock(&self.core).stats.clone()
1095 }
1096
1097 pub fn positions(&self) -> usize {
1099 lock(&self.core).order.len()
1100 }
1101
1102 pub fn journal(&self) -> Vec<(u64, u32)> {
1104 let c = lock(&self.core);
1105 c.order.iter().map(|s| (*s, c.accepted[s].revision)).collect()
1106 }
1107
1108 pub fn waiting(&self) -> Vec<(u64, Vec<String>)> {
1110 lock(&self.core).waiting.iter().map(|(k, v)| (*k, v.clone())).collect()
1111 }
1112
1113 pub fn with_session<T>(&self, f: impl FnOnce(&Session) -> T) -> Option<T> {
1115 lock(&self.core).pipeline.as_ref().map(|p| f(p.reducer()))
1116 }
1117
1118 pub fn close(&self) -> Result<Closed, ComposeError> {
1121 self.close_inner()
1122 }
1123
1124 fn close_inner(&self) -> Result<Closed, ComposeError> {
1125 let mut c = lock(&self.core);
1126 if let Some(done) = c.ended.take() {
1127 return Ok(done);
1128 }
1129 c.finish().ok_or(ComposeError::Closed)
1130 }
1131
1132 pub fn spawn(&self) -> ComposeHandle {
1135 let queue = Arc::new(Queue { st: Mutex::new(QueueState::default()), cv: Condvar::new() });
1136 let (q, node) = (Arc::clone(&queue), self.clone());
1137 let worker = std::thread::Builder::new()
1138 .name(format!("compose-{}", self.id))
1139 .spawn(move || {
1140 let mut outcomes = Vec::new();
1141 loop {
1142 let next = {
1143 let mut st = lock(&q.st);
1144 loop {
1145 if let Some(s) = st.items.pop_front() {
1146 q.cv.notify_all();
1147 break Some(s);
1148 }
1149 if st.closed {
1150 break None;
1151 }
1152 st = q.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1153 }
1154 };
1155 let Some(state) = next else { break };
1156 let end = matches!(state, FrameState::End);
1157 let r = node.compose(state);
1158 lock(&q.st).processed += 1;
1159 let fatal = matches!(r, Err(ComposeError::Session(_)));
1160 outcomes.push(r);
1161 if end || fatal {
1162 break;
1163 }
1164 }
1165 let mut st = lock(&q.st);
1166 st.closed = true;
1167 st.items.clear();
1168 q.cv.notify_all();
1169 outcomes
1170 })
1171 .expect("compose worker thread");
1172 ComposeHandle { node: self.clone(), queue, policy: self.backpressure, worker: Some(worker) }
1173 }
1174}
1175
1176impl Core {
1177 fn pl(&mut self) -> &mut Pipeline<Session> {
1178 self.pipeline.as_mut().expect("open pipeline")
1179 }
1180
1181 fn warn(&mut self, message: String) {
1182 if let Some(p) = self.pipeline.as_mut() {
1183 p.reducer().emit_external(|meta| Event::Warning { meta, message });
1184 p.poll();
1185 }
1186 }
1187
1188 fn ignore(&mut self, key: &FrameKey, reason: IgnoreReason) -> Result<Outcome, ComposeError> {
1189 if self.policy == ReconcilePolicy::Strict && reason != IgnoreReason::Closed {
1190 return Err(ComposeError::Rejected(format!("{key}: {reason}")));
1191 }
1192 self.stats.ignored += 1;
1193 self.warn(format!("compose: {key} ignored: {reason}"));
1194 Ok(Outcome::Ignored(reason))
1195 }
1196
1197 fn compose(&mut self, state: FrameState) -> Result<Outcome, ComposeError> {
1198 self.stats.declared += 1;
1199 if self.pipeline.is_none() {
1200 return match state {
1201 FrameState::End => Ok(Outcome::Ended),
1202 _ => Ok(Outcome::Ignored(IgnoreReason::Closed)),
1203 };
1204 }
1205 match state {
1206 FrameState::End => {
1207 let done = self.finish().ok_or(ComposeError::Closed)?;
1208 self.ended = Some(done);
1209 Ok(Outcome::Ended)
1210 }
1211 FrameState::Pending { key, missing_cameras, .. } => {
1212 if let Some(r) = self.check_source(&key) {
1213 return r;
1214 }
1215 self.stats.waiting += 1;
1216 if !self.accepted.contains_key(&key.sequence) {
1217 self.waiting.insert(key.sequence, missing_cameras.clone());
1218 }
1219 Ok(Outcome::Waiting { missing: missing_cameras })
1220 }
1221 FrameState::Ready(set) => self.ready(set),
1222 }
1223 }
1224
1225 fn check_source(&mut self, key: &FrameKey) -> Option<Result<Outcome, ComposeError>> {
1226 match &self.source {
1227 None => {
1228 self.source = Some(key.source_id.clone());
1229 None
1230 }
1231 Some(s) if *s == key.source_id => None,
1232 Some(s) => {
1233 let expected = s.clone();
1234 Some(self.ignore(key, IgnoreReason::ForeignSource { expected }))
1235 }
1236 }
1237 }
1238
1239 fn ready(&mut self, set: LiveFrameSet) -> Result<Outcome, ComposeError> {
1240 let key = set.key.clone();
1241 let mut by_cam: HashMap<&str, &LiveImage> = HashMap::new();
1243 for im in &set.images {
1244 if !self.cameras.contains(&im.camera) {
1245 return Err(ComposeError::InvalidFrameSet(format!("{key}: unknown camera {}", im.camera)));
1246 }
1247 if by_cam.insert(im.camera.as_str(), im).is_some() {
1248 return Err(ComposeError::InvalidFrameSet(format!("{key}: camera {} given twice", im.camera)));
1249 }
1250 }
1251 if let Some(r) = self.check_source(&key) {
1252 return r;
1253 }
1254 let missing: Vec<String> = self.cameras.iter().filter(|c| !by_cam.contains_key(c.as_str())).cloned().collect();
1255 if !missing.is_empty() && !self.accepted.contains_key(&key.sequence) {
1256 self.stats.waiting += 1;
1257 self.waiting.insert(key.sequence, missing.clone());
1258 return Ok(Outcome::Waiting { missing });
1259 }
1260
1261 if let Some(acc) = self.accepted.get(&key.sequence).copied() {
1262 if key.revision == acc.revision {
1263 self.stats.duplicates += 1;
1264 return Ok(Outcome::Duplicate { position: acc.position });
1265 }
1266 if key.revision < acc.revision {
1267 return self.ignore(&key, IgnoreReason::StaleRevision { accepted: acc.revision });
1268 }
1269 if !missing.is_empty() {
1270 return Err(ComposeError::InvalidFrameSet(format!("{key}: correction misses cameras {}", missing.join(", "))));
1271 }
1272 return self.correct(set, acc);
1273 }
1274
1275 let newest = self.order.last().copied();
1276 match newest {
1277 Some(n) if key.sequence < n => self.late(set),
1278 _ => {
1279 let skipped = newest.map_or(0, |n| key.sequence - n - 1);
1280 if skipped > 0 && self.policy == ReconcilePolicy::Strict {
1281 return Err(ComposeError::Rejected(format!("{key}: {skipped} sequence(s) missing before it")));
1282 }
1283 let fs = self.resolve(&set, None)?;
1284 self.waiting.remove(&key.sequence);
1285 let position = self.order.len();
1286 self.push(fs)?;
1287 self.order.push(key.sequence);
1288 self.accepted.insert(key.sequence, Accepted { revision: key.revision, position });
1289 self.stats.appended += 1;
1290 if skipped > 0 {
1291 self.stats.skipped_sequences += skipped;
1292 self.warn(format!("compose: {key} appended at position {position} after {skipped} missing sequence(s)"));
1293 }
1294 Ok(Outcome::Appended { position, skipped })
1295 }
1296 }
1297 }
1298
1299 fn late(&mut self, set: LiveFrameSet) -> Result<Outcome, ComposeError> {
1301 let key = set.key.clone();
1302 let ReconcilePolicy::ReplayWindow { positions: w } = self.policy else {
1303 return self.ignore(&key, IgnoreReason::Late);
1304 };
1305 let p = self.order.partition_point(|&s| s < key.sequence);
1306 if !self.in_window(p, w) {
1307 return self.ignore(&key, IgnoreReason::OutsideReplayWindow { position: p });
1308 }
1309 let fs = self.resolve(&set, None)?;
1310 let tail_sets: Vec<FrameSet> = self.recent.iter().skip(self.recent.len() - (self.order.len() - p)).cloned().collect();
1311 let mut tail = vec![fs];
1312 tail.extend(tail_sets);
1313 let mut seqs = vec![key.sequence];
1314 seqs.extend(self.order[p..].iter().copied());
1315 self.waiting.remove(&key.sequence);
1316 let n = self.replay(p, tail, &seqs, (key.sequence, key.revision))?;
1317 self.warn(format!("compose: late {key} inserted at position {p}, replayed {n} position(s)"));
1318 Ok(Outcome::Replayed { from: p, positions: n })
1319 }
1320
1321 fn correct(&mut self, set: LiveFrameSet, acc: Accepted) -> Result<Outcome, ComposeError> {
1323 let key = set.key.clone();
1324 let p = acc.position;
1325 match self.policy {
1326 ReconcilePolicy::AppendOnly | ReconcilePolicy::Strict => self.ignore(&key, IgnoreReason::CorrectionNotApplied),
1327 ReconcilePolicy::ReplayWindow { positions: w } => {
1328 if !self.in_window(p, w) {
1329 return self.ignore(&key, IgnoreReason::OutsideReplayWindow { position: p });
1330 }
1331 let fs = self.resolve(&set, None)?;
1332 let start = self.recent.len() - (self.order.len() - p);
1333 let mut tail: Vec<FrameSet> = self.recent.iter().skip(start).cloned().collect();
1334 tail[0] = fs;
1335 let seqs: Vec<u64> = self.order[p..].to_vec();
1336 let n = self.replay(p, tail, &seqs, (key.sequence, key.revision))?;
1337 self.warn(format!("compose: correction {key} applied from position {p}, replayed {n} position(s)"));
1338 Ok(Outcome::Replayed { from: p, positions: n })
1339 }
1340 ReconcilePolicy::InvalidateAffectedZones => {
1341 let names: Vec<String> = self.pl().reducer().frames()[p].iter().map(|f| f.name.clone()).collect();
1343 self.resolve(&set, Some(&names))?;
1344 let zones: Vec<usize> =
1345 self.pl().reducer().arrived_zones().into_iter().filter(|z| z.lo <= p && p < z.hi).map(|z| z.zone).collect();
1346 for &z in &zones {
1347 self.pl().command(Command::InvalidateZone(z));
1348 }
1349 self.fail_check()?;
1350 self.accepted.insert(key.sequence, Accepted { revision: key.revision, position: p });
1351 self.stats.invalidations += 1;
1352 Ok(Outcome::Invalidated { position: p, zones })
1353 }
1354 }
1355 }
1356
1357 fn in_window(&self, p: usize, w: usize) -> bool {
1359 let npos = self.order.len();
1360 npos - p <= w && npos - p <= self.recent.len()
1361 }
1362
1363 fn replay(&mut self, p: usize, tail: Vec<FrameSet>, seqs: &[u64], changed: (u64, u32)) -> Result<usize, ComposeError> {
1366 let npos = self.order.len();
1367 self.pl().command(Command::ResetFrom(p));
1368 if self.pl().reducer().positions() != p {
1369 return Err(ComposeError::Session(format!("rewind to position {p} was refused by the session")));
1370 }
1371 let revs: HashMap<u64, u32> = self.order[p..].iter().map(|s| (*s, self.accepted[s].revision)).collect();
1372 for s in &self.order[p..] {
1373 self.accepted.remove(s);
1374 }
1375 self.order.truncate(p);
1376 let keep = self.recent.len() - (npos - p);
1377 self.recent.truncate(keep);
1378 let n = tail.len();
1379 for (fs, &seq) in tail.into_iter().zip(seqs) {
1380 let revision = if seq == changed.0 { changed.1 } else { revs[&seq] };
1381 let position = self.order.len();
1382 self.push(fs)?;
1383 self.order.push(seq);
1384 self.accepted.insert(seq, Accepted { revision, position });
1385 }
1386 self.stats.replays += 1;
1387 self.stats.replayed_positions += n as u64;
1388 Ok(n)
1389 }
1390
1391 fn push(&mut self, fs: FrameSet) -> Result<(), ComposeError> {
1393 let cap = match self.policy {
1394 ReconcilePolicy::ReplayWindow { positions } => positions,
1395 _ => 0,
1396 };
1397 if cap > 0 {
1398 self.recent.push_back(fs.clone());
1399 while self.recent.len() > cap {
1400 self.recent.pop_front();
1401 }
1402 }
1403 if let Some(g) = fs.gps.first() {
1404 lock(&self.origin).get_or_insert([g.lat, g.lon, g.alt]);
1405 }
1406 self.pl().push(Input::Frames(fs));
1407 self.fail_check()
1408 }
1409
1410 fn fail_check(&mut self) -> Result<(), ComposeError> {
1411 match self.pl().reducer().failed() {
1412 Some(e) => Err(ComposeError::Session(e.to_string())),
1413 None => Ok(()),
1414 }
1415 }
1416
1417 fn resolve(&self, set: &LiveFrameSet, names: Option<&[String]>) -> Result<FrameSet, ComposeError> {
1420 let key = &set.key;
1421 let mut fs = FrameSet::default();
1422 for (i, cam) in self.cameras.iter().enumerate() {
1423 let im = set.images.iter().find(|x| &x.camera == cam).expect("complete set");
1424 let forced = names.map(|n| n[i].clone());
1425 let name = match &im.payload {
1426 FramePayload::File { relative_name } => {
1427 let src = self.image_root.join(relative_name);
1428 if !src.is_file() {
1429 return Err(ComposeError::InvalidFrameSet(format!("{key}: file not found: {}", src.display())));
1430 }
1431 match forced {
1432 Some(n) if n != *relative_name => {
1433 let bytes = std::fs::read(&src).map_err(|e| ComposeError::Io(format!("{}: {e}", src.display())))?;
1434 self.store_encoded(&n, &bytes, ext_of(relative_name))?;
1435 n
1436 }
1437 _ => relative_name.clone(),
1438 }
1439 }
1440 FramePayload::Rgb8 { width, height, pixels } => {
1441 if pixels.len() != *width as usize * *height as usize * 3 || *width == 0 || *height == 0 {
1442 return Err(ComposeError::InvalidFrameSet(format!(
1443 "{key}: camera {cam}: {} bytes for {width}x{height} RGB8",
1444 pixels.len()
1445 )));
1446 }
1447 let n = forced.unwrap_or_else(|| self.spool_name(key, cam, "png"));
1448 let img = image::RgbImage::from_raw(*width, *height, pixels.to_vec()).expect("size checked");
1449 self.store_image(&n, &image::DynamicImage::ImageRgb8(img))?;
1450 n
1451 }
1452 FramePayload::Encoded { bytes, format } => {
1453 let n = forced.unwrap_or_else(|| self.spool_name(key, cam, format.extension()));
1454 self.store_encoded(&n, bytes, Some(format.extension()))?;
1455 n
1456 }
1457 };
1458 fs.images.push(crate::session::FrameImage { camera: cam.clone(), name });
1459 }
1460 for g in &set.gps {
1461 let mut g = g.clone();
1462 if let Some(i) = self.cameras.iter().position(|c| *c == g.name) {
1463 g.name = fs.images[i].name.clone();
1464 }
1465 fs.gps.push(g);
1466 }
1467 Ok(fs)
1468 }
1469
1470 fn spool_name(&self, key: &FrameKey, cam: &str, ext: &str) -> String {
1471 let src: String = key.source_id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect();
1472 let p = self.spool.join(cam).join(format!("{src}_{:010}.{ext}", key.sequence));
1473 p.to_string_lossy().replace('\\', "/")
1474 }
1475
1476 fn store_encoded(&self, name: &str, bytes: &[u8], ext: Option<&str>) -> Result<(), ComposeError> {
1478 if ext.is_some() && ext == ext_of(name) {
1479 return self.atomic_write(name, |path| std::fs::write(path, bytes).map_err(|e| e.to_string()));
1480 }
1481 let img = image::load_from_memory(bytes).map_err(|e| ComposeError::InvalidFrameSet(format!("{name}: {e}")))?;
1482 self.store_image(name, &img)
1483 }
1484
1485 fn store_image(&self, name: &str, img: &image::DynamicImage) -> Result<(), ComposeError> {
1486 let fmt = image::ImageFormat::from_path(name).map_err(|e| ComposeError::Io(format!("{name}: {e}")))?;
1487 self.atomic_write(name, |path| img.save_with_format(path, fmt).map_err(|e| e.to_string()))
1488 }
1489
1490 fn atomic_write(&self, name: &str, write: impl FnOnce(&Path) -> Result<(), String>) -> Result<(), ComposeError> {
1492 let dst = self.image_root.join(name);
1493 let dir = dst.parent().unwrap_or(&self.image_root);
1494 std::fs::create_dir_all(dir).map_err(|e| ComposeError::Io(format!("{}: {e}", dir.display())))?;
1495 let file = dst.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
1496 let ext = ext_of(name).unwrap_or("tmp");
1497 let tmp = dir.join(format!(".{file}.partial.{ext}"));
1498 write(&tmp).map_err(|e| ComposeError::Io(format!("{}: {e}", tmp.display())))?;
1499 std::fs::rename(&tmp, &dst).map_err(|e| ComposeError::Io(format!("{}: {e}", dst.display())))
1500 }
1501
1502 fn finish(&mut self) -> Option<Closed> {
1503 let pl = self.pipeline.take()?;
1504 let (session, summary) = pl.finish();
1505 Some(Closed { session, summary, stats: self.stats.clone() })
1506 }
1507}
1508
1509fn ext_of(name: &str) -> Option<&str> {
1510 Path::new(name).extension().and_then(|e| e.to_str())
1511}
1512
1513fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
1514 m.lock().unwrap_or_else(|p| p.into_inner())
1515}
1516
1517#[derive(Default)]
1522struct QueueState {
1523 items: VecDeque<FrameState>,
1524 closed: bool,
1525 submitted: u64,
1526 dropped: u64,
1527 processed: u64,
1528 max_depth: usize,
1529}
1530
1531struct Queue {
1532 st: Mutex<QueueState>,
1533 cv: Condvar,
1534}
1535
1536#[derive(Debug)]
1538pub enum SendError {
1539 Full(FrameState),
1541 Closed(FrameState),
1543}
1544
1545impl fmt::Display for SendError {
1546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1547 match self {
1548 SendError::Full(_) => write!(f, "compose queue full"),
1549 SendError::Closed(_) => write!(f, "compose worker stopped"),
1550 }
1551 }
1552}
1553
1554impl std::error::Error for SendError {}
1555
1556#[derive(Clone, Debug, Default, PartialEq, Eq)]
1558pub struct QueueStats {
1559 pub submitted: u64,
1561 pub dropped: u64,
1563 pub processed: u64,
1565 pub depth: usize,
1567 pub max_depth: usize,
1569}
1570
1571pub struct ComposeHandle {
1573 node: ReconNode,
1574 queue: Arc<Queue>,
1575 policy: Backpressure,
1576 worker: Option<JoinHandle<Vec<Result<Outcome, ComposeError>>>>,
1577}
1578
1579impl fmt::Debug for ComposeHandle {
1580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1581 f.debug_struct("ComposeHandle").field("node", &self.node.id).field("policy", &self.policy).field("stats", &self.stats()).finish()
1582 }
1583}
1584
1585impl ComposeHandle {
1586 pub fn node(&self) -> &ReconNode {
1588 &self.node
1589 }
1590
1591 pub fn send(&self, state: FrameState) -> Result<(), SendError> {
1593 self.enqueue(state, true)
1594 }
1595
1596 pub fn try_send(&self, state: FrameState) -> Result<(), SendError> {
1598 self.enqueue(state, false)
1599 }
1600
1601 pub fn send_all(&self, states: impl IntoIterator<Item = FrameState>) -> Result<(), SendError> {
1603 states.into_iter().try_for_each(|s| self.send(s))
1604 }
1605
1606 fn enqueue(&self, state: FrameState, wait: bool) -> Result<(), SendError> {
1607 let q = &self.queue;
1608 let mut st = lock(&q.st);
1609 if st.closed {
1610 return Err(SendError::Closed(state));
1611 }
1612 let end = matches!(state, FrameState::End);
1614 match self.policy {
1615 Backpressure::Unbounded => {}
1616 Backpressure::Block { max_pending } => {
1617 while !end && st.items.len() >= max_pending && !st.closed {
1618 if !wait {
1619 return Err(SendError::Full(state));
1620 }
1621 st = q.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1622 }
1623 if st.closed {
1624 return Err(SendError::Closed(state));
1625 }
1626 }
1627 Backpressure::KeepLatest { max_pending } => {
1628 while !end && st.items.len() >= max_pending {
1629 match st.items.iter().position(|s| matches!(s, FrameState::Ready(_))) {
1630 Some(i) => {
1631 st.items.remove(i);
1632 st.dropped += 1;
1633 }
1634 None => {
1635 st.items.pop_front();
1636 }
1637 }
1638 }
1639 }
1640 }
1641 st.items.push_back(state);
1642 st.submitted += 1;
1643 st.max_depth = st.max_depth.max(st.items.len());
1644 q.cv.notify_all();
1645 Ok(())
1646 }
1647
1648 pub fn stats(&self) -> QueueStats {
1650 let st = lock(&self.queue.st);
1651 QueueStats { submitted: st.submitted, dropped: st.dropped, processed: st.processed, depth: st.items.len(), max_depth: st.max_depth }
1652 }
1653
1654 pub fn close(mut self) -> (Vec<Result<Outcome, ComposeError>>, Result<Closed, ComposeError>) {
1657 let _ = self.enqueue(FrameState::End, false);
1658 let outcomes = self.worker.take().map(|w| w.join().unwrap_or_default()).unwrap_or_default();
1659 (outcomes, self.node.close_inner())
1660 }
1661}
1662
1663impl Drop for ComposeHandle {
1664 fn drop(&mut self) {
1665 if let Some(w) = self.worker.take() {
1666 {
1667 let mut st = lock(&self.queue.st);
1668 st.closed = true;
1669 self.queue.cv.notify_all();
1670 }
1671 let _ = w.join();
1672 }
1673 }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678 use super::*;
1679
1680 #[test]
1681 fn spool_names_are_sanitized_and_stable() {
1682 let c = Core {
1683 cameras: vec!["camF".into()],
1684 image_root: PathBuf::from("/tmp/x"),
1685 spool: PathBuf::from(".live"),
1686 policy: ReconcilePolicy::AppendOnly,
1687 source: None,
1688 accepted: BTreeMap::new(),
1689 order: Vec::new(),
1690 recent: VecDeque::new(),
1691 waiting: BTreeMap::new(),
1692 stats: ComposeStats::default(),
1693 pipeline: None,
1694 ended: None,
1695 origin: Arc::new(Mutex::new(None)),
1696 };
1697 let k = FrameKey::new("rtsp://drone 1", 42, 3);
1698 assert_eq!(c.spool_name(&k, "camF", "png"), ".live/camF/rtsp___drone_1_0000000042.png");
1699 }
1700
1701 #[test]
1702 fn key_display() {
1703 assert_eq!(FrameKey::new("d", 7, 1).to_string(), "d#7r1");
1704 }
1705}