use std::cell::RefCell;
use std::rc::Rc;
use etagere::{AllocId, AtlasAllocator, size2};
use wgpu::{BindGroup, Device, Extent3d, Queue, Texture, TextureDimension, TextureUsages};
use super::textured_quad::TexturedQuadPipeline;
pub(super) const PAGE: u32 = 1024;
pub(super) const MAX_ENTRY: u32 = 256;
const GUTTER: u32 = 1;
struct Page {
#[allow(dead_code)] texture: Texture,
bind_group: Rc<BindGroup>,
allocator: RefCell<AtlasAllocator>,
}
pub(super) struct AtlasEntry {
page: Rc<Page>,
id: AllocId,
bytes: usize,
}
impl AtlasEntry {
pub(super) fn bytes(&self) -> usize {
self.bytes
}
pub(super) fn bind_group(&self) -> &Rc<BindGroup> {
&self.page.bind_group
}
}
impl Drop for AtlasEntry {
fn drop(&mut self) {
self.page.allocator.borrow_mut().deallocate(self.id);
}
}
#[derive(Default)]
pub(super) struct ImageAtlas {
pages: Vec<Rc<Page>>,
}
impl ImageAtlas {
pub(super) fn takes(width: u32, height: u32) -> bool {
width <= MAX_ENTRY && height <= MAX_ENTRY
}
pub(super) fn insert(
&mut self,
device: &Device,
queue: &Queue,
quad: &TexturedQuadPipeline,
width: u32,
height: u32,
rgba: &[u8],
) -> Option<(AtlasEntry, [f32; 4])> {
if !Self::takes(width, height) || width == 0 || height == 0 {
return None;
}
let padded = size2((width + 2 * GUTTER) as i32, (height + 2 * GUTTER) as i32);
let placed = self.pages.iter().find_map(|page| {
let allocation = page.allocator.borrow_mut().allocate(padded)?;
Some((page.clone(), allocation))
});
let (page, allocation) = match placed {
Some(placed) => placed,
None => {
let page = Rc::new(new_page(device, quad));
let allocation = page.allocator.borrow_mut().allocate(padded)?;
self.pages.push(page.clone());
(page, allocation)
}
};
let origin = allocation.rectangle.min;
let (x, y) = (origin.x as u32, origin.y as u32);
let (padded_width, padded_height) = (width + 2 * GUTTER, height + 2 * GUTTER);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &page.texture,
mip_level: 0,
origin: wgpu::Origin3d { x, y, z: 0 },
aspect: wgpu::TextureAspect::All,
},
&extruded(width, height, rgba),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * padded_width),
rows_per_image: Some(padded_height),
},
Extent3d {
width: padded_width,
height: padded_height,
depth_or_array_layers: 1,
},
);
let page_side = PAGE as f32;
let rect = [
(x + GUTTER) as f32 / page_side,
(y + GUTTER) as f32 / page_side,
width as f32 / page_side,
height as f32 / page_side,
];
let entry = AtlasEntry {
page,
id: allocation.id,
bytes: (padded_width * padded_height * 4) as usize,
};
Some((entry, rect))
}
pub(super) fn drop_empty_pages(&mut self) {
self.pages
.retain(|page| !page.allocator.borrow().is_empty());
}
#[cfg(feature = "testing")]
pub(super) fn pages(&self) -> usize {
self.pages.len()
}
}
fn new_page(device: &Device, quad: &TexturedQuadPipeline) -> Page {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Image Atlas Page"),
size: Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Page {
bind_group: quad.bind_texture(device, &view, "Image Atlas Page Bind Group"),
texture,
allocator: RefCell::new(AtlasAllocator::new(size2(PAGE as i32, PAGE as i32))),
}
}
fn extruded(width: u32, height: u32, rgba: &[u8]) -> Vec<u8> {
let (width, height) = (width as usize, height as usize);
let padded_width = width + 2;
let mut out = Vec::with_capacity(padded_width * (height + 2) * 4);
for row in 0..height + 2 {
let source_row = row.saturating_sub(1).min(height - 1);
let line = &rgba[source_row * width * 4..(source_row + 1) * width * 4];
out.extend_from_slice(&line[..4]);
out.extend_from_slice(line);
out.extend_from_slice(&line[line.len() - 4..]);
}
out
}
#[cfg(test)]
mod tests {
use super::extruded;
#[test]
fn the_gutter_repeats_the_edge() {
let (a, b, c, d) = ([1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]);
let rgba = [a, b, c, d].concat();
let padded: Vec<[u8; 4]> = extruded(2, 2, &rgba)
.chunks(4)
.map(|texel| texel.try_into().unwrap())
.collect();
#[rustfmt::skip]
let expected = vec![
a, a, b, b,
a, a, b, b,
c, c, d, d,
c, c, d, d,
];
assert_eq!(padded, expected);
}
#[test]
fn the_gutter_is_one_texel_whatever_the_shape() {
let (a, b, c) = ([1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]);
let rgba = [a, b, c].concat();
let padded: Vec<[u8; 4]> = extruded(1, 3, &rgba)
.chunks(4)
.map(|texel| texel.try_into().unwrap())
.collect();
#[rustfmt::skip]
let expected = vec![
a, a, a,
a, a, a,
b, b, b,
c, c, c,
c, c, c,
];
assert_eq!(padded, expected);
}
}