axiolid_backend_cpu/
execution.rs

1//! Runtime CPU execution context. Operation providers may compose this type.
2
3use core::fmt;
4use std::num::NonZeroUsize;
5#[cfg(feature = "parallel")]
6use std::sync::Arc;
7
8use crate::{CpuConfigError, CpuFeatures, CpuInstructionSet};
9
10/// CPU execution context with runtime ISA detection and an optional local pool.
11///
12/// This type deliberately implements no geometry operation trait. A provider
13/// only implements `MeshBoolean`, `MeshCompiler`, or another capability
14/// after the algorithm exists and is verified.
15#[derive(Clone)]
16pub struct CpuExecution {
17    instruction_set: CpuInstructionSet,
18    features: CpuFeatures,
19    threads: NonZeroUsize,
20    #[cfg(feature = "parallel")]
21    pool: Option<Arc<rayon::ThreadPool>>,
22}
23
24impl fmt::Debug for CpuExecution {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.debug_struct("CpuExecution")
27            .field("instruction_set", &self.instruction_set)
28            .field("features", &self.features)
29            .field("threads", &self.threads)
30            .finish_non_exhaustive()
31    }
32}
33
34impl CpuExecution {
35    /// Always-available portable execution context.
36    pub fn portable() -> Self {
37        Self {
38            instruction_set: CpuInstructionSet::Portable,
39            features: CpuFeatures::detect(),
40            threads: NonZeroUsize::MIN,
41            #[cfg(feature = "parallel")]
42            pool: None,
43        }
44    }
45
46    /// Best instruction path compiled into this binary and available at runtime.
47    pub fn detect() -> Self {
48        let features = CpuFeatures::detect();
49        #[cfg(feature = "simd")]
50        let instruction_set = features.best();
51        #[cfg(not(feature = "simd"))]
52        let instruction_set = CpuInstructionSet::Portable;
53        Self {
54            instruction_set,
55            features,
56            threads: NonZeroUsize::MIN,
57            #[cfg(feature = "parallel")]
58            pool: None,
59        }
60    }
61
62    pub(crate) fn from_configuration(
63        instruction_set: CpuInstructionSet,
64        features: CpuFeatures,
65        threads: NonZeroUsize,
66    ) -> Result<Self, CpuConfigError> {
67        #[cfg(not(feature = "parallel"))]
68        if threads.get() > 1 {
69            return Err(CpuConfigError::ParallelFeatureDisabled);
70        }
71
72        #[cfg(feature = "parallel")]
73        let pool = if threads.get() > 1 {
74            Some(Arc::new(
75                rayon::ThreadPoolBuilder::new()
76                    .num_threads(threads.get())
77                    .thread_name(|index| format!("axiolid-cpu-{index}"))
78                    .build()
79                    .map_err(|error| CpuConfigError::ThreadPool(error.to_string()))?,
80            ))
81        } else {
82            None
83        };
84
85        Ok(Self {
86            instruction_set,
87            features,
88            threads,
89            #[cfg(feature = "parallel")]
90            pool,
91        })
92    }
93
94    /// Runtime-selected instruction set.
95    pub const fn instruction_set(&self) -> CpuInstructionSet {
96        self.instruction_set
97    }
98
99    /// Detected machine features.
100    pub const fn features(&self) -> CpuFeatures {
101        self.features
102    }
103
104    /// Configured worker bound.
105    pub const fn thread_count(&self) -> NonZeroUsize {
106        self.threads
107    }
108
109    /// Execute one closure inside this context's local Rayon pool.
110    #[cfg(feature = "parallel")]
111    pub fn install<R: Send>(&self, operation: impl FnOnce() -> R + Send) -> R {
112        match &self.pool {
113            Some(pool) => pool.install(operation),
114            None => operation(),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::{CpuExecutionBuilder, InstructionPolicy};
123
124    fn assert_runtime_traits<T: fmt::Debug + Clone + Send + Sync>() {}
125
126    #[test]
127    fn portable_context_is_constructible_but_claims_no_operation_trait() {
128        assert_runtime_traits::<CpuExecution>();
129        let backend = CpuExecutionBuilder::new()
130            .instruction_policy(InstructionPolicy::Portable)
131            .build()
132            .expect("portable context");
133        assert_eq!(backend.instruction_set(), CpuInstructionSet::Portable);
134    }
135
136    #[cfg(not(feature = "simd"))]
137    #[test]
138    fn auto_selection_stays_portable_without_the_simd_feature() {
139        assert_eq!(
140            CpuExecution::detect().instruction_set(),
141            CpuInstructionSet::Portable
142        );
143    }
144
145    #[cfg(feature = "simd")]
146    #[test]
147    fn auto_selection_uses_the_best_runtime_supported_instruction_set() {
148        let backend = CpuExecution::detect();
149        assert_eq!(backend.instruction_set(), backend.features().best());
150        assert!(backend.features().supports(backend.instruction_set()));
151    }
152
153    #[cfg(feature = "parallel")]
154    #[test]
155    fn configured_worker_bound_controls_the_local_pool() {
156        let backend = CpuExecutionBuilder::new()
157            .threads(NonZeroUsize::new(2).unwrap())
158            .build()
159            .expect("two-worker context");
160        assert_eq!(backend.thread_count().get(), 2);
161        assert_eq!(backend.install(rayon::current_num_threads), 2);
162    }
163
164    #[cfg(not(feature = "parallel"))]
165    #[test]
166    fn multiple_workers_require_the_parallel_feature() {
167        let error = CpuExecutionBuilder::new()
168            .threads(NonZeroUsize::new(2).unwrap())
169            .build()
170            .unwrap_err();
171        assert_eq!(error, CpuConfigError::ParallelFeatureDisabled);
172    }
173}