use serde_json::{Value, json};
use crate::components::{ColorSpace, NormalConvention, TexRole};
use crate::json::JsonError;
use crate::texformat::TexFormat;
use crate::world::{Component, Entity};
use iridia_interface::TextureDim;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TextureShape {
Array { layers: u32 },
Cube { cubes: u32 },
Volume { depth: u32 },
}
impl TextureShape {
pub fn dimension(self) -> (TextureDim, bool) {
match self {
TextureShape::Array { .. } => (TextureDim::D2, true),
TextureShape::Cube { cubes } => (TextureDim::Cube, cubes > 1),
TextureShape::Volume { .. } => (TextureDim::D3, false),
}
}
pub fn layers_at(self, level: usize) -> u32 {
match self {
TextureShape::Array { layers } => layers,
TextureShape::Cube { cubes } => cubes.saturating_mul(6),
TextureShape::Volume { depth } => u32::try_from(level)
.ok()
.and_then(|l| depth.checked_shr(l))
.unwrap_or(0)
.max(1),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextureLayers {
pub shape: TextureShape,
pub width: u32,
pub height: u32,
pub format: TexFormat,
pub levels: Vec<Vec<Vec<u8>>>,
pub color_space: ColorSpace,
pub role: TexRole,
pub normal: Option<NormalConvention>,
}
impl TextureLayers {
pub fn layer(&self, level: usize, layer: usize) -> Option<&[u8]> {
self.levels.get(level)?.get(layer).map(Vec::as_slice)
}
}
impl Component for TextureLayers {
const NAME: &'static str = "core.texture_layers";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn payload_bytes(&self) -> u64 {
self.levels.iter().flatten().map(|l| l.len() as u64).sum()
}
fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
if self.width == 0 || self.height == 0 {
return Err(format!("{} x {} pixels", self.width, self.height));
}
let (count, name, extent) = match self.shape {
TextureShape::Array { layers } => (layers, "layers", 1),
TextureShape::Cube { cubes } => (cubes, "cubes", 1),
TextureShape::Volume { depth } => (depth, "depth", depth),
};
if count == 0 {
return Err(format!("0 {name}"));
}
let most = self.width.max(self.height).max(extent).ilog2() + 1;
if self.levels.is_empty() || self.levels.len() > most as usize {
return Err(format!(
"{} levels for {} x {} {:?} (1 to {most})",
self.levels.len(),
self.width,
self.height,
self.shape
));
}
for (i, level) in self.levels.iter().enumerate() {
let shift = u32::try_from(i).unwrap_or(u32::MAX);
let (w, h) = (
self.width.checked_shr(shift).unwrap_or(0).max(1),
self.height.checked_shr(shift).unwrap_or(0).max(1),
);
let layers = self.shape.layers_at(i);
if level.len() != layers as usize {
return Err(format!(
"level {i} has {} layers, a {:?} has {layers} there",
level.len(),
self.shape
));
}
let Some(size) = self.format.checked_level_size(w, h) else {
return Err(format!("level {i}: {w} x {h} doesn't fit in memory"));
};
if let Some((l, bytes)) = level.iter().enumerate().find(|(_, b)| b.len() != size) {
return Err(format!(
"level {i} layer {l} ({w} x {h} {:?}): {} bytes, not {size}",
self.format,
bytes.len()
));
}
}
Ok(())
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
Ok(Some(json!({
"shape": self.shape,
"width": self.width,
"height": self.height,
"format": format!("{:?}", self.format),
"levels": self.levels.len(),
"color_space": self.color_space,
"role": self.role,
"normal": self.normal,
})))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "hand-worked fixtures")]
mod tests {
use super::*;
use crate::World;
fn layers(shape: TextureShape, w: u32, h: u32, levels: usize) -> TextureLayers {
let format = TexFormat::Rgba8Unorm;
TextureLayers {
shape,
width: w,
height: h,
format,
levels: (0..levels)
.map(|i| {
let (lw, lh) = ((w >> i).max(1), (h >> i).max(1));
(0..shape.layers_at(i))
.map(|_| vec![0u8; format.level_size(lw, lh)])
.collect()
})
.collect(),
color_space: ColorSpace::Linear,
role: TexRole::Other,
normal: None,
}
}
fn verify(t: &TextureLayers) -> Result<(), String> {
let mut w = World::new();
let e = w.spawn();
t.verify(e, &w)
}
#[test]
fn a_shape_says_how_many_images_each_level_holds() {
assert_eq!(TextureShape::Array { layers: 5 }.layers_at(3), 5);
assert_eq!(TextureShape::Cube { cubes: 1 }.layers_at(2), 6);
assert_eq!(TextureShape::Cube { cubes: 3 }.layers_at(0), 18);
let v = TextureShape::Volume { depth: 33 };
assert_eq!([0, 1, 2, 5, 6, 40].map(|l| v.layers_at(l)), [33, 16, 8, 1, 1, 1]);
}
#[test]
fn a_shape_is_the_dimension_a_shader_resource_reads() {
assert_eq!(
TextureShape::Array { layers: 1 }.dimension(),
(TextureDim::D2, true)
);
assert_eq!(
TextureShape::Cube { cubes: 1 }.dimension(),
(TextureDim::Cube, false)
);
assert_eq!(
TextureShape::Cube { cubes: 2 }.dimension(),
(TextureDim::Cube, true)
);
assert_eq!(
TextureShape::Volume { depth: 8 }.dimension(),
(TextureDim::D3, false)
);
}
#[test]
fn each_level_holds_the_shapes_layers_of_the_formats_size() {
assert_eq!(
verify(&layers(TextureShape::Array { layers: 3 }, 8, 4, 4)),
Ok(())
);
assert_eq!(verify(&layers(TextureShape::Cube { cubes: 1 }, 4, 4, 3)), Ok(()));
assert_eq!(
verify(&layers(TextureShape::Volume { depth: 5 }, 4, 4, 3)),
Ok(())
);
assert_eq!(
verify(&layers(TextureShape::Volume { depth: 8 }, 2, 2, 4)),
Ok(())
);
let mut t = layers(TextureShape::Array { layers: 3 }, 8, 4, 2);
t.levels[1].pop();
assert!(verify(&t).unwrap_err().contains("level 1 has 2 layers"));
let mut t = layers(TextureShape::Array { layers: 2 }, 8, 4, 2);
t.levels[0][1].push(0);
assert!(verify(&t).unwrap_err().contains("level 0 layer 1"));
let mut v = layers(TextureShape::Volume { depth: 4 }, 4, 4, 2);
v.levels[1].push(vec![0; 16]);
assert!(verify(&v).unwrap_err().contains("level 1 has 3 layers"));
}
#[test]
fn a_texture_with_nothing_in_it_or_too_many_levels_is_refused() {
assert!(
verify(&layers(TextureShape::Array { layers: 0 }, 4, 4, 1))
.unwrap_err()
.contains("0 layers")
);
assert!(
verify(&layers(TextureShape::Cube { cubes: 0 }, 4, 4, 1))
.unwrap_err()
.contains("0 cubes")
);
assert!(
verify(&layers(TextureShape::Array { layers: 1 }, 0, 4, 1))
.unwrap_err()
.contains("0 x 4")
);
let mut t = layers(TextureShape::Array { layers: 1 }, 4, 4, 3);
t.levels.push(vec![vec![0; 4]]);
assert!(verify(&t).unwrap_err().contains("4 levels for 4 x 4"));
t.levels.clear();
assert!(verify(&t).unwrap_err().contains("0 levels"));
}
#[test]
fn a_layer_is_found_by_level_and_index() {
let mut t = layers(TextureShape::Array { layers: 2 }, 2, 2, 1);
t.levels[0][1][0] = 7;
assert_eq!(t.layer(0, 1).map(|l| l[0]), Some(7));
assert_eq!(t.layer(0, 2), None);
assert_eq!(t.layer(1, 0), None);
assert_eq!(t.payload_bytes(), 32);
}
}