summaryrefslogtreecommitdiff
path: root/src/texture.rs
blob: 457fea8254089a1005e47833d37c2de32484a7be (plain)
use std::error::Error;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicUsize, Ordering};

use image::error::DecodingError;
use image::{DynamicImage, EncodableLayout, GenericImage, ImageError, RgbaImage};
use texture_packer::{
	exporter::{ExportResult, ImageExporter},
	MultiTexturePacker, TexturePackerConfig,
};
use thiserror::Error;

static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0);

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TextureId(usize);

impl TextureId {
	#[allow(clippy::new_without_default)]
	pub fn new() -> Self {
		Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed))
	}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ImageFormat {
	Bmp,
	Ico,
	Farbfeld,
}

impl ImageFormat {
	const fn format(self) -> image::ImageFormat {
		match self {
			Self::Bmp => image::ImageFormat::Bmp,
			Self::Ico => image::ImageFormat::Ico,
			Self::Farbfeld => image::ImageFormat::Farbfeld,
		}
	}
}

type PackError = impl std::fmt::Debug;

#[derive(Error, Debug)]
pub enum TextureError {
	#[error("{:?}", .0)]
	TextureTooLarge(PackError), // use an error with a source
	#[error("{}", .0)]
	BadImage(#[source] DecodingError), // TODO don't export this
	#[error("Unexpected Error (this is a bug in alligator_render): {}", .0)]
	Unexpected(#[source] Box<dyn Error>),
}

impl From<ImageError> for TextureError {
	fn from(ie: ImageError) -> Self {
		match ie {
			ImageError::Decoding(de) => Self::BadImage(de),
			_ => Self::Unexpected(Box::new(ie)),
		}
	}
}

// TODO make this Debug
// TODO make these resizable
// TODO this could probably be moved into WgpuTextures
pub struct TextureAtlases<'a> {
	packer: MultiTexturePacker<'a, image::RgbaImage, TextureId>,
	images: Vec<RgbaImage>,
	width: u32,
	height: u32,
}

impl<'a> Default for TextureAtlases<'a> {
	fn default() -> Self {
		Self::new(1024, 1024)
	}
}

macro_rules! texture_info {
	($name: ident, $prop: ident) => {
		pub fn $name(&self, id: TextureId) -> Option<u32> {
			let frame = self.texture_frame(id)?;
			Some(frame.frame.$prop)
		}
	};
}

impl<'a> TextureAtlases<'a> {
	/// Creates a new texture atlas, with the given size
	// TODO why is this u32?
	pub fn new(width: u32, height: u32) -> Self {
		Self {
			packer: MultiTexturePacker::new_skyline(TexturePackerConfig {
				max_width: width,
				max_height: height,
				..Default::default()
			}),
			width,
			height,
			images: Vec::with_capacity(1),
		}
	}

	// TODO support RGBA16
	pub fn load_from_memory(
		&mut self,
		buf: &[u8],
		format: ImageFormat,
	) -> Result<TextureId, TextureError> {
		let img = image::load_from_memory_with_format(buf, format.format())?.into_rgba8();
		let id = TextureId::new();
		self.packer
			.pack_own(id, img)
			.map_err(TextureError::TextureTooLarge)?;

		Ok(id)
	}

	fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame<TextureId>> {
		self.packer
			.get_pages()
			.iter()
			.map(|a| a.get_frame(&id))
			.next()?
	}

	texture_info!(texture_width, w);
	texture_info!(texture_height, h);
	texture_info!(texture_x, x);
	texture_info!(texture_y, y);

	const fn extent_3d(&self) -> wgpu::Extent3d {
		wgpu::Extent3d {
			width: self.width,
			height: self.height,
			depth_or_array_layers: 1,
		}
	}

	fn atlases(&self) -> ExportResult<Box<[DynamicImage]>> {
		self.packer
			.get_pages()
			.iter()
			.map(ImageExporter::export)
			.collect::<ExportResult<Vec<DynamicImage>>>()
			.map(Vec::into_boxed_slice)
	}

	fn push_image(&mut self) -> &mut RgbaImage {
		self.images.push(
			RgbaImage::from_raw(
				self.width,
				self.height,
				vec![0; 4 * self.width as usize * self.height as usize],
			)
			.expect("the image was the wrong size"),
		);

		self.images
			.last_mut()
			.expect("we just added an image to the list")
	}

