use std::collections::HashMap; use std::mem::MaybeUninit; use std::sync::atomic::{AtomicUsize, Ordering}; use exun::{Expect, Expected, Unexpected}; 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); type LoadError = Expect; #[allow(clippy::missing_const_for_fn)] fn convert_image_decoding(e: image::ImageError) -> Expect { if let image::ImageError::Decoding(de) = e { Expected(de.into()) } else { Unexpected(e.into()) } } type Rgba16Texture = image::ImageBuffer, Box<[u16]>>; 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 capacity: 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.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_rgb16(); let width = texture.width(); let height = texture.height(); let buf = texture.into_raw().into_boxed_slice(); // TODO this expect can be removed by using unexpect let texture = ImageBuffer::from_raw(width, height, buf).expect("image buffer is too small"); 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 } }