From eda523c58af3b4191ad4085e9e6368c5e6a0997a Mon Sep 17 00:00:00 2001 From: crauzer Date: Sat, 6 Dec 2025 18:46:19 +0100 Subject: [PATCH 1/3] feat: add experimental support for 16bit texture formats --- crates/ltk_texture/src/tex/format.rs | 5 +- crates/ltk_texture/src/tex/mod.rs | 11 ++-- crates/ltk_texture/src/tex/surface.rs | 79 ++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/crates/ltk_texture/src/tex/format.rs b/crates/ltk_texture/src/tex/format.rs index a735be31..b31f3ede 100644 --- a/crates/ltk_texture/src/tex/format.rs +++ b/crates/ltk_texture/src/tex/format.rs @@ -12,6 +12,8 @@ pub enum Format { Bc3 = 12, /// Uncompressed BGRA8 Bgra8 = 20, + /// Uncompressed BGRA16 (16-bit per channel) + Bgra16 = 21, } impl Format { @@ -26,7 +28,7 @@ impl Format { /// Get the block size of the format pub fn block_size(&self) -> (usize, usize) { match self { - Format::Bgra8 => (1, 1), + Format::Bgra8 | Format::Bgra16 => (1, 1), _ => (4, 4), } } @@ -39,6 +41,7 @@ impl Format { Format::Bc1 => 8, Format::Bc3 => 16, Format::Bgra8 => 4, + Format::Bgra16 => 8, // 4 channels × 2 bytes each } } } diff --git a/crates/ltk_texture/src/tex/mod.rs b/crates/ltk_texture/src/tex/mod.rs index 4190a99b..202780f8 100644 --- a/crates/ltk_texture/src/tex/mod.rs +++ b/crates/ltk_texture/src/tex/mod.rs @@ -133,12 +133,15 @@ impl Tex { // size of mip let (w, h) = mip_dims(level); - let data = match matches!(self.format, Format::Bgra8) { - true => TexSurfaceData::Bgra8Slice( + let data = match self.format { + Format::Bgra8 => TexSurfaceData::Bgra8Slice( // TODO: test me (this is likely wrong) &self.data[off..off + (w * h * self.format.bytes_per_block())], ), - false => { + Format::Bgra16 => TexSurfaceData::Bgra16Slice( + &self.data[off..off + (w * h * self.format.bytes_per_block())], + ), + _ => { let mut data = vec![0; w * h]; let i = &self.data[off..off + mip_bytes((w, h))]; let o = &mut data; @@ -152,7 +155,7 @@ impl Tex { texture2ddecoder::decode_etc2_rgba8(i, w, h, o).map_err(DecodeErr::Etc2Eac) } // Safety: the outer match ensures we can't reach this arm - Format::Bgra8 => unsafe { unreachable_unchecked() }, + Format::Bgra8 | Format::Bgra16 => unsafe { unreachable_unchecked() }, }?; TexSurfaceData::Bgra8Owned(data) } diff --git a/crates/ltk_texture/src/tex/surface.rs b/crates/ltk_texture/src/tex/surface.rs index 8c2237b3..31e833ac 100644 --- a/crates/ltk_texture/src/tex/surface.rs +++ b/crates/ltk_texture/src/tex/surface.rs @@ -1,5 +1,10 @@ +use image::{ImageBuffer, Rgba}; + use super::super::ToImageError; +/// 16-bit RGBA image buffer +pub type Rgba16Image = ImageBuffer, Vec>; + /// A decoded tex mipmap pub struct TexSurface<'a> { pub width: u32, @@ -11,10 +16,15 @@ pub struct TexSurface<'a> { pub enum TexSurfaceData<'a> { Bgra8Slice(&'a [u8]), Bgra8Owned(Vec), + /// 16-bit per channel BGRA data (8 bytes per pixel) + Bgra16Slice(&'a [u8]), } impl TexSurface<'_> { - /// Convert the surface to an [image::RgbaImage] + /// Convert the surface to an [image::RgbaImage] (8-bit per channel) + /// + /// For 16-bit textures, this will normalize values to 8-bit. + /// Use [Self::into_rgba16_image] to preserve full precision. pub fn into_rgba_image(self) -> Result { image::RgbaImage::from_raw( self.width, @@ -37,6 +47,73 @@ impl TexSurface<'_> { [r, g, b, a] }) .collect(), + TexSurfaceData::Bgra16Slice(data) => data + .chunks_exact(8) + .flat_map(|pixel| { + let b = u16::from_le_bytes([pixel[0], pixel[1]]); + let g = u16::from_le_bytes([pixel[2], pixel[3]]); + let r = u16::from_le_bytes([pixel[4], pixel[5]]); + let a = u16::from_le_bytes([pixel[6], pixel[7]]); + + [ + (r >> 8) as u8, + (g >> 8) as u8, + (b >> 8) as u8, + (a >> 8) as u8, + ] + }) + .collect(), + }, + ) + .ok_or(ToImageError::InvalidContainerSize) + } + + /// Convert the surface to an [Rgba16Image] (16-bit per channel) + /// + /// For 8-bit textures, values are scaled up to 16-bit. + pub fn into_rgba16_image(self) -> Result { + Rgba16Image::from_raw( + self.width, + self.height, + match self.data { + TexSurfaceData::Bgra8Slice(data) => data + .chunks_exact(4) + .flat_map(|pixel| { + let [b, g, r, a] = pixel else { + unreachable!(); + }; + + [ + *r as u16 * 257, + *g as u16 * 257, + *b as u16 * 257, + *a as u16 * 257, + ] + }) + .collect(), + TexSurfaceData::Bgra8Owned(vec) => vec + .into_iter() + .flat_map(|pixel| { + let [b, g, r, a] = pixel.to_le_bytes(); + + [ + r as u16 * 257, + g as u16 * 257, + b as u16 * 257, + a as u16 * 257, + ] + }) + .collect(), + TexSurfaceData::Bgra16Slice(data) => data + .chunks_exact(8) + .flat_map(|pixel| { + let b = u16::from_le_bytes([pixel[0], pixel[1]]); + let g = u16::from_le_bytes([pixel[2], pixel[3]]); + let r = u16::from_le_bytes([pixel[4], pixel[5]]); + let a = u16::from_le_bytes([pixel[6], pixel[7]]); + [r, g, b, a] + }) + .collect(), }, ) .ok_or(ToImageError::InvalidContainerSize) From 8278cd2732d485aebba1e6c135f47c688b9f64fb Mon Sep 17 00:00:00 2001 From: crauzer Date: Sat, 6 Dec 2025 19:25:21 +0100 Subject: [PATCH 2/3] chore: try using f16 --- crates/ltk_texture/Cargo.toml | 1 + crates/ltk_texture/src/tex/mod.rs | 2 +- crates/ltk_texture/src/tex/surface.rs | 46 ++++++++++++++++----------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/ltk_texture/Cargo.toml b/crates/ltk_texture/Cargo.toml index ae36ea4e..d3ab49b3 100644 --- a/crates/ltk_texture/Cargo.toml +++ b/crates/ltk_texture/Cargo.toml @@ -12,6 +12,7 @@ byteorder = { workspace = true } thiserror = { workspace = true } num_enum = { workspace = true } ltk_io_ext = { version = "0.3.2", path = "../ltk_io_ext" } +half = "2.6" image = { version = "0.25.2", default-features = false, features = ["dds"] } ddsfile = "0.5.2" diff --git a/crates/ltk_texture/src/tex/mod.rs b/crates/ltk_texture/src/tex/mod.rs index 202780f8..bb64ff8e 100644 --- a/crates/ltk_texture/src/tex/mod.rs +++ b/crates/ltk_texture/src/tex/mod.rs @@ -138,7 +138,7 @@ impl Tex { // TODO: test me (this is likely wrong) &self.data[off..off + (w * h * self.format.bytes_per_block())], ), - Format::Bgra16 => TexSurfaceData::Bgra16Slice( + Format::Bgra16 => TexSurfaceData::Bgra16fSlice( &self.data[off..off + (w * h * self.format.bytes_per_block())], ), _ => { diff --git a/crates/ltk_texture/src/tex/surface.rs b/crates/ltk_texture/src/tex/surface.rs index 31e833ac..d231a7b5 100644 --- a/crates/ltk_texture/src/tex/surface.rs +++ b/crates/ltk_texture/src/tex/surface.rs @@ -1,8 +1,12 @@ +use half::f16; use image::{ImageBuffer, Rgba}; use super::super::ToImageError; -/// 16-bit RGBA image buffer +/// 16-bit unsigned integer RGBA image buffer +/// +/// Used as output format for [TexSurface::into_rgba16_image]. +/// For f16 source textures, values are converted from 0.0-1.0 float range to 0-65535 integer range. pub type Rgba16Image = ImageBuffer, Vec>; /// A decoded tex mipmap @@ -16,8 +20,8 @@ pub struct TexSurface<'a> { pub enum TexSurfaceData<'a> { Bgra8Slice(&'a [u8]), Bgra8Owned(Vec), - /// 16-bit per channel BGRA data (8 bytes per pixel) - Bgra16Slice(&'a [u8]), + /// Half-precision float (f16) per channel BGRA data (8 bytes per pixel) + Bgra16fSlice(&'a [u8]), } impl TexSurface<'_> { @@ -47,19 +51,19 @@ impl TexSurface<'_> { [r, g, b, a] }) .collect(), - TexSurfaceData::Bgra16Slice(data) => data + TexSurfaceData::Bgra16fSlice(data) => data .chunks_exact(8) .flat_map(|pixel| { - let b = u16::from_le_bytes([pixel[0], pixel[1]]); - let g = u16::from_le_bytes([pixel[2], pixel[3]]); - let r = u16::from_le_bytes([pixel[4], pixel[5]]); - let a = u16::from_le_bytes([pixel[6], pixel[7]]); + let b = f16::from_le_bytes([pixel[0], pixel[1]]).to_f32(); + let g = f16::from_le_bytes([pixel[2], pixel[3]]).to_f32(); + let r = f16::from_le_bytes([pixel[4], pixel[5]]).to_f32(); + let a = f16::from_le_bytes([pixel[6], pixel[7]]).to_f32(); [ - (r >> 8) as u8, - (g >> 8) as u8, - (b >> 8) as u8, - (a >> 8) as u8, + (r.clamp(0.0, 1.0) * 255.0) as u8, + (g.clamp(0.0, 1.0) * 255.0) as u8, + (b.clamp(0.0, 1.0) * 255.0) as u8, + (a.clamp(0.0, 1.0) * 255.0) as u8, ] }) .collect(), @@ -104,14 +108,20 @@ impl TexSurface<'_> { ] }) .collect(), - TexSurfaceData::Bgra16Slice(data) => data + TexSurfaceData::Bgra16fSlice(data) => data .chunks_exact(8) .flat_map(|pixel| { - let b = u16::from_le_bytes([pixel[0], pixel[1]]); - let g = u16::from_le_bytes([pixel[2], pixel[3]]); - let r = u16::from_le_bytes([pixel[4], pixel[5]]); - let a = u16::from_le_bytes([pixel[6], pixel[7]]); - [r, g, b, a] + let b = f16::from_le_bytes([pixel[0], pixel[1]]).to_f32(); + let g = f16::from_le_bytes([pixel[2], pixel[3]]).to_f32(); + let r = f16::from_le_bytes([pixel[4], pixel[5]]).to_f32(); + let a = f16::from_le_bytes([pixel[6], pixel[7]]).to_f32(); + + [ + (r.clamp(0.0, 1.0) * 65535.0) as u16, + (g.clamp(0.0, 1.0) * 65535.0) as u16, + (b.clamp(0.0, 1.0) * 65535.0) as u16, + (a.clamp(0.0, 1.0) * 65535.0) as u16, + ] }) .collect(), }, From a4b9cd689cbecda5c35ece7bc548501665d0e5ac Mon Sep 17 00:00:00 2001 From: crauzer Date: Sat, 6 Dec 2025 19:48:58 +0100 Subject: [PATCH 3/3] Revert "chore: try using f16" This reverts commit 8278cd2732d485aebba1e6c135f47c688b9f64fb. --- crates/ltk_texture/Cargo.toml | 1 - crates/ltk_texture/src/tex/mod.rs | 2 +- crates/ltk_texture/src/tex/surface.rs | 46 +++++++++++---------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/crates/ltk_texture/Cargo.toml b/crates/ltk_texture/Cargo.toml index d3ab49b3..ae36ea4e 100644 --- a/crates/ltk_texture/Cargo.toml +++ b/crates/ltk_texture/Cargo.toml @@ -12,7 +12,6 @@ byteorder = { workspace = true } thiserror = { workspace = true } num_enum = { workspace = true } ltk_io_ext = { version = "0.3.2", path = "../ltk_io_ext" } -half = "2.6" image = { version = "0.25.2", default-features = false, features = ["dds"] } ddsfile = "0.5.2" diff --git a/crates/ltk_texture/src/tex/mod.rs b/crates/ltk_texture/src/tex/mod.rs index bb64ff8e..202780f8 100644 --- a/crates/ltk_texture/src/tex/mod.rs +++ b/crates/ltk_texture/src/tex/mod.rs @@ -138,7 +138,7 @@ impl Tex { // TODO: test me (this is likely wrong) &self.data[off..off + (w * h * self.format.bytes_per_block())], ), - Format::Bgra16 => TexSurfaceData::Bgra16fSlice( + Format::Bgra16 => TexSurfaceData::Bgra16Slice( &self.data[off..off + (w * h * self.format.bytes_per_block())], ), _ => { diff --git a/crates/ltk_texture/src/tex/surface.rs b/crates/ltk_texture/src/tex/surface.rs index d231a7b5..31e833ac 100644 --- a/crates/ltk_texture/src/tex/surface.rs +++ b/crates/ltk_texture/src/tex/surface.rs @@ -1,12 +1,8 @@ -use half::f16; use image::{ImageBuffer, Rgba}; use super::super::ToImageError; -/// 16-bit unsigned integer RGBA image buffer -/// -/// Used as output format for [TexSurface::into_rgba16_image]. -/// For f16 source textures, values are converted from 0.0-1.0 float range to 0-65535 integer range. +/// 16-bit RGBA image buffer pub type Rgba16Image = ImageBuffer, Vec>; /// A decoded tex mipmap @@ -20,8 +16,8 @@ pub struct TexSurface<'a> { pub enum TexSurfaceData<'a> { Bgra8Slice(&'a [u8]), Bgra8Owned(Vec), - /// Half-precision float (f16) per channel BGRA data (8 bytes per pixel) - Bgra16fSlice(&'a [u8]), + /// 16-bit per channel BGRA data (8 bytes per pixel) + Bgra16Slice(&'a [u8]), } impl TexSurface<'_> { @@ -51,19 +47,19 @@ impl TexSurface<'_> { [r, g, b, a] }) .collect(), - TexSurfaceData::Bgra16fSlice(data) => data + TexSurfaceData::Bgra16Slice(data) => data .chunks_exact(8) .flat_map(|pixel| { - let b = f16::from_le_bytes([pixel[0], pixel[1]]).to_f32(); - let g = f16::from_le_bytes([pixel[2], pixel[3]]).to_f32(); - let r = f16::from_le_bytes([pixel[4], pixel[5]]).to_f32(); - let a = f16::from_le_bytes([pixel[6], pixel[7]]).to_f32(); + let b = u16::from_le_bytes([pixel[0], pixel[1]]); + let g = u16::from_le_bytes([pixel[2], pixel[3]]); + let r = u16::from_le_bytes([pixel[4], pixel[5]]); + let a = u16::from_le_bytes([pixel[6], pixel[7]]); [ - (r.clamp(0.0, 1.0) * 255.0) as u8, - (g.clamp(0.0, 1.0) * 255.0) as u8, - (b.clamp(0.0, 1.0) * 255.0) as u8, - (a.clamp(0.0, 1.0) * 255.0) as u8, + (r >> 8) as u8, + (g >> 8) as u8, + (b >> 8) as u8, + (a >> 8) as u8, ] }) .collect(), @@ -108,20 +104,14 @@ impl TexSurface<'_> { ] }) .collect(), - TexSurfaceData::Bgra16fSlice(data) => data + TexSurfaceData::Bgra16Slice(data) => data .chunks_exact(8) .flat_map(|pixel| { - let b = f16::from_le_bytes([pixel[0], pixel[1]]).to_f32(); - let g = f16::from_le_bytes([pixel[2], pixel[3]]).to_f32(); - let r = f16::from_le_bytes([pixel[4], pixel[5]]).to_f32(); - let a = f16::from_le_bytes([pixel[6], pixel[7]]).to_f32(); - - [ - (r.clamp(0.0, 1.0) * 65535.0) as u16, - (g.clamp(0.0, 1.0) * 65535.0) as u16, - (b.clamp(0.0, 1.0) * 65535.0) as u16, - (a.clamp(0.0, 1.0) * 65535.0) as u16, - ] + let b = u16::from_le_bytes([pixel[0], pixel[1]]); + let g = u16::from_le_bytes([pixel[2], pixel[3]]); + let r = u16::from_le_bytes([pixel[4], pixel[5]]); + let a = u16::from_le_bytes([pixel[6], pixel[7]]); + [r, g, b, a] }) .collect(), },