1use std::ffi::{CStr, CString, c_void};
2use std::path::Path;
3
4pub use crate::sys::ExportInfo;
6use crate::{Error, Result, TARGET_FAMILY, Target, sys};
7
8pub(crate) fn check(status: sys::Status, what: impl std::fmt::Display) -> Result<()> {
10 if sys::is_ok(status) {
11 return Ok(());
12 }
13 let code = unsafe { sys::hrx_status_code(status) };
14 let mut message: *mut std::ffi::c_char = std::ptr::null_mut();
15 let mut length: usize = 0;
16 let text = unsafe {
17 let to_string = sys::hrx_status_to_string(status, &mut message, &mut length);
18 let text = if sys::is_ok(to_string) && !message.is_null() {
19 let owned = CStr::from_ptr(message).to_string_lossy().into_owned();
20 sys::hrx_status_free_message(message);
21 owned
22 } else {
23 sys::hrx_status_ignore(to_string);
24 "unknown error".to_string()
25 };
26 sys::hrx_status_ignore(status);
27 text
28 };
29 Err(Error::Runtime {
30 context: what.to_string(),
31 code,
32 message: text,
33 })
34}
35
36struct Inner {
39 device: sys::Device,
40 target: Target,
41 stream: sys::Stream,
42}
43
44impl Drop for Inner {
45 fn drop(&mut self) {
46 unsafe { sys::hrx_stream_release(self.stream) };
49 }
50}
51
52unsafe impl Send for Inner {}
56unsafe impl Sync for Inner {}
57
58pub(crate) fn initialize_runtime() -> Result<()> {
61 sys::load()?;
62 static READY: std::sync::Mutex<Option<()>> = std::sync::Mutex::new(None);
63 crate::cached_init(&READY, || {
64 let _lock = sys::runtime_lock()?;
65 unsafe {
66 let status = sys::hrx_gpu_initialize(0);
67 if sys::hrx_status_code(status) == sys::STATUS_ALREADY_EXISTS {
68 sys::hrx_status_ignore(status);
69 } else {
70 check(status, "hrx_gpu_initialize")?;
71 }
72 }
73 Ok(())
74 })
75}
76
77pub(crate) fn open_device(index: i32) -> Result<(sys::Device, Target)> {
78 initialize_runtime()?;
79 unsafe {
80 let mut count = 0;
81 check(
82 sys::hrx_gpu_device_count(&mut count),
83 "hrx_gpu_device_count",
84 )?;
85 if index < 0 || index >= count {
86 return Err(Error::Message(
87 "no GPU device (check GPU access and the README provisioning instructions)".into(),
88 ));
89 }
90 let mut device = std::ptr::null_mut();
91 check(
92 sys::hrx_gpu_device_get(index, &mut device),
93 "hrx_gpu_device_get",
94 )?;
95 let mut architecture = [0u8; 64];
96 check(
97 sys::hrx_device_get_property(
98 device,
99 sys::DEVICE_PROPERTY_ARCHITECTURE,
100 architecture.as_mut_ptr().cast(),
101 architecture.len(),
102 ),
103 "device architecture",
104 )?;
105 let key = std::str::from_utf8(architecture.split(|b| *b == 0).next().unwrap())
106 .map_err(|_| Error::Message("invalid device architecture".into()))?;
107 Ok((device, Target::from_device_architecture(key)?))
108 }
109}
110
111impl Stream {
112 fn new(index: i32) -> Result<Self> {
113 let (device, target) = open_device(index)?;
114 unsafe {
115 let mut stream = std::ptr::null_mut();
116 check(
117 sys::hrx_stream_create(device, 0, &mut stream),
118 "hrx_stream_create",
119 )?;
120 Ok(Self {
121 inner: std::sync::Arc::new(Inner {
122 device,
123 target,
124 stream,
125 }),
126 _not_sync: std::marker::PhantomData,
127 staging: Vec::new(),
128 staging_pool: Vec::new(),
129 scratch: std::collections::BTreeMap::new(),
130 scratch_bytes: 0,
131 scratch_limit: 256 * 1024 * 1024,
132 budget: None,
133 budget_uses: std::cell::RefCell::new(BudgetUses::default()),
134 })
135 }
136 }
137
138 fn owns(&self, buffer: &Buffer) -> Result<()> {
139 owns(&self.inner, buffer)
140 }
141
142 fn synchronize_native(&self) -> Result<()> {
143 unsafe {
144 check(
145 sys::hrx_stream_synchronize(self.inner.stream),
146 "hrx_stream_synchronize",
147 )
148 }
149 }
150
151 unsafe fn loaded_export(&self, executable: sys::Executable, symbol: &str) -> Result<Kernel> {
152 let kernel = (|| {
153 let symbol_c =
154 CString::new(symbol).map_err(|_| Error::Message("export contains a NUL".into()))?;
155 let mut ordinal = 0;
156 let mut info = sys::ExportInfo::default();
157 unsafe {
158 check(
159 sys::hrx_executable_lookup_export_by_name(
160 executable,
161 symbol_c.as_ptr(),
162 &mut ordinal,
163 ),
164 "looking up export",
165 )?;
166 check(
167 sys::hrx_executable_export_info(executable, ordinal, &mut info),
168 "export metadata",
169 )?;
170 }
171 Ok(Kernel {
172 executable: std::sync::Arc::new(Executable {
173 raw: executable,
174 device: self.inner.clone(),
175 }),
176 ordinal,
177 info,
178 symbol: symbol.into(),
179 })
180 })();
181 if kernel.is_err() {
182 unsafe { sys::hrx_executable_release(executable) }
183 }
184 kernel
185 }
186}
187
188pub struct Buffer {
193 raw: sys::Buffer,
194 bytes: usize,
195 _device: std::sync::Arc<Inner>,
197 reservation: Option<std::sync::Arc<crate::residency::MemoryReservation>>,
198}
199
200#[derive(Default)]
201struct BudgetUses(
202 std::collections::HashMap<usize, std::sync::Arc<crate::residency::MemoryReservation>>,
203);
204impl BudgetUses {
205 fn retain(&mut self, views: &[View<'_>]) {
206 for view in views {
207 if let Some(reservation) = &view.owner.reservation {
208 self.0
209 .entry(std::sync::Arc::as_ptr(reservation) as usize)
210 .or_insert_with(|| reservation.clone());
211 }
212 }
213 }
214 fn clear(&mut self) {
215 self.0.clear();
216 }
217}
218
219unsafe impl Send for Buffer {}
224unsafe impl Sync for Buffer {}
225
226#[derive(Clone, Copy, Debug)]
231pub struct View<'a> {
232 raw: sys::BufferRef,
233 owner: &'a Buffer,
234}
235
236impl<'a> View<'a> {
237 fn new(raw: sys::BufferRef, owner: &'a Buffer) -> Self {
238 Self { raw, owner }
239 }
240 pub fn slice(self, offset: usize, length: usize) -> Result<Self> {
243 checked_span(offset, length, self.len())?;
244 Ok(Self::new(
245 sys::BufferRef {
246 offset: self.raw.offset + offset,
247 length,
248 ..self.raw
249 },
250 self.owner,
251 ))
252 }
253 #[must_use]
255 pub fn offset(self) -> usize {
256 self.raw.offset
257 }
258 #[must_use]
260 pub fn owner(self) -> &'a Buffer {
261 self.owner
262 }
263 #[must_use]
265 pub fn len(self) -> usize {
266 self.raw.length
267 }
268 #[must_use]
269 pub fn is_empty(self) -> bool {
271 self.raw.length == 0
272 }
273}
274
275impl Buffer {
276 pub(crate) fn charged_to(&self, budget: &crate::residency::MemoryBudget) -> bool {
277 self.reservation
278 .as_ref()
279 .is_some_and(|r| budget.contains(r))
280 }
281 pub(crate) fn device_id(&self) -> usize {
282 self._device.device as usize
283 }
284 pub fn bytes(&self) -> usize {
286 self.bytes
287 }
288
289 pub fn try_slice(&self, offset: usize, length: usize) -> Result<View<'_>> {
291 self.binding().slice(offset, length)
292 }
293
294 pub fn device_ptr(&self) -> Result<*mut std::ffi::c_void> {
299 let mut pointer = std::ptr::null_mut();
300 unsafe {
301 check(
302 sys::hrx_buffer_get_device_ptr(self.raw, &mut pointer),
303 "hrx_buffer_get_device_ptr",
304 )?;
305 }
306 Ok(pointer)
307 }
308
309 pub(crate) fn allocation_address(&self) -> Result<u64> {
311 let address = sys::interop()?.allocation_address;
312 let mut value = 0;
313 unsafe {
314 check(
315 address(self.raw, &mut value),
316 "querying GPU allocation address",
317 )?;
318 }
319 Ok(value)
320 }
321
322 #[cfg(feature = "npu")]
323 pub(crate) fn export_dmabuf(&self) -> Result<(std::os::fd::OwnedFd, u64)> {
324 use std::os::fd::FromRawFd;
325 let export = sys::interop()?.export_dmabuf;
326 let mut descriptor = -1;
327 let mut offset = 0;
328 unsafe {
329 check(
330 export(self.raw, &mut descriptor, &mut offset),
331 "exporting GPU allocation",
332 )?;
333 }
334 if descriptor < 0 {
335 return Err(Error::Message(
336 "native export returned invalid descriptor".into(),
337 ));
338 }
339 Ok((
340 unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor) },
341 offset,
342 ))
343 }
344
345 pub fn binding(&self) -> View<'_> {
347 View::new(
348 sys::BufferRef {
349 buffer: self.raw,
350 offset: 0,
351 length: self.bytes,
352 },
353 self,
354 )
355 }
356
357 pub fn slice(&self, offset: usize, length: usize) -> View<'_> {
363 self.try_slice(offset, length)
364 .expect("slice past the allocation or overflow")
365 }
366}
367
368impl Drop for Buffer {
369 fn drop(&mut self) {
370 unsafe { sys::hrx_buffer_release(self.raw) }
371 }
372}
373
374pub struct Kernel {
378 executable: std::sync::Arc<Executable>,
383 ordinal: u32,
384 info: sys::ExportInfo,
385 symbol: std::sync::Arc<str>,
386}
387
388struct Executable {
390 raw: sys::Executable,
391 device: std::sync::Arc<Inner>,
393}
394
395unsafe impl Send for Executable {}
400unsafe impl Sync for Executable {}
401
402impl Drop for Executable {
403 fn drop(&mut self) {
404 unsafe { sys::hrx_executable_release(self.raw) }
405 }
406}
407
408unsafe impl Send for Kernel {}
411unsafe impl Sync for Kernel {}
412
413impl Kernel {
414 pub(crate) fn device_id(&self) -> usize {
415 self.executable.device.device as usize
416 }
417 pub fn info(&self) -> &ExportInfo {
419 &self.info
420 }
421 pub fn symbol(&self) -> &str {
423 &self.symbol
424 }
425}
426
427impl Clone for Kernel {
431 fn clone(&self) -> Self {
432 Self {
433 executable: self.executable.clone(),
434 ordinal: self.ordinal,
435 info: self.info,
436 symbol: self.symbol.clone(),
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 #[test]
444 fn compiled_workgroup_dimensions_are_enforced() {
445 let info = super::ExportInfo {
446 workgroup_size: [64, 2, 1],
447 ..Default::default()
448 };
449 assert!(super::validate_export_launch(&info, [1; 3], [64, 2, 1]).is_ok());
450 assert!(super::validate_export_launch(&info, [1; 3], [128, 1, 1]).is_err());
451 assert!(super::validate_export_launch(&info, [0, 1, 1], [64, 2, 1]).is_err());
452 let dynamic = super::ExportInfo {
453 workgroup_size: [0, 2, 1],
454 ..Default::default()
455 };
456 assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 2, 1]).is_ok());
457 assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 1, 1]).is_err());
458 }
459 #[test]
461 fn a_slice_past_the_allocation_is_refused_even_when_the_sum_wraps() {
462 let checked = |offset, length, bytes| super::checked_span(offset, length, bytes).is_ok();
463 assert!(checked(0, 8, 8));
464 assert!(checked(4, 4, 8));
465 assert!(!checked(4, 5, 8));
466 assert!(!checked(usize::MAX, 2, 8));
468 assert!(!checked(usize::MAX - 1, 4, 8));
469 }
470}
471
472fn owns(inner: &std::sync::Arc<Inner>, buffer: &Buffer) -> Result<()> {
481 if inner.device == buffer._device.device {
482 Ok(())
483 } else {
484 Err(Error::Message("buffer belongs to another device".into()))
485 }
486}
487
488fn raw_bindings<'a>(
490 views: &[View<'_>],
491 stack: &'a mut [std::mem::MaybeUninit<sys::BufferRef>; 32],
492) -> std::borrow::Cow<'a, [sys::BufferRef]> {
493 if views.len() <= stack.len() {
494 for (out, view) in stack.iter_mut().zip(views) {
495 out.write(view.raw);
496 }
497 std::borrow::Cow::Borrowed(unsafe {
499 std::slice::from_raw_parts(stack.as_ptr().cast(), views.len())
500 })
501 } else {
502 std::borrow::Cow::Owned(views.iter().map(|v| v.raw).collect())
503 }
504}
505pub(crate) fn checked_span(offset: usize, length: usize, capacity: usize) -> Result<()> {
506 if offset.checked_add(length).is_none_or(|end| end > capacity) {
507 Err(Error::Message(format!(
508 "span {offset}+{length} exceeds {capacity} bytes"
509 )))
510 } else {
511 Ok(())
512 }
513}
514pub(crate) fn validate_launch(grid: [u32; 3], block: [u32; 3]) -> Result<()> {
515 if grid.contains(&0)
516 || block.contains(&0)
517 || block
518 .iter()
519 .try_fold(1u32, |n, b| n.checked_mul(*b))
520 .is_none_or(|n| n > 1024)
521 {
522 Err(Error::Message("invalid GPU launch dimensions".into()))
523 } else {
524 Ok(())
525 }
526}
527
528pub(crate) fn validate_export_launch(
529 info: &sys::ExportInfo,
530 grid: [u32; 3],
531 block: [u32; 3],
532) -> Result<()> {
533 validate_launch(grid, block)?;
534 if info
535 .workgroup_size
536 .iter()
537 .zip(block)
538 .any(|(&expected, actual)| expected != 0 && expected != actual)
539 {
540 return Err(Error::Message(format!(
541 "workgroup size {block:?} disagrees with compiled size {:?}",
542 info.workgroup_size
543 )));
544 }
545 Ok(())
546}
547
548#[derive(Clone, Debug)]
551pub struct Device {
552 index: i32,
553 target: Target,
554}
555impl Device {
556 pub fn supports_shared_interop(&self) -> Result<bool> {
559 match sys::interop() {
560 Ok(_) => Ok(true),
561 Err(Error::Unsupported(_)) => Ok(false),
562 Err(error) => Err(error),
563 }
564 }
565
566 pub fn open(index: i32) -> Result<Self> {
568 let (_, target) = open_device(index)?;
569 Ok(Self { index, target })
570 }
571 pub fn open_for(index: i32, expected: &str) -> Result<Self> {
573 let device = Self::open(index)?;
574 if device.target.as_str() != expected {
575 return Err(Error::Message(format!(
576 "workload requires {expected}, found {}",
577 device.target.as_str()
578 )));
579 }
580 Ok(device)
581 }
582 pub fn target(&self) -> &Target {
584 &self.target
585 }
586 pub fn stream(&self) -> Result<Stream> {
588 Stream::new(self.index)
589 }
590}
591
592pub struct Stream {
595 inner: std::sync::Arc<Inner>,
596 _not_sync: std::marker::PhantomData<std::cell::Cell<()>>,
598 staging: Vec<Buffer>,
604 staging_pool: Vec<Buffer>,
605 scratch: std::collections::BTreeMap<usize, Vec<Buffer>>,
606 scratch_bytes: usize,
607 scratch_limit: usize,
608 budget: Option<crate::residency::MemoryBudget>,
609 budget_uses: std::cell::RefCell<BudgetUses>,
613}
614impl Stream {
615 pub fn with_memory_budget(mut self, budget: crate::residency::MemoryBudget) -> Self {
621 self.budget = Some(budget);
622 self
623 }
624
625 pub fn memory_budget(&self) -> Option<&crate::residency::MemoryBudget> {
627 self.budget.as_ref()
628 }
629
630 fn reserve(
631 &self,
632 bytes: usize,
633 ) -> Result<Option<std::sync::Arc<crate::residency::MemoryReservation>>> {
634 self.budget
635 .as_ref()
636 .map(|budget| budget.reserve(bytes.max(1)).map(std::sync::Arc::new))
637 .transpose()
638 }
639 pub fn target(&self) -> &Target {
641 &self.inner.target
642 }
643
644 pub fn record_event(&mut self) -> Result<Event> {
647 let mut raw = std::ptr::null_mut();
648 unsafe {
649 check(
650 sys::hrx_event_create(self.inner.device, sys::EVENT_FLAG_DISABLE_TIMING, &mut raw),
651 "create event",
652 )?;
653 let event = Event {
654 raw,
655 inner: self.inner.clone(),
656 };
657 check(
658 sys::hrx_event_record(raw, self.inner.stream),
659 "record event",
660 )?;
661 Ok(event)
662 }
663 }
664
665 pub fn wait_event(&mut self, event: &Event) -> Result<()> {
668 if self.inner.device != event.inner.device {
669 return Err(Error::Message("event belongs to another device".into()));
670 }
671 unsafe {
672 check(
673 sys::hrx_stream_wait_event(self.inner.stream, event.raw),
674 "wait event",
675 )
676 }
677 }
678 pub fn open() -> Result<Self> {
680 Device::open(0)?.stream()
681 }
682 pub fn device_id(&self) -> usize {
686 self.inner.device as usize
687 }
688 pub fn id(&self) -> usize {
698 std::sync::Arc::as_ptr(&self.inner) as usize
699 }
700 pub fn allocate(&self, bytes: usize) -> Result<Buffer> {
702 let reservation = self.reserve(bytes)?;
703 let mut buffer = std::ptr::null_mut();
704 unsafe {
705 check(
706 sys::hrx_allocator_allocate_buffer(
707 sys::hrx_device_allocator(self.inner.device),
708 sys::BufferParams {
709 memory_type: sys::MEMORY_TYPE_DEVICE_LOCAL,
710 access: sys::MEMORY_ACCESS_ALL,
711 usage: sys::BUFFER_USAGE_DEFAULT,
712 queue_affinity: u64::MAX,
713 },
714 bytes.max(1),
715 &mut buffer,
716 ),
717 "hrx_buffer_allocate",
718 )?;
719 }
720 Ok(Buffer {
721 raw: buffer,
722 bytes: bytes.max(1),
723 _device: self.inner.clone(),
724 reservation,
725 })
726 }
727 pub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer> {
729 let buffer = self.allocate(bytes)?;
730 if bytes != 0 {
731 self.fill(buffer.try_slice(0, bytes)?, 0)?;
732 }
733 Ok(buffer)
734 }
735 pub unsafe fn import_host(
748 &self,
749 pointer: *mut std::ffi::c_void,
750 bytes: usize,
751 ) -> Result<Buffer> {
752 let reservation = self.reserve(bytes)?;
753 let mut buffer = std::ptr::null_mut();
754 unsafe {
755 check(
756 sys::hrx_allocator_import_buffer(
757 sys::hrx_device_allocator(self.inner.device),
758 sys::BufferParams {
759 memory_type: sys::MEMORY_TYPE_HOST_LOCAL
762 | sys::MEMORY_TYPE_HOST_COHERENT
763 | sys::MEMORY_TYPE_DEVICE_VISIBLE,
764 access: sys::MEMORY_ACCESS_ALL,
765 usage: sys::BUFFER_USAGE_DEFAULT,
766 queue_affinity: u64::MAX,
767 },
768 pointer,
769 bytes,
770 &mut buffer,
771 ),
772 "hrx_allocator_import_buffer",
773 )?;
774 }
775 Ok(Buffer {
776 raw: buffer,
777 bytes,
778 _device: self.inner.clone(),
779 reservation,
780 })
781 }
782
783 pub fn synchronize(&mut self) -> Result<()> {
785 self.synchronize_native()?;
786 self.reclaim_staging();
787 self.budget_uses.get_mut().clear();
788 Ok(())
789 }
790 pub fn upload_blocking(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
793 self.owns(dst.owner)?;
794 checked_span(0, bytes.len(), dst.len())?;
795 self.synchronize()?;
796 if bytes.is_empty() {
797 return Ok(());
798 }
799 unsafe {
800 check(
801 sys::hrx_synchronous_h2d(
802 self.inner.device,
803 bytes.as_ptr().cast(),
804 dst.raw.buffer,
805 dst.raw.offset,
806 bytes.len(),
807 ),
808 "hrx_synchronous_h2d",
809 )
810 }
811 }
812 pub fn upload_blocking_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
814 self.upload_blocking(dst.try_slice(offset, bytes.len())?, bytes)
815 }
816 pub fn read_blocking(&mut self, src: View<'_>, bytes: &mut [u8]) -> Result<()> {
818 self.owns(src.owner)?;
819 checked_span(0, bytes.len(), src.len())?;
820 self.synchronize()?;
821 if bytes.is_empty() {
822 return Ok(());
823 }
824 unsafe {
825 check(
826 sys::hrx_synchronous_d2h(
827 self.inner.device,
828 src.raw.buffer,
829 src.raw.offset,
830 bytes.as_mut_ptr().cast(),
831 bytes.len(),
832 ),
833 "hrx_synchronous_d2h",
834 )
835 }
836 }
837 pub fn read_blocking_at(
839 &mut self,
840 src: &Buffer,
841 offset: usize,
842 bytes: &mut [u8],
843 ) -> Result<()> {
844 self.read_blocking(src.try_slice(offset, bytes.len())?, bytes)
845 }
846 pub fn fill(&self, dst: View<'_>, value: u8) -> Result<()> {
848 self.owns(dst.owner)?;
849 if dst.is_empty() {
850 return Err(Error::Message("empty stream fill".into()));
851 }
852 self.budget_uses.borrow_mut().retain(&[dst]);
853 unsafe {
854 check(
855 sys::hrx_stream_fill_buffer(
856 self.inner.stream,
857 dst.raw.buffer,
858 dst.raw.offset,
859 dst.len(),
860 &value as *const u8 as *const c_void,
861 1,
862 ),
863 "hrx_stream_fill_buffer",
864 )
865 }
866 }
867 pub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()> {
869 self.owns(dst.owner)?;
870 self.owns(src.owner)?;
871 if dst.len() != src.len() || dst.is_empty() {
872 return Err(Error::Message(
873 "stream copy requires equal nonempty spans".into(),
874 ));
875 }
876 self.budget_uses.borrow_mut().retain(&[dst, src]);
877 unsafe {
878 check(
879 sys::hrx_stream_copy_buffer(
880 self.inner.stream,
881 src.raw.buffer,
882 src.raw.offset,
883 dst.raw.buffer,
884 dst.raw.offset,
885 src.len(),
886 ),
887 "hrx_stream_copy_buffer",
888 )
889 }
890 }
891 pub fn upload(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
896 self.owns(dst.owner)?;
897 checked_span(0, bytes.len(), dst.len())?;
898 if bytes.is_empty() {
899 return Ok(());
900 }
901 let next_capacity = self
903 .staging_pool
904 .iter()
905 .filter(|b| b.bytes >= bytes.len())
906 .map(|b| b.bytes)
907 .min()
908 .unwrap_or(bytes.len());
909 if !self.staging.is_empty()
910 && (self.staging.len() >= 8
911 || self
912 .staging
913 .iter()
914 .map(|b| b.bytes)
915 .sum::<usize>()
916 .saturating_add(next_capacity)
917 > STAGING_LIMIT)
918 && !self.submit()?.is_complete()?
919 {
920 self.synchronize()?;
921 }
922 let staging = if let Some(i) = self
923 .staging_pool
924 .iter()
925 .enumerate()
926 .filter(|(_, b)| b.bytes >= bytes.len())
927 .min_by_key(|(_, b)| b.bytes)
928 .map(|(i, _)| i)
929 {
930 self.staging_pool.swap_remove(i)
931 } else {
932 self.allocate_host(bytes.len())?
933 };
934 let raw = staging.raw;
935 self.budget_uses.get_mut().retain(&[dst, staging.binding()]);
936 unsafe {
937 let mut pointer = std::ptr::null_mut();
938 check(
939 sys::hrx_buffer_get_device_ptr(raw, &mut pointer),
940 "map staging",
941 )?;
942 std::ptr::copy_nonoverlapping(bytes.as_ptr(), pointer.cast::<u8>(), bytes.len());
943 self.staging.push(staging);
946 check(
947 sys::hrx_stream_copy_buffer(
948 self.inner.stream,
949 raw,
950 0,
951 dst.raw.buffer,
952 dst.raw.offset,
953 bytes.len(),
954 ),
955 "enqueue upload",
956 )
957 }
958 }
959 pub fn upload_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
961 self.upload(dst.try_slice(offset, bytes.len())?, bytes)
962 }
963 pub fn allocate_shared(&self, bytes: usize) -> Result<Buffer> {
969 self.allocate_host(bytes)
970 }
971
972 fn allocate_host(&self, bytes: usize) -> Result<Buffer> {
973 let reservation = self.reserve(bytes)?;
974 let mut raw = std::ptr::null_mut();
975 unsafe {
976 check(
977 sys::hrx_allocator_allocate_buffer(
978 sys::hrx_device_allocator(self.inner.device),
979 sys::BufferParams {
980 memory_type: sys::MEMORY_TYPE_HOST_LOCAL
981 | sys::MEMORY_TYPE_DEVICE_VISIBLE
982 | sys::MEMORY_TYPE_HOST_COHERENT,
983 access: sys::MEMORY_ACCESS_ALL,
984 usage: sys::BUFFER_USAGE_DEFAULT | sys::BUFFER_USAGE_MAPPING_SCOPED,
985 queue_affinity: u64::MAX,
986 },
987 bytes.max(1),
988 &mut raw,
989 ),
990 "allocate staging",
991 )?;
992 }
993 Ok(Buffer {
994 raw,
995 bytes,
996 _device: self.inner.clone(),
997 reservation,
998 })
999 }
1000 fn reclaim_staging(&mut self) {
1001 let mut cached = self.staging_pool.iter().map(|b| b.bytes).sum::<usize>();
1002 for buffer in self.staging.drain(..) {
1003 if self.staging_pool.len() < 8 && buffer.bytes <= STAGING_LIMIT.saturating_sub(cached) {
1004 cached += buffer.bytes;
1005 self.staging_pool.push(buffer);
1006 }
1007 }
1008 }
1009 pub fn submit(&mut self) -> Result<Submission<'_>> {
1012 unsafe {
1013 check(sys::hrx_stream_flush(self.inner.stream), "submit")?;
1014 }
1015 Ok(Submission { stream: self })
1016 }
1017 pub unsafe fn load(&self, path: &Path, symbol: &str) -> Result<Kernel> {
1022 let c_path = CString::new(path.as_os_str().as_encoded_bytes())
1023 .map_err(|_| Error::Message(format!("{} contains a NUL", path.display())))?;
1024 let c_family = TARGET_FAMILY;
1025 let c_key = self.inner.target.as_c_str();
1026 unsafe {
1027 let mut executable = std::ptr::null_mut();
1028 check(
1029 sys::hrx_executable_load_file(
1030 self.inner.device,
1031 c_path.as_ptr(),
1032 c_family.as_ptr(),
1033 c_key.as_ptr(),
1034 &mut executable,
1035 ),
1036 format_args!("loading {}", path.display()),
1037 )?;
1038 self.loaded_export(executable, symbol)
1039 }
1040 }
1041 pub unsafe fn load_artifact(&self, artifact: &crate::loom::Artifact) -> Result<Kernel> {
1046 if artifact.target() != self.inner.target.as_str() {
1047 return Err(Error::Message(
1048 "artifact target does not match this runtime".into(),
1049 ));
1050 }
1051 let bytes = artifact.bytes();
1052 let mut executable = std::ptr::null_mut();
1053 unsafe {
1054 check(
1055 sys::hrx_executable_load_data(
1056 self.inner.device,
1057 bytes.as_ptr().cast(),
1058 bytes.len(),
1059 TARGET_FAMILY.as_ptr(),
1060 self.inner.target.as_c_str().as_ptr(),
1061 &mut executable,
1062 ),
1063 "loading compiled artifact",
1064 )?;
1065 self.loaded_export(executable, artifact.symbol())
1066 }
1067 }
1068
1069 pub unsafe fn dispatch(
1075 &self,
1076 kernel: &Kernel,
1077 grid: [u32; 3],
1078 block: [u32; 3],
1079 constants: &Constants,
1080 bindings: &[View<'_>],
1081 ) -> Result<()> {
1082 if kernel.executable.device.device != self.inner.device {
1083 return Err(Error::Message("kernel belongs to another device".into()));
1084 }
1085 for view in bindings {
1086 owns(&self.inner, view.owner)?;
1087 }
1088 let mut binding_storage = [std::mem::MaybeUninit::uninit(); 32];
1089 let raw_bindings = raw_bindings(bindings, &mut binding_storage);
1090 validate_export_launch(&kernel.info, grid, block)?;
1091 if kernel.info.binding_count as usize != bindings.len()
1092 || kernel.info.constant_byte_length as usize != constants.len
1093 {
1094 return Err(Error::Message(
1095 "kernel binding or constant byte count mismatch".into(),
1096 ));
1097 }
1098 let config = sys::DispatchConfig {
1099 workgroup_count: grid,
1100 workgroup_size: block,
1101 subgroup_size: sys::SUBGROUP_SIZE_FROM_EXECUTABLE,
1102 };
1103 self.budget_uses.borrow_mut().retain(bindings);
1104 unsafe {
1105 check(
1106 sys::hrx_stream_dispatch(
1107 self.inner.stream,
1108 kernel.executable.raw,
1109 kernel.ordinal,
1110 &config,
1111 constants.bytes.as_ptr().cast(),
1112 constants.len,
1113 raw_bindings.as_ptr(),
1114 bindings.len(),
1115 0,
1116 ),
1117 "dispatch",
1118 )
1119 }
1120 }
1121
1122 pub fn scratch(&mut self, bytes: usize) -> Result<Buffer> {
1127 let choice = self
1128 .scratch
1129 .range(bytes.max(1)..=bytes.saturating_mul(2).max(1))
1130 .next()
1131 .map(|(&size, _)| size);
1132 if let Some(size) = choice {
1133 let buffers = self.scratch.get_mut(&size).unwrap();
1134 let buffer = buffers.pop().unwrap();
1135 if buffers.is_empty() {
1136 self.scratch.remove(&size);
1137 }
1138 self.scratch_bytes -= buffer.bytes;
1139 Ok(buffer)
1140 } else {
1141 self.allocate(bytes)
1142 }
1143 }
1144 pub fn recycle(&mut self, buffer: Buffer) -> Result<()> {
1151 if !std::sync::Arc::ptr_eq(&buffer._device, &self.inner) {
1152 return Err(Error::Message("scratch belongs to another stream".into()));
1153 }
1154 if buffer.bytes <= self.scratch_limit.saturating_sub(self.scratch_bytes) {
1155 self.scratch_bytes += buffer.bytes;
1156 self.scratch.entry(buffer.bytes).or_default().push(buffer);
1157 }
1158 Ok(())
1159 }
1160 pub fn set_scratch_limit(&mut self, bytes: usize) {
1162 self.scratch_limit = bytes;
1163 while self.scratch_bytes > bytes {
1164 if let Some((_, buffers)) = self.scratch.pop_last() {
1166 self.scratch_bytes -= buffers.iter().map(|b| b.bytes).sum::<usize>();
1167 } else {
1168 break;
1169 }
1170 }
1171 }
1172}
1173
1174pub struct Event {
1178 raw: sys::Event,
1179 inner: std::sync::Arc<Inner>,
1180}
1181unsafe impl Send for Event {}
1183unsafe impl Sync for Event {}
1184impl Event {
1185 pub fn is_complete(&self) -> Result<bool> {
1187 let mut complete = false;
1188 unsafe {
1189 check(sys::hrx_event_query(self.raw, &mut complete), "query event")?;
1190 }
1191 Ok(complete)
1192 }
1193 pub fn synchronize(&self) -> Result<()> {
1195 unsafe { check(sys::hrx_event_synchronize(self.raw), "synchronize event") }
1196 }
1197}
1198impl Drop for Event {
1199 fn drop(&mut self) {
1200 unsafe { sys::hrx_event_release(self.raw) }
1201 }
1202}
1203
1204#[must_use]
1207pub struct Submission<'a> {
1208 stream: &'a mut Stream,
1209}
1210impl Submission<'_> {
1211 pub fn is_complete(&mut self) -> Result<bool> {
1213 let mut done = false;
1214 unsafe {
1215 check(
1216 sys::hrx_stream_query(self.stream.inner.stream, &mut done),
1217 "query submission",
1218 )?;
1219 }
1220 if done {
1221 self.stream.reclaim_staging();
1222 self.stream.budget_uses.get_mut().clear();
1223 }
1224 Ok(done)
1225 }
1226 pub fn wait(self) -> Result<()> {
1228 self.stream.synchronize()
1229 }
1230}
1231
1232#[derive(Clone, Debug)]
1237pub struct Constants {
1238 bytes: [u8; 256],
1239 len: usize,
1240}
1241impl Default for Constants {
1242 fn default() -> Self {
1243 Self {
1244 bytes: [0; 256],
1245 len: 0,
1246 }
1247 }
1248}
1249impl Constants {
1250 pub fn indices(kernel: &Kernel, values: &[u32]) -> Result<Self> {
1263 let size = kernel.info.constant_byte_length as usize;
1264 let mut constants = Self::new();
1265 if values.is_empty() {
1266 if size != 0 {
1267 return Err(Error::Message("missing index constants".into()));
1268 }
1269 return Ok(constants);
1270 }
1271 if size == values.len() * 4 {
1272 for &value in values {
1273 constants.push(value)?;
1274 }
1275 } else if size == values.len() * 8 {
1276 for &value in values {
1277 constants.push(u64::from(value))?;
1278 }
1279 } else {
1280 return Err(Error::Message(
1281 "export is not a homogeneous 32/64-bit index list".into(),
1282 ));
1283 }
1284 Ok(constants)
1285 }
1286 pub fn new() -> Self {
1288 Self::default()
1289 }
1290 #[must_use = "constant packing can fail when capacity is exceeded"]
1291 pub fn push<T: Scalar>(&mut self, value: T) -> Result<()> {
1293 let bytes = value.bytes();
1294 checked_span(self.len, bytes.as_ref().len(), self.bytes.len())?;
1295 self.bytes[self.len..self.len + bytes.as_ref().len()].copy_from_slice(bytes.as_ref());
1296 self.len += bytes.as_ref().len();
1297 Ok(())
1298 }
1299 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
1301 let mut result = Self::new();
1302 checked_span(0, bytes.len(), result.bytes.len())?;
1303 result.bytes[..bytes.len()].copy_from_slice(bytes);
1304 result.len = bytes.len();
1305 Ok(result)
1306 }
1307 pub fn as_bytes(&self) -> &[u8] {
1309 &self.bytes[..self.len]
1310 }
1311}
1312mod sealed {
1313 pub trait Sealed {}
1314}
1315pub trait Scalar: sealed::Sealed {
1317 type Bytes: AsRef<[u8]>;
1319 fn bytes(self) -> Self::Bytes;
1321}
1322macro_rules! scalars {
1323 ($($ty:ty => $n:literal),*) => {
1324 $(
1325 impl sealed::Sealed for $ty {}
1326 impl Scalar for $ty {
1327 type Bytes = [u8; $n];
1328 fn bytes(self) -> Self::Bytes { self.to_le_bytes() }
1329 }
1330 )*
1331 };
1332}
1333scalars!(u32 => 4, i32 => 4, f32 => 4, u64 => 8, i64 => 8, f64 => 8);
1334impl Drop for Stream {
1335 fn drop(&mut self) {
1336 if (!self.staging.is_empty() || !self.budget_uses.get_mut().0.is_empty())
1337 && self.synchronize_native().is_err()
1338 {
1339 std::mem::forget(std::mem::take(&mut self.staging));
1341 std::mem::forget(std::mem::take(self.budget_uses.get_mut()));
1342 }
1343 }
1344}
1345
1346#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1352pub struct Node {
1353 graph: u64,
1354 index: u32,
1355}
1356
1357static NEXT_GRAPH_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1358
1359pub struct Graph<'a> {
1385 raw: sys::Graph,
1386 id: u64,
1388 nodes: Vec<sys::GraphNode>,
1389 inner: std::sync::Arc<Inner>,
1390 _resources: std::marker::PhantomData<(&'a Buffer, &'a Kernel)>,
1391 budget_uses: BudgetUses,
1392}
1393impl Stream {
1394 pub fn graph(&self) -> Result<Graph<'_>> {
1404 let mut raw = std::ptr::null_mut();
1405 unsafe {
1406 check(
1407 sys::hrx_graph_create(self.inner.device, 0, &mut raw),
1408 "create graph",
1409 )?;
1410 }
1411 Ok(Graph {
1412 raw,
1413 id: NEXT_GRAPH_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
1414 nodes: Vec::new(),
1415 inner: self.inner.clone(),
1416 _resources: std::marker::PhantomData,
1417 budget_uses: BudgetUses::default(),
1418 })
1419 }
1420 pub fn launch(&mut self, graph: &mut GraphExec) -> Result<()> {
1427 if !std::sync::Arc::ptr_eq(&graph.inner, &self.inner) {
1428 return Err(Error::Message("graph belongs to another stream".into()));
1429 }
1430 if matches!(graph.completion, GraphCompletion::Failed) {
1431 return Err(Error::Message(
1432 "graph has an untracked failed launch".into(),
1433 ));
1434 }
1435 graph.completion = GraphCompletion::Failed;
1439 unsafe {
1440 check(
1441 sys::hrx_graph_exec_launch(graph.raw, self.inner.stream),
1442 "launch graph",
1443 )?;
1444 let mut point = sys::TimelinePoint::default();
1445 check(
1446 sys::hrx_stream_get_timeline_position(self.inner.stream, &mut point),
1447 "snapshot graph completion",
1448 )?;
1449 graph.completion = GraphCompletion::Submitted(point);
1450 Ok(())
1451 }
1452 }
1453}
1454impl<'a> Graph<'a> {
1455 fn resolve<'s>(
1458 &self,
1459 after: &[Node],
1460 stack: &'s mut [std::mem::MaybeUninit<sys::GraphNode>; 16],
1461 ) -> Result<std::borrow::Cow<'s, [sys::GraphNode]>> {
1462 let handle = |node: &Node| -> Result<sys::GraphNode> {
1463 if node.graph != self.id || node.index as usize >= self.nodes.len() {
1464 return Err(Error::Message(
1465 "dependency node belongs to another graph".into(),
1466 ));
1467 }
1468 Ok(self.nodes[node.index as usize])
1469 };
1470 if after.len() <= stack.len() {
1474 let mut seen = [0u32; 16];
1477 let mut count = 0;
1478 for node in after {
1479 let raw = handle(node)?;
1480 if seen[..count].contains(&node.index) {
1481 continue;
1482 }
1483 seen[count] = node.index;
1484 stack[count].write(raw);
1485 count += 1;
1486 }
1487 Ok(std::borrow::Cow::Borrowed(unsafe {
1489 std::slice::from_raw_parts(stack.as_ptr().cast(), count)
1490 }))
1491 } else {
1492 let mut unique: Vec<sys::GraphNode> = Vec::with_capacity(after.len());
1493 for node in after {
1494 let raw = handle(node)?;
1495 if !unique.contains(&raw) {
1496 unique.push(raw);
1497 }
1498 }
1499 if unique.len() > u16::MAX as usize {
1501 return Err(Error::Message("too many graph dependencies".into()));
1502 }
1503 Ok(std::borrow::Cow::Owned(unique))
1504 }
1505 }
1506 fn record(&mut self, raw: sys::GraphNode) -> Node {
1507 let index = self.nodes.len() as u32;
1508 self.nodes.push(raw);
1509 Node {
1510 graph: self.id,
1511 index,
1512 }
1513 }
1514 pub fn fill(&mut self, after: &[Node], dst: View<'a>, pattern: u8) -> Result<Node> {
1516 owns(&self.inner, dst.owner)?;
1517 if dst.is_empty() {
1518 return Err(Error::Message("empty graph fill".into()));
1519 }
1520 self.budget_uses.retain(&[dst]);
1521 let attrs = sys::GraphFill {
1522 dst: dst.raw,
1523 pattern: pattern.into(),
1524 pattern_size: 1,
1525 };
1526 let mut storage = [std::mem::MaybeUninit::uninit(); 16];
1527 let deps = self.resolve(after, &mut storage)?;
1528 let mut next = std::ptr::null_mut();
1529 unsafe {
1530 check(
1531 sys::hrx_graph_add_fill_buffer_node(
1532 self.raw,
1533 deps.as_ptr(),
1534 deps.len(),
1535 &attrs,
1536 &mut next,
1537 ),
1538 "record fill",
1539 )?;
1540 }
1541 Ok(self.record(next))
1542 }
1543 pub fn copy(&mut self, after: &[Node], dst: View<'a>, src: View<'a>) -> Result<Node> {
1545 owns(&self.inner, dst.owner)?;
1546 owns(&self.inner, src.owner)?;
1547 if dst.len() != src.len() || dst.is_empty() {
1548 return Err(Error::Message(
1549 "graph copy requires equal nonempty spans".into(),
1550 ));
1551 }
1552 self.budget_uses.retain(&[dst, src]);
1553 let attrs = sys::GraphCopy {
1554 src: src.raw,
1555 dst: dst.raw,
1556 };
1557 let mut storage = [std::mem::MaybeUninit::uninit(); 16];
1558 let deps = self.resolve(after, &mut storage)?;
1559 let mut next = std::ptr::null_mut();
1560 unsafe {
1561 check(
1562 sys::hrx_graph_add_copy_buffer_node(
1563 self.raw,
1564 deps.as_ptr(),
1565 deps.len(),
1566 &attrs,
1567 &mut next,
1568 ),
1569 "record copy",
1570 )?;
1571 }
1572 Ok(self.record(next))
1573 }
1574 pub unsafe fn dispatch(
1581 &mut self,
1582 after: &[Node],
1583 kernel: &'a Kernel,
1584 grid: [u32; 3],
1585 block: [u32; 3],
1586 constants: &Constants,
1587 bindings: &[View<'a>],
1588 ) -> Result<Node> {
1589 if kernel.executable.device.device != self.inner.device {
1590 return Err(Error::Message("kernel belongs to another device".into()));
1591 }
1592 for view in bindings {
1593 owns(&self.inner, view.owner)?;
1594 }
1595 let mut binding_storage = [std::mem::MaybeUninit::uninit(); 32];
1596 let raw_bindings = raw_bindings(bindings, &mut binding_storage);
1597 validate_export_launch(&kernel.info, grid, block)?;
1598 if kernel.info.binding_count as usize != bindings.len()
1599 || kernel.info.constant_byte_length as usize != constants.len
1600 {
1601 return Err(Error::Message(
1602 "graph binding or constant byte count mismatch".into(),
1603 ));
1604 }
1605 self.budget_uses.retain(bindings);
1609 let attrs = sys::GraphKernel {
1610 executable: kernel.executable.raw,
1611 ordinal: kernel.ordinal,
1612 config: sys::DispatchConfig {
1613 workgroup_count: grid,
1614 workgroup_size: block,
1615 subgroup_size: sys::SUBGROUP_SIZE_FROM_EXECUTABLE,
1616 },
1617 constants: constants.bytes.as_ptr().cast(),
1618 constants_size: constants.len,
1619 bindings: raw_bindings.as_ptr(),
1620 binding_count: bindings.len(),
1621 flags: 0,
1622 };
1623 let mut storage = [std::mem::MaybeUninit::uninit(); 16];
1624 let deps = self.resolve(after, &mut storage)?;
1625 let mut next = std::ptr::null_mut();
1626 unsafe {
1627 check(
1628 sys::hrx_graph_add_kernel_node(
1629 self.raw,
1630 deps.as_ptr(),
1631 deps.len(),
1632 &attrs,
1633 &mut next,
1634 ),
1635 "record kernel",
1636 )?;
1637 }
1638 Ok(self.record(next))
1639 }
1640 pub fn join(&mut self, after: &[Node]) -> Result<Node> {
1646 let mut storage = [std::mem::MaybeUninit::uninit(); 16];
1647 let deps = self.resolve(after, &mut storage)?;
1648 let mut next = std::ptr::null_mut();
1649 unsafe {
1650 check(
1651 sys::hrx_graph_add_empty_node(self.raw, deps.as_ptr(), deps.len(), &mut next),
1652 "record join",
1653 )?;
1654 }
1655 Ok(self.record(next))
1656 }
1657 pub fn finish(mut self) -> Result<GraphExec> {
1660 let mut raw = std::ptr::null_mut();
1661 unsafe {
1662 check(
1663 sys::hrx_graph_instantiate(self.raw, 0, &mut raw),
1664 "instantiate graph",
1665 )?;
1666 }
1667 Ok(GraphExec {
1668 raw,
1669 inner: self.inner.clone(),
1670 completion: GraphCompletion::Idle,
1671 budget_uses: std::mem::take(&mut self.budget_uses),
1672 })
1673 }
1674}
1675impl Drop for Graph<'_> {
1676 fn drop(&mut self) {
1677 unsafe {
1678 sys::hrx_graph_release(self.raw);
1679 }
1680 }
1681}
1682pub struct GraphExec {
1688 raw: sys::GraphExec,
1689 inner: std::sync::Arc<Inner>,
1690 completion: GraphCompletion,
1691 budget_uses: BudgetUses,
1692}
1693
1694enum GraphCompletion {
1695 Idle,
1696 Submitted(sys::TimelinePoint),
1698 Failed,
1699}
1700unsafe impl Send for GraphExec {}
1711impl Drop for GraphExec {
1712 fn drop(&mut self) {
1713 let drained = match self.completion {
1721 GraphCompletion::Idle => true,
1722 GraphCompletion::Submitted(point) => check(
1723 unsafe { sys::hrx_semaphore_wait(point.semaphore, point.value, u64::MAX) },
1724 "wait for graph completion",
1725 )
1726 .is_ok(),
1727 GraphCompletion::Failed => false,
1728 };
1729 if drained {
1730 unsafe { sys::hrx_graph_exec_release(self.raw) };
1731 } else {
1732 std::mem::forget(std::mem::take(&mut self.budget_uses));
1733 }
1734 }
1735}
1736
1737#[must_use]
1740pub struct Readback {
1741 buffer: Buffer,
1742}
1743impl Stream {
1744 pub fn read(&mut self, source: View<'_>) -> Result<Readback> {
1746 self.owns(source.owner)?;
1747 let buffer = self.allocate_host(source.len())?;
1748 let raw = buffer.raw;
1749 self.budget_uses
1750 .get_mut()
1751 .retain(&[source, buffer.binding()]);
1752 unsafe {
1753 if !source.is_empty() {
1754 check(
1755 sys::hrx_stream_copy_buffer(
1756 self.inner.stream,
1757 source.raw.buffer,
1758 source.raw.offset,
1759 raw,
1760 0,
1761 source.len(),
1762 ),
1763 "enqueue readback",
1764 )?;
1765 }
1766 Ok(Readback { buffer })
1767 }
1768 }
1769}
1770impl Readback {
1771 pub fn wait(self, stream: &mut Stream) -> Result<Vec<u8>> {
1773 if !std::sync::Arc::ptr_eq(&self.buffer._device, &stream.inner) {
1774 return Err(Error::Message("readback belongs to another stream".into()));
1775 }
1776 stream.synchronize()?;
1777 let mut bytes = Vec::with_capacity(self.buffer.bytes);
1778 if self.buffer.bytes != 0 {
1779 let mut pointer = std::ptr::null_mut();
1780 unsafe {
1781 check(
1782 sys::hrx_buffer_get_device_ptr(self.buffer.raw, &mut pointer),
1783 "map completed readback",
1784 )?;
1785 std::ptr::copy_nonoverlapping(
1786 pointer.cast::<u8>(),
1787 bytes.as_mut_ptr(),
1788 self.buffer.bytes,
1789 );
1790 bytes.set_len(self.buffer.bytes);
1792 }
1793 }
1794 Ok(bytes)
1795 }
1796}
1797
1798impl std::fmt::Debug for Buffer {
1799 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1800 f.debug_struct("Buffer")
1801 .field("bytes", &self.bytes)
1802 .finish_non_exhaustive()
1803 }
1804}
1805
1806impl std::fmt::Debug for Kernel {
1807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1808 f.debug_struct("Kernel")
1809 .field("symbol", &self.symbol)
1810 .finish_non_exhaustive()
1811 }
1812}
1813
1814impl std::fmt::Debug for Stream {
1815 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1816 f.debug_struct("Stream")
1817 .field("scratch_bytes", &self.scratch_bytes)
1818 .finish_non_exhaustive()
1819 }
1820}
1821
1822impl std::fmt::Debug for Graph<'_> {
1823 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1824 f.debug_struct("Graph")
1825 .field("nodes", &self.nodes.len())
1826 .finish_non_exhaustive()
1827 }
1828}
1829
1830impl std::fmt::Debug for GraphExec {
1831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1832 f.debug_struct("GraphExec").finish_non_exhaustive()
1833 }
1834}
1835
1836impl std::fmt::Debug for Readback {
1837 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1838 f.debug_struct("Readback").finish_non_exhaustive()
1839 }
1840}
1841
1842impl std::fmt::Debug for Submission<'_> {
1843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1844 f.debug_struct("Submission").finish_non_exhaustive()
1845 }
1846}
1847
1848const STAGING_LIMIT: usize = 64 * 1024 * 1024;
1849
1850#[cfg(test)]
1851mod graph_completion_tests {
1852 use super::*;
1853
1854 #[test]
1855 #[ignore = "requires gfx1151"]
1856 fn dropping_a_graph_on_another_thread_does_not_submit_pending_work() -> Result<()> {
1857 let mut stream = Stream::open()?;
1858 let buffer = stream.allocate(4096)?;
1859 let mut graph = stream.graph()?;
1860 graph.fill(&[], buffer.binding(), 1)?;
1861 let mut replay = graph.finish()?;
1862 stream.launch(&mut replay)?;
1864 stream.launch(&mut replay)?;
1865 let mut before = sys::TimelinePoint::default();
1866 unsafe {
1867 check(
1868 sys::hrx_stream_get_timeline_position(stream.inner.stream, &mut before),
1869 "snapshot timeline before drop",
1870 )?;
1871 }
1872 stream.fill(buffer.binding(), 2)?;
1873 std::thread::scope(|scope| {
1874 let dropper = scope.spawn(move || drop(replay));
1875 for _ in 0..64 {
1876 stream.fill(buffer.binding(), 3).unwrap();
1877 }
1878 dropper.join().unwrap();
1879 });
1880 let mut after = sys::TimelinePoint::default();
1881 unsafe {
1882 check(
1883 sys::hrx_stream_get_timeline_position(stream.inner.stream, &mut after),
1884 "snapshot timeline after drop",
1885 )?;
1886 }
1887 assert_eq!(after.value, before.value, "drop submitted unrelated work");
1890 let mut actual = [0; 4096];
1891 stream.read_blocking(buffer.binding(), &mut actual)?;
1892 assert_eq!(actual, [3; 4096]);
1893 Ok(())
1894 }
1895}
1896
1897#[cfg(test)]
1898mod staging_tests {
1899 use super::*;
1900
1901 #[test]
1902 #[ignore = "requires gfx1151"]
1903 fn a_scratch_pool_only_accepts_the_stream_that_allocated_the_buffer() -> Result<()> {
1904 let device = Device::open(0)?;
1905 let mut owner = device.stream()?;
1906 let mut other = device.stream()?;
1907 let private = other.scratch(4096)?;
1908 let private_raw = private.raw;
1909 other.recycle(private)?;
1910
1911 let buffer = owner.allocate(4096)?;
1913 owner.fill(buffer.binding(), 0x5a)?;
1914 let ready = owner.record_event()?;
1915 other.wait_event(&ready)?;
1916 let mut seen = [0u8; 16];
1917 other.read_blocking(buffer.binding().slice(0, 16)?, &mut seen)?;
1918 assert_eq!(seen, [0x5a; 16], "a foreign stream can use the allocation");
1919
1920 assert!(
1922 other
1923 .recycle(buffer)
1924 .unwrap_err()
1925 .to_string()
1926 .contains("another stream")
1927 );
1928 assert!(owner.scratch.is_empty());
1929 assert_eq!(other.scratch_bytes, 4096);
1930 assert_eq!(other.scratch(4096)?.raw, private_raw);
1931 Ok(())
1932 }
1933
1934 #[test]
1935 #[ignore = "requires gfx1151"]
1936 fn completed_staging_is_reclaimed_and_reused_on_every_completion_path() -> Result<()> {
1937 let mut stream = Stream::open()?;
1938 let buffer = stream.allocate(1024)?;
1939 stream.upload(buffer.binding(), &[7; 1024])?;
1940 let original = stream.staging[0].raw;
1941 let mut output = [0; 1024];
1942 stream.read_blocking(buffer.binding(), &mut output)?;
1943 assert_eq!(output, [7; 1024]);
1944 assert!(stream.staging.is_empty());
1945 stream.upload(buffer.binding(), &[9; 1024])?;
1946 assert_eq!(stream.staging[0].raw, original);
1947 stream.upload_blocking(buffer.binding(), &[11; 1024])?;
1948 assert!(stream.staging.is_empty());
1949 stream.upload(buffer.binding(), &[12; 512])?;
1950 assert_eq!(stream.staging[0].raw, original);
1951 stream.synchronize()?;
1952 stream.upload(buffer.binding(), &[13; 1024])?;
1953 stream.synchronize_native()?; assert!(stream.submit()?.is_complete()?);
1955 assert!(stream.staging.is_empty());
1956 for _ in 0..32 {
1957 stream.upload(buffer.binding(), &[1; 1024])?;
1958 assert!(stream.staging.len() <= 8);
1959 }
1960 stream.synchronize()?;
1961 assert!(stream.staging_pool.len() <= 8);
1962 assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
1963 Ok(())
1964 }
1965
1966 #[test]
1967 #[ignore = "requires gfx1151"]
1968 fn uploads_batch_until_pressure_and_reuse_the_smallest_capacity() -> Result<()> {
1969 let mut stream = Stream::open()?;
1970 let buffer = stream.allocate(4096)?;
1971 for size in [1024, 2048, 4096] {
1972 stream.upload(buffer.binding(), &vec![7; size])?;
1973 }
1974 assert_eq!(stream.staging.len(), 3);
1975 let medium = stream.staging[1].raw;
1976 stream.synchronize()?;
1977 stream.upload(buffer.binding(), &[9; 1500])?;
1978 assert_eq!(stream.staging[0].raw, medium);
1979 for _ in 0..7 {
1980 stream.upload(buffer.binding(), &[11; 1024])?;
1981 }
1982 assert_eq!(stream.staging.len(), 8);
1983 stream.upload(buffer.binding(), &[13; 1024])?;
1984 assert_eq!(stream.staging.len(), 1);
1985 stream.synchronize()?;
1986 Ok(())
1987 }
1988}
1989
1990#[cfg(test)]
1991mod dag_probe {
1992 use super::*;
1993
1994 #[test]
1998 #[ignore = "requires gfx1151"]
1999 fn independent_nodes_beat_a_serial_chain() -> Result<()> {
2000 const NODES: usize = 64;
2001 let mut stream = Stream::open()?;
2002 let buffers: Vec<Buffer> = (0..NODES)
2003 .map(|_| stream.allocate(4096))
2004 .collect::<Result<_>>()?;
2005 let mut timings = Vec::new();
2006 for chained in [true, false] {
2007 let mut graph = stream.graph()?;
2008 let mut previous: Option<Node> = None;
2009 for buffer in &buffers {
2010 let after: &[Node] = match (chained, &previous) {
2011 (true, Some(node)) => std::slice::from_ref(node),
2012 _ => &[],
2013 };
2014 previous = Some(graph.fill(after, buffer.binding(), 0x3c)?);
2015 }
2016 let mut exec = graph.finish()?;
2017 for _ in 0..3 {
2018 stream.launch(&mut exec)?;
2019 stream.synchronize()?;
2020 }
2021 let mut samples = Vec::new();
2022 for _ in 0..9 {
2023 let start = std::time::Instant::now();
2024 stream.launch(&mut exec)?;
2025 stream.synchronize()?;
2026 samples.push(start.elapsed().as_secs_f64() * 1e6);
2027 }
2028 samples.sort_by(f64::total_cmp);
2029 println!(
2030 "{:<12} {NODES} nodes: {:8.1} us total, {:6.2} us/node",
2031 if chained { "serial" } else { "independent" },
2032 samples[4],
2033 samples[4] / NODES as f64
2034 );
2035 timings.push(samples[4]);
2036 let mut seen = [0u8; 4096];
2037 stream.read_blocking(buffers[NODES - 1].binding(), &mut seen)?;
2038 assert_eq!(seen, [0x3c; 4096], "every node ran");
2039 }
2040 assert!(
2044 timings[1] * 1.2 < timings[0],
2045 "omitting edges should be materially cheaper: {timings:?}"
2046 );
2047 Ok(())
2048 }
2049}