Skip to main content

hrx/execution/
handoff.rs

1use super::{Access, Buffer, Engine, GpuAccess, Runtime, scheduler::Core};
2use crate::{Error, Result};
3use std::{collections::BTreeMap, sync::Arc};
4
5struct Lease {
6    buffers: Vec<Buffer>,
7    core: Arc<Core>,
8    uncertain: bool,
9}
10impl Drop for Lease {
11    fn drop(&mut self) {
12        let _scheduler = self.core.state.lock().unwrap_or_else(|e| e.into_inner());
13        for buffer in &self.buffers {
14            let mut host = buffer
15                .storage
16                .host
17                .lock()
18                .unwrap_or_else(|e| e.into_inner());
19            if self.uncertain {
20                host.poison = Some("external GPU completion is uncertain".into());
21            }
22            host.external = false;
23        }
24        if self.uncertain {
25            // Retain mapped wrappers and imported pages beneath possible DMA.
26            std::mem::forget(std::mem::take(&mut self.buffers));
27        }
28        self.core.host_changed.notify_all();
29    }
30}
31
32impl Runtime {
33    /// Run existing Stream operations against coordinated allocations at a stage
34    /// boundary. Reservations are allocation-wide; conflicting submitted work
35    /// completes first. A live host mapping or another handoff returns `Busy`.
36    /// The supplied stream is synchronized before releasing the reservations,
37    /// including when the callback fails or panics.
38    ///
39    /// # Safety
40    /// Declare every access to these allocations accurately. The callback must
41    /// not retain pointers, buffers, or recorded uses, and must submit all such
42    /// work on the supplied stream. No work may escape onto another stream.
43    /// Kernel dispatch still carries the low-level GPU API's safety contract.
44    pub unsafe fn with_gpu_access<T>(
45        &self,
46        stream: &mut crate::gpu::Stream,
47        accesses: &[GpuAccess],
48        callback: impl FnOnce(&mut crate::gpu::Stream, &[crate::gpu::View<'_>]) -> Result<T>,
49    ) -> Result<T> {
50        let mut roots = BTreeMap::new();
51        for usage in accesses {
52            let storage = &usage.view.buffer.storage;
53            if !Arc::ptr_eq(&storage.runtime, &self.inner) {
54                return Err(Error::Message("buffer belongs to another runtime".into()));
55            }
56            if usage.view.gpu()?.owner().device_id() != stream.device_id() {
57                return Err(Error::Message("stream belongs to another device".into()));
58            }
59            roots.insert(Arc::as_ptr(storage) as usize, usage.view.buffer.clone());
60        }
61        let buffers: Vec<Buffer> = roots.into_values().collect();
62        let core = &self.inner.core;
63        let mut scheduler = core.state.lock().unwrap_or_else(|e| e.into_inner());
64        loop {
65            for buffer in &buffers {
66                buffer.storage.host_conflict(Access::ReadWrite)?;
67            }
68            if !buffers
69                .iter()
70                .any(|b| scheduler.conflicts(&b.view(), Access::ReadWrite))
71            {
72                break;
73            }
74            scheduler = core
75                .host_changed
76                .wait(scheduler)
77                .unwrap_or_else(|e| e.into_inner());
78        }
79        for buffer in &buffers {
80            buffer
81                .storage
82                .host
83                .lock()
84                .unwrap_or_else(|e| e.into_inner())
85                .external = true;
86        }
87        let mut lease = Lease {
88            buffers,
89            core: core.clone(),
90            uncertain: false,
91        };
92        drop(scheduler);
93        for buffer in &lease.buffers {
94            buffer.storage.make_visible(Engine::Gpu)?;
95        }
96        let views = accesses
97            .iter()
98            .map(|a| a.view.gpu())
99            .collect::<Result<Vec<_>>>()?;
100        let outcome =
101            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(stream, &views)));
102        let completion = stream.synchronize();
103        if completion.is_err() {
104            lease.uncertain = true;
105        } else {
106            for usage in accesses.iter().filter(|a| a.access.writes()) {
107                usage
108                    .view
109                    .buffer
110                    .storage
111                    .visibility
112                    .lock()
113                    .unwrap_or_else(|e| e.into_inner())
114                    .wrote(Engine::Gpu);
115            }
116        }
117        drop(lease);
118        match outcome {
119            Err(panic) => std::panic::resume_unwind(panic),
120            Ok(result) => {
121                completion?;
122                result
123            }
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    #[test]
132    fn leases_release_reservations_on_unwind() {
133        let runtime = Runtime::new().unwrap();
134        let buffer = super::super::tests::buffer(&runtime);
135        buffer.storage.host.lock().unwrap().external = true;
136        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
137            let _lease = Lease {
138                buffers: vec![buffer.clone()],
139                core: runtime.inner.core.clone(),
140                uncertain: false,
141            };
142            assert!(buffer.try_map_read().is_err());
143            panic!("unwind");
144        }));
145        assert!(result.is_err());
146        assert!(buffer.try_map_write().is_ok());
147    }
148    #[test]
149    #[cfg_attr(miri, ignore = "deliberate quarantine leak")]
150    fn uncertain_handoff_poison_reaches_every_alias() {
151        let runtime = Runtime::new().unwrap();
152        let buffer = super::super::tests::buffer(&runtime);
153        let alias = buffer.clone();
154        drop(Lease {
155            buffers: vec![buffer],
156            core: runtime.inner.core.clone(),
157            uncertain: true,
158        });
159        assert!(matches!(alias.try_map_read(), Err(Error::DeviceLost(_))));
160        assert!(matches!(alias.try_map_write(), Err(Error::DeviceLost(_))));
161    }
162}