summaryrefslogtreecommitdiff
path: root/alligator_resources/src/texture.rs
blob: e5b414704ecdeed339d450c81c648d8515662177 (plain)
use std::cmp::Reverse;
use std::collections::HashMap;
use std::mem::{self};
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 texture_packer::exporter::ImageExporter;
use texture_packer::{Frame, TexturePacker, TexturePackerConfig};
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"),
	}
}

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 TextureAtlas<'a> {
	width: u32,
	height: u32,
	packer: TexturePacker<'a, Rgba16Texture, TextureId>,
}

impl<'a> TextureAtlas<'a> {
	fn new(width: u32, height: u32, textures: &HashMap<TextureId, Texture>) -> Self {
		profiling::scope!("new atlas");

		let mut packer = TexturePacker::new_skyline(TexturePackerConfig {
			max_width: width,
			max_height: height,
			allow_rotation: false,
			trim: false,
			texture_padding: 0,
			..Default::default()
		});

		for (id, texture) in textures {
			if texture.is_loaded() {
				let texture = texture
					.loaded_texture()
					.expect("texture couldn't be loaded");

				// if the textures don't fit, make a bigger packer
				if packer.pack_own(*id, texture.clone()).is_err() {
					return Self::new(width * 2, height * 2, textures);
				}
			}
		}

		Self {
			width,
			height,
			packer,
		}
	}

	fn subtexture(&self, id: TextureId) -> Option<&Frame<TextureId>> {
		self.packer.get_frame(&id)
	}

	#[must_use]
	pub const fn atlas_width(&self) -> u32 {
		self.width
	}

	#[must_use]
	pub 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)
	}

	/// Get the width of a texture, if it is in the texture atlas
	#[must_use]
	#[allow(clippy::cast_precision_loss)] // TODO remove this
	pub fn subtexture_width(&self, id: TextureId) -> Option<f32> {
		let width = self.subtexture(id)?.frame.w;
		Some(width as f32 / self.width as f32)
	}

	/// Get the height of a texture, if it is in the texture atlas
	#[must_use]
	#[allow(clippy::cast_precision_loss)] // TODO remove this
	pub fn subtexture_height(&self, id: TextureId) -> Option<f32> {
		let height = self.subtexture(id)?.frame.h;
		Some(height as f32 / self.height as f32)
	}

	#[must_use]
	pub fn to_texture(&self) -> Rgba16Texture {
		profiling::scope!("export atlas");
		vec_image_to_box(
			ImageExporter::export(&self.packer)
				.expect("ImageExporter error?")
				.into_rgba16(),
		)
	}
}

pub struct TextureManager {
	textures: DashMap<TextureId, Texture>,
	max_size: usize,
	atlas_width: u32,
	atlas_height: u32,
	needs_atlas_update: AtomicBool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextureManagerConfig {
	pub initial_capacity: usize,
	pub max_size: usize,
	pub atlas_width: u32,
	pub atlas_height: u32,
}

impl Default for TextureManagerConfig {
	fn default() -> Self {
		Self {
			initial_capacity: 500,
			max_size: 10 * 1024 * 1024, // 10 MiB
			atlas_width: 3980,
			atlas_height: 2160, // 4K resolution
		}
	}
}

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,
			atlas_width: config.atlas_width,
			atlas_height: config.atlas_height,
			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(
		&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);
		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(
		&mut 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(&mut 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(&mut self) -> bool {
		self.needs_atlas_update.fetch_and(false, Ordering::AcqRel)
	}

	pub fn load_texture(&self, id: TextureId) -> Result<Rgba16Texture, LoadError> {
		self.textures
			.get_mut(&id)
			.expect("the TextureId was invalid")
			.load_texture()
			.cloned()
	}

	// Create a texture atlas
	/*pub fn atlas(&mut self) -> TextureAtlas<'_> {
		let atlas = TextureAtlas::new(self.atlas_width, self.atlas_height, &self.textures);
		self.atlas_width = atlas.atlas_width();
		self.atlas_height = atlas.atlas_height();
		atlas
	}*/
}