Skip to main content

vyre_driver/backend/
resource.rs

1//! Backend-neutral resource handles.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use crate::accounting::checked_atomic_next_u64_with_order;
6use crate::backend::error::BackendError;
7
8/// Process-wide source of backend instance identities.
9///
10/// Starts at 1 so zero is never a valid owner, and only ever counts up, so a
11/// backend that is dropped and recreated in the same process receives a
12/// distinct identity rather than inheriting the dead one's namespace.
13static NEXT_RESIDENT_OWNER: AtomicU64 = AtomicU64::new(1);
14
15/// Identity of one backend instance's resident buffer namespace.
16///
17/// Resident buffer ids come from a counter private to a backend instance, so
18/// two live instances hand out the same ids for unrelated device memory. An
19/// owner makes those namespaces distinguishable, which is what lets a handle
20/// be checked instead of merely trusted.
21#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
22pub struct ResidentOwner(u64);
23
24impl ResidentOwner {
25    /// Mint an identity for a new backend instance.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`BackendError::InvalidProgram`] if the process has exhausted
30    /// the owner id space, which is refused rather than wrapped: a wrapped id
31    /// would silently authorize a foreign handle.
32    pub fn new() -> Result<Self, BackendError> {
33        let id = checked_atomic_next_u64_with_order(
34            &NEXT_RESIDENT_OWNER,
35            Ordering::Acquire,
36            Ordering::AcqRel,
37            Ordering::Acquire,
38            |_| {
39                BackendError::InvalidProgram {
40                fix: "Fix: the process exhausted backend instance identities for resident buffers. Restart the process instead of reusing an identity, which would let a stale resident handle resolve against a live buffer."
41                    .to_string(),
42            }
43            },
44        )?;
45        Ok(Self(id))
46    }
47
48    /// Raw identity value, for diagnostics and stable ordering.
49    #[must_use]
50    pub fn get(self) -> u64 {
51        self.0
52    }
53
54    /// Mint a handle in this owner's namespace.
55    #[must_use]
56    pub fn handle(self, id: u64) -> ResidentHandle {
57        ResidentHandle { owner: self, id }
58    }
59
60    /// Unwrap a handle minted by this owner, refusing a foreign one.
61    ///
62    /// `context` names the operation for the error message, for example
63    /// `"CUDA resident upload"`.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`BackendError::InvalidProgram`] if `handle` was minted by a
68    /// different backend instance. Resolving it by bare id instead would find
69    /// this instance's unrelated buffer of the same id and read or write the
70    /// wrong device memory without any diagnostic.
71    pub fn resolve(self, handle: ResidentHandle, context: &str) -> Result<u64, BackendError> {
72        if handle.owner != self {
73            return Err(BackendError::InvalidProgram {
74                fix: format!(
75                    "Fix: {context} received resident handle {} owned by backend instance {}, but this instance is {}. A resident handle is only valid on the backend instance that allocated it; reallocate and re-upload the buffer on this instance, or keep the original instance alive for as long as the handle is held.",
76                    handle.id,
77                    handle.owner.0,
78                    self.0
79                ),
80            });
81        }
82        Ok(handle.id)
83    }
84}
85
86/// A resident buffer handle that names both the buffer and the backend
87/// instance that owns it.
88///
89/// Carrying the owner is what makes presenting a foreign handle a refusal at
90/// the API boundary rather than a silent resolve against unrelated device
91/// memory, so a caller can hold a handle across instances without having to
92/// remember a check.
93#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
94pub struct ResidentHandle {
95    owner: ResidentOwner,
96    id: u64,
97}
98
99impl ResidentHandle {
100    /// Backend instance that allocated this buffer.
101    #[must_use]
102    pub fn owner(self) -> ResidentOwner {
103        self.owner
104    }
105
106    /// Buffer id within its owner's namespace.
107    ///
108    /// Only meaningful together with [`ResidentHandle::owner`]; use
109    /// [`ResidentOwner::resolve`] to obtain it for a lookup.
110    #[must_use]
111    pub fn id(self) -> u64 {
112        self.id
113    }
114}
115
116impl std::fmt::Display for ResidentHandle {
117    /// Prints the owning instance alongside the id, never the id alone.
118    ///
119    /// A bare id is precisely the identifier that was unsafe to trust: two
120    /// live instances each have a buffer 3, so a log line or error naming
121    /// only `3` cannot be acted on.
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{} on backend instance {}", self.id, self.owner.0)
124    }
125}
126
127/// A GPU-resident or host-side resource used as an input to a Program.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub enum Resource {
130    /// Host-side byte slice. Replicated to the GPU on each dispatch.
131    Borrowed(Vec<u8>),
132    /// GPU-resident buffer handle. Zero-copy; no host transfer occurs.
133    Resident(ResidentHandle),
134}
135
136impl Default for Resource {
137    fn default() -> Self {
138        Resource::Borrowed(Vec::new())
139    }
140}
141
142impl From<Vec<u8>> for Resource {
143    fn from(bytes: Vec<u8>) -> Self {
144        Self::Borrowed(bytes)
145    }
146}
147
148impl From<ResidentHandle> for Resource {
149    fn from(handle: ResidentHandle) -> Self {
150        Self::Resident(handle)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn distinct_owners_refuse_each_others_handles() {
160        let first = ResidentOwner::new().expect("owner ids are available");
161        let second = ResidentOwner::new().expect("owner ids are available");
162        assert_ne!(first, second);
163
164        let handle = first.handle(7);
165        assert_eq!(
166            first.resolve(handle, "test resolve").expect("own handle"),
167            7
168        );
169
170        let error = second.resolve(handle, "test resolve").expect_err(
171            "Fix: a foreign resident handle must be refused, never resolved by bare id",
172        );
173        let BackendError::InvalidProgram { fix } = error else {
174            panic!("Fix: foreign resident handle refusal must be BackendError::InvalidProgram");
175        };
176        assert!(
177            fix.contains("owned by backend instance") && fix.contains("Fix: "),
178            "Fix: foreign-handle refusal must name the owning instance and carry actionable text, got {fix}"
179        );
180    }
181
182    #[test]
183    fn same_id_in_two_namespaces_stays_distinct() {
184        let first = ResidentOwner::new().expect("owner ids are available");
185        let second = ResidentOwner::new().expect("owner ids are available");
186        assert_ne!(first.handle(1), second.handle(1));
187        assert_eq!(first.handle(1), first.handle(1));
188    }
189}