use std::collections::HashMap; use std::mem::{self, MaybeUninit}; use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use image::{GenericImage, ImageBuffer}; use texture_packer::exporter::ImageExporter; use texture_packer::{Frame, TexturePacker, TexturePackerConfig}; use thiserror::Error; use crate::Priority; static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[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") } #[allow(clippy::missing_const_for_fn)] fn texture_size(image: &Rgba16Texture) -> usize { image.len() * mem::size_of::>() } struct TextureFile { path: Box, texture: Option>, } impl TextureFile { #[allow(clippy::missing_const_for_fn)] fn new(path: impl AsRef) -> Self { Self { path: path.as_ref().into(), texture: None, } } fn open(path: impl AsRef) -> Result { let mut this = Self::new(path); this.load()?; Ok(this) } 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 unload(&mut self) { if let Some(arc) = &self.texture { if Arc::strong_count(arc) == 1 { self.texture = None; } } } 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, buffer: TextureBuffer, } impl Texture { fn from_buffer(texture: Rgba16Texture) -> Self { Self { priority: Priority::Urgent, // indicates that it can't be unloaded buffer: TextureBuffer::Memory(Arc::new(texture)), } } fn from_path(path: impl AsRef, priority: Priority) -> Self { Self { priority, buffer: TextureBuffer::Disk(TextureFile::new(path)), } } const fn priority(&self) -> Priority { self.priority } fn set_priority(&mut self, priority: Priority) { // memory textures should always be urgent if let TextureBuffer::Disk(_) = self.buffer { 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(), } } fn unload(&mut self) { if let TextureBuffer::Disk(file) = &mut self.buffer { file.unload(); } } fn allocated_size(&self) -> usize { match &self.buffer { TextureBuffer::Memory(texture) => texture_size(texture), TextureBuffer::Disk(file) => file.allocated_size(), } } } pub struct TextureRef<'a> { id: TextureId, texture: Arc, manager: &'a TextureManager, } impl<'a> TextureRef<'a> { fn texture_width(&self) -> u32 { self.texture.width() } fn texture_height(&self) -> u32 { self.texture.height() } #[allow(clippy::missing_const_for_fn)] fn texture(&self) -> &Rgba16Texture { &self.texture } // TODO: it's safer to replace this with a position thingy #[must_use] pub fn atlas_x(&self) -> f32 { self.manager .subtexture_x(self.id) .expect("not in texture atlas") } #[must_use] pub fn atlas_y(&self) -> f32 { self.manager .subtexture_y(self.id) .expect("not in texture atlas") } #[must_use] pub fn width(&self) -> f32 { self.manager.texture_width(self) } #[must_use] pub fn height(&self) -> f32 { self.manager.texture_height(self) } } pub struct TextureManager { textures: HashMap, packer: TexturePacker<'static, Rgba16Texture, TextureId>, atlas: Rgba16Texture, // cached texture atlas max_size: usize, width: u32, height: u32, } #[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, } fn packer(width: u32, height: u32) -> TexturePacker<'static, Rgba16Texture, TextureId> { TexturePacker::new_skyline(TexturePackerConfig { max_width: width, max_height: height, allow_rotation: false, trim: false, texture_padding: 0, ..Default::default() }) } impl TextureManager { /// Create a new `TextureManager` with the given config options. #[must_use] pub fn new(config: TextureConfig) -> Self { let width = config.atlas_width; let height = config.atlas_height; let max_size = config.max_size; let textures = HashMap::with_capacity(config.initial_capacity); let packer = packer(width, height); let atlas: Box<[MaybeUninit]> = Box::new_zeroed_slice((4 * width * height) as _); let atlas = unsafe { atlas.assume_init() }; let atlas = Rgba16Texture::from_raw(width, height, atlas); let atlas = atlas.expect("atlas cache is too small"); Self { textures, packer, atlas, max_size, width, height, } } fn can_load(&mut self, texture: &Texture) -> bool { let mut textures: Vec<&mut Texture> = self.textures.values_mut().collect(); textures.sort_by_key(|a| a.priority()); textures.reverse(); let max_size = self.max_size; let priority = texture.priority(); let mut total_size = texture.allocated_size(); for texture in textures { if total_size + texture.allocated_size() < max_size { total_size += texture.allocated_size(); } else if texture.priority() < priority { texture.unload(); } else { return false; } } true } /// 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); Ok(id) } 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 || self.can_load(&texture) { match texture.load_texture() { Ok(_) => { self.textures.insert(id, texture); } Err(e) => { self.textures.insert(id, texture); return Err(e); } } } else { self.textures.insert(id, texture); } Ok(id) } pub fn atlas(&mut self) -> &Rgba16Texture { let atlas = { profiling::scope!("export atlas"); ImageExporter::export(&self.packer).expect("ImageExporter error?") }; profiling::scope!("copy image"); self.atlas .copy_from(&atlas.into_rgba16(), 0, 0) .expect("image cache was too small"); &self.atlas } /// Get the subtexture in the texture atlas fn subtexture(&self, id: TextureId) -> Option<&Frame> { self.packer.get_frame(&id) } /// 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)] 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 #[must_use] #[allow(clippy::cast_precision_loss)] pub fn texture_width(&self, texture: &TextureRef) -> f32 { texture.texture_width() as f32 / self.width as f32 } /// Get the height of a texture #[must_use] #[allow(clippy::cast_precision_loss)] pub fn texture_height(&self, texture: &TextureRef) -> f32 { texture.texture_height() as f32 / self.height as f32 } }