use crate::ecs::asset_id::TextureId;
use crate::ecs::generational_registry::GenerationalRegistry;
use std::collections::HashMap;
use wgpu::util::DeviceExt;
pub struct TextureCache {
pub registry: GenerationalRegistry<TextureEntry>,
pub pending_references: HashMap<String, usize>,
}
impl Default for TextureCache {
fn default() -> Self {
Self {
registry: GenerationalRegistry::new(),
pending_references: HashMap::new(),
}
}
}
impl TextureCache {
pub fn load_texture_from_raw_rgba(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
name: String,
rgba_data: &[u8],
dimensions: (u32, u32),
) -> Result<TextureId, String> {
texture_cache_load_from_raw_rgba(self, device, queue, name, rgba_data, dimensions)
}
pub fn get(&self, name: &str) -> Option<&TextureEntry> {
let index = self.registry.name_to_index.get(name)?;
self.registry.entries[*index as usize].as_ref()
}
}
pub struct TextureEntry {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
}
pub fn texture_cache_load_from_raw_rgba(
cache: &mut TextureCache,
device: &wgpu::Device,
queue: &wgpu::Queue,
name: String,
rgba_data: &[u8],
dimensions: (u32, u32),
) -> Result<TextureId, String> {
texture_cache_load_from_raw_rgba_with_format(
cache,
device,
queue,
name,
rgba_data,
dimensions,
wgpu::TextureFormat::Rgba8Unorm,
)
}
pub fn texture_cache_load_from_raw_rgba_with_format(
cache: &mut TextureCache,
device: &wgpu::Device,
queue: &wgpu::Queue,
name: String,
rgba_data: &[u8],
dimensions: (u32, u32),
format: wgpu::TextureFormat,
) -> Result<TextureId, String> {
if let Some((index, generation)) = cache.registry.lookup_index(&name) {
return Ok(TextureId { index, generation });
}
let pending_refs = cache.pending_references.remove(&name).unwrap_or(0);
let size = wgpu::Extent3d {
width: dimensions.0,
height: dimensions.1,
depth_or_array_layers: 1,
};
let texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some(&name),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
rgba_data,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!("{} Sampler", name)),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
address_mode_w: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let entry = TextureEntry {
texture,
view,
sampler,
};
let (index, generation) = cache.registry.insert(name, entry);
cache.registry.reference_counts[index as usize] = 1 + pending_refs;
Ok(TextureId { index, generation })
}
pub fn texture_cache_add_reference(cache: &mut TextureCache, name: &str) {
if let Some(&index) = cache.registry.name_to_index.get(name) {
cache.registry.add_reference(index);
} else {
*cache
.pending_references
.entry(name.to_string())
.or_insert(0) += 1;
}
}
pub fn texture_cache_remove_reference(cache: &mut TextureCache, name: &str) {
if let Some(&index) = cache.registry.name_to_index.get(name) {
cache.registry.remove_reference(index);
} else if let Some(count) = cache.pending_references.get_mut(name) {
*count = count.saturating_sub(1);
if *count == 0 {
cache.pending_references.remove(name);
}
}
}
pub fn texture_cache_remove_unused(cache: &mut TextureCache) -> Vec<String> {
cache.registry.remove_unused()
}
pub enum TextureReloadResult {
UpdatedInPlace,
Recreated,
NotFound,
}
pub fn texture_cache_reload(
cache: &mut TextureCache,
device: &wgpu::Device,
queue: &wgpu::Queue,
name: &str,
rgba_data: &[u8],
width: u32,
height: u32,
) -> TextureReloadResult {
let Some(&index) = cache.registry.name_to_index.get(name) else {
tracing::warn!("Texture reload: '{}' not found in cache", name);
return TextureReloadResult::NotFound;
};
let Some(entry) = cache.registry.entries[index as usize].as_ref() else {
tracing::warn!("Texture reload: '{}' entry is empty", name);
return TextureReloadResult::NotFound;
};
let existing_size = entry.texture.size();
if existing_size.width == width && existing_size.height == height {
tracing::info!(
"Texture reload: '{}' updated in-place ({}x{})",
name,
width,
height
);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &entry.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
TextureReloadResult::UpdatedInPlace
} else {
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some(name),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
rgba_data,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!("{} Sampler", name)),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
address_mode_w: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
if let Some(slot) = cache.registry.entries[index as usize].as_mut() {
*slot = TextureEntry {
texture,
view,
sampler,
};
}
tracing::info!(
"Texture reload: '{}' recreated ({}x{} -> {}x{})",
name,
existing_size.width,
existing_size.height,
width,
height
);
TextureReloadResult::Recreated
}
}