Part of #41: the comments of `benilla-assets`, Rust and WGSL, rewritten to the rule in `docs/METHOD.md`: what the code does and the reference fact behind it, in a line. 3,872 comment lines become 1,568. - The citations that were only a private record number go first, mechanically, each edit shown to be a pure removal. - The rewrite corrects the comments that stated wrong facts: the alpha-key cutoff now cites the reference's per-blend-mode table (`0x85ad20`), the texture-filter note names the subsystems that ignore the global setting, a test doc said an exhausted variation roll returns the last variation (it returns the first), and about twenty more found on the way (renamed or deleted items, a shader flag the doc misread, docs sitting on the wrong function). "Deviation:" now marks only a choice with a reason; an unbuilt behaviour says so plainly. - Verified: every file strips to identical code, and `cargo fmt --check` is clean. A second reader compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
543 lines
21 KiB
Rust
543 lines
21 KiB
Rust
//! Terrain tile texture arrays: the ground layers in one repeating, mipmapped `texture_2d_array`,
|
||
//! the per-chunk alpha and MCSH shadow maps in two more. As in the reference, each BLP's authored
|
||
//! mips go in verbatim, never regenerated by averaging gamma bytes, which darkens mid-tones; a BLP
|
||
//! below [`LAYER_TEX_SIZE`] fills the top mips by NEAREST upsample. One array has one format and
|
||
//! one mip-0 size, so a tile uploads its layers as S3TC blocks only when all of them can.
|
||
|
||
use benilla_formats::{BlpMipChain, BlpTexels};
|
||
use bevy::asset::RenderAssetUsages;
|
||
use bevy::image::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
|
||
use bevy::prelude::*;
|
||
use bevy::render::render_resource::{
|
||
Extent3d, TextureDataOrder, TextureDimension, TextureFormat, TextureViewDescriptor,
|
||
TextureViewDimension,
|
||
};
|
||
|
||
/// Every layer's edge in the tile array; Elwynn's ground BLPs are natively 256² with 8 mips.
|
||
pub(crate) const LAYER_TEX_SIZE: u32 = 256;
|
||
|
||
/// Per-layer mip count, the authored count at 256².
|
||
pub(crate) const LAYER_MIP_COUNT: u32 = 8;
|
||
|
||
/// Non-sRGB, so sampling does not linearize and shader math stays in gamma byte space, as in the
|
||
/// reference.
|
||
fn color_format() -> TextureFormat {
|
||
TextureFormat::Rgba8Unorm
|
||
}
|
||
|
||
/// The block form's albedo format, non-sRGB like [`color_format`]; an `Srgb` one would linearize.
|
||
fn block_color_format(texels: BlpTexels) -> TextureFormat {
|
||
match texels {
|
||
BlpTexels::Bc1 => TextureFormat::Bc1RgbaUnorm,
|
||
BlpTexels::Bc2 => TextureFormat::Bc2RgbaUnorm,
|
||
BlpTexels::Bc3 => TextureFormat::Bc3RgbaUnorm,
|
||
BlpTexels::Rgba8Unorm => TextureFormat::Rgba8Unorm,
|
||
}
|
||
}
|
||
|
||
/// One ground texture's mip chain at [`LAYER_TEX_SIZE`]², level 0 first.
|
||
pub(crate) struct LayerTexture {
|
||
pub(crate) chain: Vec<u8>,
|
||
pub(crate) mip_count: u32,
|
||
}
|
||
|
||
/// Pack a BLP's authored levels verbatim; a mip 0 below `target_size` is NEAREST-upsampled into the
|
||
/// top levels, and a chain that runs out repeats its last level.
|
||
pub(crate) fn chain_to_layer(blp: BlpMipChain, target_size: u32) -> LayerTexture {
|
||
// Decoded chains only: the resample and the matte stomp are per-texel writes, so the block
|
||
// passthrough is decided before this, in `pack_layers`.
|
||
debug_assert!(
|
||
blp.is_rgba8(),
|
||
"terrain layer packing indexes texels; it needs a decoded chain"
|
||
);
|
||
let mut chain = Vec::with_capacity(mip_chain_byte_size(target_size, LAYER_MIP_COUNT));
|
||
for level in 0..LAYER_MIP_COUNT {
|
||
let level_w = (target_size >> level).max(1);
|
||
let level_h = level_w; // tilesets are square in 1.12
|
||
if blp.width >= level_w {
|
||
// Source level `log2(width / level_w)`, clamped to the last authored level.
|
||
let ratio = (blp.width / level_w).max(1);
|
||
let src_level = (ratio.trailing_zeros() as usize).min(blp.mips.len() - 1);
|
||
let (sw, sh) = blp.mip_size(src_level as u32);
|
||
extend_nearest(&mut chain, &blp.mips[src_level], sw, sh, level_w, level_h);
|
||
} else {
|
||
// Authored smaller than this level: NEAREST-upsample mip 0.
|
||
extend_nearest(
|
||
&mut chain,
|
||
&blp.mips[0],
|
||
blp.width,
|
||
blp.height,
|
||
level_w,
|
||
level_h,
|
||
);
|
||
}
|
||
}
|
||
LayerTexture {
|
||
chain,
|
||
mip_count: LAYER_MIP_COUNT,
|
||
}
|
||
}
|
||
|
||
/// One ground texture as read, blocks intact, until the whole tile picks its array's form.
|
||
pub(crate) struct RawLayer {
|
||
pub(crate) chain: BlpMipChain,
|
||
/// No `_s` specular variant exists, so the sheen mask must read 0.
|
||
pub(crate) matte: bool,
|
||
}
|
||
|
||
/// A tile's layer array: LayerMajor bytes, their format and level count.
|
||
pub(crate) struct PackedLayers {
|
||
pub(crate) data: Vec<u8>,
|
||
pub(crate) format: TextureFormat,
|
||
pub(crate) mip_count: u32,
|
||
}
|
||
|
||
/// The block form the whole tile's layer array can upload in, or `None` to decode. Every layer
|
||
/// needs the same block form, natively [`LAYER_TEX_SIZE`]², a full [`LAYER_MIP_COUNT`] chain of
|
||
/// exactly the sizes the form implies (checked in release: a wrong byte count corrupts every later
|
||
/// layer instead of failing), and no matte stomp. [`crate::gpu_blp::bc_supported`] gates it all,
|
||
/// so `WOW_NO_BC=1`, a device without BC and every headless test take the decoded path.
|
||
fn passthrough_form(layers: &[RawLayer], bc_supported: bool) -> Option<BlpTexels> {
|
||
if !bc_supported {
|
||
return None;
|
||
}
|
||
let form = layers.first()?.chain.texels;
|
||
let uniform = form.is_block_compressed()
|
||
&& layers.iter().all(|l| {
|
||
l.chain.texels == form
|
||
&& !l.matte
|
||
&& l.chain.width == LAYER_TEX_SIZE
|
||
&& l.chain.height == LAYER_TEX_SIZE
|
||
&& l.chain.mips.len() >= LAYER_MIP_COUNT as usize
|
||
&& l.chain.mips[..LAYER_MIP_COUNT as usize]
|
||
.iter()
|
||
.enumerate()
|
||
.all(|(i, level)| level.len() == form_level_bytes(form, i as u32))
|
||
});
|
||
uniform.then_some(form)
|
||
}
|
||
|
||
/// Pack a tile's ground textures into its layer array, as blocks when [`passthrough_form`] allows.
|
||
/// `fallback` is layer 0, the solid placeholder for a texture that fails to load, synthesised in
|
||
/// the form the tile picked.
|
||
pub(crate) fn pack_layers(fallback: [u8; 4], layers: Vec<RawLayer>) -> PackedLayers {
|
||
pack(fallback, layers, crate::gpu_blp::bc_supported())
|
||
}
|
||
|
||
/// [`pack_layers`] with device support passed in, so a headless test can reach the block path.
|
||
fn pack(fallback: [u8; 4], layers: Vec<RawLayer>, bc_supported: bool) -> PackedLayers {
|
||
if let Some(form) = passthrough_form(&layers, bc_supported) {
|
||
let per_layer: usize = (0..LAYER_MIP_COUNT)
|
||
.map(|i| form_level_bytes(form, i))
|
||
.sum();
|
||
let mut data = Vec::with_capacity((layers.len() + 1) * per_layer);
|
||
data.extend_from_slice(&solid_block_chain(form, fallback));
|
||
for layer in &layers {
|
||
for level in layer.chain.mips.iter().take(LAYER_MIP_COUNT as usize) {
|
||
data.extend_from_slice(level);
|
||
}
|
||
}
|
||
debug_assert_eq!(data.len(), (layers.len() + 1) * per_layer);
|
||
return PackedLayers {
|
||
data,
|
||
format: block_color_format(form),
|
||
mip_count: LAYER_MIP_COUNT,
|
||
};
|
||
}
|
||
|
||
let fb = solid_layer_chain(fallback);
|
||
let mip_count = fb.mip_count;
|
||
let mut data = fb.chain;
|
||
for layer in layers {
|
||
let matte = layer.matte;
|
||
let mut packed = chain_to_layer(layer.chain.into_rgba8(), LAYER_TEX_SIZE);
|
||
if matte {
|
||
packed
|
||
.chain
|
||
.iter_mut()
|
||
.skip(3)
|
||
.step_by(4)
|
||
.for_each(|a| *a = 0);
|
||
}
|
||
data.extend_from_slice(&packed.chain);
|
||
}
|
||
PackedLayers {
|
||
data,
|
||
format: color_format(),
|
||
mip_count,
|
||
}
|
||
}
|
||
|
||
/// Bytes of one layer's array level in `form`, shared by the check, the packer and the fallback.
|
||
fn form_level_bytes(form: BlpTexels, level: u32) -> usize {
|
||
let w = (LAYER_TEX_SIZE >> level).max(1);
|
||
form.level_bytes(w, w)
|
||
}
|
||
|
||
/// One solid 4x4 block in `form`: a DXT1 colour half with `c0` the RGB565 colour, `c1` 0 and every
|
||
/// index 0, after `Bc2`/`Bc3`'s alpha. `Bc1` has no alpha, so its placeholder samples opaque and
|
||
/// takes the sheen; layer 0 is only sampled when an MTEX texture fails to read.
|
||
fn solid_block(form: BlpTexels, rgba: [u8; 4]) -> Vec<u8> {
|
||
let c = ((rgba[0] as u16 >> 3) << 11) | ((rgba[1] as u16 >> 2) << 5) | (rgba[2] as u16 >> 3);
|
||
let color = [c as u8, (c >> 8) as u8, 0, 0, 0, 0, 0, 0];
|
||
let mut block = match form {
|
||
// DXT3: eight bytes of explicit 4-bit alpha, two texels per byte.
|
||
BlpTexels::Bc2 => vec![(rgba[3] >> 4) * 0x11; 8],
|
||
// DXT5: `a0 == a1` and every 3-bit index 0, so every texel reads `a0` under either mode.
|
||
BlpTexels::Bc3 => vec![rgba[3], rgba[3], 0, 0, 0, 0, 0, 0],
|
||
BlpTexels::Bc1 | BlpTexels::Rgba8Unorm => Vec::new(),
|
||
};
|
||
block.extend_from_slice(&color);
|
||
block
|
||
}
|
||
|
||
/// The block form of [`solid_layer_chain`], for layer 0 of a passthrough array.
|
||
fn solid_block_chain(form: BlpTexels, rgba: [u8; 4]) -> Vec<u8> {
|
||
let block = solid_block(form, rgba);
|
||
let mut out = Vec::new();
|
||
for level in 0..LAYER_MIP_COUNT {
|
||
// A level below 4x4 still takes one whole block, as the authored chains do.
|
||
for _ in 0..(form_level_bytes(form, level) / block.len()) {
|
||
out.extend_from_slice(&block);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The decoded fallback layer 0: one colour at every level.
|
||
pub(crate) fn solid_layer_chain(rgba: [u8; 4]) -> LayerTexture {
|
||
let mut chain = Vec::with_capacity(mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT));
|
||
for level in 0..LAYER_MIP_COUNT {
|
||
let w = (LAYER_TEX_SIZE >> level).max(1);
|
||
for _ in 0..(w * w) {
|
||
chain.extend_from_slice(&rgba);
|
||
}
|
||
}
|
||
LayerTexture {
|
||
chain,
|
||
mip_count: LAYER_MIP_COUNT,
|
||
}
|
||
}
|
||
|
||
/// Total bytes for a mip pyramid of `mip_count` levels starting at `top²` RGBA8.
|
||
pub(crate) fn mip_chain_byte_size(top: u32, mip_count: u32) -> usize {
|
||
(0..mip_count)
|
||
.map(|i| {
|
||
let w = (top >> i).max(1);
|
||
(w * w * 4) as usize
|
||
})
|
||
.sum()
|
||
}
|
||
|
||
/// Append RGBA8 `src` resized from `sw × sh` to `dw × dh` by NEAREST, which never averages gamma
|
||
/// bytes.
|
||
pub(crate) fn extend_nearest(out: &mut Vec<u8>, src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) {
|
||
if sw == dw && sh == dh {
|
||
out.extend_from_slice(src);
|
||
return;
|
||
}
|
||
let Some(buf) = image::RgbaImage::from_raw(sw, sh, src.to_vec()) else {
|
||
out.extend(std::iter::repeat_n(0u8, (dw * dh * 4) as usize));
|
||
return;
|
||
};
|
||
let resized = image::imageops::resize(&buf, dw, dh, image::imageops::FilterType::Nearest);
|
||
out.extend_from_slice(resized.as_raw());
|
||
}
|
||
|
||
/// The tile's packed ground layers as a repeating, mipmapped `2d_array`.
|
||
pub(crate) fn layer_array_image(size: u32, layer_count: u32, packed: PackedLayers) -> Image {
|
||
let PackedLayers {
|
||
data,
|
||
format,
|
||
mip_count,
|
||
} = packed;
|
||
let mut image = array_image(size, layer_count, format);
|
||
image.data = Some(data);
|
||
image.texture_descriptor.mip_level_count = mip_count;
|
||
image.data_order = TextureDataOrder::LayerMajor;
|
||
// As in the reference, ground filtering comes from the process globals (`crate::tex_filter`):
|
||
// trilinear with anisotropy off on a fresh install (`hwDetect`).
|
||
let filter = crate::tex_filter::tex_filter();
|
||
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
|
||
address_mode_u: ImageAddressMode::Repeat,
|
||
address_mode_v: ImageAddressMode::Repeat,
|
||
mag_filter: ImageFilterMode::Linear,
|
||
min_filter: ImageFilterMode::Linear,
|
||
mipmap_filter: filter.mipmap_filter(),
|
||
anisotropy_clamp: filter.anisotropy_clamp(),
|
||
..default()
|
||
});
|
||
image
|
||
}
|
||
|
||
/// The per-chunk alpha maps as a linear `2d_array`: R, G and B are layers 1, 2 and 3's opacity.
|
||
pub(crate) fn alpha_array_image(size: u32, layer_count: u32, rgba: Vec<u8>) -> Image {
|
||
let mut image = array_image(size, layer_count, TextureFormat::Rgba8Unorm);
|
||
image.data = Some(rgba);
|
||
image
|
||
}
|
||
|
||
/// The MCSH baked shadow maps as a one-byte `2d_array`: `R` is shadowed, 0 lit.
|
||
pub(crate) fn shadow_array_image(size: u32, layer_count: u32, data: Vec<u8>) -> Image {
|
||
let mut image = array_image(size, layer_count, TextureFormat::R8Unorm);
|
||
image.data = Some(data);
|
||
image
|
||
}
|
||
|
||
/// A `2d_array` image with a `D2Array` view. `RENDER_WORLD` only: nothing reads a tile's arrays
|
||
/// main-side, so no main-world copy stays resident.
|
||
fn array_image(size: u32, layer_count: u32, format: TextureFormat) -> Image {
|
||
// `new_uninit`, not `new`, which would allocate a mip-0 dummy per tile; the caller sets `data`.
|
||
let mut image = Image::new_uninit(
|
||
Extent3d {
|
||
width: size,
|
||
height: size,
|
||
depth_or_array_layers: layer_count,
|
||
},
|
||
TextureDimension::D2,
|
||
format,
|
||
RenderAssetUsages::RENDER_WORLD,
|
||
);
|
||
image.texture_view_descriptor = Some(TextureViewDescriptor {
|
||
dimension: Some(TextureViewDimension::D2Array),
|
||
..default()
|
||
});
|
||
image
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// A synthetic BLP mip chain: `mips` authored levels of a solid grey fill at `size²`.
|
||
fn synthetic_chain(size: u32, mips: u32) -> BlpMipChain {
|
||
let levels = (0..mips)
|
||
.map(|i| {
|
||
let w = (size >> i).max(1);
|
||
vec![200u8; (w * w * 4) as usize]
|
||
})
|
||
.collect();
|
||
BlpMipChain {
|
||
texels: benilla_formats::BlpTexels::Rgba8Unorm,
|
||
width: size,
|
||
height: size,
|
||
mips: levels,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn chain_to_layer_packs_a_native_256_blp_to_the_full_chain() {
|
||
let layer = chain_to_layer(synthetic_chain(256, 8), LAYER_TEX_SIZE);
|
||
assert_eq!(layer.mip_count, LAYER_MIP_COUNT);
|
||
assert_eq!(
|
||
layer.chain.len(),
|
||
mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn chain_to_layer_upsamples_a_small_blp_without_darkening() {
|
||
// Averaging would dim a solid 200 below 200.
|
||
let layer = chain_to_layer(synthetic_chain(64, 7), 256);
|
||
assert_eq!(layer.chain.len(), mip_chain_byte_size(256, LAYER_MIP_COUNT));
|
||
assert!(
|
||
layer.chain.iter().all(|&b| b == 200),
|
||
"NEAREST upsample must preserve the authored gamma bytes (no averaging)"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn solid_layer_chain_is_one_colour_at_every_mip() {
|
||
let layer = solid_layer_chain([10, 20, 30, 40]);
|
||
assert_eq!(
|
||
layer.chain.len(),
|
||
mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT)
|
||
);
|
||
assert!(layer
|
||
.chain
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.all(|px| *px == [10, 20, 30, 40]));
|
||
}
|
||
|
||
#[test]
|
||
fn layer_array_image_has_the_expected_shape() {
|
||
let bytes = vec![0u8; 3 * mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT)];
|
||
let img = layer_array_image(
|
||
LAYER_TEX_SIZE,
|
||
3,
|
||
PackedLayers {
|
||
data: bytes,
|
||
format: color_format(),
|
||
mip_count: LAYER_MIP_COUNT,
|
||
},
|
||
);
|
||
assert_eq!(img.texture_descriptor.size.width, LAYER_TEX_SIZE);
|
||
assert_eq!(img.texture_descriptor.size.depth_or_array_layers, 3);
|
||
assert_eq!(img.texture_descriptor.mip_level_count, LAYER_MIP_COUNT);
|
||
}
|
||
|
||
/// Zeroed levels in `form`, padded to whole blocks as `benilla_blp::decode_native` delivers.
|
||
fn block_chain(form: BlpTexels, size: u32, mips: u32) -> BlpMipChain {
|
||
let levels = (0..mips)
|
||
.map(|i| {
|
||
let w = (size >> i).max(1);
|
||
vec![0u8; form.level_bytes(w, w)]
|
||
})
|
||
.collect();
|
||
BlpMipChain {
|
||
texels: form,
|
||
width: size,
|
||
height: size,
|
||
mips: levels,
|
||
}
|
||
}
|
||
|
||
fn raw(form: BlpTexels, size: u32, mips: u32, matte: bool) -> RawLayer {
|
||
RawLayer {
|
||
chain: block_chain(form, size, mips),
|
||
matte,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_tile_passes_its_blocks_through_only_when_every_clause_holds() {
|
||
let ok = || {
|
||
vec![
|
||
raw(BlpTexels::Bc2, 256, 8, false),
|
||
raw(BlpTexels::Bc2, 256, 8, false),
|
||
]
|
||
};
|
||
assert_eq!(passthrough_form(&ok(), true), Some(BlpTexels::Bc2));
|
||
|
||
// No device support.
|
||
assert_eq!(passthrough_form(&ok(), false), None);
|
||
// Mixed block forms, the common case: a `_s` variant is DXT3, a base without one DXT1.
|
||
let mixed = vec![
|
||
raw(BlpTexels::Bc1, 256, 8, false),
|
||
raw(BlpTexels::Bc2, 256, 8, false),
|
||
];
|
||
assert_eq!(passthrough_form(&mixed, true), None);
|
||
// Not natively 256².
|
||
let small = vec![raw(BlpTexels::Bc2, 128, 7, false)];
|
||
assert_eq!(passthrough_form(&small, true), None);
|
||
// A short chain.
|
||
let short = vec![raw(BlpTexels::Bc2, 256, 5, false)];
|
||
assert_eq!(passthrough_form(&short, true), None);
|
||
// A matte layer.
|
||
let matte = vec![raw(BlpTexels::Bc2, 256, 8, true)];
|
||
assert_eq!(passthrough_form(&matte, true), None);
|
||
// Raw1/Raw3 tilesets have no block form at all.
|
||
let raw1 = vec![RawLayer {
|
||
chain: synthetic_chain(256, 8),
|
||
matte: false,
|
||
}];
|
||
assert_eq!(passthrough_form(&raw1, true), None);
|
||
// The fallback-only tile.
|
||
assert_eq!(passthrough_form(&[], true), None);
|
||
}
|
||
|
||
#[test]
|
||
fn a_packed_block_array_is_exactly_the_size_its_format_implies() {
|
||
for form in [BlpTexels::Bc1, BlpTexels::Bc2, BlpTexels::Bc3] {
|
||
let layers = vec![raw(form, 256, 8, false), raw(form, 256, 8, false)];
|
||
// A level of the wrong size must not reach the packer.
|
||
let mut short = raw(form, 256, 8, false);
|
||
short.chain.mips[3].truncate(4);
|
||
assert_eq!(passthrough_form(&[short], true), None, "{form:?}");
|
||
let packed = pack([107, 133, 82, 0], layers, true);
|
||
assert_eq!(packed.format, block_color_format(form));
|
||
assert_eq!(packed.mip_count, LAYER_MIP_COUNT);
|
||
let per_layer: usize = (0..LAYER_MIP_COUNT)
|
||
.map(|i| form_level_bytes(form, i))
|
||
.sum();
|
||
// 3 layers: the synthesised fallback at index 0, then the two real ones.
|
||
assert_eq!(packed.data.len(), 3 * per_layer, "{form:?}");
|
||
// DXT1 is 0.5 bytes a texel, DXT3 and DXT5 are 1, decoded is 4; the sub-4x4 tail
|
||
// levels take a whole block, the 0.01 the ratio falls short.
|
||
let decoded = 3 * mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT);
|
||
let ratio = decoded as f64 / packed.data.len() as f64;
|
||
let want = if form == BlpTexels::Bc1 { 8.0 } else { 4.0 };
|
||
assert!(
|
||
(ratio - want).abs() < 0.01,
|
||
"{form:?} packs {ratio:.3}x smaller than the decoded array, expected {want}x"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_synthesised_fallback_block_decodes_to_its_colour() {
|
||
let rgba = [107u8, 133, 82, 0];
|
||
for form in [BlpTexels::Bc1, BlpTexels::Bc2, BlpTexels::Bc3] {
|
||
let chain = solid_block_chain(form, rgba);
|
||
let per_layer: usize = (0..LAYER_MIP_COUNT)
|
||
.map(|i| form_level_bytes(form, i))
|
||
.sum();
|
||
assert_eq!(chain.len(), per_layer, "{form:?}");
|
||
let top = BlpMipChain {
|
||
texels: form,
|
||
width: LAYER_TEX_SIZE,
|
||
height: LAYER_TEX_SIZE,
|
||
mips: vec![chain[..form.level_bytes(LAYER_TEX_SIZE, LAYER_TEX_SIZE)].to_vec()],
|
||
}
|
||
.into_rgba8()
|
||
.mips
|
||
.remove(0);
|
||
// Within one RGB565 step per channel.
|
||
for px in top.as_chunks::<4>().0 {
|
||
for (got, want) in px[..3].iter().zip(&rgba[..3]) {
|
||
assert!(
|
||
got.abs_diff(*want) <= 8,
|
||
"{form:?}: {got} is not {want} within an RGB565 step"
|
||
);
|
||
}
|
||
}
|
||
// `Bc1` has no alpha and samples opaque.
|
||
let alpha_ok = match form {
|
||
BlpTexels::Bc1 => true,
|
||
_ => top.as_chunks::<4>().0.iter().all(|px| px[3] == rgba[3]),
|
||
};
|
||
assert!(alpha_ok, "{form:?} must carry the fallback's alpha");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_decoded_path_still_stomps_a_matte_layers_alpha() {
|
||
let layers = vec![
|
||
RawLayer {
|
||
chain: synthetic_chain(256, 8),
|
||
matte: true,
|
||
},
|
||
RawLayer {
|
||
chain: synthetic_chain(256, 8),
|
||
matte: false,
|
||
},
|
||
];
|
||
let packed = pack([107, 133, 82, 0], layers, true);
|
||
assert_eq!(packed.format, color_format());
|
||
let per_layer = mip_chain_byte_size(LAYER_TEX_SIZE, LAYER_MIP_COUNT);
|
||
assert_eq!(packed.data.len(), 3 * per_layer);
|
||
let matte = &packed.data[per_layer..2 * per_layer];
|
||
assert!(
|
||
matte.iter().skip(3).step_by(4).all(|&a| a == 0),
|
||
"a layer with no `_s` variant must carry a zero sheen mask"
|
||
);
|
||
let sheened = &packed.data[2 * per_layer..];
|
||
assert!(
|
||
sheened.iter().skip(3).step_by(4).all(|&a| a == 200),
|
||
"a layer that found its `_s` variant keeps the authored mask"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn alpha_and_shadow_arrays_have_the_expected_shape_and_format() {
|
||
let alpha = alpha_array_image(64, 2, vec![0u8; 2 * 64 * 64 * 4]);
|
||
assert_eq!(alpha.texture_descriptor.size.depth_or_array_layers, 2);
|
||
assert_eq!(alpha.texture_descriptor.format, TextureFormat::Rgba8Unorm);
|
||
let shadow = shadow_array_image(64, 2, vec![0u8; 2 * 64 * 64]);
|
||
assert_eq!(shadow.texture_descriptor.size.depth_or_array_layers, 2);
|
||
assert_eq!(shadow.texture_descriptor.format, TextureFormat::R8Unorm);
|
||
}
|
||
}
|