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), } impl From 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 pub struct TextureAtlases<'a> { packer: MultiTexturePacker<'a, image::RgbaImage, TextureId>, images: Vec, 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 { 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 { 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> { 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> { self.packer .get_pages() .iter() .map(ImageExporter::export) .collect::>>() .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 { 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 { 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(); } }