1use std::sync::Arc;
4
5use axiolid_contracts::{BackendId, ExecutionOptions, GeomError, GeomResult, Operation};
6
7use crate::device::matches_device;
8use axiolid_core::BooleanOperator;
9use axiolid_mesh::TriMesh;
10use axiolid_mesh_boolean_contract::{conformance, BooleanOutcome, MeshBoolean};
11use axiolid_mesh_contracts::SolidRequirements;
12
13#[derive(Debug, Clone)]
14struct RegisteredBoolean {
15 priority: i32,
16 provider: Arc<dyn MeshBoolean>,
17}
18
19#[derive(Debug, Clone, Default)]
24pub struct MeshBooleanRegistry {
25 providers: Vec<RegisteredBoolean>,
26 #[cfg(feature = "parallel")]
30 execution: Option<axiolid_backend_cpu::CpuExecution>,
31}
32
33impl MeshBooleanRegistry {
34 pub const fn new() -> Self {
36 Self {
37 providers: Vec::new(),
38 #[cfg(feature = "parallel")]
39 execution: None,
40 }
41 }
42
43 #[cfg(feature = "parallel")]
49 #[must_use]
50 pub fn with_execution(mut self, execution: axiolid_backend_cpu::CpuExecution) -> Self {
51 self.execution = Some(execution);
52 self
53 }
54
55 pub fn register<B>(&mut self, priority: i32, provider: B)
57 where
58 B: MeshBoolean + 'static,
59 {
60 self.register_arc(priority, Arc::new(provider));
61 }
62
63 pub fn register_conformant<B>(
77 &mut self,
78 priority: i32,
79 provider: B,
80 ) -> Result<(), Box<conformance::ConformanceReport>>
81 where
82 B: MeshBoolean + 'static,
83 {
84 let report = conformance::run(&provider);
85 if !report.is_conformant() {
86 return Err(Box::new(report));
87 }
88 self.register_arc(priority, Arc::new(provider));
89 Ok(())
90 }
91
92 pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshBoolean>) {
94 self.providers
95 .push(RegisteredBoolean { priority, provider });
96 self.providers
97 .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
98 }
99
100 pub fn providers(&self) -> impl Iterator<Item = &dyn MeshBoolean> {
102 self.providers.iter().map(|entry| entry.provider.as_ref())
103 }
104
105 fn dispatch(
106 &self,
107 options: &ExecutionOptions,
108 elements: usize,
109 execute: impl Fn(&dyn MeshBoolean) -> GeomResult<BooleanOutcome> + Sync,
110 ) -> GeomResult<BooleanOutcome> {
111 let mut last_retryable = None;
112 let mut over_budget = None;
113 for entry in &self.providers {
114 let descriptor = entry.provider.descriptor();
115 if !matches_device(options.device(), descriptor.id, descriptor.target) {
116 continue;
117 }
118 if !entry
122 .provider
123 .scratch_requirement()
124 .fits_budget(options, elements)
125 {
126 over_budget = Some(GeomError::BudgetExceeded { resource: "memory" });
127 continue;
128 }
129 match self.run_scoped(&execute, entry.provider.as_ref()) {
130 Ok(outcome) => return Ok(outcome),
131 Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
132 last_retryable = Some(error);
133 }
134 Err(error) => return Err(error),
135 }
136 }
137 Err(last_retryable
138 .or(over_budget)
139 .unwrap_or(GeomError::Unsupported {
140 backend: BackendId::new("mesh-boolean-registry"),
141 operation: Operation::MeshBoolean,
142 }))
143 }
144
145 #[cfg(feature = "parallel")]
151 fn run_scoped(
152 &self,
153 execute: &(impl Fn(&dyn MeshBoolean) -> GeomResult<BooleanOutcome> + Sync),
154 provider: &dyn MeshBoolean,
155 ) -> GeomResult<BooleanOutcome> {
156 match &self.execution {
157 Some(execution) => execution.install(|| execute(provider)),
158 None => execute(provider),
159 }
160 }
161
162 #[cfg(not(feature = "parallel"))]
163 fn run_scoped(
164 &self,
165 execute: &impl Fn(&dyn MeshBoolean) -> GeomResult<BooleanOutcome>,
166 provider: &dyn MeshBoolean,
167 ) -> GeomResult<BooleanOutcome> {
168 execute(provider)
169 }
170
171 pub fn boolean(
173 &self,
174 subject: &TriMesh,
175 tool: &TriMesh,
176 operation: BooleanOperator,
177 options: &ExecutionOptions,
178 ) -> GeomResult<BooleanOutcome> {
179 SolidRequirements::Oriented.validate_operands(subject, &[tool])?;
182 self.dispatch(options, subject.triangle_count(), |provider| {
183 provider.boolean(subject, tool, operation, options)
184 })
185 }
186
187 pub fn subtract_many(
189 &self,
190 subject: &TriMesh,
191 tools: &[TriMesh],
192 options: &ExecutionOptions,
193 ) -> GeomResult<BooleanOutcome> {
194 let borrowed: Vec<&TriMesh> = tools.iter().collect();
195 SolidRequirements::Oriented.validate_operands(subject, &borrowed)?;
196 let elements =
197 subject.triangle_count() + tools.iter().map(TriMesh::triangle_count).sum::<usize>();
198 self.dispatch(options, elements, |provider| {
199 provider.subtract_many(subject, tools, options)
200 })
201 }
202
203 pub fn union_many(
209 &self,
210 solids: &[TriMesh],
211 options: &ExecutionOptions,
212 ) -> GeomResult<BooleanOutcome> {
213 let Some((first, rest)) = solids.split_first() else {
214 return Ok(BooleanOutcome::new(TriMesh::default(), Default::default()));
215 };
216 let borrowed: Vec<&TriMesh> = rest.iter().collect();
220 SolidRequirements::Oriented.validate_operands(first, &borrowed)?;
221 let elements = solids.iter().map(TriMesh::triangle_count).sum::<usize>();
222 self.dispatch(options, elements, |provider| {
223 provider.union_many(solids, options)
224 })
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use axiolid_contracts::{DevicePreference, ExecutionTarget};
231 fn admissible_cube() -> TriMesh {
236 let positions = vec![
237 [0.0, 0.0, 0.0].into(),
238 [1.0, 0.0, 0.0].into(),
239 [1.0, 1.0, 0.0].into(),
240 [0.0, 1.0, 0.0].into(),
241 [0.0, 0.0, 1.0].into(),
242 [1.0, 0.0, 1.0].into(),
243 [1.0, 1.0, 1.0].into(),
244 [0.0, 1.0, 1.0].into(),
245 ];
246 let indices = vec![
247 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7,
248 6, 3, 0, 4, 3, 4, 7,
249 ];
250 TriMesh::new(positions, indices)
251 }
252
253 use std::sync::{
254 atomic::{AtomicUsize, Ordering},
255 Arc,
256 };
257
258 use axiolid_contracts::{Backend, BackendDescriptor};
259 use axiolid_core::Tolerance;
260 use axiolid_mesh_boolean_contract::BooleanEvidence;
261
262 use super::*;
263
264 #[derive(Debug)]
265 struct EchoBoolean {
266 id: BackendId,
267 target: ExecutionTarget,
268 }
269
270 impl Backend for EchoBoolean {
271 fn descriptor(&self) -> BackendDescriptor {
272 BackendDescriptor {
273 id: self.id,
274 target: self.target,
275 }
276 }
277 }
278
279 impl MeshBoolean for EchoBoolean {
280 fn boolean(
281 &self,
282 subject: &TriMesh,
283 _tool: &TriMesh,
284 _operation: BooleanOperator,
285 _options: &ExecutionOptions,
286 ) -> GeomResult<BooleanOutcome> {
287 Ok(BooleanOutcome::new(
288 subject.clone(),
289 BooleanEvidence::default(),
290 ))
291 }
292 }
293
294 #[derive(Debug, Clone, Copy)]
295 enum ProbeResult {
296 Success,
297 Unsupported,
298 Unavailable,
299 Invalid,
300 }
301
302 #[derive(Debug)]
303 struct ProbeBoolean {
304 id: BackendId,
305 target: ExecutionTarget,
306 result: ProbeResult,
307 calls: Arc<AtomicUsize>,
308 }
309
310 impl Backend for ProbeBoolean {
311 fn descriptor(&self) -> BackendDescriptor {
312 BackendDescriptor::new(self.id, self.target)
313 }
314 }
315
316 impl MeshBoolean for ProbeBoolean {
317 fn boolean(
318 &self,
319 subject: &TriMesh,
320 _tool: &TriMesh,
321 _operation: BooleanOperator,
322 _options: &ExecutionOptions,
323 ) -> GeomResult<BooleanOutcome> {
324 self.calls.fetch_add(1, Ordering::Relaxed);
325 match self.result {
326 ProbeResult::Success => Ok(BooleanOutcome::new(
327 subject.clone(),
328 BooleanEvidence::default(),
329 )),
330 ProbeResult::Unsupported => Err(GeomError::Unsupported {
331 backend: self.id,
332 operation: Operation::MeshBoolean,
333 }),
334 ProbeResult::Unavailable => Err(GeomError::Unavailable {
335 backend: self.id,
336 reason: "probe unavailable".to_owned(),
337 }),
338 ProbeResult::Invalid => {
339 Err(GeomError::InvalidInput("probe rejected input".to_owned()))
340 }
341 }
342 }
343 }
344
345 #[derive(Debug)]
346 struct BatchBoolean {
347 calls: Arc<AtomicUsize>,
348 }
349
350 impl Backend for BatchBoolean {
351 fn descriptor(&self) -> BackendDescriptor {
352 BackendDescriptor::new(BackendId::new("batch"), ExecutionTarget::OptimizedCpu)
353 }
354 }
355
356 impl MeshBoolean for BatchBoolean {
357 fn boolean(
358 &self,
359 _subject: &TriMesh,
360 _tool: &TriMesh,
361 _operation: BooleanOperator,
362 _options: &ExecutionOptions,
363 ) -> GeomResult<BooleanOutcome> {
364 Err(GeomError::InvalidInput(
365 "batch provider must use its batch override".to_owned(),
366 ))
367 }
368
369 fn subtract_many(
370 &self,
371 subject: &TriMesh,
372 _tools: &[TriMesh],
373 _options: &ExecutionOptions,
374 ) -> GeomResult<BooleanOutcome> {
375 self.calls.fetch_add(1, Ordering::Relaxed);
376 Ok(BooleanOutcome::new(
377 subject.clone(),
378 BooleanEvidence::default(),
379 ))
380 }
381 }
382
383 #[test]
384 fn registry_stores_executable_traits_not_capability_flags() {
385 let mut registry = MeshBooleanRegistry::new();
386 registry.register(
387 10,
388 EchoBoolean {
389 id: BackendId::new("echo"),
390 target: ExecutionTarget::PortableCpu,
391 },
392 );
393 let options = ExecutionOptions::new(Tolerance::METRE);
394 let mesh = admissible_cube();
395 assert_eq!(
396 registry
397 .boolean(&mesh, &mesh, BooleanOperator::Difference, &options)
398 .expect("registered provider executes"),
399 BooleanOutcome::new(mesh, BooleanEvidence::default())
400 );
401 }
402
403 #[test]
404 fn registry_dispatches_batch_subtraction_to_the_provider_override() {
405 let calls = Arc::new(AtomicUsize::new(0));
406 let mut registry = MeshBooleanRegistry::new();
407 registry.register(
408 10,
409 BatchBoolean {
410 calls: calls.clone(),
411 },
412 );
413 let mesh = admissible_cube();
414 let options = ExecutionOptions::new(Tolerance::METRE);
415
416 assert_eq!(
417 registry
418 .subtract_many(&mesh, &[mesh.clone(), mesh.clone()], &options)
419 .expect("batch provider executes"),
420 BooleanOutcome::new(mesh, BooleanEvidence::default())
421 );
422 assert_eq!(calls.load(Ordering::Relaxed), 1);
423 }
424
425 #[test]
426 fn registry_falls_back_only_for_retryable_errors_and_honors_device_policy() {
427 let high_calls = Arc::new(AtomicUsize::new(0));
428 let unsupported_calls = Arc::new(AtomicUsize::new(0));
429 let low_calls = Arc::new(AtomicUsize::new(0));
430 let mut registry = MeshBooleanRegistry::new();
431 registry.register(
432 100,
433 ProbeBoolean {
434 id: BackendId::new("unavailable-gpu"),
435 target: ExecutionTarget::Gpu,
436 result: ProbeResult::Unavailable,
437 calls: high_calls.clone(),
438 },
439 );
440 registry.register(
441 50,
442 ProbeBoolean {
443 id: BackendId::new("unsupported-cpu"),
444 target: ExecutionTarget::OptimizedCpu,
445 result: ProbeResult::Unsupported,
446 calls: unsupported_calls.clone(),
447 },
448 );
449 registry.register(
450 10,
451 ProbeBoolean {
452 id: BackendId::new("portable-fallback"),
453 target: ExecutionTarget::PortableCpu,
454 result: ProbeResult::Success,
455 calls: low_calls.clone(),
456 },
457 );
458 let mesh = admissible_cube();
459 let auto = ExecutionOptions::new(Tolerance::METRE);
460 assert!(registry
461 .boolean(&mesh, &mesh, BooleanOperator::Union, &auto)
462 .is_ok());
463 assert_eq!(high_calls.load(Ordering::Relaxed), 1);
464 assert_eq!(unsupported_calls.load(Ordering::Relaxed), 1);
465 assert_eq!(low_calls.load(Ordering::Relaxed), 1);
466
467 let cpu = auto.clone().with_device(DevicePreference::Cpu);
468 assert!(registry
469 .boolean(&mesh, &mesh, BooleanOperator::Union, &cpu)
470 .is_ok());
471 assert_eq!(high_calls.load(Ordering::Relaxed), 1);
472 assert_eq!(unsupported_calls.load(Ordering::Relaxed), 2);
473 assert_eq!(low_calls.load(Ordering::Relaxed), 2);
474
475 let invalid_calls = Arc::new(AtomicUsize::new(0));
476 let skipped_calls = Arc::new(AtomicUsize::new(0));
477 let mut fail_fast = MeshBooleanRegistry::new();
478 fail_fast.register(
479 100,
480 ProbeBoolean {
481 id: BackendId::new("invalid-input"),
482 target: ExecutionTarget::PortableCpu,
483 result: ProbeResult::Invalid,
484 calls: invalid_calls.clone(),
485 },
486 );
487 fail_fast.register(
488 10,
489 ProbeBoolean {
490 id: BackendId::new("must-not-run"),
491 target: ExecutionTarget::PortableCpu,
492 result: ProbeResult::Success,
493 calls: skipped_calls.clone(),
494 },
495 );
496 assert!(matches!(
497 fail_fast.boolean(&mesh, &mesh, BooleanOperator::Union, &auto),
498 Err(GeomError::InvalidInput(_))
499 ));
500 assert_eq!(invalid_calls.load(Ordering::Relaxed), 1);
501 assert_eq!(skipped_calls.load(Ordering::Relaxed), 0);
502 }
503
504 #[test]
505 fn empty_registry_returns_structured_unsupported_error() {
506 let registry = MeshBooleanRegistry::new();
507 let mesh = admissible_cube();
508 let error = registry
509 .boolean(
510 &mesh,
511 &mesh,
512 BooleanOperator::Union,
513 &ExecutionOptions::new(Tolerance::METRE),
514 )
515 .unwrap_err();
516 assert!(matches!(
517 error,
518 GeomError::Unsupported {
519 backend,
520 operation: Operation::MeshBoolean,
521 } if backend == BackendId::new("mesh-boolean-registry")
522 ));
523 }
524
525 #[cfg(feature = "parallel")]
531 #[derive(Debug, Default)]
532 struct PoolWidthBoolean {
533 seen: std::sync::Mutex<Vec<usize>>,
534 }
535
536 #[cfg(feature = "parallel")]
537 impl Backend for PoolWidthBoolean {
538 fn descriptor(&self) -> BackendDescriptor {
539 BackendDescriptor {
540 id: BackendId::new("pool-width"),
541 target: ExecutionTarget::PortableCpu,
542 }
543 }
544 }
545
546 #[cfg(feature = "parallel")]
547 impl MeshBoolean for PoolWidthBoolean {
548 fn boolean(
549 &self,
550 subject: &TriMesh,
551 _tool: &TriMesh,
552 _operation: BooleanOperator,
553 _options: &ExecutionOptions,
554 ) -> GeomResult<BooleanOutcome> {
555 self.seen
556 .lock()
557 .expect("poisoned")
558 .push(rayon::current_num_threads());
559 Ok(BooleanOutcome::new(
560 subject.clone(),
561 BooleanEvidence::default(),
562 ))
563 }
564 }
565
566 #[test]
572 #[cfg(feature = "parallel")]
573 fn dispatch_runs_inside_the_configured_pool() {
574 use axiolid_backend_cpu::CpuExecutionBuilder;
575 use std::num::NonZeroUsize;
576 use std::sync::Arc;
577
578 let execution = CpuExecutionBuilder::new()
579 .threads(NonZeroUsize::new(3).expect("nonzero"))
580 .build()
581 .expect("cpu execution");
582 let provider = Arc::new(PoolWidthBoolean::default());
583 let mut registry = MeshBooleanRegistry::new().with_execution(execution);
584 registry.register_arc(0, provider.clone());
585
586 let cube = admissible_cube();
587 let options = ExecutionOptions::new(Tolerance::MILLIMETRE);
588 registry
589 .boolean(&cube, &cube, BooleanOperator::Union, &options)
590 .expect("dispatch");
591
592 let widths = provider.seen.lock().expect("poisoned").clone();
593 assert_eq!(
594 widths,
595 vec![3],
596 "provider must run inside the 3-worker pool"
597 );
598 }
599
600 #[test]
604 #[cfg(feature = "parallel")]
605 fn an_unscoped_registry_does_not_see_the_configured_width() {
606 use std::sync::Arc;
607
608 let provider = Arc::new(PoolWidthBoolean::default());
609 let mut registry = MeshBooleanRegistry::new();
610 registry.register_arc(0, provider.clone());
611
612 let cube = admissible_cube();
613 let options = ExecutionOptions::new(Tolerance::MILLIMETRE);
614 registry
615 .boolean(&cube, &cube, BooleanOperator::Union, &options)
616 .expect("dispatch");
617
618 let widths = provider.seen.lock().expect("poisoned").clone();
619 assert_eq!(widths.len(), 1, "one dispatch");
620 assert_eq!(
621 widths[0],
622 rayon::current_num_threads(),
623 "unscoped dispatch must observe the ambient pool"
624 );
625 }
626}