	fn fill_images(&mut self) -> ExportResult<()> {
		for (i, atlas) in self.atlases()?.iter().enumerate() {
			#[allow(clippy::option_if_let_else)]
			if let Some(image) = self.images.get_mut(i) {
				image
					.copy_from(atlas, 0, 0)
					.expect("atlases shouldn't be too large");
			} else {
				let image = self.push_image();
				image
					.copy_from(atlas, 0, 0)
					.expect("atlases shouldn't be too large");
			}
		}

		Ok(())
	}

	fn clear(&mut self) {
		self.packer = MultiTexturePacker::new_skyline(TexturePackerConfig {
			max_width: self.width,
			max_height: self.height,
			..Default::default()
		});
	}

	fn images(&mut self) -> ExportResult<&[RgbaImage]> {
		self.fill_images()?;
		Ok(&self.images)
	}
}

pub struct WgpuTextures {
	atlases: TextureAtlases<'static>,
	diffuse_texture: wgpu::Texture,
	diffuse_bind_group: wgpu::BindGroup,
}

macro_rules! get_info {
	($name: ident, $divisor: ident) => {
		// TODO try to remove this
		#[allow(clippy::cast_precision_loss)]
		pub fn $name(&self, id: TextureId) -> Option<f32> {
			self.atlases
				.$name(id)
				.map(|u| u as f32 / self.atlases.extent_3d().$divisor as f32)
		}
	};
}

impl WgpuTextures {
	// TODO this is still too large
	pub fn new(device: &wgpu::Device, width: u32, height: u32) -> (Self, wgpu::BindGroupLayout) {
		let atlases = TextureAtlases::new(width, height);
		let atlas_size = atlases.extent_3d();

		let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor {
			label: Some("Diffuse Texture"),
			size: atlas_size,
			mip_level_count: 1,
			sample_count: 1,
			dimension: wgpu::TextureDimension::D2,
			format: wgpu::TextureFormat::Rgba8UnormSrgb,
			usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
		});

		// TODO I don't think this refreshes anything
		let diffuse_texture_view =
			diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());

		let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());

		let texture_bind_group_layout =
			device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
				label: Some("Texture Bind Group Layout"),
				entries: &[
					wgpu::BindGroupLayoutEntry {
						binding: 0,
						visibility: wgpu::ShaderStages::FRAGMENT,
						ty: wgpu::BindingType::Texture {
							sample_type: wgpu::TextureSampleType::Float { filterable: true },
							view_dimension: wgpu::TextureViewDimension::D2,
							multisampled: false,
						},
						count: None,
					},
					wgpu::BindGroupLayoutEntry {
						binding: 1,
						visibility: wgpu::ShaderStages::FRAGMENT,
						ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
						count: None,
					},
				],
			});

		let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
			label: Some("Diffuse Bind Group"),
			layout: &texture_bind_group_layout,
			entries: &[
				wgpu::BindGroupEntry {
					binding: 0,
					resource: wgpu::BindingResource::TextureView(&diffuse_texture_view),
				},
				wgpu::BindGroupEntry {
					binding: 1,
					resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
				},
			],
		});

		(
			Self {
				atlases,
				diffuse_texture,
				diffuse_bind_group,
			},
			texture_bind_group_layout,
		)
	}

	/// Loads a texture from memory, in the given file format
	///
	/// # Errors
	///
	/// This returns an error if the texture is not in the given format, or if
	/// the texture is so large that it cannot fit in the texture atlas.
	pub fn texture_from_memory(
		&mut self,
		texture: &[u8],
		format: ImageFormat,
	) -> Result<TextureId, TextureError> {
		self.atlases.load_from_memory(texture, format)
	}

	get_info!(texture_width, width);
	get_info!(texture_height, height);
	get_info!(texture_x, width);
	get_info!(texture_y, height);

	pub const fn bind_group(&self) -> &wgpu::BindGroup {
		&self.diffuse_bind_group
	}

	pub fn fill_textures(&mut self, queue: &wgpu::Queue) {
		let atlas_size = self.atlases.extent_3d();

		// put the packed texture into the base image
		let Ok(atlases) = self.atlases.images() else { return };
		let Some(atlas) = atlases.first() else { return };

		// copy that to the gpu
		queue.write_texture(
			wgpu::ImageCopyTexture {
				texture: &self.diffuse_texture,
				mip_level: 0,
				origin: wgpu::Origin3d::ZERO,
				aspect: wgpu::TextureAspect::All,
			},
			atlas.as_bytes(),
			wgpu::ImageDataLayout {
				offset: 0,
				bytes_per_row: NonZeroU32::new(atlas_size.width * 4),
				rows_per_image: NonZeroU32::new(atlas_size.height),
			},
			atlas_size,
		);
	}

	pub fn clear_textures(&mut self) {
		self.atlases.clear();
	}
}