use crate::allocation_policy::{AllocCommit, AllocFreeEvent, AllocRequest, AllocationPolicy, NoPolicy};
use crate::buffer::Allocation;
use crate::device::Device;
use crate::texture::TextureBacking;
use crate::timeline::TimelineValue;
use crate::types::*;
use anyhow::Result;
use std::any::Any;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, RwLock};
pub struct DeferredPayload(pub(crate) Vec<Box<dyn Any + Send>>);
impl DeferredPayload {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push<T: Send + 'static>(&mut self, resource: T) -> &mut Self {
self.0.push(Box::new(resource));
self
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl Default for DeferredPayload {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ParcelType {
Buffer,
Texture,
}
#[derive(Clone)]
pub(crate) struct ParcelDeed {
allocator: std::sync::Weak<dyn VramAllocatorAlloc>,
}
impl ParcelDeed {
pub(crate) fn new(allocator: std::sync::Weak<dyn VramAllocatorAlloc>) -> Self {
Self { allocator }
}
pub(crate) fn notify_freed(&self, reserved: u64, committed: u64, kind: ParcelType) {
if let Some(alloc) = self.allocator.upgrade() {
alloc.notify_freed(reserved, committed, kind);
}
}
}
pub(crate) trait VramAllocator: Send + Sync {
fn notify_freed(&self, _reserved: u64, _committed: u64, _kind: ParcelType) {}
fn allocated_bytes(&self) -> u64 {
0
}
fn defer_release(&self, _epoch: TimelineValue, _payload: DeferredPayload) {}
fn boundary_crossed(&self, _gpu_progress: TimelineValue) -> usize {
0
}
fn has_deferred_payloads(&self) -> bool {
false
}
fn drain(&self) {}
fn set_allocation_policy(&self, _policy: Arc<dyn AllocationPolicy>) -> Result<()> {
anyhow::bail!("this VramAllocator does not support allocation policies")
}
fn ensure_allocation_policy(&self, policy: Arc<dyn AllocationPolicy>) -> Result<()> {
self.set_allocation_policy(policy)
}
}
pub(crate) trait VramAllocatorAlloc: VramAllocator {
fn alloc_buffer(
&self,
device: &Device,
size: u64,
access: BufferKind,
element_stride: Option<u32>,
flags: BufferFlags,
) -> Result<Allocation> {
Allocation::new_with_stride_and_flags(device, size, access, element_stride, flags)
}
#[cfg(test)]
fn alloc_buffer_with_capacity(
&self,
device: &Device,
initial_size: u64,
expected_max: u64,
access: BufferKind,
flags: BufferFlags,
) -> Result<Allocation> {
Allocation::new_with_capacity_hint_and_flags(device, initial_size, expected_max, access, flags)
}
fn alloc_texture(
&self,
device: &Device,
width: u32,
height: u32,
format: TextureFormat,
access: TextureKind,
flags: TextureFlags,
) -> Result<TextureBacking> {
TextureBacking::new(device, width, height, format, access, flags)
}
}
pub(crate) struct DefaultVramAllocator {
deferred: Mutex<VecDeque<(TimelineValue, DeferredPayload)>>,
policy: RwLock<Arc<dyn AllocationPolicy>>,
}
impl DefaultVramAllocator {
pub fn new() -> Self {
Self {
deferred: Mutex::new(VecDeque::new()),
policy: RwLock::new(Arc::new(NoPolicy)),
}
}
pub fn set_policy(&self, policy: Arc<dyn AllocationPolicy>) -> Result<()> {
let mut guard = self.policy.write().unwrap();
if !guard.is_noop() {
anyhow::bail!("allocation policy already installed");
}
*guard = policy;
Ok(())
}
pub fn ensure_policy(&self, policy: Arc<dyn AllocationPolicy>) -> Result<()> {
let mut guard = self.policy.write().unwrap();
if guard.is_noop() {
*guard = policy;
}
Ok(())
}
fn with_policy_read<R>(&self, f: impl FnOnce(&dyn AllocationPolicy) -> R) -> R {
let policy = self.policy.read().unwrap();
f(policy.as_ref())
}
}
impl Default for DefaultVramAllocator {
fn default() -> Self {
Self::new()
}
}
impl VramAllocatorAlloc for DefaultVramAllocator {
fn alloc_buffer(
&self,
device: &Device,
size: u64,
access: BufferKind,
element_stride: Option<u32>,
flags: BufferFlags,
) -> Result<Allocation> {
let req = AllocRequest {
reserved_estimate: size,
committed_estimate: size,
kind: ParcelType::Buffer,
};
self.with_policy_read(|policy| policy.before_alloc(&req))?;
let buf = Allocation::new_with_stride_and_flags(device, size, access, element_stride, flags)?;
self.with_policy_read(|policy| {
policy.after_alloc(&AllocCommit::from_buffer(&buf));
});
Ok(buf)
}
#[cfg(test)]
fn alloc_buffer_with_capacity(
&self,
device: &Device,
initial_size: u64,
expected_max: u64,
access: BufferKind,
flags: BufferFlags,
) -> Result<Allocation> {
let estimate = expected_max.max(initial_size);
let req = AllocRequest {
reserved_estimate: estimate,
committed_estimate: initial_size,
kind: ParcelType::Buffer,
};
self.with_policy_read(|policy| policy.before_alloc(&req))?;
let buf = Allocation::new_with_capacity_hint_and_flags(device, initial_size, expected_max, access, flags)?;
self.with_policy_read(|policy| {
policy.after_alloc(&AllocCommit::from_buffer(&buf));
});
Ok(buf)
}
fn alloc_texture(
&self,
device: &Device,
width: u32,
height: u32,
format: TextureFormat,
access: TextureKind,
flags: TextureFlags,
) -> Result<TextureBacking> {
let estimated = (width as u64) * (height as u64) * (format.bytes_per_pixel() as u64);
let req = AllocRequest {
reserved_estimate: estimated,
committed_estimate: estimated,
kind: ParcelType::Texture,
};
self.with_policy_read(|policy| policy.before_alloc(&req))?;
let tex = TextureBacking::new(device, width, height, format, access, flags)?;
self.with_policy_read(|policy| {
policy.after_alloc(&AllocCommit::from_texture(&tex));
});
Ok(tex)
}
}
impl VramAllocator for DefaultVramAllocator {
fn notify_freed(&self, reserved: u64, committed: u64, kind: ParcelType) {
let event = AllocFreeEvent {
reserved,
committed,
kind,
};
self.with_policy_read(|policy| policy.on_freed(&event));
}
fn allocated_bytes(&self) -> u64 {
self.with_policy_read(|policy| policy.allocated_bytes())
}
fn set_allocation_policy(&self, policy: Arc<dyn AllocationPolicy>) -> Result<()> {
self.set_policy(policy)
}
fn ensure_allocation_policy(&self, policy: Arc<dyn AllocationPolicy>) -> Result<()> {
self.ensure_policy(policy)
}
fn defer_release(&self, epoch: TimelineValue, payload: DeferredPayload) {
if payload.is_empty() {
return;
}
self.deferred.lock().unwrap().push_back((epoch, payload));
}
fn boundary_crossed(&self, gpu_progress: TimelineValue) -> usize {
let drained: Vec<(TimelineValue, DeferredPayload)> = {
let mut ring = self.deferred.lock().unwrap();
let mut drained = Vec::new();
while let Some((epoch, _)) = ring.front() {
if *epoch <= gpu_progress {
drained.push(ring.pop_front().unwrap());
} else {
break;
}
}
drained
};
let count = drained.len();
drop(drained);
count
}
fn has_deferred_payloads(&self) -> bool {
!self.deferred.lock().unwrap().is_empty()
}
fn drain(&self) {
self.deferred.lock().unwrap().clear();
}
}
pub(crate) fn bytesize(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 {
format!("{:.1} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else if bytes >= 1024 * 1024 {
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.1} KiB", bytes as f64 / 1024.0)
} else {
format!("{bytes} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::allocation_policy::BudgetPolicy;
use crate::backend::mock::MockBackend;
fn test_device() -> Device {
Device::from_backend(Box::new(MockBackend::new())).unwrap()
}
fn device_with_budget_policy(budget_bytes: u64) -> (Device, Arc<BudgetPolicy>) {
let device = test_device();
let policy = Arc::new(BudgetPolicy::with_budget(budget_bytes));
device
.set_allocation_policy(policy.clone())
.expect("install budget policy in test fixture");
(device, policy)
}
fn device_with_policy() -> (Device, Arc<BudgetPolicy>) {
let device = test_device();
let policy = Arc::new(BudgetPolicy::new());
device
.set_allocation_policy(policy.clone())
.expect("install budget policy in test fixture");
(device, policy)
}
mod allocation_policy {
use super::*;
#[test]
fn budget_rejects_over_cap() {
let (device, policy) = device_with_budget_policy(8192);
let buf = device
.alloc_buffer(4096, BufferKind::Scattered, None, BufferFlags::empty())
.unwrap();
assert_eq!(policy.allocated_bytes(), 4096);
let err = device.alloc_buffer(8192, BufferKind::Scattered, None, BufferFlags::empty());
assert!(err.is_err(), "allocation policy budget should reject second alloc");
assert_eq!(policy.allocated_bytes(), 4096);
drop(buf);
assert_eq!(policy.allocated_bytes(), 0);
}
#[test]
fn second_install_fails_on_default_allocator() {
let device = test_device();
device
.set_allocation_policy(Arc::new(BudgetPolicy::new()))
.expect("first policy install");
let err = device.set_allocation_policy(Arc::new(BudgetPolicy::new()));
assert!(err.is_err(), "second policy install should fail");
assert!(
err.unwrap_err().to_string().contains("already installed"),
"expected duplicate-install error, got different failure"
);
}
}
#[test]
fn default_allocator_creates_buffer() {
let device = test_device();
let alloc = DefaultVramAllocator::new();
let buf = alloc
.alloc_buffer(&device, 1024, BufferKind::Scattered, None, BufferFlags::empty())
.unwrap();
assert_eq!(buf.size(), 1024);
}
#[test]
fn default_allocator_creates_texture() {
let device = test_device();
let alloc = DefaultVramAllocator::new();
let tex = alloc
.alloc_texture(
&device,
64,
64,
TextureFormat::Rgba8Unorm,
TextureKind::Interpolated,
TextureFlags::COPY_DST | TextureFlags::COPY_SRC,
)
.unwrap();
assert_eq!(tex.width(), 64);
assert_eq!(tex.height(), 64);
}
#[test]
fn budget_policy_tracks_bytes() {
let (device, policy) = device_with_policy();
assert_eq!(policy.allocated_bytes(), 0);
let buf = device
.alloc_buffer(4096, BufferKind::Scattered, None, BufferFlags::empty())
.unwrap();
assert!(policy.allocated_bytes() >= 4096);
drop(buf);
assert_eq!(policy.allocated_bytes(), 0);
}
#[test]
fn budget_policy_tracks_textures() {
let (device, policy) = device_with_policy();
let tex = device
.alloc_texture(
32,
32,
TextureFormat::Rgba8Unorm,
TextureKind::Interpolated,
TextureFlags::COPY_DST | TextureFlags::COPY_SRC,
)
.unwrap();
assert!(policy.allocated_bytes() > 0);
drop(tex);
assert_eq!(policy.allocated_bytes(), 0);
}
#[test]
fn accounting_round_trip_mixed_parcels() {
let (device, policy) = device_with_policy();
const N: usize = 8;
for _ in 0..N {
assert_eq!(policy.allocated_bytes(), 0);
let buf = device
.alloc_buffer(1024, BufferKind::Scattered, None, BufferFlags::empty())
.unwrap();
let hinted = device
.alloc_buffer_with_capacity(512, 4096, BufferKind::Scattered, BufferFlags::empty())
.unwrap();
let tex = device
.alloc_texture(
16,
16,
TextureFormat::Rgba8Unorm,
TextureKind::Interpolated,
TextureFlags::COPY_DST,
)
.unwrap();
let bytes_with_parcels = policy.allocated_bytes();
assert!(bytes_with_parcels > 0);
let view = buf.create_view(0, 256, Some(4)).unwrap();
let bytes_before_view_drop = policy.allocated_bytes();
drop(view);
assert_eq!(
policy.allocated_bytes(),
bytes_before_view_drop,
"BufferView drop must not change allocator accounting"
);
drop(buf);
drop(hinted);
drop(tex);
assert_eq!(policy.allocated_bytes(), 0);
}
}
#[test]
fn bytesize_formatting() {
assert_eq!(bytesize(500), "500 B");
assert_eq!(bytesize(1024), "1.0 KiB");
assert_eq!(bytesize(1024 * 1024), "1.0 MiB");
assert_eq!(bytesize(1024 * 1024 * 1024), "1.0 GiB");
}
#[test]
fn deferred_payload_push_and_len() {
let mut p = DeferredPayload::new();
assert!(p.is_empty());
assert_eq!(p.len(), 0);
p.push(42u32).push("hello");
assert!(!p.is_empty());
assert_eq!(p.len(), 2);
}
#[test]
fn deferred_payload_default_is_empty() {
let p = DeferredPayload::default();
assert!(p.is_empty());
}
#[test]
fn default_allocator_boundary_crossed_drops_retired_entries() {
let alloc = DefaultVramAllocator::new();
let alive = Arc::new(());
let weak = Arc::downgrade(&alive);
let mut p = DeferredPayload::new();
p.push(alive);
alloc.defer_release(5, p);
assert_eq!(alloc.boundary_crossed(4), 0);
assert!(weak.upgrade().is_some(), "resource should still be alive");
assert_eq!(alloc.boundary_crossed(5), 1);
assert!(weak.upgrade().is_none(), "resource should have been dropped");
}
#[test]
fn default_allocator_boundary_crossed_preserves_future_entries() {
let alloc = DefaultVramAllocator::new();
let alive_early = Arc::new(1u32);
let weak_early = Arc::downgrade(&alive_early);
let alive_late = Arc::new(2u32);
let weak_late = Arc::downgrade(&alive_late);
let mut p1 = DeferredPayload::new();
p1.push(alive_early);
alloc.defer_release(2, p1);
let mut p2 = DeferredPayload::new();
p2.push(alive_late);
alloc.defer_release(10, p2);
assert_eq!(alloc.boundary_crossed(2), 1);
assert!(weak_early.upgrade().is_none(), "epoch=2 should be dropped");
assert!(weak_late.upgrade().is_some(), "epoch=10 should survive");
assert_eq!(alloc.boundary_crossed(10), 1);
assert!(weak_late.upgrade().is_none(), "epoch=10 should now be dropped");
}
#[test]
fn default_allocator_drain_drops_all() {
let alloc = DefaultVramAllocator::new();
let alive = Arc::new(99u32);
let weak = Arc::downgrade(&alive);
let mut p = DeferredPayload::new();
p.push(alive);
alloc.defer_release(9999, p);
alloc.drain();
assert!(weak.upgrade().is_none(), "drain should drop all resources");
}
#[test]
fn default_allocator_empty_payload_skipped() {
let alloc = DefaultVramAllocator::new();
alloc.defer_release(1, DeferredPayload::new());
assert_eq!(alloc.boundary_crossed(100), 0);
}
}