Implement atlas padding in image pipeline

This commit is contained in:
Héctor Ramón Jiménez 2025-10-27 16:45:09 +01:00
parent 704144728f
commit ce126f5ea3
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
4 changed files with 159 additions and 63 deletions

View file

@ -10,7 +10,7 @@ pub use layer::Layer;
use allocator::Allocator;
pub const DEFAULT_SIZE: u32 = 512;
pub const DEFAULT_SIZE: u32 = 2048;
pub const MAX_SIZE: u32 = 2048;
use crate::core::Size;
@ -114,7 +114,7 @@ impl Atlas {
belt: &mut wgpu::util::StagingBelt,
width: u32,
height: u32,
data: &[u8],
pixels: &[u8],
) -> Option<Entry> {
let entry = {
let current_size = self.layers.len();
@ -129,56 +129,25 @@ impl Atlas {
log::debug!("Allocated atlas entry: {entry:?}");
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
// So we calculate padded_width by rounding width up to the next
// multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padding = (align - (4 * width) % align) % align;
let padded_width = (4 * width + padding) as usize;
let padded_data_size = padded_width * height as usize;
let buffer_slice = belt.allocate(
wgpu::BufferSize::new(padded_data_size as u64).unwrap(),
wgpu::BufferSize::new(8 * 4).unwrap(),
device,
);
let mut padded_data = buffer_slice.get_mapped_range_mut();
for row in 0..height as usize {
let offset = row * padded_width;
padded_data[offset..offset + 4 * width as usize].copy_from_slice(
&data[row * 4 * width as usize..(row + 1) * 4 * width as usize],
);
}
match &entry {
Entry::Contiguous(allocation) => {
self.upload_allocation(
buffer_slice.buffer(),
width,
height,
padding,
buffer_slice.offset() as usize,
allocation,
encoder,
pixels, width, 0, allocation, device, encoder, belt,
);
}
Entry::Fragmented { fragments, .. } => {
for fragment in fragments {
let (x, y) = fragment.position;
let offset = (y * padded_width as u32 + 4 * x) as usize;
let offset = (y * width + 4 * x) as usize;
self.upload_allocation(
buffer_slice.buffer(),
pixels,
width,
height,
padding,
offset + buffer_slice.offset() as usize,
offset,
&fragment.allocation,
device,
encoder,
belt,
);
}
}
@ -339,44 +308,129 @@ impl Atlas {
fn upload_allocation(
&self,
buffer: &wgpu::Buffer,
pixels: &[u8],
image_width: u32,
image_height: u32,
padding: 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();
let extent = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
// So we calculate padded_width 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,
);
let mut padded_data = buffer_slice.get_mapped_range_mut();
let padding_width = padding.width as usize;
let padding_height = padding.height as usize;
// Copy image rows
for row in 0..height as usize {
let offset = offset + row * 4 * image_width as usize;
let start = (row + padding_height) * bytes_per_row;
let stride = 4 * width as usize;
padded_data
[start + 4 * padding_width..start + 4 * padding_width + stride]
.copy_from_slice(&pixels[offset..offset + stride]);
// Add padding to the sides, if needed
for i in 0..padding_width {
padded_data[start + 4 * i..start + 4 * (i + 1)]
.copy_from_slice(&pixels[offset..offset + 4]);
padded_data[start + stride + 4 * (padding_width + i)
..start + stride + 4 * (padding_width + i + 1)]
.copy_from_slice(
&pixels[offset + stride - 4..offset + stride],
);
}
}
// Add padding on top and bottom
for row in 0..padding_height {
let start = row * bytes_per_row;
let end = (padding_height + height as usize + row) * bytes_per_row;
let end_offset =
offset + height as usize * 4 * image_width as usize;
// Top
padded_data[start + 4 * padding_width
..start + 4 * (padding_width + width as usize)]
.copy_from_slice(&pixels[offset..offset + 4 * width as usize]);
// Bottom
padded_data[end + 4 * padding_width
..end + 4 * (padding_width + width as usize)]
.copy_from_slice(
&pixels[end_offset - 4 * width as usize..end_offset],
);
// Corners
for i in 0..padding_width {
padded_data[start + 4 * i..start + 4 * (i + 1)]
.copy_from_slice(&pixels[offset..4]);
padded_data[start + 4 * (width as usize + padding_width + i)
..start + 4 * (width as usize + padding_width + i + 1)]
.copy_from_slice(
&pixels[offset + 4 * (width - 1) as usize
..offset + 4 * width as usize],
);
padded_data[end + 4 * i..end + 4 * (i + 1)].copy_from_slice(
&pixels[end_offset - 4 * width as usize
..end_offset - 4 * (width as usize - 1)],
);
padded_data[end + 4 * (width as usize + padding_width + i)
..end + 4 * (width as usize + padding_width + i + 1)]
.copy_from_slice(&pixels[end_offset - 4..end_offset]);
}
}
// Copy actual image
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer,
buffer: buffer_slice.buffer(),
layout: wgpu::TexelCopyBufferLayout {
offset: offset as u64,
bytes_per_row: Some(4 * image_width + padding),
rows_per_image: Some(image_height),
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,
y,
x: x - padding.width,
y: y - padding.height,
z: layer as u32,
},
aspect: wgpu::TextureAspect::default(),
},
extent,
wgpu::Extent3d {
width: width + padding.width * 2,
height: height + padding.height * 2,
depth_or_array_layers: 1,
},
);
}

View file

@ -29,6 +29,13 @@ impl Allocation {
}
}
pub fn padding(&self) -> Size<u32> {
match self {
Allocation::Partial { region, .. } => region.padding(),
Allocation::Full { .. } => Size::new(0, 0),
}
}
pub fn layer(&self) -> usize {
match self {
Allocation::Partial { layer, .. } => *layer,

View file

@ -1,3 +1,5 @@
use crate::core;
use guillotiere::{AtlasAllocator, Size};
pub struct Allocator {
@ -6,6 +8,8 @@ pub struct Allocator {
}
impl Allocator {
const PADDING: u32 = 1;
pub fn new(size: u32) -> Allocator {
let raw = AtlasAllocator::new(Size::new(size as i32, size as i32));
@ -16,12 +20,38 @@ impl Allocator {
}
pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> {
let allocation =
self.raw.allocate(Size::new(width as i32, height as i32))?;
let size = self.raw.size();
let padded_width = width + Self::PADDING * 2;
let padded_height = height + Self::PADDING * 2;
let pad_width = padded_width as i32 <= size.width;
let pad_height = padded_height as i32 <= size.height;
let mut allocation = self.raw.allocate(Size::new(
if pad_width { padded_width } else { width } as i32,
if pad_height { padded_height } else { height } as i32,
))?;
if pad_width {
allocation.rectangle.min.x += Self::PADDING as i32;
allocation.rectangle.max.x -= Self::PADDING as i32;
}
if pad_height {
allocation.rectangle.min.y += Self::PADDING as i32;
allocation.rectangle.max.y -= Self::PADDING as i32;
}
self.allocations += 1;
Some(Region { allocation })
Some(Region {
allocation,
padding: core::Size::new(
if pad_width { Self::PADDING } else { 0 },
if pad_height { Self::PADDING } else { 0 },
),
})
}
pub fn deallocate(&mut self, region: &Region) {
@ -41,6 +71,7 @@ impl Allocator {
pub struct Region {
allocation: guillotiere::Allocation,
padding: core::Size<u32>,
}
impl Region {
@ -50,10 +81,14 @@ impl Region {
(rectangle.min.x as u32, rectangle.min.y as u32)
}
pub fn size(&self) -> crate::core::Size<u32> {
pub fn size(&self) -> core::Size<u32> {
let size = self.allocation.rectangle.size();
crate::core::Size::new(size.width as u32, size.height as u32)
core::Size::new(size.width as u32, size.height as u32)
}
pub fn padding(&self) -> crate::core::Size<u32> {
self.padding
}
}

View file

@ -735,12 +735,12 @@ fn add_instance(
_rotation: rotation,
_opacity: opacity,
_position_in_atlas: [
(x as f32 + 0.5) / atlas_size as f32,
(y as f32 + 0.5) / atlas_size as f32,
x as f32 / atlas_size as f32,
y as f32 / atlas_size as f32,
],
_size_in_atlas: [
(width as f32 - 1.0) / atlas_size as f32,
(height as f32 - 1.0) / atlas_size as f32,
width as f32 / atlas_size as f32,
height as f32 / atlas_size as f32,
],
_layer: layer as u32,
_snap: snap as u32,