use basisu_c_sys::encoder;
use bevy::{
image::Image,
render::render_resource::{TextureDimension, TextureFormat, TextureViewDimension},
};
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
static BASISU_INITIALIZED: OnceLock<()> = OnceLock::new();
pub fn basisu_init() {
BASISU_INITIALIZED.get_or_init(|| {
unsafe { encoder::bu_init() };
});
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[repr(u32)]
pub enum BasisTextureFormat {
Etc1s = encoder::BTF_ETC1S,
UastcLdr4x4 = encoder::BTF_UASTC_LDR_4X4,
UastcHdr4x4 = encoder::BTF_UASTC_HDR_4X4,
AstcHdr6x6 = encoder::BTF_ASTC_HDR_6X6,
UastcHdr6x6 = encoder::BTF_UASTC_HDR_6X6,
XuastcLdr4x4 = encoder::BTF_XUASTC_LDR_4X4,
XuastcLdr5x4 = encoder::BTF_XUASTC_LDR_5X4,
XuastcLdr5x5 = encoder::BTF_XUASTC_LDR_5X5,
XuastcLdr6x5 = encoder::BTF_XUASTC_LDR_6X5,
XuastcLdr6x6 = encoder::BTF_XUASTC_LDR_6X6,
XuastcLdr8x5 = encoder::BTF_XUASTC_LDR_8X5,
XuastcLdr8x6 = encoder::BTF_XUASTC_LDR_8X6,
XuastcLdr10x5 = encoder::BTF_XUASTC_LDR_10X5,
XuastcLdr10x6 = encoder::BTF_XUASTC_LDR_10X6,
XuastcLdr8x8 = encoder::BTF_XUASTC_LDR_8X8,
XuastcLdr10x8 = encoder::BTF_XUASTC_LDR_10X8,
XuastcLdr10x10 = encoder::BTF_XUASTC_LDR_10X10,
XuastcLdr12x10 = encoder::BTF_XUASTC_LDR_12X10,
XuastcLdr12x12 = encoder::BTF_XUASTC_LDR_12X12,
AstcLdr4x4 = encoder::BTF_ASTC_LDR_4X4,
AstcLdr5x4 = encoder::BTF_ASTC_LDR_5X4,
AstcLdr5x5 = encoder::BTF_ASTC_LDR_5X5,
AstcLdr6x5 = encoder::BTF_ASTC_LDR_6X5,
AstcLdr6x6 = encoder::BTF_ASTC_LDR_6X6,
AstcLdr8x5 = encoder::BTF_ASTC_LDR_8X5,
AstcLdr8x6 = encoder::BTF_ASTC_LDR_8X6,
AstcLdr10x5 = encoder::BTF_ASTC_LDR_10X5,
AstcLdr10x6 = encoder::BTF_ASTC_LDR_10X6,
AstcLdr8x8 = encoder::BTF_ASTC_LDR_8X8,
AstcLdr10x8 = encoder::BTF_ASTC_LDR_10X8,
AstcLdr10x10 = encoder::BTF_ASTC_LDR_10X10,
AstcLdr12x10 = encoder::BTF_ASTC_LDR_12X10,
AstcLdr12x12 = encoder::BTF_ASTC_LDR_12X12,
}
pub use encoder::BU_COMP_FLAGS_DEBUG_IMAGES;
pub use encoder::BU_COMP_FLAGS_DEBUG_OUTPUT;
pub use encoder::BU_COMP_FLAGS_GEN_MIPS_CLAMP;
pub use encoder::BU_COMP_FLAGS_GEN_MIPS_WRAP;
pub use encoder::BU_COMP_FLAGS_KTX2_OUTPUT;
pub use encoder::BU_COMP_FLAGS_KTX2_UASTC_ZSTD;
pub use encoder::BU_COMP_FLAGS_NONE;
pub use encoder::BU_COMP_FLAGS_PRINT_STATS;
pub use encoder::BU_COMP_FLAGS_PRINT_STATUS;
pub use encoder::BU_COMP_FLAGS_REC2020;
pub use encoder::BU_COMP_FLAGS_SRGB;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_2D;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_2D_ARRAY;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_CUBEMAP_ARRAY;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_MASK;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_SHIFT;
pub use encoder::BU_COMP_FLAGS_TEXTURE_TYPE_VIDEO_FRAMES;
pub use encoder::BU_COMP_FLAGS_THREADED;
pub use encoder::BU_COMP_FLAGS_USE_OPENCL;
pub use encoder::BU_COMP_FLAGS_VALIDATE_OUTPUT;
pub use encoder::BU_COMP_FLAGS_VERBOSE;
pub use encoder::BU_COMP_FLAGS_XUASTC_LDR_FULL_ARITH;
pub use encoder::BU_COMP_FLAGS_XUASTC_LDR_FULL_ZSTD;
pub use encoder::BU_COMP_FLAGS_XUASTC_LDR_HYBRID;
pub use encoder::BU_COMP_FLAGS_XUASTC_LDR_SYNTAX_MASK;
pub use encoder::BU_COMP_FLAGS_XUASTC_LDR_SYNTAX_SHIFT;
pub use encoder::BU_COMP_FLAGS_Y_FLIP;
pub struct BasisuEncoder {
params: u64,
}
impl Default for BasisuEncoder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum BasisuEncodeError {
#[error("Image data must not be None")]
DataIsNone,
#[error("Mip level count must be 1")]
MipLevelCountNotOne,
#[error("Unsupported texture format: {0:?}")]
UnsupportedTextureFormat(TextureFormat),
#[error("Unsupported texture dimension: {0:?}")]
UnsupportedTextureDimension(TextureDimension),
#[error("Unsupported texture view dimension: {0:?}")]
UnsupportedTextureViewDimension(TextureViewDimension),
#[error("`BaisuEncoder::set_image_slice` only accepts image with 1 layer or depth")]
SetImageSliceOnlyAcceptsOneLayer,
#[error("bu_comp_params_set_image_* failed")]
BuSetImageFailed,
#[error("bu_compress_texture failed")]
BuCompressFailed,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BasisuEncoderParams {
pub basis_tex_format: BasisTextureFormat,
pub quality_level: i32,
pub effort_level: i32,
pub flags_and_quality: u64,
pub low_level_uastc_rdo_or_dct_quality: f32,
}
impl BasisuEncoderParams {
pub const fn new_with_srgb_defaults(basis_tex_format: BasisTextureFormat) -> Self {
Self {
basis_tex_format,
quality_level: 75,
effort_level: 2,
flags_and_quality: (BU_COMP_FLAGS_THREADED
| BU_COMP_FLAGS_SRGB
| BU_COMP_FLAGS_KTX2_OUTPUT
| BU_COMP_FLAGS_KTX2_UASTC_ZSTD) as u64,
low_level_uastc_rdo_or_dct_quality: 0.0,
}
}
pub const fn new_with_linear_defaults(basis_tex_format: BasisTextureFormat) -> Self {
Self {
basis_tex_format,
quality_level: 75,
effort_level: 2,
flags_and_quality: (BU_COMP_FLAGS_THREADED
| BU_COMP_FLAGS_KTX2_OUTPUT
| BU_COMP_FLAGS_KTX2_UASTC_ZSTD) as u64,
low_level_uastc_rdo_or_dct_quality: 0.0,
}
}
pub const fn with_tex_type(mut self, tex_type: TextureViewDimension) -> Self {
self.flags_and_quality = self.flags_and_quality
& !(BU_COMP_FLAGS_TEXTURE_TYPE_MASK << BU_COMP_FLAGS_TEXTURE_TYPE_SHIFT) as u64;
self.flags_and_quality = self.flags_and_quality
| match tex_type {
TextureViewDimension::D2 => BU_COMP_FLAGS_TEXTURE_TYPE_2D,
TextureViewDimension::D2Array => BU_COMP_FLAGS_TEXTURE_TYPE_2D_ARRAY,
TextureViewDimension::Cube | TextureViewDimension::CubeArray => {
BU_COMP_FLAGS_TEXTURE_TYPE_CUBEMAP_ARRAY
}
TextureViewDimension::D1 | TextureViewDimension::D3 => {
panic!("Compressing 1D or 3D texture is unsupported")
}
} as u64;
self
}
}
impl BasisuEncoder {
pub fn new() -> Self {
Self {
params: unsafe { encoder::bu_new_comp_params() },
}
}
pub fn set_image(&mut self, image: &Image) -> Result<(), BasisuEncodeError> {
self.clear_image();
let Some(data) = image.data.as_ref() else {
return Err(BasisuEncodeError::DataIsNone);
};
if image.texture_descriptor.mip_level_count != 1 {
return Err(BasisuEncodeError::MipLevelCountNotOne);
}
match image.texture_descriptor.dimension {
TextureDimension::D1 | TextureDimension::D3 => {
return Err(BasisuEncodeError::UnsupportedTextureDimension(
image.texture_descriptor.dimension,
));
}
TextureDimension::D2 => {}
}
if let Some(view_desc) = &image.texture_view_descriptor
&& let Some(dimension) = view_desc.dimension
{
match dimension {
TextureViewDimension::D1 | TextureViewDimension::D3 => {
return Err(BasisuEncodeError::UnsupportedTextureViewDimension(
dimension,
));
}
_ => {}
}
};
match image.texture_descriptor.format {
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => unsafe {
for i in 0..image.texture_descriptor.array_layer_count() {
if encoder::bu_comp_params_set_image_rgba32(
self.params,
i,
data.as_ptr() as u64 + (i * image.width() * image.height() * 4) as u64,
image.width(),
image.height(),
image.width() * 4,
) == 0
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
}
},
TextureFormat::Rgba32Float => unsafe {
for i in 0..image.texture_descriptor.array_layer_count() {
if encoder::bu_comp_params_set_image_float_rgba(
self.params,
i,
data.as_ptr() as u64 + (i * image.width() * image.height() * 16) as u64,
image.width(),
image.height(),
image.width() * 16,
) == 0
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
}
},
_ => {
return Err(BasisuEncodeError::UnsupportedTextureFormat(
image.texture_descriptor.format,
));
}
}
Ok(())
}
pub fn clear_image(&mut self) {
assert!(unsafe { encoder::bu_comp_params_clear(self.params) } != 0);
}
pub fn set_image_slice(&mut self, index: u32, image: &Image) -> Result<(), BasisuEncodeError> {
let Some(data) = image.data.as_ref() else {
return Err(BasisuEncodeError::DataIsNone);
};
if image.texture_descriptor.mip_level_count != 1 {
return Err(BasisuEncodeError::MipLevelCountNotOne);
}
match image.texture_descriptor.dimension {
TextureDimension::D1 | TextureDimension::D3 => {
return Err(BasisuEncodeError::UnsupportedTextureDimension(
image.texture_descriptor.dimension,
));
}
TextureDimension::D2 => {}
}
if image.texture_descriptor.array_layer_count() != 1 {
return Err(BasisuEncodeError::SetImageSliceOnlyAcceptsOneLayer);
}
if let Some(view_desc) = &image.texture_view_descriptor
&& let Some(dimension) = view_desc.dimension
{
match dimension {
TextureViewDimension::D1 | TextureViewDimension::D3 => {
return Err(BasisuEncodeError::UnsupportedTextureViewDimension(
dimension,
));
}
_ => {}
}
};
match image.texture_descriptor.format {
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => unsafe {
if encoder::bu_comp_params_set_image_rgba32(
self.params,
index,
data.as_ptr() as u64,
image.width(),
image.height(),
image.width() * 4,
) == 0
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
},
TextureFormat::Rgba32Float => unsafe {
if encoder::bu_comp_params_set_image_float_rgba(
self.params,
index,
data.as_ptr() as u64,
image.width(),
image.height(),
image.width() * 16,
) == 0
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
},
_ => {
return Err(BasisuEncodeError::UnsupportedTextureFormat(
image.texture_descriptor.format,
));
}
}
Ok(())
}
pub fn compress(&mut self, params: BasisuEncoderParams) -> Result<Vec<u8>, BasisuEncodeError> {
unsafe {
if encoder::bu_compress_texture(
self.params,
params.basis_tex_format as u32,
params.quality_level,
params.effort_level,
params.flags_and_quality,
params.low_level_uastc_rdo_or_dct_quality,
) == 0
{
return Err(BasisuEncodeError::BuCompressFailed);
}
let out_size = encoder::bu_comp_params_get_comp_data_size(self.params);
let out_ptr = encoder::bu_comp_params_get_comp_data_ofs(self.params) as *const u8;
let mut result = vec![0u8; out_size as usize];
core::ptr::copy_nonoverlapping(out_ptr, result.as_mut_ptr(), out_size as usize);
Ok(result)
}
}
}
impl Drop for BasisuEncoder {
fn drop(&mut self) {
unsafe {
encoder::bu_delete_comp_params(self.params);
}
}
}