use crate::context::Context;
use crate::parcel::{BookkeepingGuard, BytesByKind, Parcel, PoolBookkeeping, Texture};
use crate::retained_pool::{RetainedHold, StampedParcel};
use crate::timeline::ReferenceTable;
use crate::types::{BufferFlags, BufferKind, TextureFlags, TextureFormat, TextureKind};
use crate::vram_allocator::ParcelType;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
const MAX_BUFFER_BIN_READY_SPARES: usize = 1;
const MAX_TEXTURE_BIN_READY_SPARES: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TextureKey {
width: u32,
height: u32,
format: TextureFormat,
access: TextureKind,
flags: TextureFlags,
}
struct TexturePendingEntry {
parcel: Parcel,
ready_after: ReferenceTable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct BufferKey {
size: u64,
kind: BufferKind,
flags: BufferFlags,
element_stride: Option<u32>,
}
impl BufferKey {
fn from_parcel(parcel: &Parcel) -> Self {
let (kind, flags) = parcel
.buffer_descriptor()
.expect("BufferKey::from_parcel requires a whole-buffer parcel");
Self {
size: parcel.byte_size(),
kind,
flags,
element_stride: parcel.buffer_element_stride(),
}
}
}
struct BufferBinEntry {
parcel: Parcel,
ready_after: ReferenceTable,
}
pub(crate) struct TransientPool {
pending: Arc<PoolBookkeeping>,
outstanding: Arc<PoolBookkeeping>,
texture_bins: HashMap<TextureKey, Vec<TexturePendingEntry>>,
buffer_bins: HashMap<BufferKey, Vec<BufferBinEntry>>,
buffer_alloc_count: usize,
texture_alloc_count: usize,
}
impl TransientPool {
pub fn new() -> Self {
Self {
pending: Arc::new(PoolBookkeeping::new()),
outstanding: Arc::new(PoolBookkeeping::new()),
texture_bins: HashMap::new(),
buffer_bins: HashMap::new(),
buffer_alloc_count: 0,
texture_alloc_count: 0,
}
}
pub fn acquire_texture(
&mut self,
ctx: &Context,
width: u32,
height: u32,
format: TextureFormat,
access: TextureKind,
flags: TextureFlags,
) -> Result<Texture> {
let home_device = Arc::downgrade(&ctx.device().inner);
let key = TextureKey {
width,
height,
format,
access,
flags,
};
if let Some(bin) = self.texture_bins.get_mut(&key) {
if let Some(pos) = bin.iter().position(|e| ctx.parcel_ready(&e.ready_after)) {
let entry = bin.swap_remove(pos);
let bytes = entry.parcel.byte_size();
self.pending.subtract(ParcelType::Texture, bytes);
let guard = BookkeepingGuard::new(Arc::downgrade(&self.outstanding), ParcelType::Texture, bytes);
self.outstanding.add(ParcelType::Texture, bytes);
return Ok(Texture::from_parcel(entry.parcel, guard, home_device));
}
}
let tex = ctx
.device()
.alloc_texture(width, height, format, access, flags)
.map_err(|e| anyhow::anyhow!("{e}"))?;
self.texture_alloc_count += 1;
let bytes = tex.byte_size() as u64;
self.outstanding.add(ParcelType::Texture, bytes);
let guard = BookkeepingGuard::new(Arc::downgrade(&self.outstanding), ParcelType::Texture, bytes);
Ok(Texture::from_backing(tex, guard, home_device))
}
pub fn acquire_buffer(
&mut self,
ctx: &Context,
size: u64,
kind: BufferKind,
flags: BufferFlags,
element_stride: Option<u32>,
) -> Result<Parcel> {
let key = BufferKey {
size,
kind,
flags,
element_stride,
};
if let Some(bin) = self.buffer_bins.get_mut(&key) {
if let Some(pos) = bin.iter().position(|e| ctx.parcel_ready(&e.ready_after)) {
let entry = bin.swap_remove(pos);
let bytes = entry.parcel.byte_size();
self.pending.subtract(ParcelType::Buffer, bytes);
let mut parcel = entry.parcel;
parcel.attach_bookkeeping(BookkeepingGuard::new(
Arc::downgrade(&self.outstanding),
ParcelType::Buffer,
bytes,
));
self.outstanding.add(ParcelType::Buffer, bytes);
return Ok(parcel);
}
}
let alloc = ctx
.device()
.alloc_buffer(size, kind, element_stride, flags)
.map_err(|e| anyhow::anyhow!("{e}"))?;
self.buffer_alloc_count += 1;
let bytes = alloc.byte_size();
self.outstanding.add(ParcelType::Buffer, bytes);
let guard = BookkeepingGuard::new(Arc::downgrade(&self.outstanding), ParcelType::Buffer, bytes);
let mut parcel = Parcel::from_whole_buffer(Arc::new(alloc), Arc::downgrade(&ctx.device().inner));
parcel.attach_bookkeeping(guard);
Ok(parcel)
}
pub fn acquire_whole_buffer(
&mut self,
ctx: &Context,
size: u64,
kind: BufferKind,
flags: BufferFlags,
element_stride: Option<u32>,
) -> Result<crate::parcel::Buffer> {
let home_device = Arc::downgrade(&ctx.device().inner);
let parcel = self.acquire_buffer(ctx, size, kind, flags, element_stride)?;
crate::parcel::Buffer::from_transient_parcel(parcel, home_device)
}
pub(crate) fn return_buffer_parcel(&mut self, mut parcel: Parcel, ready_after: ReferenceTable) {
parcel.retire_stamp_for_pool_return();
let bytes = parcel.byte_size();
let key = BufferKey::from_parcel(&parcel);
self.pending.add(ParcelType::Buffer, bytes);
self.buffer_bins
.entry(key)
.or_default()
.push(BufferBinEntry { parcel, ready_after });
}
pub(crate) fn return_texture(&mut self, mut texture: Texture, ready_after: ReferenceTable) {
texture.release_bookkeeping();
let mut parcel = texture.into_parcel();
parcel.retire_stamp_for_pool_return();
let bytes = parcel.byte_size();
let (width, height, format, access, flags) = parcel.texture_descriptor().expect("texture descriptor");
let key = TextureKey {
width,
height,
format,
access,
flags,
};
self.pending.add(ParcelType::Texture, bytes);
self.texture_bins
.entry(key)
.or_default()
.push(TexturePendingEntry { parcel, ready_after });
}
pub(crate) fn adopt(&mut self, stamped: StampedParcel) {
let StampedParcel { hold, ready_after } = stamped;
match hold {
RetainedHold::Texture(texture) => {
self.park_texture(texture, ready_after);
}
RetainedHold::Buffer(buffer) => {
let byte_size = buffer.byte_size();
match buffer.into_transient_parcel() {
Ok(parcel) => {
self.return_buffer_parcel(parcel, ready_after);
}
Err(_) => {
tracing::trace!(
byte_size,
"transient_pool: dropping partitioned buffer — \
epoch-gated reclamation via backend deletion queue",
);
}
}
}
}
}
fn park_texture(&mut self, texture: Texture, ready_after: ReferenceTable) {
self.return_texture(texture, ready_after);
}
pub(crate) fn clear_textures(&mut self) {
for bin in self.texture_bins.values() {
for entry in bin {
self.pending.subtract(ParcelType::Texture, entry.parcel.byte_size());
}
}
self.texture_bins.clear();
}
pub fn drain_ready(&mut self, ctx: &Context) -> usize {
let mut released = self.trim_texture_bins(ctx);
released += self.trim_buffer_bins(ctx);
released
}
fn trim_texture_bins(&mut self, ctx: &Context) -> usize {
let mut trimmed = 0;
for bin in self.texture_bins.values_mut() {
let mut ready_indices: Vec<usize> = bin
.iter()
.enumerate()
.filter(|(_, e)| ctx.parcel_ready(&e.ready_after))
.map(|(i, _)| i)
.collect();
ready_indices.sort_unstable();
let excess = ready_indices.len().saturating_sub(MAX_TEXTURE_BIN_READY_SPARES);
let to_drop = ready_indices.split_off(ready_indices.len().saturating_sub(excess));
for idx in to_drop.into_iter().rev() {
let entry = bin.swap_remove(idx);
self.pending.subtract(ParcelType::Texture, entry.parcel.byte_size());
trimmed += 1;
}
}
self.texture_bins.retain(|_, bin| !bin.is_empty());
trimmed
}
fn trim_buffer_bins(&mut self, ctx: &Context) -> usize {
let mut trimmed = 0;
for bin in self.buffer_bins.values_mut() {
let mut ready_indices: Vec<usize> = bin
.iter()
.enumerate()
.filter(|(_, e)| ctx.parcel_ready(&e.ready_after))
.map(|(i, _)| i)
.collect();
ready_indices.sort_unstable();
let excess = ready_indices.len().saturating_sub(MAX_BUFFER_BIN_READY_SPARES);
let to_drop = ready_indices.split_off(ready_indices.len().saturating_sub(excess));
for idx in to_drop.into_iter().rev() {
let entry = bin.swap_remove(idx);
self.pending.subtract(ParcelType::Buffer, entry.parcel.byte_size());
trimmed += 1;
}
}
self.buffer_bins.retain(|_, bin| !bin.is_empty());
trimmed
}
#[cfg(test)]
pub(crate) fn pending_bytes(&self) -> BytesByKind {
self.pending.snapshot()
}
pub(crate) fn outstanding_bytes(&self) -> BytesByKind {
self.outstanding.snapshot()
}
#[cfg(test)]
pub(crate) fn pending_count(&self) -> usize {
self.texture_bins.values().map(Vec::len).sum::<usize>() + self.buffer_bins.values().map(Vec::len).sum::<usize>()
}
pub fn buffer_alloc_count(&self) -> usize {
self.buffer_alloc_count
}
pub fn texture_alloc_count(&self) -> usize {
self.texture_alloc_count
}
}
impl Default for TransientPool {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::mock::MockBackend;
use crate::device::Device;
use crate::retained_pool::RetainedPool;
fn test_device() -> Arc<Device> {
Arc::new(Device::from_backend(Box::new(MockBackend::new())).expect("mock device"))
}
fn rgba_interpolated() -> (TextureFormat, TextureKind, TextureFlags) {
(
TextureFormat::Rgba8Unorm,
TextureKind::Interpolated,
TextureFlags::COPY_DST | TextureFlags::COPY_SRC,
)
}
const TEST_BUFFER_SIZE: u64 = 64;
const SCATTERED_EMPTY: (BufferKind, BufferFlags) = (BufferKind::Scattered, BufferFlags::empty());
fn park_ready_buffer(ctx: &Context) {
let alloc = ctx
.device()
.alloc_buffer(TEST_BUFFER_SIZE, SCATTERED_EMPTY.0, None, SCATTERED_EMPTY.1)
.expect("alloc");
let p = Parcel::from_whole_buffer(Arc::new(alloc), Arc::downgrade(&ctx.device().inner));
ctx.with_transient_pool(|pool| pool.return_buffer_parcel(p, ReferenceTable::new()));
}
fn park_not_ready_buffer(ctx: &Context) {
let mut ready_after = ReferenceTable::new();
crate::timeline::mark_reference(&mut ready_after, ctx.test_backend_handle(), u64::MAX);
let alloc = ctx
.device()
.alloc_buffer(TEST_BUFFER_SIZE, SCATTERED_EMPTY.0, None, SCATTERED_EMPTY.1)
.expect("alloc");
let p = Parcel::from_whole_buffer(Arc::new(alloc), Arc::downgrade(&ctx.device().inner));
ctx.with_transient_pool(|pool| pool.return_buffer_parcel(p, ready_after));
}
fn park_ready_texture(ctx: &Context) {
let (fmt, acc, flags) = rgba_interpolated();
let tex = ctx.device().alloc_texture(8, 8, fmt, acc, flags).expect("alloc");
let home = Arc::downgrade(&ctx.device().inner);
let mut parcel = Parcel::from_texture(tex, home);
parcel.retire_stamp_for_pool_return();
ctx.with_transient_pool(|pool| {
let bytes = parcel.byte_size();
pool.pending.add(ParcelType::Texture, bytes);
let key = TextureKey {
width: 8,
height: 8,
format: fmt,
access: acc,
flags,
};
pool.texture_bins.entry(key).or_default().push(TexturePendingEntry {
parcel,
ready_after: ReferenceTable::new(),
});
});
}
fn park_not_ready_texture(ctx: &Context) {
let (fmt, acc, flags) = rgba_interpolated();
let tex = ctx.device().alloc_texture(8, 8, fmt, acc, flags).expect("alloc");
let home = Arc::downgrade(&ctx.device().inner);
let mut parcel = Parcel::from_texture(tex, home);
let mut ready_after = ReferenceTable::new();
crate::timeline::mark_reference(&mut ready_after, ctx.test_backend_handle(), u64::MAX);
parcel.retire_stamp_for_pool_return();
ctx.with_transient_pool(|pool| {
let bytes = parcel.byte_size();
pool.pending.add(ParcelType::Texture, bytes);
let key = TextureKey {
width: 8,
height: 8,
format: fmt,
access: acc,
flags,
};
pool.texture_bins
.entry(key)
.or_default()
.push(TexturePendingEntry { parcel, ready_after });
});
}
#[test]
fn adopt_from_retained_pool_and_reuse() {
let device = test_device();
let ctx = device.create_context().unwrap();
let mut retained = RetainedPool::new(device.clone());
let (fmt, acc, flags) = rgba_interpolated();
let p = retained.acquire_texture(8, 8, fmt, acc, flags, None).unwrap();
let handle_before = p.texture_handle().unwrap();
retained.release_texture(&ctx, p);
assert_eq!(retained.bytes_by_kind().texture, 0);
assert_eq!(ctx.with_transient_pool(|t| t.pending_count()), 1);
let p2 = ctx
.with_transient_pool(|transient| transient.acquire_texture(&ctx, 8, 8, fmt, acc, flags))
.unwrap();
assert_eq!(p2.texture_handle(), Some(handle_before), "adopted parcel is reusable");
}
#[test]
fn adopted_buffer_bins_and_reissues_via_acquire_buffer() {
let device = test_device();
let ctx = device.create_context().unwrap();
let mut retained = RetainedPool::new(device.clone());
let b = retained
.acquire_buffer(
64,
crate::types::BufferKind::Scattered,
None,
crate::types::BufferFlags::empty(),
None,
)
.unwrap();
let handle_before = b.whole().buffer_handle().unwrap();
retained.release_buffer(&ctx, b);
assert_eq!(ctx.with_transient_pool(|t| t.pending_count()), 1);
assert!(ctx.with_transient_pool(|t| t.pending_bytes().buffer >= 64));
let released = ctx.with_transient_pool(|t| t.drain_ready(&ctx));
assert_eq!(
released, 0,
"1 ready entry is within the cap (<= MAX_BUFFER_BIN_READY_SPARES); nothing trimmed"
);
let p = ctx
.with_transient_pool(|pool| {
pool.acquire_buffer(
&ctx,
64,
crate::types::BufferKind::Scattered,
crate::types::BufferFlags::empty(),
None,
)
})
.expect("reuse binned buffer");
assert_eq!(
p.buffer_handle(),
Some(handle_before),
"adopted buffer parcel is reusable from buffer_bins"
);
}
#[test]
fn return_buffer_parcel_reissues_on_acquire() {
let device = test_device();
let ctx = device.create_context().unwrap();
let mut p = ctx
.with_transient_pool(|pool| {
pool.acquire_buffer(
&ctx,
64,
crate::types::BufferKind::Scattered,
crate::types::BufferFlags::empty(),
None,
)
})
.expect("initial acquire");
let handle_before = p.buffer_handle().expect("buffer handle");
let ready_after = p.last_referenced();
p.release_bookkeeping();
ctx.with_transient_pool(|pool| pool.return_buffer_parcel(p, ready_after));
assert_eq!(ctx.with_transient_pool(|t| t.pending_count()), 1, "parcel is pending");
let p2 = ctx
.with_transient_pool(|pool| {
pool.acquire_buffer(
&ctx,
64,
crate::types::BufferKind::Scattered,
crate::types::BufferFlags::empty(),
None,
)
})
.expect("reuse after return");
assert_eq!(
p2.buffer_handle(),
Some(handle_before),
"return_buffer_parcel → acquire_buffer must reuse the same GPU buffer"
);
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
0,
"bin emptied after reuse"
);
}
#[test]
fn context_acquire_return_transient_buffer_reuses() {
let device = test_device();
let ctx = device.create_context().unwrap();
let alloc_before = ctx.transient_buffer_alloc_count();
let buf = ctx
.acquire_transient_buffer(128, BufferKind::Scattered, BufferFlags::empty(), Some(16))
.expect("acquire");
let handle = buf.whole().buffer_handle().expect("handle");
assert_eq!(ctx.transient_buffer_alloc_count(), alloc_before + 1);
ctx.return_transient_buffer(buf);
let buf2 = ctx
.acquire_transient_buffer(128, BufferKind::Scattered, BufferFlags::empty(), Some(16))
.expect("reacquire");
assert_eq!(buf2.whole().buffer_handle(), Some(handle));
assert_eq!(
ctx.transient_buffer_alloc_count(),
alloc_before + 1,
"reuse must not allocate again"
);
}
#[test]
fn buffer_bin_trim_drops_excess_ready_entries() {
let device = test_device();
let ctx = device.create_context().unwrap();
for _ in 0..=MAX_BUFFER_BIN_READY_SPARES {
park_ready_buffer(&ctx);
}
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
MAX_BUFFER_BIN_READY_SPARES + 1
);
ctx.with_transient_pool(|pool| pool.drain_ready(&ctx));
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
MAX_BUFFER_BIN_READY_SPARES,
"one excess ready entry must be trimmed"
);
assert_eq!(
ctx.with_transient_pool(|t| t.pending_bytes().buffer),
(MAX_BUFFER_BIN_READY_SPARES as u64) * TEST_BUFFER_SIZE
);
}
#[test]
fn buffer_bin_trim_preserves_not_ready_entries() {
let device = test_device();
let ctx = device.create_context().unwrap();
park_not_ready_buffer(&ctx);
park_not_ready_buffer(&ctx);
park_ready_buffer(&ctx);
ctx.with_transient_pool(|pool| pool.drain_ready(&ctx));
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
3,
"in-flight entries and the single ready spare within cap must survive trim"
);
}
#[test]
fn buffer_bin_trim_drops_excess_but_preserves_not_ready() {
let device = test_device();
let ctx = device.create_context().unwrap();
park_not_ready_buffer(&ctx);
park_not_ready_buffer(&ctx);
for _ in 0..3 {
park_ready_buffer(&ctx);
}
ctx.with_transient_pool(|pool| pool.drain_ready(&ctx));
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
3,
"2 not-ready + 1 ready spare; 2 excess ready entries dropped"
);
}
#[test]
fn texture_bin_trim_keeps_one_ready_spare() {
let device = test_device();
let ctx = device.create_context().unwrap();
for _ in 0..=MAX_TEXTURE_BIN_READY_SPARES {
park_ready_texture(&ctx);
}
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
MAX_TEXTURE_BIN_READY_SPARES + 1
);
ctx.with_transient_pool(|pool| pool.drain_ready(&ctx));
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
MAX_TEXTURE_BIN_READY_SPARES,
"ready texture spares must survive drain_ready (same policy as buffers)"
);
}
#[test]
fn texture_bin_trim_preserves_not_ready_entries() {
let device = test_device();
let ctx = device.create_context().unwrap();
park_not_ready_texture(&ctx);
park_not_ready_texture(&ctx);
park_ready_texture(&ctx);
ctx.with_transient_pool(|pool| pool.drain_ready(&ctx));
assert_eq!(
ctx.with_transient_pool(|t| t.pending_count()),
3,
"in-flight texture entries and one ready spare must survive trim"
);
}
#[test]
fn return_transient_texture_survives_flush_and_reissues() {
let device = test_device();
let ctx = device.create_context().unwrap();
let (fmt, acc, flags) = rgba_interpolated();
let tex = ctx.acquire_transient_texture(8, 8, fmt, acc, flags).expect("texture");
let handle = tex.texture_handle();
let allocs_after_first = ctx.transient_texture_alloc_count();
ctx.return_transient_texture(tex);
ctx.flush_deferred_deletions();
let tex2 = ctx.acquire_transient_texture(8, 8, fmt, acc, flags).expect("reacquire");
assert_eq!(tex2.texture_handle(), handle, "warm spare must reissue after flush");
assert_eq!(
ctx.transient_texture_alloc_count(),
allocs_after_first,
"reissue must not fresh-alloc"
);
}
}