diff --git a/hui-painter/src/lib.rs b/hui-painter/src/lib.rs index f900b58..aa909f7 100644 --- a/hui-painter/src/lib.rs +++ b/hui-painter/src/lib.rs @@ -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 + } } diff --git a/hui-painter/src/texture.rs b/hui-painter/src/texture.rs index 0451609..df970b7 100644 --- a/hui-painter/src/texture.rs +++ b/hui-painter/src/texture.rs @@ -1,2 +1,2 @@ -pub(crate) mod atlas; -pub use atlas::TextureHandle; +mod atlas; +pub use atlas::*; diff --git a/hui-painter/src/texture/atlas.rs b/hui-painter/src/texture/atlas.rs index a1a3d48..ba949bc 100644 --- a/hui-painter/src/texture/atlas.rs +++ b/hui-painter/src/texture/atlas.rs @@ -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, + + /// 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>, + + /// 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, } 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) + } +} diff --git a/hui/src/lib.rs b/hui/src/lib.rs index 212dc8f..acb0965 100644 --- a/hui/src/lib.rs +++ b/hui/src/lib.rs @@ -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;