use std::sync::Arc;
use base64::Engine;
use crate::diagnostics::AssetError;
use crate::material::{Color, TextureColorSpace};
use super::AssetPath;
#[path = "texture_ktx2.rs"]
mod texture_ktx2;
#[cfg(feature = "ktx2")]
use texture_ktx2::validate_rgba8_payload_len;
use texture_ktx2::{decode_ktx2_basisu_rgba8, ktx2_descriptor_only_error};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextureDesc {
path: AssetPath,
color_space: TextureColorSpace,
sampler: TextureSamplerDesc,
source_format: TextureSourceFormat,
pixels: Option<Arc<TexturePixels>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TexturePixels {
levels: Vec<TextureMipLevel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TextureMipLevel {
width: u32,
height: u32,
rgba8: Vec<u8>,
}
impl TexturePixels {
fn single_level(width: u32, height: u32, rgba8: Vec<u8>) -> Self {
Self {
levels: vec![TextureMipLevel {
width,
height,
rgba8,
}],
}
}
#[cfg(feature = "ktx2")]
fn from_mip_levels(path: &AssetPath, levels: Vec<TextureMipLevel>) -> Result<Self, AssetError> {
if levels.is_empty() {
return Err(AssetError::Parse {
path: path.as_str().to_string(),
reason: "texture decode returned zero mip levels".to_string(),
});
}
for (index, level) in levels.iter().enumerate() {
validate_rgba8_payload_len(path, level.width, level.height, level.rgba8.len())
.map_err(|error| match error {
AssetError::Parse { path, reason } => AssetError::Parse {
path,
reason: format!("mip level {index}: {reason}"),
},
other => other,
})?;
}
Ok(Self { levels })
}
fn base_level(&self) -> Option<&TextureMipLevel> {
self.levels.first()
}
fn mip_metadata(&self) -> Vec<(u32, u32, usize)> {
self.levels
.iter()
.map(|level| (level.width, level.height, level.rgba8.len()))
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextureSourceFormat {
Png,
Jpeg,
Webp,
Ktx2Basisu,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextureFilter {
Nearest,
Linear,
NearestMipmapNearest,
LinearMipmapNearest,
NearestMipmapLinear,
LinearMipmapLinear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextureWrap {
ClampToEdge,
MirroredRepeat,
Repeat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextureSamplerDesc {
mag_filter: Option<TextureFilter>,
min_filter: Option<TextureFilter>,
wrap_s: TextureWrap,
wrap_t: TextureWrap,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct TextureCacheKey {
pub(crate) path: AssetPath,
pub(crate) color_space: TextureColorSpace,
pub(crate) sampler: TextureSamplerDesc,
pub(crate) source_format: TextureSourceFormat,
}
impl TextureDesc {
pub(crate) fn new_with_bytes(
path: AssetPath,
color_space: TextureColorSpace,
sampler: TextureSamplerDesc,
source_format: TextureSourceFormat,
source_bytes: Option<&[u8]>,
) -> Result<Self, AssetError> {
let pixels =
decode_texture_pixels(&path, color_space, source_format, source_bytes)?.map(Arc::new);
Ok(Self {
path,
color_space,
sampler,
source_format,
pixels,
})
}
pub fn path(&self) -> &AssetPath {
&self.path
}
pub const fn color_space(&self) -> TextureColorSpace {
self.color_space
}
pub const fn sampler(&self) -> TextureSamplerDesc {
self.sampler
}
pub const fn source_format(&self) -> TextureSourceFormat {
self.source_format
}
pub fn has_decoded_pixels(&self) -> bool {
self.pixels.is_some()
}
pub fn decoded_dimensions(&self) -> Option<(u32, u32)> {
self.pixels
.as_ref()
.and_then(|pixels| pixels.base_level())
.map(|level| (level.width, level.height))
}
pub fn decoded_rgba8(&self) -> Option<(u32, u32, &[u8])> {
self.pixels
.as_ref()
.and_then(|pixels| pixels.base_level())
.map(|level| (level.width, level.height, level.rgba8.as_slice()))
}
pub fn decoded_mip_metadata(&self) -> Option<Vec<(u32, u32, usize)>> {
self.pixels.as_ref().map(|pixels| pixels.mip_metadata())
}
pub(crate) fn decode_missing_pixels_from_bytes(
&mut self,
source_bytes: Option<&[u8]>,
) -> Result<(), AssetError> {
if self.pixels.is_none() {
self.pixels = decode_texture_pixels(
&self.path,
self.color_space,
self.source_format,
source_bytes,
)?
.map(Arc::new);
}
Ok(())
}
pub(crate) fn sample_bilinear(&self, uv: [f32; 2]) -> Option<Color> {
let pixels = self.pixels.as_ref()?;
let level = pixels.base_level()?;
let u = wrap_texture_coordinate(uv[0], self.sampler.wrap_s);
let v = wrap_texture_coordinate(uv[1], self.sampler.wrap_t);
let x = u * level.width.saturating_sub(1) as f32;
let y = v * level.height.saturating_sub(1) as f32;
let x0 = x.floor() as u32;
let y0 = y.floor() as u32;
let x1 = (x0 + 1).min(level.width.saturating_sub(1));
let y1 = (y0 + 1).min(level.height.saturating_sub(1));
let tx = x - x0 as f32;
let ty = y - y0 as f32;
let c00 = self.sample_pixel_color(level, x0, y0)?;
let c10 = self.sample_pixel_color(level, x1, y0)?;
let c01 = self.sample_pixel_color(level, x0, y1)?;
let c11 = self.sample_pixel_color(level, x1, y1)?;
Some(lerp_color(
lerp_color(c00, c10, tx),
lerp_color(c01, c11, tx),
ty,
))
}
fn sample_pixel_color(&self, level: &TextureMipLevel, x: u32, y: u32) -> Option<Color> {
let offset = ((y * level.width + x) as usize) * 4;
let rgba = level.rgba8.get(offset..offset + 4)?;
let alpha = f32::from(rgba[3]) / 255.0;
let mut color = match self.color_space {
TextureColorSpace::Srgb => Color::from_srgb_u8(rgba[0], rgba[1], rgba[2]),
TextureColorSpace::Linear => Color::from_linear_rgba(
f32::from(rgba[0]) / 255.0,
f32::from(rgba[1]) / 255.0,
f32::from(rgba[2]) / 255.0,
alpha,
),
};
color.a = alpha;
Some(color)
}
}
fn lerp_color(left: Color, right: Color, amount: f32) -> Color {
Color::from_linear_rgba(
left.r + (right.r - left.r) * amount,
left.g + (right.g - left.g) * amount,
left.b + (right.b - left.b) * amount,
left.a + (right.a - left.a) * amount,
)
}
impl TextureSamplerDesc {
pub const fn new(
mag_filter: Option<TextureFilter>,
min_filter: Option<TextureFilter>,
wrap_s: TextureWrap,
wrap_t: TextureWrap,
) -> Self {
Self {
mag_filter,
min_filter,
wrap_s,
wrap_t,
}
}
pub const fn mag_filter(self) -> Option<TextureFilter> {
self.mag_filter
}
pub const fn min_filter(self) -> Option<TextureFilter> {
self.min_filter
}
pub const fn wrap_s(self) -> TextureWrap {
self.wrap_s
}
pub const fn wrap_t(self) -> TextureWrap {
self.wrap_t
}
}
impl Default for TextureSamplerDesc {
fn default() -> Self {
Self {
mag_filter: None,
min_filter: None,
wrap_s: TextureWrap::Repeat,
wrap_t: TextureWrap::Repeat,
}
}
}
pub(crate) fn validate_texture_source_format(
path: &AssetPath,
) -> Result<TextureSourceFormat, AssetError> {
let lower = path.as_str().to_ascii_lowercase();
if lower.ends_with(".png") || lower.starts_with("data:image/png") {
return Ok(TextureSourceFormat::Png);
}
if lower.ends_with(".jpg") || lower.ends_with(".jpeg") || lower.starts_with("data:image/jpeg") {
return Ok(TextureSourceFormat::Jpeg);
}
if lower.ends_with(".webp") || lower.starts_with("data:image/webp") {
return Ok(TextureSourceFormat::Webp);
}
#[cfg(feature = "ktx2")]
{
if lower.ends_with(".ktx2") || lower.starts_with("data:image/ktx2") {
return Ok(TextureSourceFormat::Ktx2Basisu);
}
}
Err(AssetError::UnsupportedTextureFormat {
path: path.as_str().to_string(),
help: "supported texture format set is PNG, JPEG, and WebP; compressed texture decoders need an explicit feature/policy",
})
}
fn wrap_texture_coordinate(value: f32, wrap: TextureWrap) -> f32 {
if !value.is_finite() {
return 0.0;
}
match wrap {
TextureWrap::Repeat => value.rem_euclid(1.0),
TextureWrap::ClampToEdge => value.clamp(0.0, 1.0),
TextureWrap::MirroredRepeat => {
let wrapped = value.rem_euclid(2.0);
if wrapped <= 1.0 {
wrapped
} else {
2.0 - wrapped
}
}
}
}
fn decode_texture_pixels(
path: &AssetPath,
color_space: TextureColorSpace,
source_format: TextureSourceFormat,
source_bytes: Option<&[u8]>,
) -> Result<Option<TexturePixels>, AssetError> {
let bytes = if let Some(bytes) = source_bytes {
bytes.to_vec()
} else if path.as_str().starts_with("data:") {
decode_data_uri(path)?
} else {
return match source_format {
TextureSourceFormat::Ktx2Basisu => Err(ktx2_descriptor_only_error(path)),
TextureSourceFormat::Png | TextureSourceFormat::Jpeg | TextureSourceFormat::Webp => {
Ok(None)
}
};
};
match source_format {
TextureSourceFormat::Png => decode_png_rgba8(path, &bytes).map(Some),
TextureSourceFormat::Jpeg => decode_jpeg_rgba8(path, &bytes).map(Some),
TextureSourceFormat::Webp => Ok(None),
TextureSourceFormat::Ktx2Basisu => {
decode_ktx2_basisu_rgba8(path, &bytes, color_space).map(Some)
}
}
}
fn decode_data_uri(path: &AssetPath) -> Result<Vec<u8>, AssetError> {
let Some((_, encoded)) = path.as_str().split_once(";base64,") else {
return Err(AssetError::Parse {
path: path.as_str().to_string(),
reason: "only base64 texture data URIs are supported for embedded texture decoding"
.to_string(),
});
};
base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|error| AssetError::Parse {
path: path.as_str().to_string(),
reason: format!("invalid embedded texture base64: {error}"),
})
}
fn decode_png_rgba8(path: &AssetPath, bytes: &[u8]) -> Result<TexturePixels, AssetError> {
decode_via_image_crate(path, bytes, image::ImageFormat::Png)
}
fn decode_jpeg_rgba8(path: &AssetPath, bytes: &[u8]) -> Result<TexturePixels, AssetError> {
decode_via_image_crate(path, bytes, image::ImageFormat::Jpeg)
}
fn decode_via_image_crate(
path: &AssetPath,
bytes: &[u8],
format: image::ImageFormat,
) -> Result<TexturePixels, AssetError> {
let image =
image::load_from_memory_with_format(bytes, format).map_err(|error| AssetError::Parse {
path: path.as_str().to_string(),
reason: format!("invalid texture payload: {error}"),
})?;
let rgba = image.into_rgba8();
let width = rgba.width();
let height = rgba.height();
Ok(TexturePixels::single_level(width, height, rgba.into_raw()))
}