summaryrefslogtreecommitdiff
path: root/alligator_resources/src/texture.rs
blob: 9aca56d6abaa3d49d29602c0002024a9b00e6a54 (plain)
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 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 unload(&mut self) {
		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,
	buffer: TextureBuffer,
}

impl Texture {
	fn from_buffer(texture: Rgba16Texture) -> Self {
		Self {
			priority: Priority::Urgent,
			buffer: TextureBuffer::Memory(Arc::new(texture)),
		}
	}

	fn from_path(path: impl AsRef<Path>, priority: Priority) -> Self {
		Self {
			priority,
			buffer: TextureBuffer::Disk(TextureFile::new(path)),
		}
	}

	const fn priority(&self) -> Priority {
		self.priority
	}

	fn set_priority(&mut self, priority: Priority) {
		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(),
		}
	}
}

pub struct TextureRef<'a> {
	id: TextureId,
	texture: Arc<Rgba16Texture>,
	manager: &'a TextureManager,
}

impl<'a> TextureRef<'a> {
	fn texture_width(&self) -> u32 {
		self.texture.width()
	}

	fn texture_height(&self) -> u32 {
		self.texture.height()
	}

	#[allow(clippy::missing_const_for_fn)]
	fn texture(&self) -> &Rgba16Texture {
		&self.texture
	}

	// TODO: it's safer to replace this with a position thingy
	#[must_use]
	pub fn atlas_x(&self) -> f32 {
		self.manager
			.subtexture_x(self.id)
			.expect("not in texture atlas")
	}

	#[must_use]
	pub fn atlas_y(&self) -> f32 {
		self.manager
			.subtexture_y(self.id)
			.expect("not in texture atlas")
	}

	#[must_use]
	pub fn width(&self) -> f32 {
		self.manager.texture_width(self)
	}

	#[must_use]
	pub fn height(&self) -> f32 {
		self.manager.texture_height(self)
	}
}

pub struct TextureManager {
	textures: HashMap<TextureId, Texture>,
	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 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 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,
			width,
			height,
		}
	}

	pub fn load_to_atlas(&mut self, id: TextureId, texture: &TextureRef) {
		let get_texture = || texture.texture().clone();

		if self.packer.pack_own(id, get_texture()).is_err() {
			let texture = get_texture();
			self.resize_atlas(self.width + texture.width(), self.height + texture.height());

			self.packer
				.pack_own(id, get_texture())
				.expect("packer is still too small after resizing");
		}
	}

	/// Clear and 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.
	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 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)
	}

	/// 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, texture: &TextureRef) -> f32 {
		texture.texture_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, texture: &TextureRef) -> f32 {
		texture.texture_height() as f32 / self.height as f32
	}
}