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.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ImageFormat {
Bmp,
Ico,
Farbfeld,
}
impl From<ImageFormat> 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<DecodingError>;
#[allow(clippy::missing_const_for_fn)]
fn convert_image_decoding(e: image::ImageError) -> Expect<DecodingError> {
if let image::ImageError::Decoding(de) = e {
Expected(de.into())
} else {
Unexpected(e.into())
}
}
type Rgba16Texture = image::ImageBuffer<image::Rgba<u16>, Box<[u16]>>;
pub struct TextureManager {
textures: HashMap<TextureId, Rgba16Texture>,
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<u16>]> = Box::new_zeroed_slice((4 * width * height) as _);
let atlas = unsafe { atlas.assume_init() };
let atlas =
Rgba16Texture::from_raw(width, height, atlas).expect("atlas cache is too small");
Self {
textures,
packer,
atlas,
width,
height,
}
}
/// Resize the texture atlas
pub fn resize_atlas(&mut self, width: u32, height: u32) {
self.packer = packer(width, height);
}
/// 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.
#[allow(clippy::missing_panics_doc)]
pub fn load_from_memory(
&mut self,
buf: &[u8],
format: ImageFormat,
) -> Result<TextureId, LoadError> {
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();
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) -> Option<&Rgba16Texture> {
self.textures.get(&id)
}
/// Get the subtexture
fn subtexture(&self, id: TextureId) -> Option<&Frame<TextureId>> {
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<f32> {
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<f32> {
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).expect("invalid TextureId").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).expect("invalid TextureId").height();
height as f32 / self.height as f32
}
}
|