use alloc::vec::Vec;
use crate::common;
use crate::encoder as enc_sys;
use crate::extra::types;
use crate::utils::BasisTextureFormat;
#[cfg(not(feature = "std"))]
use spin::Once;
#[cfg(feature = "std")]
use std::sync::Once;
#[derive(Debug, Clone, Copy)]
pub enum SourceImageFormat {
Rgba8,
Rgba32Float,
}
impl SourceImageFormat {
fn pixel_bytes(&self) -> u32 {
match self {
SourceImageFormat::Rgba8 => 4 * size_of::<u8>() as u32,
SourceImageFormat::Rgba32Float => 4 * size_of::<f32>() as u32,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SourceImage<'a> {
pub data: &'a [u8],
pub format: SourceImageFormat,
pub size: types::Extent3d,
}
impl SourceImage<'_> {
fn expected_per_layer_bytes(&self) -> u32 {
self.size.width * self.size.height * self.format.pixel_bytes()
}
fn expected_bytes(&self) -> u32 {
self.expected_per_layer_bytes() * self.size.depth_or_array_layers
}
fn validate_image_data(&self) -> Result<(), BasisuEncodeError> {
if self.data.is_empty() {
return Err(BasisuEncodeError::EmptyImageData);
}
if self.data.len() != self.expected_bytes() as usize {
return Err(BasisuEncodeError::ImageUnmatchedDataAndSize {
image_size: self.size,
expected_len: self.expected_bytes() as usize,
data_len: self.data.len(),
});
}
Ok(())
}
}
static BASISU_ENCODER_INITIALIZED: Once = Once::new();
pub(crate) fn encoder_is_init() -> bool {
#[cfg(not(feature = "std"))]
return BASISU_ENCODER_INITIALIZED.get().is_some();
#[cfg(feature = "std")]
return BASISU_ENCODER_INITIALIZED.is_completed();
}
pub(crate) fn encoder_is_uninit() -> bool {
!encoder_is_init()
}
pub fn basisu_encoder_init() {
BASISU_ENCODER_INITIALIZED.call_once(|| unsafe {
enc_sys::bu_init();
});
}
pub fn basisu_encoder_enable_debug_printf(enable: bool) {
unsafe { enc_sys::bu_enable_debug_printf(enable as u32) };
}
pub struct BasisuEncoder {
params: u64,
}
impl Default for BasisuEncoder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum BasisuEncodeError {
#[error("`BasisuEncoder::set_image_slice` only accepts image with 1 layer")]
SetImageSliceOnlyAcceptsOneLayer,
#[error("Image data is empty")]
EmptyImageData,
#[error("Image {image_size:?} Expects data length {expected_len}, got {data_len}")]
ImageUnmatchedDataAndSize {
image_size: types::Extent3d,
expected_len: usize,
data_len: usize,
},
#[error("bu_comp_params_set_image_* failed")]
BuSetImageFailed,
#[error("bu_compress_texture failed")]
BuCompressFailed,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::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: common::BU_COMP_FLAGS_THREADED
| common::BU_COMP_FLAGS_SRGB
| common::BU_COMP_FLAGS_KTX2_OUTPUT
| common::BU_COMP_FLAGS_KTX2_UASTC_ZSTD,
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: common::BU_COMP_FLAGS_THREADED
| common::BU_COMP_FLAGS_KTX2_OUTPUT
| common::BU_COMP_FLAGS_KTX2_UASTC_ZSTD,
low_level_uastc_rdo_or_dct_quality: 0.0,
}
}
pub const fn with_tex_type(mut self, tex_type: types::TextureViewDimension) -> Self {
self.flags_and_quality = self.flags_and_quality
& !(common::BU_COMP_FLAGS_TEXTURE_TYPE_MASK
<< common::BU_COMP_FLAGS_TEXTURE_TYPE_SHIFT);
self.flags_and_quality = self.flags_and_quality
| match tex_type {
types::TextureViewDimension::D2 => common::BU_COMP_FLAGS_TEXTURE_TYPE_2D,
types::TextureViewDimension::D2Array => common::BU_COMP_FLAGS_TEXTURE_TYPE_2D_ARRAY,
types::TextureViewDimension::Cube | types::TextureViewDimension::CubeArray => {
common::BU_COMP_FLAGS_TEXTURE_TYPE_CUBEMAP_ARRAY
}
};
self
}
pub const fn with_flags(mut self, flags: u64) -> Self {
self.flags_and_quality |= flags;
self
}
pub const fn with_removed_flags(mut self, flags: u64) -> Self {
self.flags_and_quality &= !flags;
self
}
}
impl BasisuEncoder {
pub fn new() -> Self {
if encoder_is_uninit() {
panic!("`basisu_encoder_init` must be called before create encoder");
}
Self {
params: unsafe { enc_sys::bu_new_comp_params() },
}
}
pub fn set_image(&mut self, image: SourceImage) -> Result<(), BasisuEncodeError> {
self.clear_image();
image.validate_image_data()?;
let ptr = image.data.as_ptr().addr() as u64;
match image.format {
SourceImageFormat::Rgba8 => unsafe {
for i in 0..image.size.depth_or_array_layers {
if enc_sys::bu_comp_params_set_image_rgba32(
self.params,
i,
ptr + (i * image.expected_per_layer_bytes()) as u64,
image.size.width,
image.size.height,
image.size.width * image.format.pixel_bytes(),
)
.is_err()
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
}
},
SourceImageFormat::Rgba32Float => unsafe {
for i in 0..image.size.depth_or_array_layers {
if enc_sys::bu_comp_params_set_image_float_rgba(
self.params,
i,
ptr + (i * image.expected_per_layer_bytes()) as u64,
image.size.width,
image.size.height,
image.size.width * image.format.pixel_bytes(),
)
.is_err()
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
}
},
}
Ok(())
}
pub fn clear_image(&mut self) {
assert!(unsafe { enc_sys::bu_comp_params_clear(self.params) }.is_ok());
}
pub fn set_image_slice(
&mut self,
index: u32,
image: SourceImage,
) -> Result<(), BasisuEncodeError> {
if image.size.depth_or_array_layers != 1 {
return Err(BasisuEncodeError::SetImageSliceOnlyAcceptsOneLayer);
}
image.validate_image_data()?;
let ptr = image.data.as_ptr().addr() as u64;
match image.format {
SourceImageFormat::Rgba8 => unsafe {
if enc_sys::bu_comp_params_set_image_rgba32(
self.params,
index,
ptr,
image.size.width,
image.size.height,
image.size.width * image.format.pixel_bytes(),
)
.is_err()
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
},
SourceImageFormat::Rgba32Float => unsafe {
if enc_sys::bu_comp_params_set_image_float_rgba(
self.params,
index,
ptr,
image.size.width,
image.size.height,
image.size.width * image.format.pixel_bytes(),
)
.is_err()
{
return Err(BasisuEncodeError::BuSetImageFailed);
}
},
}
Ok(())
}
pub fn compress(&mut self, params: BasisuEncoderParams) -> Result<Vec<u8>, BasisuEncodeError> {
unsafe {
if enc_sys::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,
)
.is_err()
{
return Err(BasisuEncodeError::BuCompressFailed);
}
let out_size = enc_sys::bu_comp_params_get_comp_data_size(self.params);
let out_ptr = enc_sys::bu_comp_params_get_comp_data_ofs(self.params);
let result = copy_basisu_memory_to_host(out_ptr, out_size);
Ok(result)
}
}
}
unsafe fn copy_basisu_memory_to_host(basisu_ptr: u64, count: u64) -> Vec<u8> {
let mut dst = alloc::vec![0u8; count as usize];
unsafe {
core::ptr::copy_nonoverlapping(basisu_ptr as *mut u8, dst.as_mut_ptr(), count as usize)
};
dst
}
impl Drop for BasisuEncoder {
fn drop(&mut self) {
assert!(unsafe { enc_sys::bu_delete_comp_params(self.params).is_ok() });
}
}
#[cfg(test)]
mod tests {
use crate::extra::{
BasisuEncodeError, BasisuEncoder, SourceImage, SourceImageFormat, basisu_encoder_init,
types,
};
#[test]
#[should_panic]
fn encoder_create_before_init() {
if super::encoder_is_init() {
panic!("Basisu is already initialized, panic to skip this test");
} else {
BasisuEncoder::new();
}
}
#[test]
fn invalid_image_data() {
basisu_encoder_init();
let mut encoder = BasisuEncoder::new();
assert_eq!(
encoder.set_image(SourceImage {
data: &[1],
format: SourceImageFormat::Rgba8,
size: types::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1
},
}),
Err(BasisuEncodeError::ImageUnmatchedDataAndSize {
image_size: types::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1
},
expected_len: 4,
data_len: 1
})
);
}
}