use std::error::Error;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicUsize, Ordering};
use image::error::DecodingError;
use image::{EncodableLayout, GenericImage, ImageError, RgbaImage};
use texture_packer::TexturePacker;
use texture_packer::{
exporter::{ExportResult, ImageExporter},
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 {
Ico,
Farbfeld,
}
impl ImageFormat {
const fn format(self) -> image::ImageFormat {
match self {
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)),
}
}
}
const fn extent_3d(width: u32, height: u32) -> wgpu::Extent3d {
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
}
}
// TODO make this Debug
// TODO make these resizable
pub struct TextureAtlas {
packer: TexturePacker<'static, image::RgbaImage, TextureId>,
diffuse_texture: wgpu::Texture,
diffuse_bind_group: wgpu::BindGroup,
image: RgbaImage,
width: u32,
height: u32,
changed: bool,
}
macro_rules! texture_info {
($name: ident, $prop: ident, $divisor: ident) => {
pub fn $name(&self, id: TextureId) -> Option<f32> {
let frame = self.texture_frame(id)?;
let property = frame.frame.$prop;
let value = property as f32 / self.$divisor as f32;
Some(value)
}
};
}
impl TextureAtlas {
/// Creates a new texture atlas, with the given size
// TODO why is this u32?
// TODO this is still too large
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> (Self, wgpu::BindGroupLayout) {
let atlas_size = extent_3d(width, height);
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 {
packer: TexturePacker::new_skyline(TexturePackerConfig {
max_width: width,
max_height: height,
allow_rotation: false,
trim: false,
texture_padding: 0,
..Default::default()
}),
diffuse_texture,
diffuse_bind_group,
width,
height,
image: RgbaImage::from_raw(
width,
height,
vec![0; 4 * width as usize * height as usize],
)
.unwrap(),
changed: true,
},
texture_bind_group_layout,
)
}
pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup {
&self.diffuse_bind_group
}
// 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_frame(&id)
}
texture_info!(texture_width, w, width);
texture_info!(texture_height, h, height);
texture_info!(texture_x, x, width);
texture_info!(texture_y, y, height);
fn fill_image(&mut self) -> ExportResult<()> {
let atlas = {
profiling::scope!("export atlas");
ImageExporter::export(&self.packer)?
};
profiling::scope!("copy image");
self.image
.copy_from(&atlas, 0, 0)
.expect("image cache is too small");
Ok(())
}
pub fn clear(&mut self) {
self.packer = TexturePacker::new_skyline(TexturePackerConfig {
max_width: self.width,
max_height: self.height,
..Default::default()
});
}
#[profiling::function]
pub fn fill_textures(&mut self, queue: &wgpu::Queue) {
// saves time if nothing changed since the last time we did this
// FIXME This doesn't do much good once we get procedurally generated animation
// We'll have to create our own texture packer, with mutable subtextures,
// and more efficient algorithms. This'll also make frame times more consistent
if !self.changed {
return;
}
let atlas_size = extent_3d(self.width, self.height);
// put the packed texture into the base image
drop(self.fill_image());
// 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,
},
self.image.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,
);
self.changed = false;
}
}
|