#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
Rgba8,
Rgba16f,
Rgb10A2,
R8,
Rg8,
Yuv420,
Nv12,
}
impl TextureFormat {
#[must_use]
pub fn bytes_per_pixel(&self) -> f32 {
match self {
Self::Rgba8 | Self::Rgb10A2 => 4.0,
Self::Rgba16f => 8.0,
Self::R8 => 1.0,
Self::Rg8 => 2.0,
Self::Yuv420 | Self::Nv12 => 1.5,
}
}
#[must_use]
pub fn is_yuv(&self) -> bool {
matches!(self, Self::Yuv420 | Self::Nv12)
}
#[must_use]
pub fn channel_count(&self) -> u8 {
match self {
Self::R8 => 1,
Self::Rg8 => 2,
Self::Rgba8 | Self::Rgba16f | Self::Rgb10A2 => 4,
Self::Yuv420 | Self::Nv12 => 3,
}
}
}
#[derive(Debug, Clone)]
pub struct TextureDescriptor {
pub width: u32,
pub height: u32,
pub format: TextureFormat,
pub mip_levels: u8,
pub array_layers: u16,
}
impl TextureDescriptor {
#[must_use]
pub fn new(width: u32, height: u32, format: TextureFormat) -> Self {
Self {
width,
height,
format,
mip_levels: 1,
array_layers: 1,
}
}
#[must_use]
pub fn size_bytes(&self) -> usize {
let bpp = self.format.bytes_per_pixel();
let layers = self.array_layers as usize;
let mut total_pixels: f64 = 0.0;
let (mut w, mut h) = (f64::from(self.width), f64::from(self.height));
for _ in 0..self.mip_levels {
total_pixels += w * h;
w = (w / 2.0).max(1.0);
h = (h / 2.0).max(1.0);
}
(total_pixels * f64::from(bpp) * layers as f64) as usize
}
#[must_use]
pub fn total_pixels(&self) -> u64 {
u64::from(self.width) * u64::from(self.height)
}
}
pub struct TexturePool {
descriptors: Vec<Option<TextureDescriptor>>,
allocated_bytes: usize,
pub(crate) max_bytes: usize,
max_textures: usize,
access_clock: u64,
last_access: Vec<u64>,
}
impl TexturePool {
#[must_use]
pub fn new(max_gb: f64) -> Self {
Self {
descriptors: Vec::new(),
allocated_bytes: 0,
max_bytes: (max_gb * 1024.0 * 1024.0 * 1024.0) as usize,
max_textures: 0,
access_clock: 0,
last_access: Vec::new(),
}
}
#[must_use]
pub fn with_capacity(max: usize) -> Self {
Self {
descriptors: Vec::with_capacity(max),
allocated_bytes: 0,
max_bytes: usize::MAX,
max_textures: max,
access_clock: 0,
last_access: Vec::with_capacity(max),
}
}
pub fn evict_lru(&mut self) -> usize {
let mut evicted = 0usize;
while self.max_textures > 0 && self.live_count() > self.max_textures {
match self.lru_handle() {
Some(h) => {
self.free(h);
evicted += 1;
}
None => break,
}
}
evicted
}
pub fn allocate(&mut self, desc: TextureDescriptor) -> Option<usize> {
let bytes = desc.size_bytes();
if self.allocated_bytes + bytes > self.max_bytes {
return None;
}
if self.max_textures > 0 && self.live_count() >= self.max_textures {
return None;
}
self.access_clock += 1;
let ts = self.access_clock;
if let Some(idx) = self
.descriptors
.iter()
.position(std::option::Option::is_none)
{
self.descriptors[idx] = Some(desc);
self.last_access[idx] = ts;
self.allocated_bytes += bytes;
return Some(idx);
}
let idx = self.descriptors.len();
self.descriptors.push(Some(desc));
self.last_access.push(ts);
self.allocated_bytes += bytes;
Some(idx)
}
pub fn allocate_with_lru_eviction(&mut self, desc: TextureDescriptor) -> Option<usize> {
let bytes = desc.size_bytes();
let count_ok = self.max_textures == 0 || self.live_count() < self.max_textures;
if self.allocated_bytes + bytes <= self.max_bytes && count_ok {
return self.allocate(desc);
}
loop {
let bytes_ok = self.allocated_bytes + bytes <= self.max_bytes;
let cnt_ok = self.max_textures == 0 || self.live_count() < self.max_textures;
if bytes_ok && cnt_ok {
return self.allocate(desc);
}
let lru = self.lru_handle()?;
self.free(lru);
}
}
#[must_use]
pub fn lru_handle(&self) -> Option<usize> {
self.descriptors
.iter()
.enumerate()
.filter_map(|(i, slot)| slot.as_ref().map(|_| i))
.min_by_key(|&i| self.last_access[i])
}
pub fn touch(&mut self, handle: usize) {
if handle < self.descriptors.len() && self.descriptors[handle].is_some() {
self.access_clock += 1;
self.last_access[handle] = self.access_clock;
}
}
pub fn free(&mut self, id: usize) {
if let Some(slot) = self.descriptors.get_mut(id) {
if let Some(desc) = slot.take() {
let bytes = desc.size_bytes();
self.allocated_bytes = self.allocated_bytes.saturating_sub(bytes);
self.last_access[id] = 0;
}
}
}
#[must_use]
pub fn utilization(&self) -> f64 {
if self.max_bytes == 0 {
return 0.0;
}
self.allocated_bytes as f64 / self.max_bytes as f64
}
#[must_use]
pub fn live_count(&self) -> usize {
self.descriptors.iter().filter(|s| s.is_some()).count()
}
#[must_use]
pub fn allocated_bytes(&self) -> usize {
self.allocated_bytes
}
#[must_use]
pub fn max_bytes(&self) -> usize {
self.max_bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgba8_bytes_per_pixel() {
assert!((TextureFormat::Rgba8.bytes_per_pixel() - 4.0).abs() < f32::EPSILON);
}
#[test]
fn test_yuv_formats_are_yuv() {
assert!(TextureFormat::Yuv420.is_yuv());
assert!(TextureFormat::Nv12.is_yuv());
assert!(!TextureFormat::Rgba8.is_yuv());
}
#[test]
fn test_channel_counts() {
assert_eq!(TextureFormat::R8.channel_count(), 1);
assert_eq!(TextureFormat::Rg8.channel_count(), 2);
assert_eq!(TextureFormat::Rgba8.channel_count(), 4);
assert_eq!(TextureFormat::Yuv420.channel_count(), 3);
}
#[test]
fn test_descriptor_new_defaults() {
let d = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
assert_eq!(d.mip_levels, 1);
assert_eq!(d.array_layers, 1);
}
#[test]
fn test_descriptor_total_pixels() {
let d = TextureDescriptor::new(100, 200, TextureFormat::R8);
assert_eq!(d.total_pixels(), 20_000);
}
#[test]
fn test_descriptor_size_bytes_rgba8() {
let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
assert_eq!(d.size_bytes(), 64);
}
#[test]
fn test_descriptor_size_bytes_with_mips() {
let mut d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
d.mip_levels = 3;
assert_eq!(d.size_bytes(), 84);
}
#[test]
fn test_pool_basic_allocation() {
let mut pool = TexturePool::new(1.0);
let desc = TextureDescriptor::new(64, 64, TextureFormat::Rgba8);
let handle = pool.allocate(desc);
assert!(handle.is_some());
assert_eq!(pool.live_count(), 1);
}
#[test]
fn test_pool_free_reduces_bytes() {
let mut pool = TexturePool::new(1.0);
let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
let handle = pool.allocate(desc).expect("allocation should succeed");
let before = pool.allocated_bytes();
pool.free(handle);
assert!(pool.allocated_bytes() < before);
assert_eq!(pool.live_count(), 0);
}
#[test]
fn test_pool_reuses_freed_slot() {
let mut pool = TexturePool::new(1.0);
let d1 = TextureDescriptor::new(4, 4, TextureFormat::R8);
let h1 = pool.allocate(d1).expect("allocation should succeed");
pool.free(h1);
let d2 = TextureDescriptor::new(4, 4, TextureFormat::R8);
let h2 = pool.allocate(d2).expect("allocation should succeed");
assert_eq!(h1, h2);
}
#[test]
fn test_pool_budget_exceeded_returns_none() {
let mut pool = TexturePool::new(0.0);
pool.max_bytes = 1;
let desc = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
assert!(pool.allocate(desc).is_none());
}
#[test]
fn test_pool_utilization_after_alloc() {
let mut pool = TexturePool::new(0.0);
let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8); pool.max_bytes = 128;
pool.allocate(desc).expect("allocation should succeed");
let util = pool.utilization();
assert!((util - 0.5).abs() < 1e-6, "expected 0.5, got {util}");
}
#[test]
fn test_lru_handle_on_empty_pool() {
let pool = TexturePool::new(1.0);
assert!(pool.lru_handle().is_none());
}
#[test]
fn test_lru_handle_returns_oldest() {
let mut pool = TexturePool::new(1.0);
let h0 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc");
let h1 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc");
assert_eq!(pool.lru_handle(), Some(h0));
pool.touch(h0);
assert_eq!(pool.lru_handle(), Some(h1));
let _ = h1; }
#[test]
fn test_touch_updates_lru_order() {
let mut pool = TexturePool::new(1.0);
let h0 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc");
let h1 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc");
let h2 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc");
assert_eq!(pool.lru_handle(), Some(h0));
pool.touch(h0);
pool.touch(h1);
assert_eq!(pool.lru_handle(), Some(h2));
}
#[test]
fn test_allocate_with_lru_eviction_makes_space() {
let mut pool = TexturePool::new(0.0);
pool.max_bytes = 64;
let h0 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.expect("first alloc should succeed");
assert_eq!(pool.live_count(), 1);
assert!(pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.is_none());
let h1 = pool
.allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.expect("lru eviction alloc should succeed");
assert_eq!(pool.live_count(), 1);
assert_eq!(h0, h1);
}
#[test]
fn test_allocate_with_lru_eviction_preserves_mru() {
let mut pool = TexturePool::new(0.0);
pool.max_bytes = 128;
let h0 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.expect("alloc h0");
let _h1 = pool
.allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.expect("alloc h1");
pool.touch(h0);
let h2 = pool
.allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
.expect("lru eviction");
assert_eq!(h2, _h1);
assert_eq!(pool.live_count(), 2);
}
#[test]
fn test_lru_eviction_returns_none_when_budget_impossible() {
let mut pool = TexturePool::new(0.0);
pool.max_bytes = 16;
pool.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
.expect("alloc small");
let result =
pool.allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8));
assert!(result.is_none());
}
#[test]
fn test_with_capacity_rejects_when_full() {
let mut pool = TexturePool::with_capacity(2);
let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
assert!(
pool.allocate(d.clone()).is_some(),
"first alloc should succeed"
);
assert!(
pool.allocate(d.clone()).is_some(),
"second alloc should succeed"
);
assert!(
pool.allocate(d.clone()).is_none(),
"third alloc must fail (capacity = 2)"
);
}
#[test]
fn test_evict_lru_reduces_count_to_capacity() {
let mut pool = TexturePool::with_capacity(2);
let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
pool.max_textures = 0; pool.allocate(d.clone()).expect("alloc 1");
pool.allocate(d.clone()).expect("alloc 2");
pool.allocate(d.clone()).expect("alloc 3");
assert_eq!(pool.live_count(), 3);
pool.max_textures = 2; let evicted = pool.evict_lru();
assert_eq!(
evicted, 1,
"one texture should be evicted to reach capacity 2"
);
assert_eq!(pool.live_count(), 2);
}
#[test]
fn test_evict_lru_correct_order() {
let mut pool = TexturePool::with_capacity(3);
let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
pool.max_textures = 0;
let h0 = pool.allocate(d.clone()).expect("h0");
let h1 = pool.allocate(d.clone()).expect("h1");
let h2 = pool.allocate(d.clone()).expect("h2");
pool.touch(h0);
pool.touch(h1);
pool.max_textures = 2;
let evicted = pool.evict_lru();
assert_eq!(evicted, 1, "one eviction expected");
assert!(
pool.descriptors[h2].is_none(),
"h2 should have been evicted (LRU)"
);
assert!(pool.descriptors[h0].is_some(), "h0 should still be alive");
assert!(pool.descriptors[h1].is_some(), "h1 should still be alive");
}
#[test]
fn test_evict_lru_noop_when_under_capacity() {
let mut pool = TexturePool::with_capacity(5);
let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
pool.allocate(d.clone()).expect("alloc");
pool.allocate(d.clone()).expect("alloc");
let evicted = pool.evict_lru();
assert_eq!(evicted, 0, "no eviction expected when under capacity");
}
#[test]
fn test_evict_lru_on_empty_pool() {
let mut pool = TexturePool::with_capacity(2);
let evicted = pool.evict_lru();
assert_eq!(evicted, 0, "no eviction on empty pool");
}
#[test]
fn test_with_capacity_allocate_after_evict() {
let mut pool = TexturePool::with_capacity(1);
let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
let h0 = pool.allocate(d.clone()).expect("first alloc");
assert!(pool.allocate(d.clone()).is_none());
let h1 = pool
.allocate_with_lru_eviction(d.clone())
.expect("evict+alloc");
assert_eq!(pool.live_count(), 1, "still 1 live after evict+alloc");
assert_eq!(h0, h1, "freed slot should be reused");
}
}