atlas stuff

This commit is contained in:
griffi-gh 2024-08-07 15:11:07 +02:00
parent e0d370844a
commit 8202e99c8f
4 changed files with 225 additions and 54 deletions

View file

@ -1,6 +1,19 @@
pub mod paint;
pub mod texture;
pub struct Painter {
use texture::TextureAtlas;
#[derive(Default)]
pub struct Painter {
atlas: TextureAtlas,
}
impl Painter {
pub fn new() -> Self {
Self::default()
}
pub fn atlas(&self) -> &TextureAtlas {
&self.atlas
}
}

View file

@ -1,2 +1,2 @@
pub(crate) mod atlas;
pub use atlas::TextureHandle;
mod atlas;
pub use atlas::*;

View file

@ -4,10 +4,16 @@ use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
//TODO support rotation
const ALLOW_ROTATION: bool = false;
const DEFAULT_ATLAS_SIZE: UVec2 = uvec2(128, 128);
// const ALLOW_ROTATION: bool = false;
// Destination format is always RGBA
const RGBA_BYTES_PER_PIXEL: usize = 4;
/// Assert that the passed texture size is valid, panicking if it's not.
///
/// - The size must be greater than 0.
/// - The size must be less than `i32::MAX`.
fn assert_size(size: UVec2) {
assert!(
size.x > 0 &&
@ -21,58 +27,124 @@ fn assert_size(size: UVec2) {
);
}
/// The format of the source texture data to use when updating a texture in the atlas.
#[derive(Clone, Copy, Debug, Default)]
pub enum SourceTextureFormat {
/// RGBA, 8-bit per channel\
/// (Default and preferred format)
/// RGBA, 8-bit per channel
#[default]
RGBA8,
/// RGB, 8-bit per channel
/// (Alpha channel is assumed to be 255)
//TODO native-endian RGBA32 format
/// ARGB, 8-bit per channel
ARGB8,
/// BGRA, 8-bit per channel
BGRA8,
/// ABGR, 8-bit per channel
ABGR8,
/// RGB, 8-bit per channel (Alpha = 255)
RGB8,
/// Alpha only, 8-bit per channel
/// (All other channels are assumed to be 255 (white))
A8,
/// BGR, 8-bit per channel (Alpha = 255)
BGR8,
//TODO ARGB, BGRA, etc.
/// Alpha only, 8-bit per channel (RGB = #ffffff)
A8,
}
impl SourceTextureFormat {
pub const fn bytes_per_pixel(&self) -> usize {
match self {
SourceTextureFormat::RGBA8 => 4,
SourceTextureFormat::RGB8 => 3,
SourceTextureFormat::RGBA8 |
SourceTextureFormat::ARGB8 |
SourceTextureFormat::BGRA8 |
SourceTextureFormat::ABGR8 => 4,
SourceTextureFormat::RGB8 |
SourceTextureFormat::BGR8 => 3,
SourceTextureFormat::A8 => 1,
}
}
}
pub type TextureId = u32;
type TextureId = u32;
/// A handle to a texture in the texture atlas.
///
/// Can be cheaply copied and passed around.\
/// The handle is only valid for the texture atlas it was created from.
#[derive(Clone, Copy)]
pub struct TextureHandle {
pub(crate) id: TextureId,
pub(crate) size: UVec2,
}
struct TextureAllocation {
handle: TextureHandle,
offset: UVec2,
size: UVec2,
impl TextureHandle {
pub fn size(&self) -> UVec2 {
self.size
}
}
pub struct TextureAtlas {
/// Represents an area allocated to a specific texture handle in the texture atlas.
struct TextureAllocation {
/// Corresponding copyable texture handle
handle: TextureHandle,
/// The offset of the allocation in the atlas, in pixels
offset: UVec2,
/// The requested size of the allocation, in pixels
size: UVec2,
/// The maximum size of the allocation, used for reusing deallocated allocations
///
/// Usually equal to `size`, but may be larger than the requested size
/// if the allocation was reused by a smaller texture at some point
max_size: UVec2,
}
impl TextureAllocation {
/// Create a new texture allocation with the specified parameters.
///
/// The `max_size` parameter will be set equal to `size`.
pub fn new(handle: TextureHandle, offset: UVec2, size: UVec2) -> Self {
Self {
handle,
offset,
size,
max_size: size,
}
}
}
/// A texture atlas that can be used to pack multiple textures into a single texture.
pub struct TextureAtlas {
/// The size of the atlas, in pixels
size: UVec2,
/// The texture data of the atlas, ALWAYS in RGBA8 format
data: Vec<u8>,
/// The packer used to allocate space for textures in the atlas
packer: DensePacker,
/// The next id to be used for a texture handle\
/// Gets incremented every time a new texture is allocated
next_id: TextureId,
/// Active allocated textures, indexed by id of their handle
allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>,
/// Deallocated allocations that can be reused, sorted by size
//TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now
reuse_allocations: Vec<TextureAllocation>,
}
impl TextureAtlas {
pub fn new(size: UVec2) -> Self {
/// Create a new texture atlas with the specified size.
pub(crate) fn new(size: UVec2) -> Self {
assert_size(size);
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
Self {
@ -84,9 +156,16 @@ impl TextureAtlas {
),
next_id: 0,
allocations: HashMap::default(),
reuse_allocations: Vec::new(),
}
}
/// Get the next handle
///
/// Does not allocate a texture associated with it
/// This handle will be invalid until it's associated with a texture.
///
/// Used internally in `allocate` and `allocate_with_data`.
fn next_handle(&mut self, size: UVec2) -> TextureHandle {
let handle = TextureHandle {
id: self.next_id,
@ -96,7 +175,6 @@ impl TextureAtlas {
handle
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The data present in the texture is undefined, and may include garbage data.
///
@ -105,11 +183,28 @@ impl TextureAtlas {
pub fn allocate(&mut self, size: UVec2) -> TextureHandle {
assert_size(size);
// Check if any deallocated allocations can be reused
// Find the smallest allocation that fits the requested size
// (The list is already sorted by size)
for (idx, allocation) in self.reuse_allocations.iter().enumerate() {
if allocation.max_size.x >= size.x && allocation.max_size.y >= size.y {
let allocation = self.reuse_allocations.remove(idx);
let handle = self.next_handle(size);
self.allocations.insert_unique_unchecked(handle.id, TextureAllocation {
handle,
offset: allocation.offset,
size,
max_size: allocation.max_size,
});
return handle;
}
}
// Pack the texture
let pack = self.packer.pack(
size.x as i32,
size.y as i32,
ALLOW_ROTATION
false
);
//TODO: handle pack failure by either resizing the atlas or returning an error
@ -118,12 +213,97 @@ impl TextureAtlas {
// Allocate the texture
let handle = self.next_handle(size);
let allocation = TextureAllocation { handle, offset, size };
let allocation = TextureAllocation::new(handle, offset, size);
self.allocations.insert_unique_unchecked(handle.id, allocation);
handle
}
/// Deallocate a texture in the atlas, allowing its space to be reused by future allocations.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
pub fn deallocate(&mut self, handle: TextureHandle) {
// Remove the allocation from the active allocations
let allocation = self.allocations
.remove(&handle.id)
.expect("invalid texture handle");
// TODO: this is not the most efficient way to do this:
// And put it in the reuse allocations queue
self.reuse_allocations.push(allocation);
self.reuse_allocations.sort_unstable_by_key(|a| a.size.x * a.size.y);
}
/// Update the data of a texture in the atlas.\
/// The texture must have been previously allocated with `allocate` or `allocate_with_data`.
///
/// The source data must be in the format specified by the `format` parameter.\
/// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.)
///
/// The function will silently ignore any data that doesn't fit in the texture.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
/// - The length of the data array is less than the size of the texture.
pub fn update(&mut self, handle: TextureHandle, format: SourceTextureFormat, data: &[u8]) {
assert!(
data.len() >= handle.size.x as usize * handle.size.y as usize * format.bytes_per_pixel(),
"data length must be at least the size of the texture"
);
let bpp = format.bytes_per_pixel();
let TextureAllocation { size, offset, ..} = self.allocations
.get(&handle.id)
.expect("invalid texture handle");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize * bpp;
let dst_idx: usize = (
(offset.y + y) * size.x +
(offset.x + x)
) as usize * RGBA_BYTES_PER_PIXEL;
let src = &data[src_idx..src_idx + bpp];
let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL];
match format {
SourceTextureFormat::RGBA8 => {
dst.copy_from_slice(src);
},
SourceTextureFormat::ARGB8 => {
dst[..3].copy_from_slice(&src[1..]);
dst[3] = src[0];
},
SourceTextureFormat::BGRA8 => {
dst.copy_from_slice(src);
dst.rotate_right(1);
dst.reverse();
},
SourceTextureFormat::ABGR8 => {
dst.copy_from_slice(src);
dst.reverse();
},
SourceTextureFormat::RGB8 => {
dst[..3].copy_from_slice(src);
dst[3] = 0xff;
},
SourceTextureFormat::BGR8 => {
dst[..3].copy_from_slice(src);
dst[..3].reverse();
dst[3] = 0xff;
},
SourceTextureFormat::A8 => {
dst[..3].fill(0xff);
dst[3] = src[0];
},
}
}
}
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The texture is initialized with the provided data.
///
@ -156,37 +336,16 @@ impl TextureAtlas {
// Allocate the texture
let handle = self.allocate(size);
let allocation = self.allocations.get(&handle.id).unwrap();
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize * bytes_per_pixel;
let dst_idx: usize = (
(allocation.offset.y + y) * size.x +
(allocation.offset.x + x)
) as usize * RGBA_BYTES_PER_PIXEL;
let src = &data[src_idx..src_idx + bytes_per_pixel];
let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL];
match format {
SourceTextureFormat::RGBA8 => {
dst.copy_from_slice(src);
}
SourceTextureFormat::RGB8 => {
dst[..3].copy_from_slice(src);
dst[3] = 255;
}
SourceTextureFormat::A8 => {
dst[0] = src[0];
dst[1] = src[0];
dst[2] = src[0];
dst[3] = 255;
}
}
}
}
// Write the data to the texture
self.update(handle, format, data);
handle
}
}
impl Default for TextureAtlas {
fn default() -> Self {
Self::new(DEFAULT_ATLAS_SIZE)
}
}

View file

@ -23,7 +23,6 @@ pub mod draw;
pub mod measure;
pub mod state;
pub mod text;
pub mod color;
pub mod signal;
pub mod frame;