use std::cmp::Reverse; use std::collections::HashMap; use std::mem::{self}; use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use image::ImageBuffer; use parking_lot::Mutex; use texture_packer::exporter::ImageExporter; use texture_packer::{Frame, TexturePacker, TexturePackerConfig}; use thiserror::Error; use crate::Priority; /// The next texture ID static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); /// A unique identifier for a texture #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct TextureId(usize); impl TextureId { fn new() -> Self { Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) } } /// These are the formats supported by the renderer. // TODO make these feature-enabled #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ImageFormat { Bmp, Ico, Farbfeld, } impl From for image::ImageFormat { fn from(format: ImageFormat) -> Self { match format { ImageFormat::Bmp => Self::Bmp, ImageFormat::Ico => Self::Ico, ImageFormat::Farbfeld => Self::Farbfeld, } } } #[derive(Debug, Error)] #[error("{}", .0)] pub struct DecodingError(#[from] image::error::DecodingError); #[allow(clippy::missing_const_for_fn)] fn convert_image_decoding(e: image::ImageError) -> DecodingError { if let image::ImageError::Decoding(de) = e { de.into() } else { unreachable!("No other error should be possible") } } #[derive(Debug, Error)] pub enum LoadError { #[error("{}", .0)] Decoding(#[from] DecodingError), #[error("{}", .0)] Io(#[from] std::io::Error), } fn convert_image_load_error(e: image::ImageError) -> LoadError { match e { image::ImageError::Decoding(de) => LoadError::Decoding(de.into()), image::ImageError::IoError(ioe) => ioe.into(), _ => unreachable!("No other error should be possible"), } } type Rgba16Texture = image::ImageBuffer, Box<[u16]>>; fn vec_image_to_box(vec_image: image::ImageBuffer, Vec>) -> Rgba16Texture { let width = vec_image.width(); let height = vec_image.height(); let buf = vec_image.into_raw().into_boxed_slice(); ImageBuffer::from_raw(width, height, buf).expect("image buffer is too small") } /// Get the size, in bytes, of the texture #[allow(clippy::missing_const_for_fn)] fn texture_size(image: &Rgba16Texture) -> usize { image.len() * mem::size_of::>() } /// A texture from disk struct TextureFile { path: Box, texture: Option>, } impl TextureFile { /// This doesn't load the texture #[allow(clippy::missing_const_for_fn)] fn new(path: impl AsRef) -> Self { Self { path: path.as_ref().into(), texture: None, } } const fn is_loaded(&self) -> bool { self.texture.is_some() } fn load(&mut self) -> Result<&Rgba16Texture, LoadError> { if self.texture.is_none() { let texture = image::open(&self.path).map_err(convert_image_load_error)?; let texture = texture.to_rgba16(); let texture = Arc::new(vec_image_to_box(texture)); self.texture = Some(texture); } Ok(self.texture.as_ref().expect("the texture wasn't loaded")) } fn loaded_texture(&self) -> Option<&Rgba16Texture> { self.texture.as_deref() } fn is_used(&self) -> bool { let Some(arc) = &self.texture else { return false }; Arc::strong_count(arc) > 1 } /// Unloads the texture from memory if it isn't being used fn unload(&mut self) { if !self.is_used() { self.texture = None; } } /// The amount of heap memory used, in bytes. This returns 0 if the texture /// hasn't been loaded yet. fn allocated_size(&self) -> usize { self.texture.as_ref().map_or(0, |t| texture_size(t)) } } enum TextureBuffer { Memory(Arc), Disk(TextureFile), } struct Texture { priority: Priority, queued_priority: Arc>>, buffer: TextureBuffer, } impl Texture { fn from_buffer(texture: Rgba16Texture) -> Self { Self { priority: Priority::Urgent, // indicates that it can't be unloaded queued_priority: Arc::new(Mutex::new(None)), buffer: TextureBuffer::Memory(Arc::new(texture)), } } fn from_path(path: impl AsRef, priority: Priority) -> Self { Self { priority, queued_priority: Arc::new(Mutex::new(None)), buffer: TextureBuffer::Disk(TextureFile::new(path)), } } const fn priority(&self) -> Priority { self.priority } fn _set_priority(buffer: &TextureBuffer, src: &mut Priority, priority: Priority) -> bool { // memory textures and textures in use should always be urgent if let TextureBuffer::Disk(disk) = buffer && !disk.is_used() { *src = priority; true } else { false } } fn unqueue_priority(&mut self) { let mut queued_priority = self.queued_priority.lock(); let unqueued_priority = queued_priority.unwrap_or(Priority::Unnecessary); if Self::_set_priority(&self.buffer, &mut self.priority, unqueued_priority) { *queued_priority = None; } } fn set_priority(&mut self, priority: Priority) { Self::_set_priority(&self.buffer, &mut self.priority, priority); } fn load_texture(&mut self) -> Result<&Rgba16Texture, LoadError> { match &mut self.buffer { TextureBuffer::Memory(ref texture) => Ok(texture), TextureBuffer::Disk(file) => file.load(), } } /// If the texture is loaded, return it. fn loaded_texture(&self) -> Option<&Rgba16Texture> { match &self.buffer { TextureBuffer::Memory(ref texture) => Some(texture), TextureBuffer::Disk(file) => file.loaded_texture(), } } fn unload(&mut self) { if let TextureBuffer::Disk(file) = &mut self.buffer { file.unload(); } } /// The amount of heap memory used for the texture, if any fn allocated_size(&self) -> usize { match &self.buffer { TextureBuffer::Memory(texture) => texture_size(texture), TextureBuffer::Disk(file) => file.allocated_size(), } } const fn is_loaded(&self) -> bool { match &self.buffer { TextureBuffer::Memory(_) => true, TextureBuffer::Disk(file) => file.is_loaded(), } } } pub struct TextureAtlas<'a> { width: u32, height: u32, packer: TexturePacker<'a, Rgba16Texture, TextureId>, } impl<'a> TextureAtlas<'a> { fn new(width: u32, height: u32, textures: &HashMap) -> Self { profiling::scope!("new atlas"); let mut packer = TexturePacker::new_skyline(TexturePackerConfig { max_width: width, max_height: height, allow_rotation: false, trim: false, texture_padding: 0, ..Default::default() }); for (id, texture) in textures { if texture.is_loaded() { let texture = texture .loaded_texture() .expect("texture couldn't be loaded"); // if the textures don't fit, make a bigger packer if packer.pack_own(*id, texture.clone()).is_err() { return Self::new(width * 2, height * 2, textures); } } } Self { width, height, packer, } } fn subtexture(&self, id: TextureId) -> Option<&Frame> { self.packer.get_frame(&id) } #[must_use] pub const fn atlas_width(&self) -> u32 { self.width } #[must_use] pub const fn atlas_height(&self) -> u32 { self.height } /// Get the x-position of a texture, if it is in the texture atlas #[must_use] #[allow(clippy::cast_precision_loss)] // TODO remove this pub fn subtexture_x(&self, id: TextureId) -> Option { let x = self.subtexture(id)?.frame.x; Some(x as f32 / self.width as f32) } /// Get the y-position of a texture, if it is in the texture atlas #[must_use] #[allow(clippy::cast_precision_loss)] // TODO remove this pub fn subtexture_y(&self, id: TextureId) -> Option { let y = self.subtexture(id)?.frame.y; Some(y as f32 / self.height as f32) } /// Get the width of a texture, if it is in the texture atlas #[must_use] #[allow(clippy::cast_precision_loss)] // TODO remove this pub fn subtexture_width(&self, id: TextureId) -> Option { let width = self.subtexture(id)?.frame.w; Some(width as f32 / self.width as f32) } /// Get the height of a texture, if it is in the texture atlas #[must_use] #[allow(clippy::cast_precision_loss)] // TODO remove this pub fn subtexture_height(&self, id: TextureId) -> Option { let height = self.subtexture(id)?.frame.h; Some(height as f32 / self.height as f32) } #[must_use] pub fn to_texture(&self) -> Rgba16Texture { profiling::scope!("export atlas"); vec_image_to_box( ImageExporter::export(&self.packer) .expect("ImageExporter error?") .into_rgba16(), ) } } pub struct TextureManager { textures: HashMap, max_size: usize, atlas_width: u32, atlas_height: u32, needs_atlas_update: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct TextureConfig { pub initial_capacity: usize, pub max_size: usize, pub atlas_width: u32, pub atlas_height: u32, } impl Default for TextureConfig { fn default() -> Self { Self { initial_capacity: 500, max_size: 10 * 1024 * 1024, // 10 MiB atlas_width: 3980, atlas_height: 2160, // 4K resolution } } } impl TextureManager { /// Create a new `TextureManager` with the given config options. #[must_use] pub fn new(config: TextureConfig) -> Self { let textures = HashMap::with_capacity(config.initial_capacity); Self { textures, max_size: config.max_size, atlas_width: config.atlas_width, atlas_height: config.atlas_height, needs_atlas_update: false, } } /// Load textures into memory that will be needed soon. Unload unnecessary textures pub fn cache_files(&mut self) { let mut textures: Vec<&mut Texture> = self .textures .values_mut() .map(|t| { t.unqueue_priority(); t }) .collect(); textures.sort_by_key(|t2| Reverse(t2.priority())); let max_size = self.max_size; let mut total_size = 0; for texture in textures { drop(texture.load_texture()); total_size += texture.allocated_size(); if total_size > max_size && texture.priority() != Priority::Urgent { texture.unload(); return; } } } /// Loads a texture from memory in the given format. /// /// # Errors /// /// This returns `Expected(DecodingError)` if the given buffer was invalid /// for the given format. pub fn load_from_memory( &mut self, buf: &[u8], format: ImageFormat, ) -> Result { let id = TextureId::new(); let texture = image::load_from_memory_with_format(buf, format.into()); let texture = texture.map_err(convert_image_decoding)?; let texture = texture.into_rgba16(); let texture = vec_image_to_box(texture); let texture = Texture::from_buffer(texture); self.textures.insert(id, texture); self.needs_atlas_update = true; Ok(id) } /// Loads a texture from disk. /// /// # Errors /// /// This returns an error if `priority` is set to [`Priority::Urgent`] but /// there was an error in loading the file to a texture. pub fn load_from_file( &mut self, path: impl AsRef, priority: Priority, ) -> Result { let id = TextureId::new(); let mut texture = Texture::from_path(path, priority); if priority == Priority::Urgent { match texture.load_texture() { Ok(_) => { self.textures.insert(id, texture); self.needs_atlas_update = true; } Err(e) => { self.textures.insert(id, texture); return Err(e); } } } else { self.textures.insert(id, texture); } Ok(id) } /// Loads a texture from disk. /// /// # Errors /// /// This returns an error if `priority` is set to [`Priority::Urgent`] but /// there was an error in loading the file to a texture. pub fn set_priority(&mut self, id: TextureId, priority: Priority) -> Result<(), LoadError> { let texture = self.textures.get_mut(&id).expect("invalid texture id"); texture.set_priority(priority); if !texture.is_loaded() && priority == Priority::Urgent { let texture = self.textures.get_mut(&id).expect("invalid texture id"); texture.load_texture()?; self.needs_atlas_update = true; } Ok(()) } /// This returns `true` if a texture has been set to have an urgent /// priority since the last time this function was called. pub fn needs_atlas_update(&mut self) -> bool { let needs_update = self.needs_atlas_update; self.needs_atlas_update = false; needs_update } /// Create a texture atlas pub fn atlas(&mut self) -> TextureAtlas<'_> { let atlas = TextureAtlas::new(self.atlas_width, self.atlas_height, &self.textures); self.atlas_width = atlas.atlas_width(); self.atlas_height = atlas.atlas_height(); atlas } }