mirror of
https://github.com/griffi-gh/hUI.git
synced 2024-11-21 14:48:42 -06:00
atlas stuff
This commit is contained in:
parent
e0d370844a
commit
8202e99c8f
|
@ -1,6 +1,19 @@
|
||||||
pub mod paint;
|
pub mod paint;
|
||||||
pub mod texture;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
pub(crate) mod atlas;
|
mod atlas;
|
||||||
pub use atlas::TextureHandle;
|
pub use atlas::*;
|
||||||
|
|
|
@ -4,10 +4,16 @@ use hashbrown::HashMap;
|
||||||
use nohash_hasher::BuildNoHashHasher;
|
use nohash_hasher::BuildNoHashHasher;
|
||||||
|
|
||||||
//TODO support rotation
|
//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;
|
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) {
|
fn assert_size(size: UVec2) {
|
||||||
assert!(
|
assert!(
|
||||||
size.x > 0 &&
|
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)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub enum SourceTextureFormat {
|
pub enum SourceTextureFormat {
|
||||||
/// RGBA, 8-bit per channel\
|
/// RGBA, 8-bit per channel
|
||||||
/// (Default and preferred format)
|
|
||||||
#[default]
|
#[default]
|
||||||
RGBA8,
|
RGBA8,
|
||||||
|
|
||||||
/// RGB, 8-bit per channel
|
//TODO native-endian RGBA32 format
|
||||||
/// (Alpha channel is assumed to be 255)
|
|
||||||
|
/// 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,
|
RGB8,
|
||||||
|
|
||||||
/// Alpha only, 8-bit per channel
|
/// BGR, 8-bit per channel (Alpha = 255)
|
||||||
/// (All other channels are assumed to be 255 (white))
|
BGR8,
|
||||||
A8,
|
|
||||||
|
|
||||||
//TODO ARGB, BGRA, etc.
|
/// Alpha only, 8-bit per channel (RGB = #ffffff)
|
||||||
|
A8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceTextureFormat {
|
impl SourceTextureFormat {
|
||||||
pub const fn bytes_per_pixel(&self) -> usize {
|
pub const fn bytes_per_pixel(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
SourceTextureFormat::RGBA8 => 4,
|
SourceTextureFormat::RGBA8 |
|
||||||
SourceTextureFormat::RGB8 => 3,
|
SourceTextureFormat::ARGB8 |
|
||||||
|
SourceTextureFormat::BGRA8 |
|
||||||
|
SourceTextureFormat::ABGR8 => 4,
|
||||||
|
SourceTextureFormat::RGB8 |
|
||||||
|
SourceTextureFormat::BGR8 => 3,
|
||||||
SourceTextureFormat::A8 => 1,
|
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)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct TextureHandle {
|
pub struct TextureHandle {
|
||||||
pub(crate) id: TextureId,
|
pub(crate) id: TextureId,
|
||||||
pub(crate) size: UVec2,
|
pub(crate) size: UVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TextureAllocation {
|
impl TextureHandle {
|
||||||
handle: TextureHandle,
|
pub fn size(&self) -> UVec2 {
|
||||||
offset: UVec2,
|
self.size
|
||||||
size: UVec2,
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
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>,
|
data: Vec<u8>,
|
||||||
|
|
||||||
|
/// The packer used to allocate space for textures in the atlas
|
||||||
packer: DensePacker,
|
packer: DensePacker,
|
||||||
|
|
||||||
|
/// The next id to be used for a texture handle\
|
||||||
|
/// Gets incremented every time a new texture is allocated
|
||||||
next_id: TextureId,
|
next_id: TextureId,
|
||||||
|
|
||||||
|
/// Active allocated textures, indexed by id of their handle
|
||||||
allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>,
|
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 {
|
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);
|
assert_size(size);
|
||||||
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
|
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
|
||||||
Self {
|
Self {
|
||||||
|
@ -84,9 +156,16 @@ impl TextureAtlas {
|
||||||
),
|
),
|
||||||
next_id: 0,
|
next_id: 0,
|
||||||
allocations: HashMap::default(),
|
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 {
|
fn next_handle(&mut self, size: UVec2) -> TextureHandle {
|
||||||
let handle = TextureHandle {
|
let handle = TextureHandle {
|
||||||
id: self.next_id,
|
id: self.next_id,
|
||||||
|
@ -96,7 +175,6 @@ impl TextureAtlas {
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Allocate a texture in the atlas, returning a handle to it.\
|
/// Allocate a texture in the atlas, returning a handle to it.\
|
||||||
/// The data present in the texture is undefined, and may include garbage data.
|
/// 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 {
|
pub fn allocate(&mut self, size: UVec2) -> TextureHandle {
|
||||||
assert_size(size);
|
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
|
// Pack the texture
|
||||||
let pack = self.packer.pack(
|
let pack = self.packer.pack(
|
||||||
size.x as i32,
|
size.x as i32,
|
||||||
size.y as i32,
|
size.y as i32,
|
||||||
ALLOW_ROTATION
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
//TODO: handle pack failure by either resizing the atlas or returning an error
|
//TODO: handle pack failure by either resizing the atlas or returning an error
|
||||||
|
@ -118,12 +213,97 @@ impl TextureAtlas {
|
||||||
|
|
||||||
// Allocate the texture
|
// Allocate the texture
|
||||||
let handle = self.next_handle(size);
|
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);
|
self.allocations.insert_unique_unchecked(handle.id, allocation);
|
||||||
|
|
||||||
handle
|
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.\
|
/// Allocate a texture in the atlas, returning a handle to it.\
|
||||||
/// The texture is initialized with the provided data.
|
/// The texture is initialized with the provided data.
|
||||||
///
|
///
|
||||||
|
@ -156,37 +336,16 @@ impl TextureAtlas {
|
||||||
|
|
||||||
// Allocate the texture
|
// Allocate the texture
|
||||||
let handle = self.allocate(size);
|
let handle = self.allocate(size);
|
||||||
let allocation = self.allocations.get(&handle.id).unwrap();
|
|
||||||
|
|
||||||
for y in 0..size.y {
|
// Write the data to the texture
|
||||||
for x in 0..size.x {
|
self.update(handle, format, data);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for TextureAtlas {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(DEFAULT_ATLAS_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ pub mod draw;
|
||||||
pub mod measure;
|
pub mod measure;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod color;
|
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue