#![allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::many_single_char_names,
clippy::missing_panics_doc,
clippy::verbose_bit_mask
)]
use roxlap_formats::vxl::Vxl;
pub const CHUNK_Z: u32 = 256;
pub const BEDROCK_RGB: u32 = 0x0040_4040;
#[derive(Debug, Clone)]
pub struct ChunkUpload {
pub vsid: u32,
pub occupancy: Vec<u32>,
pub color_offsets: Vec<u32>,
pub colors: Vec<u32>,
pub mips: Vec<MipUpload>,
}
pub const OCC_WORDS_PER_COLUMN: u32 = CHUNK_Z / 32;
pub const GPU_MAX_MIPS: u32 = 6;
#[must_use]
pub fn gpu_mip_count(vsid: u32) -> u32 {
let mut n = 1u32;
let mut v = vsid;
let mut z = CHUNK_Z as i32;
while v > 1 && z > 1 && n < GPU_MAX_MIPS {
v >>= 1;
z >>= 1;
n += 1;
}
n
}
#[must_use]
pub fn occ_words_per_column_for_mip(mip: u32) -> u32 {
(CHUNK_Z >> mip).div_ceil(32).max(1)
}
#[derive(Debug, Clone)]
pub struct MipUpload {
pub vsid: u32,
pub cz: u32,
pub occ_words_per_col: u32,
pub occupancy: Vec<u32>,
pub solid_occupancy: Vec<u32>,
pub color_offsets: Vec<u32>,
pub colors: Vec<u32>,
}
impl ChunkUpload {
#[must_use]
pub fn voxel_at(&self, x: u32, y: u32, z: u32) -> Option<u32> {
if x >= self.vsid || y >= self.vsid || z >= CHUNK_Z {
return None;
}
let col_idx = (x + y * self.vsid) as usize;
let col_word_base = col_idx * OCC_WORDS_PER_COLUMN as usize;
let z_word = (z / 32) as usize;
let z_bit = z & 31;
let bit = (self.occupancy[col_word_base + z_word] >> z_bit) & 1;
if bit == 0 {
return None;
}
let mut rank = 0u32;
for w in 0..z_word {
rank += self.occupancy[col_word_base + w].count_ones();
}
let mask = if z_bit == 0 {
0u32
} else {
(1u32 << z_bit) - 1
};
rank += (self.occupancy[col_word_base + z_word] & mask).count_ones();
let base = self.color_offsets[col_idx];
Some(self.colors[(base + rank) as usize])
}
}
#[must_use]
pub fn decompress_chunk(vxl: &Vxl) -> ChunkUpload {
let vsid = vxl.vsid;
let target = gpu_mip_count(vsid);
let owned;
let src: &Vxl = if vxl.mip_count() >= target {
vxl
} else {
let mut c = vxl.clone();
c.generate_mips(GPU_MAX_MIPS);
owned = c;
&owned
};
let mut mips: Vec<MipUpload> = Vec::with_capacity(target as usize);
let mut color_base = 0u32;
for m in 0..target {
let mip = decompress_mip(src, m, color_base);
color_base = *mip.color_offsets.last().expect("offsets non-empty");
mips.push(mip);
}
let m0 = &mips[0];
ChunkUpload {
vsid,
occupancy: m0.occupancy.clone(),
color_offsets: m0.color_offsets.clone(),
colors: m0.colors.clone(),
mips,
}
}
#[must_use]
fn decompress_mip(src: &Vxl, mip: u32, color_base: u32) -> MipUpload {
let vsid = src.vsid >> mip;
let cz = CHUNK_Z >> mip;
let occ_words_per_col = occ_words_per_column_for_mip(mip);
let vsid_usize = vsid as usize;
let n_cols = vsid_usize * vsid_usize;
let n_occ_words = n_cols * (occ_words_per_col as usize);
let mut occupancy = vec![0u32; n_occ_words];
let mut solid_occupancy = vec![0u32; n_occ_words];
let mut color_offsets = vec![0u32; n_cols + 1];
let mut colors: Vec<u32> = Vec::with_capacity(n_cols * 4);
for y in 0..vsid {
for x in 0..vsid {
let col_idx = (y as usize) * vsid_usize + (x as usize);
color_offsets[col_idx] =
color_base + u32::try_from(colors.len()).expect("colours fit in u32");
let slab = src.column_data_for_mip(mip, col_idx);
decompress_column(
slab,
x,
y,
vsid,
cz,
occ_words_per_col,
&mut occupancy,
&mut solid_occupancy,
&mut colors,
);
}
}
color_offsets[n_cols] = color_base + u32::try_from(colors.len()).expect("colours fit in u32");
MipUpload {
vsid,
cz,
occ_words_per_col,
occupancy,
solid_occupancy,
color_offsets,
colors,
}
}
#[allow(clippy::too_many_arguments)]
fn decompress_column(
slab: &[u8],
x: u32,
y: u32,
vsid: u32,
cz: u32,
occ_words_per_col: u32,
occupancy: &mut [u32],
solid_occupancy: &mut [u32],
colors: &mut Vec<u32>,
) {
let vsid_usize = vsid as usize;
let runs = expand_solid_runs(slab, cz);
let ranges = build_color_ranges(slab);
let col_idx = (x as usize) + (y as usize) * vsid_usize;
let col_word_base = col_idx * (occ_words_per_col as usize);
let mut have_surface = false;
let mut range_cursor = 0usize;
for (top, bot) in runs {
for z in top..bot {
while range_cursor < ranges.len() && z >= ranges[range_cursor].z_end {
range_cursor += 1;
}
let in_range = range_cursor < ranges.len() && z >= ranges[range_cursor].z_start;
let mut rgb = 0u32;
if in_range {
let off = ((z - ranges[range_cursor].z_start) as usize) * 4;
let bytes = &ranges[range_cursor].colours[off..off + 4];
rgb = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
}
let z_word = (z as usize) / 32;
let z_bit = (z as u32) & 31;
if in_range && (rgb & 0x00ff_ffff) != 0 {
occupancy[col_word_base + z_word] |= 1u32 << z_bit;
solid_occupancy[col_word_base + z_word] |= 1u32 << z_bit;
colors.push(rgb);
have_surface = true;
} else if !in_range && have_surface {
solid_occupancy[col_word_base + z_word] |= 1u32 << z_bit;
}
}
}
}
fn expand_solid_runs(slab: &[u8], cz: u32) -> Vec<(i32, i32)> {
let mut uind = [0i32; (CHUNK_Z as usize) + 2];
uind[0] = i32::from(slab[1]);
let mut i = 2usize;
let mut v = 0usize;
while slab[v] != 0 {
v += usize::from(slab[v]) * 4;
if slab[v + 3] >= slab[v + 1] {
continue;
}
uind[i - 1] = i32::from(slab[v + 3]);
uind[i] = i32::from(slab[v + 1]);
i += 2;
}
uind[i - 1] = cz as i32;
let n_runs = i / 2;
let mut runs = Vec::with_capacity(n_runs);
for k in 0..n_runs {
runs.push((uind[2 * k], uind[2 * k + 1]));
}
runs
}
struct ColorRange<'s> {
z_start: i32,
z_end: i32,
colours: &'s [u8],
}
fn build_color_ranges(slab: &[u8]) -> Vec<ColorRange<'_>> {
let mut ranges: Vec<ColorRange<'_>> = Vec::new();
let mut v = 0usize;
loop {
let z_start = i32::from(slab[v + 1]);
let z1c = i32::from(slab[v + 2]);
let z_end = z1c + 1;
let n_voxels = usize::try_from((z_end - z_start).max(0)).expect("non-negative");
let off = v + 4;
ranges.push(ColorRange {
z_start,
z_end,
colours: &slab[off..off + n_voxels * 4],
});
let nextptr = slab[v];
if nextptr == 0 {
break;
}
let prev_z1 = z_start;
let prev_z1c = z1c;
let prev_nextptr = i32::from(nextptr);
v += usize::from(nextptr) * 4;
let ze = i32::from(slab[v + 3]);
let ceil_z_start = ze + prev_z1c - prev_z1 - prev_nextptr + 2;
let ceil_z_end = ze;
let ceil_n = usize::try_from((ceil_z_end - ceil_z_start).max(0)).expect("non-negative");
let ceil_start = v - ceil_n * 4;
ranges.push(ColorRange {
z_start: ceil_z_start,
z_end: ceil_z_end,
colours: &slab[ceil_start..v],
});
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
use roxlap_formats::vxl::Vxl;
pub(crate) fn fixture_one_voxel_per_column() -> Vxl {
let vsid: u32 = 4;
let n_cols = (vsid as usize) * (vsid as usize);
let mut data = Vec::with_capacity(n_cols * 8);
let mut column_offset = Vec::with_capacity(n_cols + 1);
let bgra = [0x00, 0x80, 0xff, 0x80];
for _ in 0..n_cols {
column_offset.push(u32::try_from(data.len()).expect("offset fits"));
data.extend_from_slice(&[0, 100, 100, 0]); data.extend_from_slice(&bgra);
}
column_offset.push(u32::try_from(data.len()).expect("offset fits"));
Vxl {
vsid,
ipo: [0.0; 3],
ist: [1.0, 0.0, 0.0],
ihe: [0.0, 0.0, 1.0],
ifo: [0.0, 1.0, 0.0],
data: data.into_boxed_slice(),
column_offset: column_offset.into_boxed_slice(),
mip_base_offsets: Box::new([0, n_cols + 1]),
vbit: Box::new([]),
vbiti: 0,
}
}
#[test]
fn fixture_textured_voxel_carries_slab_colour() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
assert_eq!(chunk.voxel_at(1, 2, 100), Some(0x80ff_8000));
}
#[test]
fn fixture_air_above_textured_is_empty() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
for z in 0..100 {
assert_eq!(chunk.voxel_at(1, 2, z), None, "z={z} expected air");
}
}
#[test]
fn fixture_below_textured_is_air_after_bedrock_strip() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
for z in 101..CHUNK_Z {
assert_eq!(
chunk.voxel_at(1, 2, z),
None,
"z={z} expected air (bedrock stripped)"
);
}
}
#[test]
fn only_textured_voxels_are_marked_solid() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
let solid: u32 = chunk.occupancy.iter().map(|w| w.count_ones()).sum();
let expected = chunk.vsid * chunk.vsid;
assert_eq!(solid, expected);
}
#[test]
fn gpu_mip_count_matches_generate_mips() {
assert_eq!(gpu_mip_count(128), 6);
assert_eq!(gpu_mip_count(4), 3); assert_eq!(gpu_mip_count(2), 2); assert_eq!(gpu_mip_count(1), 1);
}
#[test]
fn occ_words_per_column_halves_with_z() {
assert_eq!(occ_words_per_column_for_mip(0), 8); assert_eq!(occ_words_per_column_for_mip(1), 4); assert_eq!(occ_words_per_column_for_mip(2), 2); assert_eq!(occ_words_per_column_for_mip(3), 1); assert_eq!(occ_words_per_column_for_mip(4), 1); assert_eq!(occ_words_per_column_for_mip(5), 1); }
#[test]
fn mip0_mirrors_legacy_top_level_fields() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
assert!(chunk.mips.len() >= 2, "fixture should build a mip ladder");
let m0 = &chunk.mips[0];
assert_eq!(m0.vsid, 4);
assert_eq!(m0.cz, CHUNK_Z);
assert_eq!(m0.occ_words_per_col, OCC_WORDS_PER_COLUMN);
assert_eq!(m0.occupancy, chunk.occupancy, "mip-0 occupancy == legacy");
assert_eq!(m0.colors, chunk.colors, "mip-0 colours == legacy");
assert_eq!(
m0.color_offsets, chunk.color_offsets,
"mip-0 offsets == legacy"
);
assert_eq!(m0.color_offsets[0], 0, "mip-0 starts at colour 0");
}
#[test]
fn each_mip_popcount_equals_color_count() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
assert_eq!(chunk.mips.len() as u32, gpu_mip_count(4));
for (m, mip) in chunk.mips.iter().enumerate() {
let solid: u32 = mip.occupancy.iter().map(|w| w.count_ones()).sum();
let in_mip = mip.colors.len() as u32;
assert_eq!(
solid, in_mip,
"mip {m}: {solid} solid bits but {in_mip} colours",
);
assert_eq!(mip.vsid, 4 >> m as u32);
assert_eq!(mip.cz, CHUNK_Z >> m as u32);
assert_eq!(
mip.occ_words_per_col,
occ_words_per_column_for_mip(m as u32)
);
assert_eq!(
mip.occupancy.len() as u32,
mip.vsid * mip.vsid * mip.occ_words_per_col,
);
assert_eq!(mip.color_offsets.len() as u32, mip.vsid * mip.vsid + 1);
}
}
#[test]
fn solid_occupancy_fills_bedrock_below_surface() {
let vxl = fixture_one_voxel_per_column(); let chunk = decompress_chunk(&vxl);
let m0 = &chunk.mips[0];
let base = 0usize; let bit = |buf: &[u32], z: u32| (buf[base + (z / 32) as usize] >> (z & 31)) & 1 == 1;
assert!(bit(&m0.occupancy, 100), "surface textured");
assert!(bit(&m0.solid_occupancy, 100), "surface solid");
assert!(!bit(&m0.occupancy, 150), "bedrock is not textured");
assert!(
bit(&m0.solid_occupancy, 150),
"bedrock below surface is solid"
);
assert!(bit(&m0.solid_occupancy, 255), "bedrock fills to the bottom");
assert!(
!bit(&m0.solid_occupancy, 50),
"air above the surface stays air"
);
let solid: u32 = m0.solid_occupancy.iter().map(|w| w.count_ones()).sum();
let tex: u32 = m0.occupancy.iter().map(|w| w.count_ones()).sum();
assert!(solid > tex, "solid (surface + bedrock) exceeds textured");
}
#[test]
fn color_offsets_are_absolute_and_monotonic_across_mips() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
let mut prev_end = 0u32;
for (m, mip) in chunk.mips.iter().enumerate() {
for w in mip.color_offsets.windows(2) {
assert!(w[0] <= w[1], "mip {m} offsets not monotonic");
}
assert_eq!(
mip.color_offsets[0], prev_end,
"mip {m} colour base not contiguous",
);
assert_eq!(
*mip.color_offsets.last().unwrap(),
prev_end + mip.colors.len() as u32,
);
prev_end = *mip.color_offsets.last().unwrap();
}
}
#[test]
fn color_offsets_partition_colours_correctly() {
let vxl = fixture_one_voxel_per_column();
let chunk = decompress_chunk(&vxl);
let n_cols = (chunk.vsid * chunk.vsid) as usize;
assert_eq!(chunk.color_offsets.len(), n_cols + 1);
assert_eq!(chunk.color_offsets[0], 0);
let per_col = 1;
for i in 0..=n_cols {
assert_eq!(
chunk.color_offsets[i],
u32::try_from(i).expect("test fixture small") * per_col,
);
}
assert_eq!(
*chunk.color_offsets.last().unwrap() as usize,
chunk.colors.len()
);
}
}