use crate::compat::Vec;
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuMemoryProfile {
Discrete,
Integrated,
Cpu,
}
impl GpuMemoryProfile {
pub fn from_device_type(device_type: super::adapter::GpuDeviceType) -> Self {
match device_type {
super::adapter::GpuDeviceType::DiscreteGpu => Self::Discrete,
super::adapter::GpuDeviceType::IntegratedGpu => Self::Integrated,
_ => Self::Cpu,
}
}
pub fn buffer_pool_size(&self) -> usize {
match self {
Self::Discrete => 64 * 1024 * 1024, Self::Integrated => 16 * 1024 * 1024, Self::Cpu => 4 * 1024 * 1024, }
}
pub fn ring_buffer_slots(&self) -> usize {
match self {
Self::Discrete => 3, Self::Integrated => 2, Self::Cpu => 2, }
}
pub fn max_upload_batch_size(&self) -> usize {
match self {
Self::Discrete => 4 * 1024 * 1024, Self::Integrated => 1024 * 1024, Self::Cpu => 256 * 1024, }
}
pub fn merge_small_uploads(&self) -> bool {
matches!(self, Self::Discrete | Self::Integrated)
}
pub fn small_upload_threshold(&self) -> usize {
match self {
Self::Discrete => 64 * 1024, Self::Integrated => 16 * 1024, Self::Cpu => 4 * 1024, }
}
pub fn mapping_strategy(&self) -> MappingStrategy {
match self {
Self::Discrete => MappingStrategy::PersistentMapped,
Self::Integrated => MappingStrategy::PersistentMapped,
Self::Cpu => MappingStrategy::WriteCombined,
}
}
pub fn fence_strategy(&self) -> FenceStrategy {
match self {
Self::Discrete => FenceStrategy::GpuTimestamp,
Self::Integrated => FenceStrategy::CpuFence,
Self::Cpu => FenceStrategy::CpuFence,
}
}
pub fn use_coherent_memory(&self) -> bool {
matches!(self, Self::Integrated | Self::Cpu)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MappingStrategy {
PersistentMapped,
WriteCombined,
Cached,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FenceStrategy {
GpuTimestamp,
CpuFence,
Spinlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuBufferPoolError {
NoRingSlots,
InvalidAlignment,
}
impl fmt::Display for GpuBufferPoolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoRingSlots => write!(f, "ring_slots must be at least one"),
Self::InvalidAlignment => write!(f, "alignment must be a non-zero power of two"),
}
}
}
#[cfg(not(alloc_frugal))]
impl core::error::Error for GpuBufferPoolError {}
#[derive(Debug, Clone)]
pub struct StagingBufferPoolConfig {
pub pool_size: usize,
pub ring_slots: usize,
pub max_batch_size: usize,
pub merge_uploads: bool,
pub small_upload_threshold: usize,
pub mapping_strategy: MappingStrategy,
pub fence_strategy: FenceStrategy,
pub use_coherent_memory: bool,
pub alignment: usize,
}
impl StagingBufferPoolConfig {
pub fn new() -> Self {
Self::for_profile(GpuMemoryProfile::Discrete)
}
pub fn for_profile(profile: GpuMemoryProfile) -> Self {
Self {
pool_size: profile.buffer_pool_size(),
ring_slots: profile.ring_buffer_slots(),
max_batch_size: profile.max_upload_batch_size(),
merge_uploads: profile.merge_small_uploads(),
small_upload_threshold: profile.small_upload_threshold(),
mapping_strategy: profile.mapping_strategy(),
fence_strategy: profile.fence_strategy(),
use_coherent_memory: profile.use_coherent_memory(),
alignment: 256, }
}
pub fn discrete() -> Self {
Self::for_profile(GpuMemoryProfile::Discrete)
}
pub fn integrated() -> Self {
Self::for_profile(GpuMemoryProfile::Integrated)
}
pub fn cpu() -> Self {
Self::for_profile(GpuMemoryProfile::Cpu)
}
pub fn with_memory_limit(mut self, available_memory: usize) -> Self {
let max_pool_size = available_memory / 4; self.pool_size = self.pool_size.min(max_pool_size);
self
}
pub fn validate(&self) -> Result<(), GpuBufferPoolError> {
if self.ring_slots == 0 {
return Err(GpuBufferPoolError::NoRingSlots);
}
if self.alignment == 0 || !self.alignment.is_power_of_two() {
return Err(GpuBufferPoolError::InvalidAlignment);
}
Ok(())
}
}
crate::impl_default_via_new!(StagingBufferPoolConfig);
#[derive(Debug)]
pub struct GpuRingBufferSlot {
pub index: usize,
pub offset: usize,
pub size: usize,
pub in_use: bool,
pub last_used_frame: u64,
}
fn align_up_checked(value: usize, alignment: usize) -> Option<usize> {
value.checked_add(alignment - 1).map(|v| v & !(alignment - 1))
}
pub struct GpuStagingBufferPool {
config: StagingBufferPoolConfig,
slots: Vec<GpuRingBufferSlot>,
current_slot: usize,
current_frame: u64,
total_allocated: usize,
total_used: usize,
fallback_pool: Option<crate::memory::BufferPool>,
next_fallback_id: u64,
}
impl GpuStagingBufferPool {
pub fn new(config: StagingBufferPoolConfig) -> Result<Self, GpuBufferPoolError> {
config.validate()?;
let alignment = config.alignment;
let slot_size = (config.pool_size / config.ring_slots) & !(alignment - 1);
let mut slots = Vec::with_capacity(config.ring_slots);
for i in 0..config.ring_slots {
slots.push(GpuRingBufferSlot {
index: i,
offset: i * slot_size,
size: slot_size,
in_use: false,
last_used_frame: 0,
});
}
Ok(Self {
config,
slots,
current_slot: 0,
current_frame: 0,
total_allocated: 0,
total_used: 0,
fallback_pool: None,
next_fallback_id: 0,
})
}
pub fn for_gpu_type(device_type: super::adapter::GpuDeviceType) -> Self {
let profile = GpuMemoryProfile::from_device_type(device_type);
Self::new(StagingBufferPoolConfig::for_profile(profile))
.expect("profile-derived pool configuration is always valid")
}
pub fn with_fallback_pool(mut self, pool: crate::memory::BufferPool) -> Self {
self.fallback_pool = Some(pool);
self
}
pub fn next_frame(&mut self) {
self.current_frame += 1;
if let Some(slot) = self.slots.get_mut(self.current_slot) {
slot.in_use = true;
slot.last_used_frame = self.current_frame;
}
self.current_slot = (self.current_slot + 1) % self.config.ring_slots;
if let Some(slot) = self.slots.get_mut(self.current_slot) {
slot.in_use = false;
self.total_used = 0;
}
}
pub fn allocate(&mut self, size: usize) -> Option<GpuBufferAllocation> {
let aligned_size = align_up_checked(size, self.config.alignment)?;
if aligned_size > self.config.max_batch_size {
return self.allocate_fallback(size);
}
if self.config.merge_uploads && size < self.config.small_upload_threshold {
if let Some(allocation) = self.try_merge_allocate(size) {
return Some(allocation);
}
}
let slot = self.slots.get(self.current_slot)?;
let new_used = self.total_used.checked_add(aligned_size)?;
if new_used > slot.size {
return self.allocate_fallback(size);
}
let offset = slot.offset + self.total_used;
self.total_used = new_used;
self.total_allocated = self.total_allocated.saturating_add(aligned_size);
Some(GpuBufferAllocation {
slot_index: self.current_slot,
offset,
size: aligned_size,
frame_index: self.current_frame,
is_fallback: false,
storage: None,
fallback_id: 0,
})
}
fn allocate_fallback(&mut self, size: usize) -> Option<GpuBufferAllocation> {
if let Some(ref mut pool) = self.fallback_pool {
let buffer = pool.acquire_sized(size);
let size = buffer.len();
let fallback_id = self.next_fallback_id;
self.next_fallback_id = self.next_fallback_id.saturating_add(1);
Some(GpuBufferAllocation {
slot_index: usize::MAX, offset: 0,
size,
frame_index: self.current_frame,
is_fallback: true,
storage: Some(buffer),
fallback_id,
})
} else {
None
}
}
fn try_merge_allocate(&mut self, size: usize) -> Option<GpuBufferAllocation> {
let aligned_size = align_up_checked(size, self.config.alignment)?;
if aligned_size > self.config.max_batch_size {
return None;
}
let slot = self.slots.get(self.current_slot)?;
if slot.in_use {
return None;
}
let remaining_space = slot.size.saturating_sub(self.total_used);
if remaining_space < aligned_size {
return None;
}
let offset = slot.offset + self.total_used;
self.total_used = self.total_used.checked_add(aligned_size)?;
self.total_allocated = self.total_allocated.saturating_add(aligned_size);
Some(GpuBufferAllocation {
slot_index: slot.index,
offset,
size: aligned_size,
frame_index: self.current_frame,
is_fallback: false,
storage: None,
fallback_id: 0,
})
}
pub fn release(&mut self, mut allocation: GpuBufferAllocation) {
if !allocation.is_fallback {
return;
}
if let (Some(ref mut pool), Some(buffer)) =
(&mut self.fallback_pool, allocation.storage.take())
{
pool.release(buffer);
}
}
pub fn current_frame(&self) -> u64 {
self.current_frame
}
pub fn config(&self) -> &StagingBufferPoolConfig {
&self.config
}
pub fn memory_stats(&self) -> GpuBufferPoolStats {
GpuBufferPoolStats {
total_size: self.config.pool_size,
used_size: self.total_used,
allocated_size: self.total_allocated,
slot_count: self.config.ring_slots,
current_slot: self.current_slot,
current_frame: self.current_frame,
fallback_used: self.fallback_pool.as_ref().map(|p| p.available()).unwrap_or(0),
}
}
pub fn wait_for_slot(&mut self, slot_index: usize) {
if let Some(slot) = self.slots.get_mut(slot_index) {
slot.in_use = false;
}
}
}
#[derive(Debug)]
pub struct GpuBufferAllocation {
pub slot_index: usize,
pub offset: usize,
pub size: usize,
pub frame_index: u64,
pub is_fallback: bool,
pub storage: Option<Vec<u8>>,
pub fallback_id: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct GpuBufferPoolStats {
pub total_size: usize,
pub used_size: usize,
pub allocated_size: usize,
pub slot_count: usize,
pub current_slot: usize,
pub current_frame: u64,
pub fallback_used: usize,
}
pub struct GpuUploadBatcher {
config: StagingBufferPoolConfig,
pending_uploads: Vec<GpuPendingUpload>,
current_batch_size: usize,
}
#[derive(Debug)]
struct GpuPendingUpload {
data: Vec<u8>,
destination_offset: usize,
}
impl GpuUploadBatcher {
pub fn new(config: StagingBufferPoolConfig) -> Self {
Self { config, pending_uploads: Vec::new(), current_batch_size: 0 }
}
pub fn add_upload(&mut self, data: Vec<u8>, destination_offset: usize) -> bool {
let size = data.len();
if self.current_batch_size + size > self.config.max_batch_size {
return false; }
if self.config.merge_uploads && size < self.config.small_upload_threshold {
if let Some(merged) = self.try_merge(&data, destination_offset) {
self.current_batch_size += merged;
return true;
}
}
self.pending_uploads.push(GpuPendingUpload { data, destination_offset });
self.current_batch_size += size;
true
}
fn try_merge(&mut self, data: &[u8], offset: usize) -> Option<usize> {
let new_end = offset.saturating_add(data.len());
let candidate = self.pending_uploads.iter().position(|upload| {
upload.destination_offset.saturating_add(upload.data.len()) == offset
})?;
let overlaps = self.pending_uploads.iter().enumerate().any(|(i, other)| {
if i == candidate {
return false;
}
let other_end = other.destination_offset.saturating_add(other.data.len());
other.destination_offset < new_end && offset < other_end
});
if overlaps {
return None;
}
self.pending_uploads[candidate].data.extend_from_slice(data);
Some(data.len())
}
pub fn batch_size(&self) -> usize {
self.current_batch_size
}
pub fn is_full(&self) -> bool {
self.current_batch_size >= self.config.max_batch_size
}
pub fn flush(&mut self) -> Vec<(Vec<u8>, usize)> {
let uploads =
self.pending_uploads.drain(..).map(|u| (u.data, u.destination_offset)).collect();
self.current_batch_size = 0;
uploads
}
pub fn pending_count(&self) -> usize {
self.pending_uploads.len()
}
}
pub struct GpuBufferPoolMonitor {
stats_history: crate::compat::VecDeque<GpuBufferPoolStats>,
max_history: usize,
}
impl GpuBufferPoolMonitor {
pub fn new(max_history: usize) -> Self {
let max_history = max_history.max(1);
Self { stats_history: crate::compat::VecDeque::with_capacity(max_history), max_history }
}
pub fn record(&mut self, stats: GpuBufferPoolStats) {
if self.stats_history.len() >= self.max_history {
self.stats_history.pop_front();
}
self.stats_history.push_back(stats);
}
pub fn average_utilization(&self) -> f32 {
if self.stats_history.is_empty() {
return 0.0;
}
let total: f32 =
self.stats_history.iter().map(|s| s.used_size as f32 / s.total_size as f32).sum();
total / self.stats_history.len() as f32
}
pub fn is_under_pressure(&self) -> bool {
if self.stats_history.len() < 3 {
return false;
}
let recent: Vec<_> = self.stats_history.iter().rev().take(3).collect();
recent.iter().all(|s| s.used_size as f32 / s.total_size as f32 > 0.8)
}
pub fn is_underutilized(&self) -> bool {
if self.stats_history.len() < 10 {
return false;
}
self.average_utilization() < 0.3
}
pub fn is_fallback_heavy(&self) -> bool {
if self.stats_history.len() < 5 {
return false;
}
let recent: Vec<_> = self.stats_history.iter().rev().take(5).collect();
recent.iter().any(|s| s.fallback_used > 0)
}
}
pub mod integration {
use super::*;
use crate::memory::{BufferPool, PoolConfig};
pub fn create_gpu_buffer_pool_config(profile: GpuMemoryProfile) -> PoolConfig {
PoolConfig {
initial_size: profile.ring_buffer_slots(),
max_size: profile.ring_buffer_slots() * 2,
growth_factor: 1.0, }
}
pub fn create_fallback_pool(profile: GpuMemoryProfile) -> BufferPool {
let buffer_size = match profile {
GpuMemoryProfile::Discrete => 4 * 1024 * 1024,
GpuMemoryProfile::Integrated => 1024 * 1024,
GpuMemoryProfile::Cpu => 256 * 1024,
};
BufferPool::new(buffer_size, profile.ring_buffer_slots(), profile.ring_buffer_slots() * 2)
}
}
#[cfg(test)]
mod tests {
use super::super::adapter::GpuDeviceType;
use super::*;
#[test]
fn test_gpu_memory_profile_discrete() {
let profile = GpuMemoryProfile::Discrete;
assert_eq!(profile.buffer_pool_size(), 64 * 1024 * 1024);
assert_eq!(profile.ring_buffer_slots(), 3);
assert!(profile.merge_small_uploads());
}
#[test]
fn test_gpu_memory_profile_integrated() {
let profile = GpuMemoryProfile::Integrated;
assert_eq!(profile.buffer_pool_size(), 16 * 1024 * 1024);
assert_eq!(profile.ring_buffer_slots(), 2);
assert!(profile.use_coherent_memory());
}
#[test]
fn test_gpu_memory_profile_cpu() {
let profile = GpuMemoryProfile::Cpu;
assert_eq!(profile.buffer_pool_size(), 4 * 1024 * 1024);
assert_eq!(profile.ring_buffer_slots(), 2);
assert!(!profile.merge_small_uploads());
}
#[test]
fn test_buffer_pool_allocation() {
let config = StagingBufferPoolConfig::discrete();
let mut pool = GpuStagingBufferPool::new(config).unwrap();
let allocation = pool.allocate(1024).unwrap();
assert_eq!(allocation.slot_index, 0);
assert_eq!(allocation.offset, 0);
assert!(allocation.size >= 1024);
assert!(!allocation.is_fallback);
}
#[test]
fn test_buffer_pool_ring_rotation() {
let config = StagingBufferPoolConfig::discrete();
let mut pool = GpuStagingBufferPool::new(config).unwrap();
pool.next_frame();
assert_eq!(pool.current_frame(), 1);
pool.next_frame();
assert_eq!(pool.current_frame(), 2);
}
#[test]
fn test_wait_for_slot_recycles_a_busy_slot() {
let config = StagingBufferPoolConfig::discrete();
let mut pool = GpuStagingBufferPool::new(config).unwrap();
pool.next_frame();
assert!(pool.slots[0].in_use);
pool.wait_for_slot(0);
assert!(!pool.slots[0].in_use);
}
#[test]
fn test_wait_for_slot_is_noop_for_free_or_missing_slots() {
let config = StagingBufferPoolConfig::discrete();
let mut pool = GpuStagingBufferPool::new(config).unwrap();
pool.wait_for_slot(0);
assert!(!pool.slots[0].in_use);
pool.wait_for_slot(usize::MAX);
assert!(!pool.slots[0].in_use);
}
#[test]
fn test_upload_batcher() {
let config = StagingBufferPoolConfig::discrete();
let mut batcher = GpuUploadBatcher::new(config);
assert!(batcher.add_upload(vec![0u8; 1024], 0));
assert_eq!(batcher.batch_size(), 1024);
let uploads = batcher.flush();
assert_eq!(uploads.len(), 1);
assert!(batcher.batch_size() == 0);
}
#[test]
fn test_buffer_pool_monitor() {
let mut monitor = GpuBufferPoolMonitor::new(10);
let stats = GpuBufferPoolStats {
total_size: 1024,
used_size: 512,
allocated_size: 512,
slot_count: 3,
current_slot: 0,
current_frame: 1,
fallback_used: 0,
};
monitor.record(stats);
assert_eq!(monitor.average_utilization(), 0.5);
}
#[test]
fn test_from_device_type() {
assert_eq!(
GpuMemoryProfile::from_device_type(GpuDeviceType::DiscreteGpu),
GpuMemoryProfile::Discrete
);
assert_eq!(
GpuMemoryProfile::from_device_type(GpuDeviceType::IntegratedGpu),
GpuMemoryProfile::Integrated
);
assert_eq!(GpuMemoryProfile::from_device_type(GpuDeviceType::Cpu), GpuMemoryProfile::Cpu);
}
#[test]
fn test_integration_config() {
let config = integration::create_gpu_buffer_pool_config(GpuMemoryProfile::Discrete);
assert_eq!(config.initial_size, 3);
assert_eq!(config.max_size, 6);
}
#[test]
fn test_slot_offsets_are_aligned_and_merge_matches_normal() {
let base = || {
let mut config = StagingBufferPoolConfig::discrete();
config.pool_size = 1024;
config.ring_slots = 3;
config.alignment = 256;
config.max_batch_size = 1024;
config.small_upload_threshold = 1024;
config.merge_uploads = true;
config
};
let mut pool = GpuStagingBufferPool::new(base()).unwrap();
assert_eq!(pool.slots[0].offset, 0);
assert_eq!(pool.slots[1].offset, 256);
assert_eq!(pool.slots[2].offset, 512);
for slot in &pool.slots {
assert_eq!(slot.offset % 256, 0);
}
let a = pool.allocate(64).unwrap();
assert_eq!((a.slot_index, a.offset), (0, 0));
pool.next_frame();
let b = pool.allocate(64).unwrap();
assert_eq!((b.slot_index, b.offset), (1, 256));
assert_eq!(b.offset % 256, 0);
let mut config = base();
config.merge_uploads = false;
let mut pool = GpuStagingBufferPool::new(config).unwrap();
let a = pool.allocate(64).unwrap();
assert_eq!((a.slot_index, a.offset), (0, 0));
pool.next_frame();
let b = pool.allocate(64).unwrap();
assert_eq!((b.slot_index, b.offset), (1, 256));
assert_eq!(b.offset % 256, 0);
}
#[test]
fn test_invalid_config_is_rejected() {
let mut config = StagingBufferPoolConfig::discrete();
config.ring_slots = 0;
assert!(matches!(GpuStagingBufferPool::new(config), Err(GpuBufferPoolError::NoRingSlots)));
let mut config = StagingBufferPoolConfig::discrete();
config.alignment = 0;
assert!(matches!(
GpuStagingBufferPool::new(config),
Err(GpuBufferPoolError::InvalidAlignment)
));
let mut config = StagingBufferPoolConfig::discrete();
config.alignment = 3; assert!(matches!(
GpuStagingBufferPool::new(config),
Err(GpuBufferPoolError::InvalidAlignment)
));
}
#[test]
fn test_huge_request_is_rejected_without_corrupting_state() {
let mut pool = GpuStagingBufferPool::new(StagingBufferPoolConfig::discrete()).unwrap();
let before = pool.memory_stats();
assert!(pool.allocate(usize::MAX).is_none());
let after = pool.memory_stats();
assert_eq!(after.used_size, before.used_size);
assert_eq!(after.allocated_size, before.allocated_size);
assert_eq!(after.current_slot, before.current_slot);
assert!(pool.allocate(1024).is_some());
}
#[test]
fn test_fallback_owns_storage_and_releases_back() {
let mut config = StagingBufferPoolConfig::discrete();
config.pool_size = 512;
config.ring_slots = 1;
config.max_batch_size = 1024;
config.merge_uploads = false;
config.alignment = 256;
let mut pool = GpuStagingBufferPool::new(config)
.unwrap()
.with_fallback_pool(crate::memory::BufferPool::new(512, 1, 2));
assert_eq!(pool.memory_stats().fallback_used, 1);
let _ = pool.allocate(256).unwrap(); let _ = pool.allocate(256).unwrap(); let a = pool.allocate(256).expect("slot exhausted, must fall back");
assert!(a.is_fallback);
assert_eq!(a.storage.as_ref().map(|s| s.len()), Some(256));
assert_eq!(pool.memory_stats().fallback_used, 0);
pool.release(a);
assert_eq!(pool.memory_stats().fallback_used, 1);
}
#[test]
fn test_simultaneous_fallbacks_are_distinguishable() {
let mut config = StagingBufferPoolConfig::discrete();
config.pool_size = 512;
config.ring_slots = 1;
config.max_batch_size = 1024;
config.merge_uploads = false;
config.alignment = 256;
let mut pool = GpuStagingBufferPool::new(config)
.unwrap()
.with_fallback_pool(crate::memory::BufferPool::new(512, 1, 4));
let _ = pool.allocate(256).unwrap();
let _ = pool.allocate(256).unwrap();
let a = pool.allocate(256).expect("first fallback");
let b = pool.allocate(256).expect("second fallback");
assert!(a.is_fallback && b.is_fallback);
assert_ne!(a.fallback_id, b.fallback_id);
assert!(a.storage.is_some() && b.storage.is_some());
}
#[test]
fn test_upload_batcher_preserves_write_order_on_overlap() {
let config = StagingBufferPoolConfig::discrete();
let mut batcher = GpuUploadBatcher::new(config);
assert!(batcher.add_upload(vec![1, 1, 1], 0));
assert!(batcher.add_upload(vec![2, 2], 2));
assert!(batcher.add_upload(vec![3], 3));
let uploads = batcher.flush();
assert_eq!(
uploads,
vec![(vec![1, 1, 1], 0), (vec![2, 2], 2), (vec![3], 3)],
"the trailing write must not be folded into the earlier upload across an overlap",
);
}
#[test]
fn test_small_over_batch_request_takes_fallback_even_with_merge() {
let mut config = StagingBufferPoolConfig::discrete();
config.pool_size = 64 * 1024;
config.ring_slots = 1;
config.merge_uploads = true;
config.small_upload_threshold = 4096; config.max_batch_size = 256; config.alignment = 256;
let mut pool = GpuStagingBufferPool::new(config)
.unwrap()
.with_fallback_pool(crate::memory::BufferPool::new(512, 1, 4));
let alloc = pool.allocate(272).expect("falls back to the fallback pool");
assert!(alloc.is_fallback, "an over-batch small request must not merge");
let normal = pool.allocate(128).expect("normal allocation");
assert!(!normal.is_fallback);
}
#[test]
fn test_buffer_pool_monitor_zero_history_does_not_panic() {
let mut monitor = GpuBufferPoolMonitor::new(0);
let stats = GpuBufferPoolStats {
total_size: 1024,
used_size: 512,
allocated_size: 512,
slot_count: 1,
current_slot: 0,
current_frame: 0,
fallback_used: 0,
};
monitor.record(stats);
assert_eq!(monitor.average_utilization(), 0.5);
monitor.record(GpuBufferPoolStats { used_size: 1024, ..stats });
assert_eq!(monitor.average_utilization(), 1.0);
}
}