axiolid_dispatch/
reconstruction.rs

1//! Provider registration, ordering, fallback, and budget policy for
2//! pointcloud reconstruction.
3
4use std::sync::Arc;
5
6use axiolid_contracts::{BackendId, ExecutionOptions, GeomError, GeomResult, Operation};
7
8use crate::device::matches_device;
9use axiolid_pointcloud::PointCloud;
10use axiolid_pointcloud_reconstruction_contract::{
11    conformance, PointcloudReconstruction, Reconstruction, ReconstructionRequest,
12};
13
14#[derive(Clone)]
15struct RegisteredReconstruction {
16    priority: i32,
17    provider: Arc<dyn PointcloudReconstruction>,
18}
19
20impl core::fmt::Debug for RegisteredReconstruction {
21    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22        f.debug_struct("RegisteredReconstruction")
23            .field("priority", &self.priority)
24            .field("backend", &self.provider.descriptor().id)
25            .finish()
26    }
27}
28
29/// Ordered executable providers for pointcloud reconstruction.
30///
31/// Fallback happens only for `Unsupported` or `Unavailable`. A *refusal* is
32/// not a fallback trigger: when a provider says the data cannot support the
33/// request, that is an answer about the data, and asking a second provider
34/// the same question would only find one willing to guess.
35#[derive(Debug, Clone, Default)]
36pub struct PointcloudReconstructionRegistry {
37    providers: Vec<RegisteredReconstruction>,
38}
39
40impl PointcloudReconstructionRegistry {
41    /// Empty registry.
42    pub const fn new() -> Self {
43        Self {
44            providers: Vec::new(),
45        }
46    }
47
48    /// Register an implementation. Higher priorities run first.
49    pub fn register<B>(&mut self, priority: i32, provider: B)
50    where
51        B: PointcloudReconstruction + 'static,
52    {
53        self.register_arc(priority, Arc::new(provider));
54    }
55
56    /// Register only if the provider passes the shared conformance suite.
57    ///
58    /// Conformance is a *precondition* of registration rather than a test
59    /// someone might remember to run: a provider that violates the contract
60    /// is rejected here, with the failing report, instead of being
61    /// discovered later by a caller receiving an unexplained empty mesh.
62    ///
63    /// # Errors
64    ///
65    /// Returns the report when the provider violates any obligation.
66    pub fn register_conformant<B>(
67        &mut self,
68        priority: i32,
69        provider: B,
70    ) -> Result<(), Box<conformance::ConformanceReport>>
71    where
72        B: PointcloudReconstruction + 'static,
73    {
74        let report = conformance::run(&provider);
75        if !report.is_conformant() {
76            return Err(Box::new(report));
77        }
78        self.register_arc(priority, Arc::new(provider));
79        Ok(())
80    }
81
82    /// Register a shared trait object.
83    pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn PointcloudReconstruction>) {
84        self.providers
85            .push(RegisteredReconstruction { priority, provider });
86        self.providers
87            .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
88    }
89
90    /// Registered providers in dispatch order.
91    pub fn providers(&self) -> impl Iterator<Item = &dyn PointcloudReconstruction> {
92        self.providers.iter().map(|entry| entry.provider.as_ref())
93    }
94
95    /// Whether any provider is registered.
96    pub fn is_empty(&self) -> bool {
97        self.providers.is_empty()
98    }
99
100    /// Reconstruct through the first provider able to take the work.
101    ///
102    /// # Errors
103    ///
104    /// Returns `Unsupported` when no registered provider can run at all, and
105    /// `BudgetExceeded` when every candidate was excluded by the caller's
106    /// memory bound.
107    pub fn reconstruct(
108        &self,
109        cloud: &PointCloud,
110        request: &ReconstructionRequest,
111        options: &ExecutionOptions,
112    ) -> GeomResult<Reconstruction> {
113        let mut last_retryable = None;
114        let mut over_budget = None;
115
116        for entry in &self.providers {
117            let descriptor = entry.provider.descriptor();
118            if !matches_device(options.device(), descriptor.id, descriptor.target) {
119                continue;
120            }
121            // Budget is checked before dispatch, not after: a provider that
122            // cannot fit the caller's memory bound must never get the chance
123            // to allocate. Reconstruction is the operation most likely to
124            // exhaust memory, so this matters more here than elsewhere.
125            if !entry
126                .provider
127                .scratch_requirement()
128                .fits_budget(options, cloud.len())
129            {
130                over_budget = Some(GeomError::BudgetExceeded { resource: "memory" });
131                continue;
132            }
133            match entry.provider.reconstruct(cloud, request, options) {
134                // A refusal is a real answer about the data. Returning it
135                // immediately is what stops fallback from shopping for a
136                // provider willing to fabricate a surface.
137                Ok(result) => return Ok(result),
138                Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
139                    last_retryable = Some(error);
140                }
141                Err(error) => return Err(error),
142            }
143        }
144
145        Err(last_retryable
146            .or(over_budget)
147            .unwrap_or(GeomError::Unsupported {
148                backend: BackendId::new("pointcloud-reconstruction-registry"),
149                operation: Operation::PointcloudReconstruction,
150            }))
151    }
152}