1use std::cmp::{Ordering as CmpOrdering, Reverse};
4use std::collections::BinaryHeap;
5use std::hash::{Hash, Hasher};
6use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
8use std::time::Instant;
9
10use dashmap::DashMap;
11use rustc_hash::{FxHashMap, FxHasher};
12use smallvec::SmallVec;
13use vyre_driver::BackendError;
14
15use super::pool::PoolReturn;
16
17static NEXT_BUFFER_ID: AtomicU64 = AtomicU64::new(1);
18static RESIDENT_BUFFERS: OnceLock<DashMap<u64, Weak<GpuBufferInner>>> = OnceLock::new();
19const STAGING_BUFFER_POOL_CLASS_CAP: usize = 16;
20
21fn resident_buffers() -> &'static DashMap<u64, Weak<GpuBufferInner>> {
22 RESIDENT_BUFFERS.get_or_init(DashMap::new)
23}
24
25fn pointer_identity_key<T>(ptr: *const T) -> u64 {
26 let mut hasher = FxHasher::default();
27 ptr.addr().hash(&mut hasher);
28 hasher.finish()
29}
30
31#[derive(Clone)]
37pub struct GpuBufferHandle {
38 inner: Arc<GpuBufferInner>,
39}
40
41struct GpuBufferInner {
42 id: u64,
43 buffer: Arc<wgpu::Buffer>,
44 byte_len: u64,
45 allocation_len: u64,
46 element_count: usize,
47 usage: wgpu::BufferUsages,
48 pool_return: Option<PoolReturn>,
49}
50
51#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
53pub struct StagingBufferPoolStats {
54 pub allocations: usize,
56 pub hits: usize,
58}
59
60#[derive(Clone, Default)]
68pub struct StagingBufferPool {
69 inner: Arc<Mutex<StagingBufferPoolInner>>,
70}
71
72impl std::fmt::Debug for StagingBufferPool {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("StagingBufferPool").finish_non_exhaustive()
75 }
76}
77
78#[derive(Default)]
79struct StagingBufferPoolInner {
80 free: FxHashMap<(u64, u32), SmallVec<[wgpu::Buffer; STAGING_BUFFER_POOL_CLASS_CAP]>>,
81 allocations: usize,
82 hits: usize,
83}
84
85impl StagingBufferPool {
86 fn lock_inner(&self) -> MutexGuard<'_, StagingBufferPoolInner> {
87 self.inner.lock().unwrap_or_else(|error| {
88 tracing::error!(
89 "Vyre WGPU staging buffer pool lock was poisoned: {error}. Fix: discard the pool after a panic; continuing with recovered state."
90 );
91 error.into_inner()
92 })
93 }
94
95 #[must_use]
97 #[inline]
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 #[must_use]
104 pub fn stats(&self) -> StagingBufferPoolStats {
105 let inner = self.lock_inner();
106 StagingBufferPoolStats {
107 allocations: inner.allocations,
108 hits: inner.hits,
109 }
110 }
111
112 pub fn acquire(
117 &self,
118 device: &wgpu::Device,
119 size: u64,
120 usage: wgpu::BufferUsages,
121 ) -> wgpu::Buffer {
122 let key = (size, usage.bits());
123 let mut inner = self.lock_inner();
124 if let Some(buffers) = inner.free.get_mut(&key) {
125 if let Some(buffer) = buffers.pop() {
126 inner.hits += 1;
127 return buffer;
128 }
129 }
130 inner.allocations += 1;
131 drop(inner);
132 device.create_buffer(&wgpu::BufferDescriptor {
133 label: Some("vyre staging readback"),
134 size,
135 usage,
136 mapped_at_creation: false,
137 })
138 }
139
140 pub fn release(&self, buffer: wgpu::Buffer, size: u64, usage: wgpu::BufferUsages) {
145 let key = (size, usage.bits());
146 let mut inner = self.lock_inner();
147 let buffers = inner.free.entry(key).or_insert_with(SmallVec::new);
148 if buffers.len() == STAGING_BUFFER_POOL_CLASS_CAP {
149 buffers.remove(0);
150 }
151 buffers.push(buffer);
152 }
153}
154
155impl GpuBufferHandle {
156 pub fn upload(
165 device: &wgpu::Device,
166 queue: &wgpu::Queue,
167 bytes: &[u8],
168 usage: wgpu::BufferUsages,
169 ) -> Result<Self, BackendError> {
170 let allocation_len = aligned_len(bytes.len())?;
171 let final_usage = usage | wgpu::BufferUsages::COPY_DST;
172 if allocation_len > 0 {
185 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
186 label: Some("vyre persistent upload"),
187 size: allocation_len,
188 usage: final_usage,
189 mapped_at_creation: true,
190 });
191 {
192 let mut mapped = buffer.slice(..).get_mapped_range_mut();
193 crate::padded_upload::write_padded_into_mapped(&mut mapped, bytes)?;
194 }
195 buffer.unmap();
196 let logical_len = u64::try_from(bytes.len()).map_err(|source| {
197 BackendError::new(format!(
198 "GPU upload logical byte length cannot fit u64: {source}. Fix: split the dispatch input."
199 ))
200 })?;
201 return Ok(Self::from_parts(
202 Arc::new(buffer),
203 logical_len,
204 allocation_len,
205 bytes.len(),
206 final_usage,
207 None,
208 ));
209 }
210 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
214 label: Some("vyre persistent upload"),
215 size: allocation_len,
216 usage: final_usage,
217 mapped_at_creation: false,
218 });
219 write_padded(queue, &buffer, bytes, allocation_len)?;
220 Ok(Self::from_parts(
221 Arc::new(buffer),
222 0,
223 allocation_len,
224 bytes.len(),
225 final_usage,
226 None,
227 ))
228 }
229
230 pub fn alloc(
237 device: &wgpu::Device,
238 len: u64,
239 usage: wgpu::BufferUsages,
240 ) -> Result<Self, BackendError> {
241 let allocation_len = aligned_len_u64(len, "persistent GPU allocation length")?;
242 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
243 label: Some("vyre persistent alloc"),
244 size: allocation_len,
245 usage,
246 mapped_at_creation: false,
247 });
248 let host_len = usize::try_from(len).map_err(|error| {
249 BackendError::new(format!(
250 "GpuBufferHandle::alloc received logical byte length {len} that does not fit usize on this host: {error}. Fix: shard the GPU buffer before allocating or run on a host with a wide enough address space."
251 ))
252 })?;
253 Ok(Self::from_parts(
254 Arc::new(buffer),
255 len,
256 allocation_len,
257 host_len,
258 usage,
259 None,
260 ))
261 }
262
263 pub fn readback(
273 &self,
274 device: &wgpu::Device,
275 queue: &wgpu::Queue,
276 out: &mut Vec<u8>,
277 ) -> Result<(), BackendError> {
278 self.readback_until(device, None, queue, out, None)
279 }
280
281 pub fn readback_prefix(
293 &self,
294 device: &wgpu::Device,
295 queue: &wgpu::Queue,
296 len: u64,
297 out: &mut Vec<u8>,
298 ) -> Result<(), BackendError> {
299 self.readback_prefix_until(device, None, queue, len, out, None)
300 }
301
302 pub fn readback_range(
312 &self,
313 device: &wgpu::Device,
314 queue: &wgpu::Queue,
315 byte_offset: u64,
316 len: u64,
317 out: &mut Vec<u8>,
318 ) -> Result<(), BackendError> {
319 self.readback_range_until(device, None, queue, byte_offset, len, out, None)
320 }
321
322 pub(crate) fn readback_until(
323 &self,
324 device: &wgpu::Device,
325 pool: Option<&StagingBufferPool>,
326 queue: &wgpu::Queue,
327 out: &mut Vec<u8>,
328 deadline: Option<Instant>,
329 ) -> Result<(), BackendError> {
330 self.readback_prefix_until(device, pool, queue, self.byte_len(), out, deadline)
331 }
332
333 pub(crate) fn readback_prefix_until(
334 &self,
335 device: &wgpu::Device,
336 pool: Option<&StagingBufferPool>,
337 queue: &wgpu::Queue,
338 len: u64,
339 out: &mut Vec<u8>,
340 deadline: Option<Instant>,
341 ) -> Result<(), BackendError> {
342 self.readback_range_until(device, pool, queue, 0, len, out, deadline)
343 }
344
345 pub(crate) fn readback_range_until(
346 &self,
347 device: &wgpu::Device,
348 pool: Option<&StagingBufferPool>,
349 queue: &wgpu::Queue,
350 byte_offset: u64,
351 len: u64,
352 out: &mut Vec<u8>,
353 deadline: Option<Instant>,
354 ) -> Result<(), BackendError> {
355 if !self.usage().contains(wgpu::BufferUsages::COPY_SRC) {
356 return Err(BackendError::new(
357 "GpuBufferHandle readback requires COPY_SRC usage. Fix: allocate terminal-output buffers with COPY_SRC.",
358 ));
359 }
360 let logical_end = byte_offset.checked_add(len).ok_or_else(|| {
361 BackendError::new(format!(
362 "GpuBufferHandle range readback overflows u64 at offset {byte_offset} len {len}. Fix: split the readback range before dispatch."
363 ))
364 })?;
365 if logical_end > self.byte_len() {
366 return Err(BackendError::new(format!(
367 "GpuBufferHandle range readback requested bytes [{byte_offset}..{logical_end}) from a {} byte buffer. Fix: clamp the requested range to the device-published count.",
368 self.byte_len()
369 )));
370 }
371 if len == 0 {
372 out.clear();
373 return Ok(());
374 }
375 let copy_start = byte_offset & !3;
376 let trim_start = byte_offset - copy_start;
377 let visible_copy_len = trim_start.checked_add(len).ok_or_else(|| {
378 BackendError::new(format!(
379 "GpuBufferHandle range readback copy length overflows u64 at trim {trim_start} len {len}. Fix: split the readback range before dispatch."
380 ))
381 })?;
382 let read_len = aligned_len_u64(visible_copy_len, "GPU readback visible copy length")?;
383 let copy_end = copy_start.checked_add(read_len).ok_or_else(|| {
384 BackendError::new(format!(
385 "GpuBufferHandle range readback aligned copy overflows u64 at start {copy_start} len {read_len}. Fix: split the readback range before dispatch."
386 ))
387 })?;
388 if copy_end > self.inner.allocation_len {
389 return Err(BackendError::new(format!(
390 "GpuBufferHandle range readback rounded bytes [{byte_offset}..{logical_end}) to aligned bytes [{copy_start}..{copy_end}), beyond allocation length {}. Fix: allocate buffers with 4-byte padding.",
391 self.inner.allocation_len
392 )));
393 }
394 let readback_usage = wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ;
395 let readback = if let Some(pool) = pool {
396 pool.acquire(device, read_len, readback_usage)
397 } else {
398 device.create_buffer(&wgpu::BufferDescriptor {
399 label: Some("vyre persistent handle readback"),
400 size: read_len,
401 usage: readback_usage,
402 mapped_at_creation: false,
403 })
404 };
405 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
406 label: Some("vyre persistent handle readback encoder"),
407 });
408 encoder.copy_buffer_to_buffer(self.buffer(), copy_start, &readback, 0, read_len);
409 let submission = queue.submit(std::iter::once(encoder.finish()));
410 let slice = readback.slice(0..read_len);
411 let (sender, receiver) = std::sync::mpsc::channel();
412 slice.map_async(wgpu::MapMode::Read, move |result| {
413 if let Err(error) = sender.send(result) {
414 tracing::error!(
415 ?error,
416 "persistent buffer readback map_async result was lost because the receiver dropped"
417 );
418 }
419 });
420 let mapping = if let Some(deadline) = deadline {
421 let mut backoff = crate::wait_backoff::AdaptiveWaitBackoff::from_micros(64, 2, 50, 5);
422 loop {
423 crate::runtime::device::poll_device_once(device)?;
424 match receiver.try_recv() {
425 Ok(result) => break result,
426 Err(std::sync::mpsc::TryRecvError::Empty) => {}
427 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
428 return Err(BackendError::new(
429 "persistent buffer readback channel closed before completion. Fix: keep the GPU device alive until readback completes.",
430 ));
431 }
432 }
433 let now = Instant::now();
434 if now >= deadline {
435 return Err(BackendError::new(
436 "dispatch cancelled after DispatchConfig.timeout before readback completed. Fix: raise DispatchConfig.timeout or split the program into smaller chunks.",
437 ));
438 }
439 backoff.idle_for(deadline.saturating_duration_since(now));
440 }
441 } else {
442 crate::runtime::device::poll_device_wait_for(device, submission)?;
443 receiver
444 .recv_timeout(std::time::Duration::from_secs(30))
445 .map_err(|source| {
446 BackendError::new(format!(
447 "persistent buffer readback callback did not complete after submission wait: {source}. Fix: keep the GPU device alive and inspect driver callback progress."
448 ))
449 })?
450 };
451 let result = mapping.map_err(|source| {
452 BackendError::new(format!(
453 "persistent buffer readback mapping failed: {source:?}. Fix: use COPY_SRC handles and MAP_READ staging buffers."
454 ))
455 });
456 result?;
457 let mapped = slice.get_mapped_range();
458 let visible_len = usize::try_from(len).map_err(|source| {
459 BackendError::new(format!(
460 "persistent buffer prefix length {len} cannot fit usize: {source}. Fix: split the buffer before readback.",
461 ))
462 })?;
463 let trim_start = usize::try_from(trim_start).map_err(|source| {
464 BackendError::new(format!(
465 "persistent buffer range trim offset {trim_start} cannot fit usize: {source}. Fix: split the buffer before readback.",
466 ))
467 })?;
468 let trim_end = trim_start.checked_add(visible_len).ok_or_else(|| {
469 BackendError::new(format!(
470 "persistent buffer range trim overflows usize at offset {trim_start} len {visible_len}. Fix: split the buffer before readback."
471 ))
472 })?;
473 let visible = &mapped[trim_start..trim_end];
474 if out.len() == visible_len {
475 out.copy_from_slice(visible);
476 } else {
477 if visible_len > out.capacity() {
478 let additional = visible_len - out.capacity();
479 out.try_reserve_exact(additional).map_err(|source| {
480 BackendError::new(format!(
481 "persistent buffer readback could not reserve {visible_len} output bytes exactly: {source}. Fix: lower max_output_bytes or stream readback in smaller shards."
482 ))
483 })?;
484 }
485 out.clear();
486 out.extend_from_slice(visible);
487 }
488 drop(mapped);
489 readback.unmap();
490 if let Some(pool) = pool {
491 pool.release(readback, read_len, readback_usage);
492 }
493 Ok(())
494 }
495
496 #[must_use]
498 pub fn id(&self) -> u64 {
499 self.inner.id
500 }
501
502 #[must_use]
509 pub(crate) fn allocation_identity(&self) -> u64 {
510 pointer_identity_key(Arc::as_ptr(&self.inner.buffer))
511 }
512
513 #[must_use]
515 pub fn from_resident_id(id: u64) -> Option<Self> {
516 let registry = resident_buffers();
517 let entry = registry.get(&id)?;
518 let upgraded = entry.value().upgrade();
519 drop(entry);
520 match upgraded {
521 Some(inner) => Some(Self { inner }),
522 None => {
523 registry.remove(&id);
524 None
525 }
526 }
527 }
528
529 #[must_use]
531 pub fn buffer(&self) -> &wgpu::Buffer {
532 &self.inner.buffer
533 }
534
535 #[must_use]
539 pub fn buffer_arc(&self) -> Arc<wgpu::Buffer> {
540 Arc::clone(&self.inner.buffer)
541 }
542
543 #[must_use]
545 pub fn byte_len(&self) -> u64 {
546 self.inner.byte_len
547 }
548
549 #[must_use]
551 pub fn allocation_len(&self) -> u64 {
552 self.inner.allocation_len
553 }
554
555 #[must_use]
557 pub fn element_count(&self) -> usize {
558 self.inner.element_count
559 }
560
561 #[must_use]
563 pub fn usage(&self) -> wgpu::BufferUsages {
564 self.inner.usage
565 }
566
567 pub(crate) fn from_parts(
568 buffer: Arc<wgpu::Buffer>,
569 byte_len: u64,
570 allocation_len: u64,
571 element_count: usize,
572 usage: wgpu::BufferUsages,
573 pool_return: Option<PoolReturn>,
574 ) -> Self {
575 let inner = Arc::new(GpuBufferInner {
576 id: NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed),
577 buffer,
578 byte_len,
579 allocation_len,
580 element_count,
581 usage,
582 pool_return,
583 });
584 resident_buffers().insert(inner.id, Arc::downgrade(&inner));
585 Self { inner }
586 }
587}
588
589impl std::fmt::Debug for GpuBufferHandle {
590 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591 formatter
592 .debug_struct("GpuBufferHandle")
593 .field("id", &self.id())
594 .field("byte_len", &self.byte_len())
595 .field("allocation_len", &self.allocation_len())
596 .field("element_count", &self.element_count())
597 .field("usage", &self.usage())
598 .finish()
599 }
600}
601
602impl Drop for GpuBufferInner {
603 fn drop(&mut self) {
604 resident_buffers().remove(&self.id);
605 if let Some(pool_return) = self.pool_return.take() {
606 pool_return.release(
607 Arc::clone(&self.buffer),
608 self.byte_len,
609 self.allocation_len,
610 self.usage,
611 );
612 }
613 }
614}
615
616pub(crate) fn aligned_len(len: usize) -> Result<u64, BackendError> {
617 let padded = aligned_len_usize(len, "GPU buffer length")?;
618 u64::try_from(padded).map_err(|source| {
619 BackendError::new(format!(
620 "GPU buffer length {padded} cannot fit u64: {source}. Fix: split the dispatch input."
621 ))
622 })
623}
624
625fn aligned_len_u64(len: u64, label: &'static str) -> Result<u64, BackendError> {
626 crate::numeric::WGPU_NUMERIC.align_up_u64(len, 4, 4, label)
627}
628
629fn aligned_len_usize(len: usize, label: &'static str) -> Result<usize, BackendError> {
630 crate::numeric::WGPU_NUMERIC.align_up_usize(len, 4, 4, label)
631}
632
633pub(crate) fn write_padded(
634 queue: &wgpu::Queue,
635 buffer: &wgpu::Buffer,
636 bytes: &[u8],
637 allocation_len: u64,
638) -> Result<(), BackendError> {
639 crate::padded_upload::write_padded_and_zero_fill(queue, buffer, bytes, allocation_len)
640}
641
642const BIND_GROUP_CACHE_CAP: usize = 256;
644
645type BindGroupHandleKey = SmallVec<[u64; 16]>;
648
649#[derive(Clone)]
657pub struct BindGroupCache {
658 cache: Arc<Mutex<BindGroupCacheInner>>,
659 hits: Arc<AtomicUsize>,
660 misses: Arc<AtomicUsize>,
661 evictions: Arc<AtomicUsize>,
662}
663
664struct BindGroupCacheInner {
665 entries: FxHashMap<BindGroupCacheKey, BindGroupCacheEntry>,
666 lru: BinaryHeap<Reverse<BindGroupLruEntry>>,
667 cap: usize,
668 next_generation: u64,
669}
670
671struct BindGroupCacheEntry {
672 bind_group: Arc<wgpu::BindGroup>,
673 last_seen: u64,
674}
675
676#[derive(Clone, Debug, Eq, PartialEq)]
677struct BindGroupLruEntry {
678 last_seen: u64,
679 key: BindGroupCacheKey,
680}
681
682impl Ord for BindGroupLruEntry {
683 fn cmp(&self, other: &Self) -> CmpOrdering {
684 self.last_seen
685 .cmp(&other.last_seen)
686 .then_with(|| self.key.cmp(&other.key))
687 }
688}
689
690impl PartialOrd for BindGroupLruEntry {
691 fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
692 Some(self.cmp(other))
693 }
694}
695
696fn push_bind_group_handle_key(key: &mut BindGroupHandleKey, handle: &GpuBufferHandle) -> bool {
697 key.push(handle.allocation_identity());
698 let Ok(aligned_len) = aligned_len_u64(handle.byte_len(), "bind-group handle key byte length")
699 else {
700 key.pop();
701 return false;
702 };
703 key.push(aligned_len);
704 true
705}
706
707#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
708struct BindGroupCacheKey {
709 layout_id: usize,
710 handles: BindGroupHandleKey,
711}
712
713impl std::fmt::Debug for BindGroupCache {
714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 f.debug_struct("BindGroupCache")
716 .field("hits", &self.hits.load(Ordering::Relaxed))
717 .field("misses", &self.misses.load(Ordering::Relaxed))
718 .field("evictions", &self.evictions.load(Ordering::Relaxed))
719 .field("entries", &self.lock_cache().entries.len())
720 .finish_non_exhaustive()
721 }
722}
723
724impl Default for BindGroupCache {
725 fn default() -> Self {
726 Self::new()
727 }
728}
729
730impl BindGroupCacheInner {
731 fn next_lru_generation(&mut self) -> u64 {
732 let generation = self.next_generation;
733 self.next_generation = self.next_generation.wrapping_add(1);
734 generation
735 }
736
737 fn touch_existing(&mut self, key: &BindGroupCacheKey) -> Option<Arc<wgpu::BindGroup>> {
738 let generation = self.next_lru_generation();
739 let bind_group = {
740 let entry = self.entries.get_mut(key)?;
741 entry.last_seen = generation;
742 Arc::clone(&entry.bind_group)
743 };
744 self.lru.push(Reverse(BindGroupLruEntry {
745 last_seen: generation,
746 key: key.clone(),
747 }));
748 self.compact_lru_if_needed();
749 Some(bind_group)
750 }
751
752 fn insert_entry(&mut self, key: BindGroupCacheKey, bind_group: Arc<wgpu::BindGroup>) {
753 let generation = self.next_lru_generation();
754 self.entries.insert(
755 key.clone(),
756 BindGroupCacheEntry {
757 bind_group,
758 last_seen: generation,
759 },
760 );
761 self.lru.push(Reverse(BindGroupLruEntry {
762 last_seen: generation,
763 key,
764 }));
765 self.compact_lru_if_needed();
766 }
767
768 fn evict_to_cap(&mut self, mut on_evict: impl FnMut()) {
769 while self.entries.len() > self.cap {
770 let Some(key) = self.pop_lru_key() else { break };
771 if self.entries.remove(&key).is_some() {
772 on_evict();
773 }
774 }
775 }
776
777 fn pop_lru_key(&mut self) -> Option<BindGroupCacheKey> {
778 while let Some(Reverse(entry)) = self.lru.pop() {
779 if self
780 .entries
781 .get(&entry.key)
782 .is_some_and(|current| current.last_seen == entry.last_seen)
783 {
784 return Some(entry.key);
785 }
786 }
787 None
788 }
789
790 fn compact_lru_if_needed(&mut self) {
791 let live = self.entries.len();
792 if let Some(limit) = stale_lru_limit(live) {
793 if self.lru.len() <= limit {
794 return;
795 }
796 }
797 self.lru.clear();
798 self.lru.extend(self.entries.iter().map(|(key, entry)| {
799 Reverse(BindGroupLruEntry {
800 last_seen: entry.last_seen,
801 key: key.clone(),
802 })
803 }));
804 }
805}
806
807fn stale_lru_limit(live: usize) -> Option<usize> {
808 live.checked_mul(4).map(|limit| limit.max(8))
809}
810
811impl BindGroupCache {
812 fn lock_cache(&self) -> MutexGuard<'_, BindGroupCacheInner> {
813 self.cache.lock().unwrap_or_else(|error| {
814 tracing::error!(
815 "Vyre WGPU bind-group cache lock was poisoned: {error}. Fix: discard the cache after a panic; continuing with recovered state."
816 );
817 error.into_inner()
818 })
819 }
820
821 #[must_use]
823 pub fn new() -> Self {
824 Self::with_cap(BIND_GROUP_CACHE_CAP)
825 }
826
827 #[must_use]
830 pub fn with_cap(cap: usize) -> Self {
831 Self {
832 cache: Arc::new(Mutex::new(BindGroupCacheInner {
833 entries: FxHashMap::default(),
834 lru: BinaryHeap::new(),
835 cap: cap.max(1),
836 next_generation: 0,
837 })),
838 hits: Arc::new(AtomicUsize::new(0)),
839 misses: Arc::new(AtomicUsize::new(0)),
840 evictions: Arc::new(AtomicUsize::new(0)),
841 }
842 }
843
844 pub fn get_or_create(
852 &self,
853 layout_id: usize,
854 handles: &[GpuBufferHandle],
855 factory: impl FnOnce() -> wgpu::BindGroup,
856 ) -> Arc<wgpu::BindGroup> {
857 let Some(key_part_count) = handles.len().checked_mul(2) else {
858 self.misses.fetch_add(1, Ordering::Relaxed);
859 return Arc::new(factory());
860 };
861 let mut key_parts = SmallVec::with_capacity(key_part_count);
862 for handle in handles {
863 if !push_bind_group_handle_key(&mut key_parts, handle) {
864 self.misses.fetch_add(1, Ordering::Relaxed);
865 return Arc::new(factory());
866 }
867 }
868 self.get_or_create_by_ids(layout_id, key_parts, factory)
869 }
870
871 pub(crate) fn get_or_create_by_ids(
872 &self,
873 layout_id: usize,
874 handles: SmallVec<[u64; 16]>,
875 factory: impl FnOnce() -> wgpu::BindGroup,
876 ) -> Arc<wgpu::BindGroup> {
877 let key = BindGroupCacheKey { layout_id, handles };
878 {
879 let mut cache = self.lock_cache();
880 if let Some(existing) = cache.touch_existing(&key) {
881 self.hits.fetch_add(1, Ordering::Relaxed);
882 return existing;
883 }
884 }
885 let bg = Arc::new(factory());
886 let mut cache = self.lock_cache();
887 cache.insert_entry(key, Arc::clone(&bg));
888 cache.evict_to_cap(|| {
889 self.evictions.fetch_add(1, Ordering::Relaxed);
890 });
891 self.misses.fetch_add(1, Ordering::Relaxed);
892 bg
893 }
894
895 pub(crate) fn get_by_ids(
896 &self,
897 layout_id: usize,
898 handles: &[u64],
899 ) -> Option<Arc<wgpu::BindGroup>> {
900 let key = BindGroupCacheKey {
901 layout_id,
902 handles: SmallVec::from_slice(handles),
903 };
904 let mut cache = self.lock_cache();
905 let existing = cache.touch_existing(&key)?;
906 self.hits.fetch_add(1, Ordering::Relaxed);
907 Some(existing)
908 }
909
910 pub(crate) fn insert_by_ids(
911 &self,
912 layout_id: usize,
913 handles: &[u64],
914 bind_group: wgpu::BindGroup,
915 ) -> Arc<wgpu::BindGroup> {
916 let key = BindGroupCacheKey {
917 layout_id,
918 handles: SmallVec::from_slice(handles),
919 };
920 let mut cache = self.lock_cache();
921 if let Some(existing) = cache.touch_existing(&key) {
922 self.hits.fetch_add(1, Ordering::Relaxed);
923 return existing;
924 }
925 let bg = Arc::new(bind_group);
926 cache.insert_entry(key, Arc::clone(&bg));
927 cache.evict_to_cap(|| {
928 self.evictions.fetch_add(1, Ordering::Relaxed);
929 });
930 self.misses.fetch_add(1, Ordering::Relaxed);
931 bg
932 }
933
934 #[must_use]
936 pub fn stats(&self) -> BindGroupCacheStats {
937 BindGroupCacheStats {
938 hits: self.hits.load(Ordering::Relaxed),
939 misses: self.misses.load(Ordering::Relaxed),
940 evictions: self.evictions.load(Ordering::Relaxed),
941 entries: self.lock_cache().entries.len(),
942 }
943 }
944}
945
946#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
948pub struct BindGroupCacheStats {
949 pub hits: usize,
951 pub misses: usize,
953 pub evictions: usize,
955 pub entries: usize,
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962
963 #[test]
966 fn staging_pool_reuses_buffers_on_hot_readback_loop() {
967 let arc = crate::runtime::cached_device()
968 .expect("Fix: GPU device is required for staging pool test");
969 let (device, queue) = &*arc;
970
971 let contents: Vec<u8> = vec![0xAB; 64];
973 let handle =
974 GpuBufferHandle::upload(device, queue, &contents, wgpu::BufferUsages::COPY_SRC)
975 .expect("Fix: upload should succeed");
976
977 let pool = StagingBufferPool::new();
978
979 for _ in 0..100 {
980 let mut out = Vec::new();
981 handle
982 .readback_until(device, Some(&pool), queue, &mut out, None)
983 .expect("Fix: pooled readback should succeed");
984 assert_eq!(out, contents, "readback bytes must match uploaded bytes");
985 }
986
987 let stats = pool.stats();
988 assert!(
989 stats.allocations <= 2,
990 "hot loop of 100 identical readbacks should allocate at most 2 staging buffers, got {} allocations and {} hits",
991 stats.allocations,
992 stats.hits
993 );
994 }
995
996 #[test]
998 fn readback_without_pool_always_allocates() {
999 let arc = crate::runtime::cached_device()
1000 .expect("Fix: GPU device is required for readback regression test");
1001 let (device, queue) = &*arc;
1002
1003 let contents: Vec<u8> = vec![0xCD; 32];
1004 let handle =
1005 GpuBufferHandle::upload(device, queue, &contents, wgpu::BufferUsages::COPY_SRC)
1006 .expect("Fix: upload should succeed");
1007
1008 for _ in 0..5 {
1009 let mut out = Vec::new();
1010 handle
1011 .readback(device, queue, &mut out)
1012 .expect("Fix: unpooled readback should succeed");
1013 assert_eq!(out, contents);
1014 }
1015 }
1016
1017 #[test]
1023 fn mapped_upload_roundtrips_exact_bytes_across_boundaries() {
1024 let arc = crate::runtime::cached_device()
1025 .expect("Fix: live GPU device required for mapped upload roundtrip test");
1026 let (device, queue) = &*arc;
1027 for &len in &[1usize, 3, 4, 5, 257, (1 << 20) + 3] {
1031 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
1032 let handle = GpuBufferHandle::upload(
1033 device,
1034 queue,
1035 &contents,
1036 wgpu::BufferUsages::COPY_SRC,
1037 )
1038 .expect("Fix: mapped upload should succeed at every size");
1039 let mut out = Vec::new();
1040 handle
1041 .readback(device, queue, &mut out)
1042 .expect("Fix: readback should succeed");
1043 assert_eq!(
1044 out, contents,
1045 "mapped upload corrupted {len}-byte payload: readback != input"
1046 );
1047 }
1048 }
1049
1050 #[test]
1054 fn write_padded_into_mapped_zeroes_the_tail() {
1055 let mut mapped = [0xAAu8; 8];
1058 let bytes = [1u8, 2, 3, 4, 5];
1059 crate::padded_upload::write_padded_into_mapped(&mut mapped, &bytes)
1060 .expect("Fix: filling a large-enough mapped slice must succeed");
1061 assert_eq!(&mapped[..5], &bytes);
1062 assert_eq!(&mapped[5..], &[0u8, 0, 0], "alignment tail must be zeroed");
1063 let mut too_small = [0u8; 2];
1065 assert!(crate::padded_upload::write_padded_into_mapped(&mut too_small, &bytes).is_err());
1066 }
1067
1068 #[test]
1069 fn resident_registry_handles_concurrent_lookup_and_drop() {
1070 let arc = crate::runtime::cached_device()
1071 .expect("Fix: GPU device is required for resident registry concurrency test");
1072 let (device, queue) = &*arc;
1073 let handle =
1074 GpuBufferHandle::upload(device, queue, &[1, 2, 3, 4], wgpu::BufferUsages::COPY_SRC)
1075 .expect("Fix: upload should register a resident buffer");
1076 let id = handle.id();
1077
1078 let readers = (0..8)
1082 .map(|_| {
1083 std::thread::spawn(move || {
1084 for _ in 0..1_000 {
1085 let resident = GpuBufferHandle::from_resident_id(id)
1086 .expect("Fix: resident id must resolve while handle is alive");
1087 assert_eq!(resident.id(), id);
1088 }
1089 })
1090 })
1091 .collect::<Vec<_>>();
1092 for reader in readers {
1093 reader
1094 .join()
1095 .expect("Fix: concurrent resident lookups must not panic");
1096 }
1097
1098 drop(handle);
1101 assert!(
1102 GpuBufferHandle::from_resident_id(id).is_none(),
1103 "dropped handles must be removed from the resident registry"
1104 );
1105 }
1106
1107 #[test]
1108 fn poisoned_staging_pool_lock_recovers_without_aborting_dispatch_path() {
1109 let pool = StagingBufferPool::new();
1110 let poisoned = pool.clone();
1111 let _ = std::thread::spawn(move || {
1112 let _guard = poisoned.lock_inner();
1113 panic!("poison staging buffer pool");
1114 })
1115 .join();
1116
1117 std::panic::catch_unwind(|| {
1118 let _ = pool.stats();
1119 })
1120 .expect("Fix: poisoned staging pool must recover so GPU readback pooling does not abort");
1121 }
1122
1123 #[test]
1124 fn poisoned_bind_group_cache_lock_recovers_without_aborting_dispatch_path() {
1125 let cache = BindGroupCache::new();
1126 let poisoned = cache.clone();
1127 let _ = std::thread::spawn(move || {
1128 let _guard = poisoned.lock_cache();
1129 panic!("poison bind group cache");
1130 })
1131 .join();
1132
1133 std::panic::catch_unwind(|| {
1134 let _ = cache.stats();
1135 })
1136 .expect("Fix: poisoned bind-group cache must recover so GPU dispatch does not abort");
1137 }
1138
1139 #[test]
1140 fn bind_group_cache_lru_heap_stays_capacity_scale() {
1141 let arc = crate::runtime::cached_device()
1142 .expect("Fix: GPU device is required for bind-group cache test");
1143 let (device, _) = &*arc;
1144 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1145 label: Some("vyre bind-group cache lru test layout"),
1146 entries: &[wgpu::BindGroupLayoutEntry {
1147 binding: 0,
1148 visibility: wgpu::ShaderStages::COMPUTE,
1149 ty: wgpu::BindingType::Buffer {
1150 ty: wgpu::BufferBindingType::Storage { read_only: true },
1151 has_dynamic_offset: false,
1152 min_binding_size: wgpu::BufferSize::new(4),
1153 },
1154 count: None,
1155 }],
1156 });
1157 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1158 label: Some("vyre bind-group cache lru test buffer"),
1159 size: 4,
1160 usage: wgpu::BufferUsages::STORAGE,
1161 mapped_at_creation: false,
1162 });
1163 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1164 label: Some("vyre bind-group cache lru test bind group"),
1165 layout: &layout,
1166 entries: &[wgpu::BindGroupEntry {
1167 binding: 0,
1168 resource: buffer.as_entire_binding(),
1169 }],
1170 });
1171 let cache = BindGroupCache::with_cap(4);
1172
1173 for i in 0..64u64 {
1174 cache.insert_by_ids(1, &[i, 4], bind_group.clone());
1175 }
1176
1177 let inner = cache.lock_cache();
1178 assert_eq!(inner.entries.len(), 4);
1179 assert!(
1180 inner.lru.len() <= inner.entries.len().saturating_mul(4).max(8),
1181 "Fix: bind-group LRU heap must compact stale entries to cache-capacity scale"
1182 );
1183 }
1184}