Skip to main content

spatialrust_core/
execution.rs

1use crate::{DeviceKind, SpatialError, SpatialResult, TransferDirection, TransferStats};
2
3/// Execution policy for spatial algorithms.
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
5pub enum ExecutionPolicy {
6    /// Single-threaded CPU execution.
7    #[default]
8    CpuSingle,
9    /// Parallel CPU execution.
10    CpuParallel,
11    /// GPU execution on a device of the given kind.
12    Gpu(DeviceKind),
13    /// Automatic selection based on runtime heuristics.
14    Auto,
15}
16
17impl ExecutionPolicy {
18    /// Validates that the policy names a meaningful execution request.
19    pub fn validate(self) -> SpatialResult<()> {
20        if matches!(self, Self::Gpu(DeviceKind::Cpu)) {
21            return Err(SpatialError::InvalidArgument(
22                "GPU execution policy cannot target the CPU device".to_owned(),
23            ));
24        }
25        Ok(())
26    }
27
28    /// Returns whether the policy names a concrete execution backend.
29    #[must_use]
30    pub const fn is_explicit(self) -> bool {
31        !matches!(self, Self::Auto)
32    }
33
34    /// Returns whether the policy requests an accelerator backend.
35    #[must_use]
36    pub const fn requests_gpu(self) -> bool {
37        matches!(self, Self::Gpu(kind) if kind.is_gpu())
38    }
39
40    /// Returns whether the policy lets an algorithm choose a fallback backend.
41    ///
42    /// `Auto` is the only policy with fallback semantics. An explicit CPU or GPU
43    /// policy is a request for that backend and must be reported as unsupported
44    /// when the backend cannot satisfy it.
45    #[must_use]
46    pub const fn allows_fallback(self) -> bool {
47        matches!(self, Self::Auto)
48    }
49
50    /// Returns the device kind targeted by this policy when known.
51    #[must_use]
52    pub const fn device_kind(&self) -> Option<DeviceKind> {
53        match self {
54            Self::CpuSingle | Self::CpuParallel => Some(DeviceKind::Cpu),
55            Self::Gpu(kind) => Some(*kind),
56            Self::Auto => None,
57        }
58    }
59}
60
61/// Account of one algorithm execution.
62///
63/// The requested policy is kept separately from the resolved policy so callers
64/// can distinguish an automatic CPU choice from an explicit CPU request. The
65/// transfer counters describe the explicit host/device boundary copies made by
66/// the operation; backend-specific internal work remains an implementation
67/// detail of the executing crate.
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
69pub struct ExecutionReceipt {
70    requested_policy: ExecutionPolicy,
71    resolved_policy: ExecutionPolicy,
72    transfers: TransferStats,
73    stages: Vec<&'static str>,
74}
75
76impl ExecutionReceipt {
77    /// Creates a receipt for a requested policy and the policy actually used.
78    #[must_use]
79    pub fn new(requested_policy: ExecutionPolicy, resolved_policy: ExecutionPolicy) -> Self {
80        Self {
81            requested_policy,
82            resolved_policy,
83            transfers: TransferStats::default(),
84            stages: Vec::new(),
85        }
86    }
87
88    /// Returns the policy supplied by the caller.
89    #[must_use]
90    pub const fn requested_policy(&self) -> ExecutionPolicy {
91        self.requested_policy
92    }
93
94    /// Returns the backend policy selected for this execution.
95    #[must_use]
96    pub const fn resolved_policy(&self) -> ExecutionPolicy {
97        self.resolved_policy
98    }
99
100    /// Returns transfer accounting for this execution.
101    #[must_use]
102    pub const fn transfer_stats(&self) -> TransferStats {
103        self.transfers
104    }
105
106    /// Returns bytes copied from host memory to a device.
107    #[must_use]
108    pub const fn host_to_device_bytes(&self) -> u64 {
109        self.transfers.host_to_device_bytes()
110    }
111
112    /// Returns bytes copied between buffers on a device.
113    #[must_use]
114    pub const fn device_to_device_bytes(&self) -> u64 {
115        self.transfers.device_to_device_bytes()
116    }
117
118    /// Returns bytes copied from a device back to host memory.
119    #[must_use]
120    pub const fn device_to_host_bytes(&self) -> u64 {
121        self.transfers.device_to_host_bytes()
122    }
123
124    /// Returns logical stages recorded by the algorithm or pipeline.
125    #[must_use]
126    pub fn stages(&self) -> &[&'static str] {
127        &self.stages
128    }
129
130    /// Records an explicit transfer made by the operation.
131    pub fn record_transfer(&mut self, direction: TransferDirection, bytes: u64) {
132        self.transfers.record(direction, bytes);
133    }
134
135    /// Records a logical stage name in execution order.
136    pub fn record_stage(&mut self, stage: &'static str) {
137        self.stages.push(stage);
138    }
139}
140
141/// Output value paired with the receipt for its execution.
142#[derive(Clone, Debug, PartialEq)]
143pub struct ExecutionOutput<T> {
144    output: T,
145    receipt: ExecutionReceipt,
146}
147
148impl<T> ExecutionOutput<T> {
149    /// Creates an output/receipt pair.
150    #[must_use]
151    pub const fn new(output: T, receipt: ExecutionReceipt) -> Self {
152        Self { output, receipt }
153    }
154
155    /// Borrows the algorithm output.
156    #[must_use]
157    pub const fn output(&self) -> &T {
158        &self.output
159    }
160
161    /// Borrows the execution receipt.
162    #[must_use]
163    pub const fn receipt(&self) -> &ExecutionReceipt {
164        &self.receipt
165    }
166
167    /// Splits the output and receipt without cloning either value.
168    #[must_use]
169    pub fn into_parts(self) -> (T, ExecutionReceipt) {
170        (self.output, self.receipt)
171    }
172
173    /// Returns only the algorithm output.
174    #[must_use]
175    pub fn into_output(self) -> T {
176        self.output
177    }
178
179    /// Maps the output while preserving its execution receipt.
180    #[must_use]
181    pub fn map<U>(self, map: impl FnOnce(T) -> U) -> ExecutionOutput<U> {
182        ExecutionOutput::new(map(self.output), self.receipt)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{ExecutionOutput, ExecutionPolicy, ExecutionReceipt};
189    use crate::{DeviceKind, TransferDirection};
190
191    #[test]
192    fn policy_contract_distinguishes_auto_from_explicit_backend() {
193        assert!(ExecutionPolicy::Auto.validate().is_ok());
194        assert!(!ExecutionPolicy::Auto.is_explicit());
195        assert!(ExecutionPolicy::Auto.allows_fallback());
196        assert!(ExecutionPolicy::Gpu(DeviceKind::Wgpu).is_explicit());
197        assert!(ExecutionPolicy::Gpu(DeviceKind::Wgpu).requests_gpu());
198        assert!(!ExecutionPolicy::Gpu(DeviceKind::Wgpu).allows_fallback());
199        assert!(ExecutionPolicy::CpuSingle.device_kind().unwrap().is_cpu());
200        assert!(ExecutionPolicy::Gpu(DeviceKind::Cpu).validate().is_err());
201    }
202
203    #[test]
204    fn receipt_keeps_requested_and_resolved_policies() {
205        let mut receipt = ExecutionReceipt::new(ExecutionPolicy::Auto, ExecutionPolicy::CpuSingle);
206        receipt.record_transfer(TransferDirection::HostToDevice, 24);
207        receipt.record_stage("voxel");
208
209        assert_eq!(receipt.requested_policy(), ExecutionPolicy::Auto);
210        assert_eq!(receipt.resolved_policy(), ExecutionPolicy::CpuSingle);
211        assert_eq!(receipt.host_to_device_bytes(), 24);
212        assert_eq!(receipt.stages(), &["voxel"]);
213    }
214
215    #[test]
216    fn execution_output_can_split_or_map() {
217        let receipt = ExecutionReceipt::new(ExecutionPolicy::CpuSingle, ExecutionPolicy::CpuSingle);
218        let output = ExecutionOutput::new(3_u32, receipt).map(|value| value * 2);
219        let (value, receipt) = output.into_parts();
220        assert_eq!(value, 6);
221        assert_eq!(receipt.resolved_policy(), ExecutionPolicy::CpuSingle);
222    }
223}