use std::collections::HashMap; use std::mem::{self, MaybeUninit}; use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use image::{GenericImage, ImageBuffer}; use texture_packer::exporter::ImageExporter; use texture_packer::{Frame, TexturePacker, TexturePackerConfig}; use thiserror::Error; 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 ImageFile { path: Box, texture: Option, } impl ImageFile { #[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 texture = image::open(&path).map_err(convert_image_load_error)?; let texture = texture.to_rgba16(); let texture = vec_image_to_box(texture); Ok(Self { path: path.as_ref().into(), texture: Some(texture), }) } 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 = vec_image_to_box(texture); self.texture = Some(texture); } Ok(self.texture.as_ref().expect("the texture wasn't loaded")) } fn unload(&mut self) { self.texture = None; } fn allocated_size(&self) -> usize { self.texture .as_ref() .map_or(0, |texture| texture.len() * mem::size_of::()) } } pub struct TextureManager { textures: HashMap, packer: TexturePacker<'static, Rgba16Texture, TextureId>, atlas: Rgba16Texture, // cached texture atlas 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 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, width, height, } } pub fn load_to_atlas(&mut self, id: TextureId) { let texture = self.texture(id); if self.packer.pack_own(id, texture.clone()).is_err() { let texture = self.texture(id); self.resize_atlas(self.width + texture.width(), self.height + texture.height()); let texture = self.texture(id); self.packer .pack_own(id, texture.clone()) .expect("packer is still too small after resizing"); } } /// Resize the texture atlas pub fn resize_atlas(&mut self, width: u32, height: u32) { let old_packer = &self.packer; let mut new_packer = packer(width, height); for id in old_packer.get_frames().keys() { let texture = self.texture(*id).clone(); new_packer .pack_own(*id, texture) .expect("resized packer is too small to hold subtextures"); } self.packer = new_packer; } /// Clear the texture atlas pub fn clear_atlas(&mut self) { self.packer = packer(self.width, self.height); } /// 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); self.textures.insert(id, texture); Ok(id) } pub fn atlas(&mut self) -> &Rgba16Texture { let atlas = { profiling::scope!("export atlas"); // TODO unexpect_msg? 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 } fn texture(&self, id: TextureId) -> &Rgba16Texture { self.textures.get(&id).expect("invalid TextureId") } /// 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, id: TextureId) -> f32 { let width = self.texture(id).width(); 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, id: TextureId) -> f32 { let height = self.texture(id).height(); height as f32 / self.height as f32 } }