Previous behavior: images larger that 2048px would have their bottom right corners transparent because the info didn't get uploaded to the GPU
568 lines
19 KiB
Rust
568 lines
19 KiB
Rust
pub mod entry;
|
|
|
|
mod allocation;
|
|
mod allocator;
|
|
mod layer;
|
|
|
|
pub use allocation::Allocation;
|
|
pub use entry::Entry;
|
|
pub use layer::Layer;
|
|
|
|
use allocator::Allocator;
|
|
|
|
pub const DEFAULT_SIZE: u32 = 2048;
|
|
pub const MAX_SIZE: u32 = 2048;
|
|
|
|
use crate::core::Size;
|
|
use crate::graphics::color;
|
|
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Debug)]
|
|
pub struct Atlas {
|
|
size: u32,
|
|
backend: wgpu::Backend,
|
|
texture: wgpu::Texture,
|
|
texture_view: wgpu::TextureView,
|
|
texture_bind_group: Arc<wgpu::BindGroup>,
|
|
texture_layout: wgpu::BindGroupLayout,
|
|
layers: Vec<Layer>,
|
|
}
|
|
|
|
impl Atlas {
|
|
pub fn new(
|
|
device: &wgpu::Device,
|
|
backend: wgpu::Backend,
|
|
texture_layout: wgpu::BindGroupLayout,
|
|
) -> Self {
|
|
Self::with_size(device, backend, texture_layout, DEFAULT_SIZE)
|
|
}
|
|
|
|
pub fn with_size(
|
|
device: &wgpu::Device,
|
|
backend: wgpu::Backend,
|
|
texture_layout: wgpu::BindGroupLayout,
|
|
size: u32,
|
|
) -> Self {
|
|
let size = size.min(MAX_SIZE);
|
|
|
|
let layers = match backend {
|
|
// On the GL backend we start with 2 layers, to help wgpu figure
|
|
// out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
|
|
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371
|
|
wgpu::Backend::Gl => vec![Layer::Empty, Layer::Empty],
|
|
_ => vec![Layer::Empty],
|
|
};
|
|
|
|
let extent = wgpu::Extent3d {
|
|
width: size,
|
|
height: size,
|
|
depth_or_array_layers: layers.len() as u32,
|
|
};
|
|
|
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some("iced_wgpu::image texture atlas"),
|
|
size: extent,
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format: if color::GAMMA_CORRECTION {
|
|
wgpu::TextureFormat::Rgba8UnormSrgb
|
|
} else {
|
|
wgpu::TextureFormat::Rgba8Unorm
|
|
},
|
|
usage: wgpu::TextureUsages::COPY_DST
|
|
| wgpu::TextureUsages::COPY_SRC
|
|
| wgpu::TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
});
|
|
|
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
|
|
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
|
..Default::default()
|
|
});
|
|
|
|
let texture_bind_group =
|
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("iced_wgpu::image texture atlas bind group"),
|
|
layout: &texture_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(&texture_view),
|
|
}],
|
|
});
|
|
|
|
Atlas {
|
|
size,
|
|
backend,
|
|
texture,
|
|
texture_view,
|
|
texture_bind_group: Arc::new(texture_bind_group),
|
|
texture_layout,
|
|
layers,
|
|
}
|
|
}
|
|
|
|
pub fn bind_group(&self) -> &Arc<wgpu::BindGroup> {
|
|
&self.texture_bind_group
|
|
}
|
|
|
|
pub fn upload(
|
|
&mut self,
|
|
device: &wgpu::Device,
|
|
encoder: &mut wgpu::CommandEncoder,
|
|
belt: &mut wgpu::util::StagingBelt,
|
|
width: u32,
|
|
height: u32,
|
|
pixels: &[u8],
|
|
) -> Option<Entry> {
|
|
let entry = {
|
|
let current_size = self.layers.len();
|
|
let entry = self.allocate(width, height)?;
|
|
|
|
// We grow the internal texture after allocating if necessary
|
|
let new_layers = self.layers.len() - current_size;
|
|
self.grow(new_layers, device, encoder, self.backend);
|
|
|
|
entry
|
|
};
|
|
|
|
log::debug!("Allocated atlas entry: {entry:?}");
|
|
|
|
match &entry {
|
|
Entry::Contiguous(allocation) => {
|
|
self.upload_allocation(
|
|
pixels, width, 0, allocation, device, encoder, belt,
|
|
);
|
|
}
|
|
Entry::Fragmented { fragments, .. } => {
|
|
for fragment in fragments {
|
|
let (x, y) = fragment.position;
|
|
let offset = 4 * (y * width + x) as usize;
|
|
|
|
self.upload_allocation(
|
|
pixels,
|
|
width,
|
|
offset,
|
|
&fragment.allocation,
|
|
device,
|
|
encoder,
|
|
belt,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if log::log_enabled!(log::Level::Debug) {
|
|
log::debug!(
|
|
"Atlas layers: {} (busy: {}, allocations: {})",
|
|
self.layers.len(),
|
|
self.layers.iter().filter(|layer| !layer.is_empty()).count(),
|
|
self.layers.iter().map(Layer::allocations).sum::<usize>(),
|
|
);
|
|
}
|
|
|
|
Some(entry)
|
|
}
|
|
|
|
pub fn remove(&mut self, entry: &Entry) {
|
|
log::debug!("Removing atlas entry: {entry:?}");
|
|
|
|
match entry {
|
|
Entry::Contiguous(allocation) => {
|
|
self.deallocate(allocation);
|
|
}
|
|
Entry::Fragmented { fragments, .. } => {
|
|
for fragment in fragments {
|
|
self.deallocate(&fragment.allocation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
|
|
// Allocate one layer if texture fits perfectly
|
|
if width == self.size && height == self.size {
|
|
let mut empty_layers = self
|
|
.layers
|
|
.iter_mut()
|
|
.enumerate()
|
|
.filter(|(_, layer)| layer.is_empty());
|
|
|
|
if let Some((i, layer)) = empty_layers.next() {
|
|
*layer = Layer::Full;
|
|
|
|
return Some(Entry::Contiguous(Allocation::Full {
|
|
layer: i,
|
|
size: self.size,
|
|
}));
|
|
}
|
|
|
|
self.layers.push(Layer::Full);
|
|
|
|
return Some(Entry::Contiguous(Allocation::Full {
|
|
layer: self.layers.len() - 1,
|
|
size: self.size,
|
|
}));
|
|
}
|
|
|
|
// Split big textures across multiple layers
|
|
if width > self.size || height > self.size {
|
|
let mut fragments = Vec::new();
|
|
let mut y = 0;
|
|
|
|
while y < height {
|
|
let height = std::cmp::min(height - y, self.size);
|
|
let mut x = 0;
|
|
|
|
while x < width {
|
|
let width = std::cmp::min(width - x, self.size);
|
|
|
|
let allocation = self.allocate(width, height)?;
|
|
|
|
if let Entry::Contiguous(allocation) = allocation {
|
|
fragments.push(entry::Fragment {
|
|
position: (x, y),
|
|
allocation,
|
|
});
|
|
}
|
|
|
|
x += width;
|
|
}
|
|
|
|
y += height;
|
|
}
|
|
|
|
return Some(Entry::Fragmented {
|
|
size: Size::new(width, height),
|
|
fragments,
|
|
});
|
|
}
|
|
|
|
// Try allocating on an existing layer
|
|
for (i, layer) in self.layers.iter_mut().enumerate() {
|
|
match layer {
|
|
Layer::Empty => {
|
|
let mut allocator = Allocator::new(self.size);
|
|
|
|
if let Some(region) = allocator.allocate(width, height) {
|
|
*layer = Layer::Busy(allocator);
|
|
|
|
return Some(Entry::Contiguous(Allocation::Partial {
|
|
region,
|
|
layer: i,
|
|
atlas_size: self.size,
|
|
}));
|
|
}
|
|
}
|
|
Layer::Busy(allocator) => {
|
|
if let Some(region) = allocator.allocate(width, height) {
|
|
return Some(Entry::Contiguous(Allocation::Partial {
|
|
region,
|
|
layer: i,
|
|
atlas_size: self.size,
|
|
}));
|
|
}
|
|
}
|
|
Layer::Full => {}
|
|
}
|
|
}
|
|
|
|
// Create new layer with atlas allocator
|
|
let mut allocator = Allocator::new(self.size);
|
|
|
|
if let Some(region) = allocator.allocate(width, height) {
|
|
self.layers.push(Layer::Busy(allocator));
|
|
|
|
return Some(Entry::Contiguous(Allocation::Partial {
|
|
region,
|
|
layer: self.layers.len() - 1,
|
|
atlas_size: self.size,
|
|
}));
|
|
}
|
|
|
|
// We ran out of memory (?)
|
|
None
|
|
}
|
|
|
|
fn deallocate(&mut self, allocation: &Allocation) {
|
|
log::debug!("Deallocating atlas: {allocation:?}");
|
|
|
|
match allocation {
|
|
Allocation::Full { layer, .. } => {
|
|
self.layers[*layer] = Layer::Empty;
|
|
}
|
|
Allocation::Partial { layer, region, .. } => {
|
|
let layer = &mut self.layers[*layer];
|
|
|
|
if let Layer::Busy(allocator) = layer {
|
|
allocator.deallocate(region);
|
|
|
|
if allocator.is_empty() {
|
|
*layer = Layer::Empty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn upload_allocation(
|
|
&self,
|
|
pixels: &[u8],
|
|
image_width: u32,
|
|
offset: usize,
|
|
allocation: &Allocation,
|
|
device: &wgpu::Device,
|
|
encoder: &mut wgpu::CommandEncoder,
|
|
belt: &mut wgpu::util::StagingBelt,
|
|
) {
|
|
let (x, y) = allocation.position();
|
|
let Size { width, height } = allocation.size();
|
|
let layer = allocation.layer();
|
|
let padding = allocation.padding();
|
|
|
|
// It is a webgpu requirement that:
|
|
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
|
|
// So we calculate bytes_per_row by rounding width up to the next
|
|
// multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
|
|
let bytes_per_row = (4 * (width + padding.width * 2))
|
|
.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
|
|
as usize;
|
|
let total_bytes =
|
|
bytes_per_row * (height + padding.height * 2) as usize;
|
|
|
|
let buffer_slice = belt.allocate(
|
|
wgpu::BufferSize::new(total_bytes as u64).unwrap(),
|
|
wgpu::BufferSize::new(8 * 4).unwrap(),
|
|
device,
|
|
);
|
|
|
|
const PIXEL: usize = 4;
|
|
|
|
let mut fragment = buffer_slice.get_mapped_range_mut();
|
|
let w = width as usize;
|
|
let h = height as usize;
|
|
let pad_w = padding.width as usize;
|
|
let pad_h = padding.height as usize;
|
|
let stride = PIXEL * w;
|
|
|
|
// bounds check for source pixels to fragment
|
|
// The upload loop accesses rows 0..h; the last row starts at
|
|
// `offset + (h-1) * PIXEL * image_width` and reads `stride` bytes.
|
|
// Using `h` instead of `h-1` over-estimates by one full image_width
|
|
// row, causing false positives for bottom-right fragments whose
|
|
// x-offset is non-zero and whose bottom edge reaches the image edge.
|
|
if h > 0
|
|
&& pixels.len()
|
|
< offset + (h - 1) * PIXEL * image_width as usize + stride
|
|
{
|
|
return;
|
|
}
|
|
|
|
// bounds check for pad_w low / high to fragment
|
|
if pad_w > 0
|
|
&& (offset + stride < PIXEL
|
|
|| offset
|
|
+ image_width as usize
|
|
* PIXEL
|
|
* (h.checked_sub(1).unwrap_or(0))
|
|
+ PIXEL
|
|
> pixels.len())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Copy image rows
|
|
for row in 0..h {
|
|
let src = offset + row * PIXEL * image_width as usize;
|
|
let dst = (row + pad_h) * bytes_per_row;
|
|
|
|
fragment[dst + PIXEL * pad_w..dst + PIXEL * pad_w + stride]
|
|
.copy_from_slice(&pixels[src..src + stride]);
|
|
|
|
// Add padding to the sides, if needed
|
|
for i in 0..pad_w {
|
|
fragment[dst + PIXEL * i..dst + PIXEL * (i + 1)]
|
|
.copy_from_slice(&pixels[src..src + PIXEL]);
|
|
|
|
fragment[dst + stride + PIXEL * (pad_w + i)
|
|
..dst + stride + PIXEL * (pad_w + i + 1)]
|
|
.copy_from_slice(
|
|
&pixels[src + stride - PIXEL..src + stride],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add padding on top and bottom
|
|
for row in 0..pad_h {
|
|
let dst_top = row * bytes_per_row;
|
|
let dst_bottom = (pad_h + h + row) * bytes_per_row;
|
|
let src_top = offset;
|
|
let src_bottom = offset + (h - 1) * PIXEL * image_width as usize;
|
|
|
|
// Top
|
|
fragment[dst_top + PIXEL * pad_w..dst_top + PIXEL * (pad_w + w)]
|
|
.copy_from_slice(&pixels[src_top..src_top + PIXEL * w]);
|
|
|
|
// Bottom
|
|
fragment
|
|
[dst_bottom + PIXEL * pad_w..dst_bottom + PIXEL * (pad_w + w)]
|
|
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL * w]);
|
|
|
|
// Corners
|
|
for i in 0..pad_w {
|
|
// Top left
|
|
fragment[dst_top + PIXEL * i..dst_top + PIXEL * (i + 1)]
|
|
.copy_from_slice(&pixels[offset..offset + PIXEL]);
|
|
|
|
// Top right
|
|
fragment[dst_top + PIXEL * (w + pad_w + i)
|
|
..dst_top + PIXEL * (w + pad_w + i + 1)]
|
|
.copy_from_slice(
|
|
&pixels[offset + PIXEL * (w - 1)..offset + PIXEL * w],
|
|
);
|
|
|
|
// Bottom left
|
|
fragment[dst_bottom + PIXEL * i..dst_bottom + PIXEL * (i + 1)]
|
|
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL]);
|
|
|
|
// Bottom right
|
|
fragment[dst_bottom + PIXEL * (w + pad_w + i)
|
|
..dst_bottom + PIXEL * (w + pad_w + i + 1)]
|
|
.copy_from_slice(
|
|
&pixels[src_bottom + PIXEL * (w - 1)
|
|
..src_bottom + PIXEL * w],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Copy actual image
|
|
encoder.copy_buffer_to_texture(
|
|
wgpu::TexelCopyBufferInfo {
|
|
buffer: buffer_slice.buffer(),
|
|
layout: wgpu::TexelCopyBufferLayout {
|
|
offset: buffer_slice.offset(),
|
|
bytes_per_row: Some(bytes_per_row as u32),
|
|
rows_per_image: Some(height + padding.height * 2),
|
|
},
|
|
},
|
|
wgpu::TexelCopyTextureInfo {
|
|
texture: &self.texture,
|
|
mip_level: 0,
|
|
origin: wgpu::Origin3d {
|
|
x: x - padding.width,
|
|
y: y - padding.height,
|
|
z: layer as u32,
|
|
},
|
|
aspect: wgpu::TextureAspect::default(),
|
|
},
|
|
wgpu::Extent3d {
|
|
width: width + padding.width * 2,
|
|
height: height + padding.height * 2,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn grow(
|
|
&mut self,
|
|
amount: usize,
|
|
device: &wgpu::Device,
|
|
encoder: &mut wgpu::CommandEncoder,
|
|
backend: wgpu::Backend,
|
|
) {
|
|
if amount == 0 {
|
|
return;
|
|
}
|
|
|
|
// On the GL backend if layers.len() is a multiple of 6 we need to help wgpu figure out that this texture
|
|
// is still a `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_CUBE_MAP` or `GL_TEXTURE_CUBE_ARRAY`.
|
|
// This will over-allocate some unused memory on GL, but it's better than not being able to
|
|
// grow the atlas past multiples of 6!
|
|
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371
|
|
let depth_or_array_layers = match backend {
|
|
wgpu::Backend::Gl if self.layers.len().is_multiple_of(6) => {
|
|
self.layers.len() as u32 + 1
|
|
}
|
|
_ => self.layers.len() as u32,
|
|
};
|
|
|
|
let new_texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some("iced_wgpu::image texture atlas"),
|
|
size: wgpu::Extent3d {
|
|
width: self.size,
|
|
height: self.size,
|
|
depth_or_array_layers,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format: if color::GAMMA_CORRECTION {
|
|
wgpu::TextureFormat::Rgba8UnormSrgb
|
|
} else {
|
|
wgpu::TextureFormat::Rgba8Unorm
|
|
},
|
|
usage: wgpu::TextureUsages::COPY_DST
|
|
| wgpu::TextureUsages::COPY_SRC
|
|
| wgpu::TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
});
|
|
|
|
let amount_to_copy = self.layers.len() - amount;
|
|
|
|
for (i, layer) in
|
|
self.layers.iter_mut().take(amount_to_copy).enumerate()
|
|
{
|
|
if layer.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
encoder.copy_texture_to_texture(
|
|
wgpu::TexelCopyTextureInfo {
|
|
texture: &self.texture,
|
|
mip_level: 0,
|
|
origin: wgpu::Origin3d {
|
|
x: 0,
|
|
y: 0,
|
|
z: i as u32,
|
|
},
|
|
aspect: wgpu::TextureAspect::default(),
|
|
},
|
|
wgpu::TexelCopyTextureInfo {
|
|
texture: &new_texture,
|
|
mip_level: 0,
|
|
origin: wgpu::Origin3d {
|
|
x: 0,
|
|
y: 0,
|
|
z: i as u32,
|
|
},
|
|
aspect: wgpu::TextureAspect::default(),
|
|
},
|
|
wgpu::Extent3d {
|
|
width: self.size,
|
|
height: self.size,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
self.texture = new_texture;
|
|
self.texture_view =
|
|
self.texture.create_view(&wgpu::TextureViewDescriptor {
|
|
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
|
..Default::default()
|
|
});
|
|
|
|
self.texture_bind_group =
|
|
Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("iced_wgpu::image texture atlas bind group"),
|
|
layout: &self.texture_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(
|
|
&self.texture_view,
|
|
),
|
|
}],
|
|
}));
|
|
}
|
|
}
|