summaryrefslogtreecommitdiff
path: root/resources/src
diff options
context:
space:
mode:
Diffstat (limited to 'resources/src')
-rw-r--r--resources/src/lib.rs13
-rw-r--r--resources/src/texture.rs394
2 files changed, 407 insertions, 0 deletions
diff --git a/resources/src/lib.rs b/resources/src/lib.rs
new file mode 100644
index 0000000..9cbbba0
--- /dev/null
+++ b/resources/src/lib.rs
@@ -0,0 +1,13 @@
+#![feature(new_uninit, let_chains)]
+#![warn(clippy::nursery, clippy::pedantic)]
+#![allow(clippy::module_name_repetitions)]
+
+pub mod texture;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Priority {
+ Unnecessary,
+ Possible(u8),
+ Eventual(u8),
+ Urgent,
+}
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()
+ }
+}