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 parking_lot::Mutex;
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<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);
#[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<image::Rgba<u16>, Box<[u16]>>;
fn vec_image_to_box(vec_image: image::ImageBuffer<image::Rgba<u16>, Vec<u16>>) -> 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::<image::Rgba<u16>>()
}
struct TextureFile {
path: Box<Path>,
texture: Option<Arc<Rgba16Texture>>,
}
impl TextureFile {
#[allow(clippy::missing_const_for_fn)]
fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().into(),
texture: None,
}
}
fn open(path: impl AsRef<Path>) -> Result<Self, LoadError> {
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 is_used(&self) -> bool {
let Some(arc) = &self.texture else { return false };
Arc::strong_count(arc) > 1
}
fn unload(&mut self) {
if !self.is_used() {
self.texture = None;
}
}
fn allocated_size(&self) -> usize {
self.texture.as_ref().map_or(0, |t| texture_size(t))
}
}
enum TextureBuffer {
Memory(Arc<Rgba16Texture>),
Disk(TextureFile),
}
struct Texture {
priority: Priority,
queued_priority: Arc<Mutex<Option<Priority>>>,
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<Path>, 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(),
}
}
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(),
}
}
const fn is_loaded(&self) -> bool {
match &self.buffer {
TextureBuffer::Memory(_) => true,
TextureBuffer::Disk(file) => file.is_loaded(),
}
}
}
pub struct TextureRef {
id: TextureId,
// TODO we don't need this. Just the width and height will do
texture: Arc<Rgba16Texture>,
queued_priority: Arc<Mutex<Option<Priority>>>,
}
impl TextureRef {
#[must_use]
pub const fn id(&self) -> TextureId {
self.id
}
// TODO: it's safer to replace this with a position thingy
#[must_use]
pub fn atlas_x(&self, manager: &TextureManager) -> f32 {
manager.subtexture_x(self.id).expect("not in texture atlas")
}
#[must_use]
pub fn atlas_y(&self, manager: &TextureManager) -> f32 {
manager.subtexture_y(self.id).expect("not in texture atlas")
}
// TODO: it's safer to replace this with a position thingy
#[must_use]
#[allow(clippy::cast_precision_loss)] // TODO remove this
pub fn width(&self, manager: &TextureManager) -> f32 {
self.texture.width() as f32 / manager.atlas_width() as f32
}
#[must_use]
#[allow(clippy::cast_precision_loss)] // TODO remove this
pub fn height(&self, manager: &TextureManager) -> f32 {
self.texture.height() as f32 / manager.atlas_height() as f32
}
pub fn queue_priority(&self, priority: Priority) {
let mut queued_priority = self.queued_priority.lock();
*queued_priority = Some(priority);
}
}
pub struct TextureManager {
textures: HashMap<TextureId, Texture>,
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<u16>]> = 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, size: usize, priority: Priority) -> 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 = priority;
let mut total_size = size;
for texture in textures {
texture.unqueue_priority();
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<TextureId, DecodingError> {
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<Path>,
priority: Priority,
) -> Result<TextureId, LoadError> {
let id = TextureId::new();
let mut texture = Texture::from_path(path, priority);
let size = texture.allocated_size();
if priority != Priority::Unnecessary
&& (priority == Priority::Urgent || self.can_load(size, texture.priority()))
{
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 set_priority(&mut self, id: TextureId, priority: Priority) -> Result<(), LoadError> {
let texture = self.textures.get_mut(&id).expect("invalid texture id");
let old_priority = texture.priority();
texture.set_priority(priority);
let size = texture.allocated_size();
let priority = texture.priority();
if priority > old_priority
&& priority != Priority::Unnecessary
&& !texture.is_loaded()
&& (priority == Priority::Urgent || self.can_load(size, priority))
{
let texture = self.textures.get_mut(&id).expect("invalid texture id");
texture.load_texture()?;
}
Ok(())
}
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<TextureId>> {
self.packer.get_frame(&id)
}
const fn atlas_width(&self) -> u32 {
self.width
}
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<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)] // TODO remove this
pub fn subtexture_y(&self, id: TextureId) -> Option<f32> {
let y = self.subtexture(id)?.frame.y;
Some(y as f32 / self.height as f32)
}
}
|