diff --git a/flake.nix b/flake.nix index 93f9890..dab3ea6 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ "rust-analyzer" "clippy" ]) + cargo-nextest pkg-config cmake ]; diff --git a/hui-painter/src/text.rs b/hui-painter/src/text.rs index 4f96bd0..a971e11 100644 --- a/hui-painter/src/text.rs +++ b/hui-painter/src/text.rs @@ -12,11 +12,15 @@ pub struct FontManager { } impl FontManager { - pub fn new() -> Self { - let mut this = Self { + pub(crate) fn new_internal() -> Self { + Self { fonts: font::FontHandleManager::new(), ftm: ftm::FontTextureManager::new(), - }; + } + } + + pub fn new() -> Self { + let mut this = Self::new_internal(); #[cfg(feature="default-font")] { this.fonts.idc = 0; this.add(include_bytes!("../assets/font/ProggyTiny.ttf")); diff --git a/hui-painter/src/texture.rs b/hui-painter/src/texture.rs index c0ac03e..5b07111 100644 --- a/hui-painter/src/texture.rs +++ b/hui-painter/src/texture.rs @@ -162,11 +162,12 @@ pub struct TextureAtlas { } impl TextureAtlas { - /// Create a new texture atlas with the specified size. - pub(crate) fn new(size: UVec2) -> Self { + /// Internal function, only directly used in tests + pub(crate) fn new_internal(size: UVec2) -> Self { assert_size(size); + let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL; - let mut this = Self { + Self { size, data: vec![0; data_bytes], packer: DensePacker::new( @@ -177,7 +178,12 @@ impl TextureAtlas { allocations: HashMap::default(), reuse_allocations: Vec::new(), version: 0, - }; + } + } + + /// Create a new texture atlas with the specified size. + pub(crate) fn new(size: UVec2) -> Self { + let mut this = Self::new_internal(size); // HACK?: ensure 0,0 is a white pixel let h = this.allocate_with_data(SourceTextureFormat::A8, &[255], 1); @@ -320,11 +326,13 @@ impl TextureAtlas { .get(&handle.id) .expect("invalid texture handle"); + debug_assert_eq!(*size, handle.size, "texture size mismatch"); + 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.y + y) * self.size.x + (offset.x + x) ) as usize * RGBA_BYTES_PER_PIXEL; @@ -333,6 +341,7 @@ impl TextureAtlas { match format { SourceTextureFormat::RGBA8 => { + // TODO opt: copy entire row in this case dst.copy_from_slice(src); }, SourceTextureFormat::ARGB8 => { @@ -429,3 +438,164 @@ impl Default for TextureAtlas { Self::new(DEFAULT_ATLAS_SIZE) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_assert_size_valid() { + assert_size(uvec2(1, 1)); + assert_size(uvec2(i32::MAX as u32, i32::MAX as u32)); + } + + #[test] + #[should_panic(expected = "size must be greater than 0")] + fn test_assert_size_zero() { + assert_size(uvec2(0, 0)); + } + + #[test] + #[should_panic(expected = "size must be less than i32::MAX")] + fn test_assert_size_too_large() { + assert_size(uvec2(i32::MAX as u32 + 1, i32::MAX as u32 + 1)); + } + + #[test] + fn test_texture_handle_new_broken() { + let handle = TextureHandle::new_broken(); + assert_eq!(handle.id, u32::MAX); + assert_eq!(handle.size, uvec2(0, 0)); + } + + #[test] + fn test_texture_allocation_new() { + let handle = TextureHandle::new_broken(); + let allocation = TextureAllocation::new(handle, uvec2(1, 1), uvec2(2, 2)); + assert_eq!(allocation.handle, handle); + assert_eq!(allocation.offset, uvec2(1, 1)); + assert_eq!(allocation.size, uvec2(2, 2)); + assert_eq!(allocation.max_size, uvec2(2, 2)); + } + + #[test] + fn test_texture_atlas_new() { + const SIZE: u32 = 128; + + let atlas = TextureAtlas::new_internal(uvec2(SIZE, SIZE)); + assert_eq!(atlas.size, uvec2(SIZE, SIZE)); + assert_eq!(atlas.data.len(), (SIZE as usize) * (SIZE as usize) * RGBA_BYTES_PER_PIXEL); + assert_eq!(atlas.next_id, 0); + assert_eq!(atlas.allocations.len(), 0); + assert_eq!(atlas.reuse_allocations.len(), 0); + assert_eq!(atlas.version, 0); + } + + #[test] + fn test_texture_atlas_allocate() { + let mut atlas = TextureAtlas::new_internal(uvec2(128, 128)); + let handle = atlas.allocate(uvec2(32, 32)); + assert_eq!(handle.size, uvec2(32, 32)); + assert_eq!(atlas.get_uv(handle).unwrap().bottom_right, vec2(32. / 128., 32. / 128.)); + assert_eq!(atlas.allocations.len(), 1); + } + + #[test] + fn test_texture_atlas_allocate_with_data() { + fn make_data(o: u8)-> Vec<u8> { + let mut data = vec![o; 32 * 32 * 4]; + for y in 0..32 { + for x in 0..32 { + let idx = (y * 32 + x) * 4; + data[idx] = x as u8; + data[idx + 1] = y as u8; + } + } + data + } + + let mut atlas = TextureAtlas::new_internal(uvec2(128, 128)); + + let data = make_data(1); + let handle = atlas.allocate_with_data(SourceTextureFormat::RGBA8, &data, 32); + assert_eq!(handle.size, uvec2(32, 32)); + assert_eq!(atlas.allocations.len(), 1); + let uv = atlas.get_uv(handle).unwrap(); + assert_eq!(uv.top_left, vec2(0.0, 0.0)); + assert_eq!(uv.top_right, vec2(32.0 / 128.0, 0.0)); + assert_eq!(uv.bottom_left, vec2(0.0, 32.0 / 128.0)); + assert_eq!(uv.bottom_right, vec2(32.0 / 128.0, 32.0 / 128.0)); + + let data = make_data(2); + let handle = atlas.allocate_with_data(SourceTextureFormat::RGBA8, &data, 32); + assert_eq!(handle.size, uvec2(32, 32)); + assert_eq!(atlas.allocations.len(), 2); + let uv = atlas.get_uv(handle).unwrap(); + assert_eq!(uv.top_left, vec2(32.0 / 128.0, 0.0)); + assert_eq!(uv.top_right, vec2(64.0 / 128.0, 0.0)); + assert_eq!(uv.bottom_left, vec2(32.0 / 128.0, 32.0 / 128.0)); + assert_eq!(uv.bottom_right, vec2(64.0 / 128.0, 32.0 / 128.0)); + + // now, check the texture data + assert_eq!(atlas.version(), 2); + let data = atlas.data_rgba(); + + // for y in 0..128 { + // for x in 0..128 { + // let idx = (y * 128 + x) * 4; + // print!("{}", data[idx + 2]); + // } + // println!(); + // } + + for y in 0..128 { + for x in 0..128 { + let idx = (y * 128 + x) * 4; + if y >= 32 || x >= 64 { + continue + } + assert_eq!( + if x < 32 { + [x as u8, y as u8, 1, 1] + } else if x < 64 { + [x as u8 - 32, y as u8, 2, 2] + } else { + unreachable!() + }, + data[idx..idx + 4], + "pixel at ({x}, {y}) idx: {idx} is incorrect", + ); + } + } + } + + // #[test] + // fn test_texture_atlas_update() { + // let mut atlas = TextureAtlas::new(uvec2(128, 128)); + // let data = vec![255; 32 * 32 * 4]; + // let handle = atlas.allocate_with_data(SourceTextureFormat::RGBA8, &data, 32); + // let new_data = vec![0; 32 * 32 * 4]; + // atlas.update(handle, SourceTextureFormat::RGBA8, &new_data); + // assert_eq!(atlas.data_rgba()[..32 * 32 * 4], new_data[..]); + // } + + #[test] + fn test_texture_atlas_deallocate() { + let mut atlas = TextureAtlas::new_internal(uvec2(128, 128)); + let handle = atlas.allocate(uvec2(32, 32)); + atlas.deallocate(handle); + assert_eq!(atlas.allocations.len(), 0); + assert_eq!(atlas.reuse_allocations.len(), 1); + } + + #[test] + fn test_texture_atlas_get_uv() { + let mut atlas = TextureAtlas::new_internal(uvec2(128, 128)); + let handle = atlas.allocate(uvec2(32, 32)); + let uv = atlas.get_uv(handle).unwrap(); + assert_eq!(uv.top_left, vec2(0.0, 0.0)); + assert_eq!(uv.top_right, vec2(32.0 / 128.0, 0.0)); + assert_eq!(uv.bottom_left, vec2(0.0, 32.0 / 128.0)); + assert_eq!(uv.bottom_right, vec2(32.0 / 128.0, 32.0 / 128.0)); + } +} \ No newline at end of file