spatialrust_core/
execution.rs1use crate::{DeviceKind, SpatialError, SpatialResult, TransferDirection, TransferStats};
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
5pub enum ExecutionPolicy {
6 #[default]
8 CpuSingle,
9 CpuParallel,
11 Gpu(DeviceKind),
13 Auto,
15}
16
17impl ExecutionPolicy {
18 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 #[must_use]
30 pub const fn is_explicit(self) -> bool {
31 !matches!(self, Self::Auto)
32 }
33
34 #[must_use]
36 pub const fn requests_gpu(self) -> bool {
37 matches!(self, Self::Gpu(kind) if kind.is_gpu())
38 }
39
40 #[must_use]
46 pub const fn allows_fallback(self) -> bool {
47 matches!(self, Self::Auto)
48 }
49
50 #[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#[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 #[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 #[must_use]
90 pub const fn requested_policy(&self) -> ExecutionPolicy {
91 self.requested_policy
92 }
93
94 #[must_use]
96 pub const fn resolved_policy(&self) -> ExecutionPolicy {
97 self.resolved_policy
98 }
99
100 #[must_use]
102 pub const fn transfer_stats(&self) -> TransferStats {
103 self.transfers
104 }
105
106 #[must_use]
108 pub const fn host_to_device_bytes(&self) -> u64 {
109 self.transfers.host_to_device_bytes()
110 }
111
112 #[must_use]
114 pub const fn device_to_device_bytes(&self) -> u64 {
115 self.transfers.device_to_device_bytes()
116 }
117
118 #[must_use]
120 pub const fn device_to_host_bytes(&self) -> u64 {
121 self.transfers.device_to_host_bytes()
122 }
123
124 #[must_use]
126 pub fn stages(&self) -> &[&'static str] {
127 &self.stages
128 }
129
130 pub fn record_transfer(&mut self, direction: TransferDirection, bytes: u64) {
132 self.transfers.record(direction, bytes);
133 }
134
135 pub fn record_stage(&mut self, stage: &'static str) {
137 self.stages.push(stage);
138 }
139}
140
141#[derive(Clone, Debug, PartialEq)]
143pub struct ExecutionOutput<T> {
144 output: T,
145 receipt: ExecutionReceipt,
146}
147
148impl<T> ExecutionOutput<T> {
149 #[must_use]
151 pub const fn new(output: T, receipt: ExecutionReceipt) -> Self {
152 Self { output, receipt }
153 }
154
155 #[must_use]
157 pub const fn output(&self) -> &T {
158 &self.output
159 }
160
161 #[must_use]
163 pub const fn receipt(&self) -> &ExecutionReceipt {
164 &self.receipt
165 }
166
167 #[must_use]
169 pub fn into_parts(self) -> (T, ExecutionReceipt) {
170 (self.output, self.receipt)
171 }
172
173 #[must_use]
175 pub fn into_output(self) -> T {
176 self.output
177 }
178
179 #[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}