diff options
| author | Micha White <botahamec@outlook.com> | 2023-02-13 00:24:07 -0500 |
|---|---|---|
| committer | Micha White <botahamec@outlook.com> | 2023-02-13 00:24:07 -0500 |
| commit | 861b467b95be55db3a42182b77dba944869bf49f (patch) | |
| tree | f58057d5ab9ad53601332981a31553b0ad05fe1b /resources/src/texture.rs | |
| parent | c31d51487082c6cf243ecd10da71a15a78d41add (diff) | |
Rename the subdirectories
Diffstat (limited to 'resources/src/texture.rs')
| -rw-r--r-- | resources/src/texture.rs | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/resources/src/texture.rs b/resources/src/texture.rs new file mode 100644 index 0000000..3a5bf3e --- /dev/null +++ b/resources/src/texture.rs @@ -0,0 +1,394 @@ +use std::cmp::Reverse; +use std::mem; +use std::path::Path; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; + +use dashmap::DashMap; +use image::ImageBuffer; +use parking_lot::Mutex; +use thiserror::Error; + +use crate::Priority; + +/// The next texture ID +static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +/// A unique identifier for a texture +#[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"), + } +} + +pub 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") +} + +/// Get the size, in bytes, of the texture +#[allow(clippy::missing_const_for_fn)] +fn texture_size(image: &Rgba16Texture) -> usize { + image.len() * mem::size_of::<image::Rgba<u16>>() +} + +/// A texture from disk +struct TextureFile { + path: Box<Path>, + texture: Option<Arc<Rgba16Texture>>, +} + +impl TextureFile { + /// This doesn't load the texture + #[allow(clippy::missing_const_for_fn)] + fn new(path: impl AsRef<Path>) -> Self { + Self { + path: path.as_ref().into(), + texture: None, + } + } + + const fn is_loaded(&self) -> bool { + self.texture.is_some() + } + + fn load(&mut self) -> Result<&Rgba16Texture, LoadError> { + if self.texture.is_none() { + log::warn!("{} was not pre-loaded", self.path.to_string_lossy()); + 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 loaded_texture(&self) -> Option<&Rgba16Texture> { + self.texture.as_deref() + } + + fn is_used(&self) -> bool { + let Some(arc) = &self.texture else { return false }; + Arc::strong_count(arc) > 1 + } + + /// Unloads the texture from memory if it isn't being used + fn unload(&mut self) { + if !self.is_used() { + self.texture = None; + } + } + + /// The amount of heap memory used, in bytes. This returns 0 if the texture + /// hasn't been loaded yet. + 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(), + } + } + + /// If the texture is loaded, return it. + fn loaded_texture(&self) -> Option<&Rgba16Texture> { + match &self.buffer { + TextureBuffer::Memory(ref texture) => Some(texture), + TextureBuffer::Disk(file) => file.loaded_texture(), + } + } + + fn unload(&mut self) { + if let TextureBuffer::Disk(file) = &mut self.buffer { + file.unload(); + } + } + + /// The amount of heap memory used for the texture, if any + 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 TextureManager { + textures: DashMap<TextureId, Texture>, + max_size: usize, + needs_atlas_update: AtomicBool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TextureManagerConfig { + /// The initial capacity of the texture manager. This defaults to 500 textures. + pub initial_capacity: usize, + /// The maximum amount of heap usage acceptable. Defaults to 10 MiB. + pub max_size: usize, +} + +impl Default for TextureManagerConfig { + fn default() -> Self { + Self { + initial_capacity: 500, + max_size: 10 * 1024 * 1024, // 10 MiB + } + } +} + +impl TextureManager { + /// Create a new `TextureManager` with the given config options. + #[must_use] + pub fn new(config: &TextureManagerConfig) -> Self { + let textures = DashMap::with_capacity(config.initial_capacity); + + Self { + textures, + max_size: config.max_size, + needs_atlas_update: AtomicBool::new(false), + } + } + + /// Load textures into memory that will be needed soon. Unload unnecessary textures + pub fn cache_files(&self) { + let mut textures: Vec<_> = self + .textures + .iter_mut() + .map(|mut t| { + t.value_mut().unqueue_priority(); + t + }) + .collect(); + textures.sort_by_key(|t2| Reverse(t2.priority())); + + let max_size = self.max_size; + let mut total_size = 0; + + for texture in &mut textures { + drop(texture.load_texture()); + total_size += texture.allocated_size(); + if total_size > max_size && texture.priority() != Priority::Urgent { + texture.unload(); + return; + } + } + } + + /// 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( + &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); + self.needs_atlas_update.store(true, Ordering::Release); + + Ok(id) + } + + /// Loads a texture from disk. + /// + /// # Errors + /// + /// This returns an error if `priority` is set to [`Priority::Urgent`] but + /// there was an error in loading the file to a texture. + pub fn load_from_file( + &self, + path: impl AsRef<Path>, + priority: Priority, + ) -> Result<TextureId, LoadError> { + let id = TextureId::new(); + let mut texture = Texture::from_path(path, priority); + + if priority == Priority::Urgent { + match texture.load_texture() { + Ok(_) => { + self.textures.insert(id, texture); + self.needs_atlas_update.store(true, Ordering::Release); + } + Err(e) => { + self.textures.insert(id, texture); + return Err(e); + } + } + } else { + self.textures.insert(id, texture); + } + + Ok(id) + } + + /// Loads a texture from disk. + /// + /// # Errors + /// + /// This returns an error if `priority` is set to [`Priority::Urgent`] but + /// there was an error in loading the file to a texture. + pub fn set_priority(&self, id: TextureId, priority: Priority) -> Result<(), LoadError> { + let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); + texture.set_priority(priority); + + if !texture.is_loaded() && priority == Priority::Urgent { + let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); + texture.load_texture()?; + self.needs_atlas_update.store(true, Ordering::Release); + } + + Ok(()) + } + + /// This returns `true` if a texture has been set to have an urgent + /// priority since the last time this function was called. + pub fn needs_atlas_update(&self) -> bool { + self.needs_atlas_update.fetch_and(false, Ordering::AcqRel) + } + + /// Load a texture into memory, if it hasn't been already. Then return a + /// copy of the texture. + /// + /// # Errors + /// + /// This returns an error if an error occurs in loading the texture from + /// disk, such as the file not existing, or not being a valid texture. + pub fn load_texture(&self, id: TextureId) -> Result<Rgba16Texture, LoadError> { + self.textures + .get_mut(&id) + .expect("the TextureId was invalid") + .load_texture() + .cloned() + } +} |
