Skip to main content

spatialrust_viz/
transfer.rs

1use crate::{VizError, VizResult};
2
3/// Stable identity of an explicit rendering or compute device.
4#[derive(Clone, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct DeviceIdentity {
7    /// Backend name such as `wgpu` or `cuda`.
8    pub backend: String,
9    /// Adapter or device identifier supplied by the backend.
10    pub device: String,
11}
12
13impl DeviceIdentity {
14    /// Creates a non-empty backend/device identity.
15    pub fn try_new(backend: impl Into<String>, device: impl Into<String>) -> VizResult<Self> {
16        let backend = backend.into();
17        let device = device.into();
18        if backend.trim().is_empty() || device.trim().is_empty() {
19            return Err(VizError::InvalidTransfer(
20                "device backend and identifier must not be empty".into(),
21            ));
22        }
23        Ok(Self { backend, device })
24    }
25}
26
27/// Residency of visual data before or after an explicit transfer.
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum VisualResidency {
31    /// Caller-owned host memory.
32    Host,
33    /// Memory owned by a named device.
34    Device(DeviceIdentity),
35}
36
37/// Direction of an explicit host/device transfer.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum TransferDirection {
41    /// Host to device.
42    Upload,
43    /// Device to host.
44    Readback,
45    /// One explicitly named device to another.
46    DeviceToDevice,
47}
48
49/// One named explicit data transfer.
50#[derive(Clone, Debug, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct TransferEvent {
53    /// Caller-visible stage name.
54    pub stage: String,
55    /// Transfer direction.
56    pub direction: TransferDirection,
57    /// Source residency.
58    pub source: VisualResidency,
59    /// Destination residency.
60    pub destination: VisualResidency,
61    /// Number of transferred bytes.
62    pub bytes: u64,
63}
64
65impl TransferEvent {
66    /// Creates and validates an explicit transfer event.
67    pub fn try_new(
68        stage: impl Into<String>,
69        direction: TransferDirection,
70        source: VisualResidency,
71        destination: VisualResidency,
72        bytes: u64,
73    ) -> VizResult<Self> {
74        let stage = stage.into();
75        if stage.trim().is_empty() {
76            return Err(VizError::InvalidTransfer("transfer stage must not be empty".into()));
77        }
78        let residency_matches = match direction {
79            TransferDirection::Upload => {
80                matches!(&source, VisualResidency::Host)
81                    && matches!(&destination, VisualResidency::Device(_))
82            }
83            TransferDirection::Readback => {
84                matches!(&source, VisualResidency::Device(_))
85                    && matches!(&destination, VisualResidency::Host)
86            }
87            TransferDirection::DeviceToDevice => {
88                matches!(&source, VisualResidency::Device(_))
89                    && matches!(&destination, VisualResidency::Device(_))
90            }
91        };
92        if !residency_matches {
93            return Err(VizError::InvalidTransfer(
94                "transfer direction does not match source and destination residency".into(),
95            ));
96        }
97        Ok(Self { stage, direction, source, destination, bytes })
98    }
99}
100
101/// Ordered ledger of explicit visual-data transfers.
102#[derive(Clone, Debug, Default, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub struct TransferReceipt {
105    events: Vec<TransferEvent>,
106}
107
108impl TransferReceipt {
109    /// Creates an empty receipt.
110    #[must_use]
111    pub const fn new() -> Self {
112        Self { events: Vec::new() }
113    }
114
115    /// Appends an already validated event.
116    pub fn push(&mut self, event: TransferEvent) {
117        self.events.push(event);
118    }
119
120    /// Returns events in execution order.
121    #[must_use]
122    pub fn events(&self) -> &[TransferEvent] {
123        &self.events
124    }
125
126    /// Returns the checked total number of transferred bytes.
127    pub fn total_bytes(&self) -> VizResult<u64> {
128        self.events.iter().try_fold(0_u64, |total, event| {
129            total.checked_add(event.bytes).ok_or_else(|| {
130                VizError::InvalidTransfer("transfer byte total overflowed u64".into())
131            })
132        })
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{
139        DeviceIdentity, TransferDirection, TransferEvent, TransferReceipt, VisualResidency,
140    };
141
142    #[test]
143    fn receipt_requires_direction_to_match_residency() {
144        let device = VisualResidency::Device(DeviceIdentity::try_new("wgpu", "adapter-0").unwrap());
145        let upload = TransferEvent::try_new(
146            "point-upload",
147            TransferDirection::Upload,
148            VisualResidency::Host,
149            device.clone(),
150            96,
151        )
152        .unwrap();
153        let mut receipt = TransferReceipt::new();
154        receipt.push(upload);
155        assert_eq!(receipt.total_bytes().unwrap(), 96);
156
157        assert!(TransferEvent::try_new(
158            "wrong",
159            TransferDirection::Readback,
160            VisualResidency::Host,
161            device,
162            1,
163        )
164        .is_err());
165    }
166
167    #[test]
168    fn total_bytes_fails_closed_on_overflow() {
169        let device = VisualResidency::Device(DeviceIdentity::try_new("wgpu", "adapter-0").unwrap());
170        let mut receipt = TransferReceipt::new();
171        receipt.push(
172            TransferEvent::try_new(
173                "first",
174                TransferDirection::Upload,
175                VisualResidency::Host,
176                device.clone(),
177                u64::MAX,
178            )
179            .unwrap(),
180        );
181        receipt.push(
182            TransferEvent::try_new(
183                "second",
184                TransferDirection::Upload,
185                VisualResidency::Host,
186                device,
187                1,
188            )
189            .unwrap(),
190        );
191        assert!(receipt.total_bytes().is_err());
192    }
193}