#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::similar_names,
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::many_single_char_names,
clippy::must_use_candidate,
clippy::unnecessary_cast,
clippy::cast_lossless,
clippy::needless_bool_assign,
clippy::needless_range_loop,
clippy::no_effect,
clippy::identity_op,
clippy::if_not_else
)]
use rayon::prelude::*;
use crate::engine::LightSrc;
pub(crate) const MAXZDIM: i32 = 256;
pub(crate) const ESTNORMRAD: i32 = 2;
pub(crate) const BITNUM: [i8; 32] = [
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
];
#[rustfmt::skip]
pub(crate) const BITSNUM: [i32; 32] = [
0, 1 - (2 << 16), 1 - (1 << 16), 2 - (3 << 16),
1, 2 - (2 << 16), 2 - (1 << 16), 3 - (3 << 16),
1 + (1 << 16), 2 - (1 << 16), 2, 3 - (2 << 16),
2 + (1 << 16), 3 - (1 << 16), 3, 4 - (2 << 16),
1 + (2 << 16), 2, 2 + (1 << 16), 3 - (1 << 16),
2 + (2 << 16), 3, 3 + (1 << 16), 4 - (1 << 16),
2 + (3 << 16), 3 + (1 << 16), 3 + (2 << 16), 4,
3 + (3 << 16), 4 + (1 << 16), 4 + (2 << 16), 5,
];
pub(crate) const fn xbsflor(k: usize) -> u32 {
if k >= 32 {
0
} else {
(-1i32 << k) as u32
}
}
pub(crate) const fn xbsceil(k: usize) -> u32 {
!xbsflor(k)
}
pub(crate) fn expandbit256(column: &[u8], bits: &mut [u32; 8]) {
let mut src_idx: usize = 0;
let mut dst_idx: usize = 0;
let mut bitpos: i32 = 32;
let mut word: u32 = 0;
let nbits: i32 = (bits.len() as i32) * 32;
let mut next_len: i32;
let mut delta: i32;
let mut go_to_v3 = false;
'outer: loop {
if go_to_v3 {
if src_idx + 3 >= column.len() {
break;
}
delta = i32::from(column[src_idx + 3]) - bitpos;
while delta >= 0 {
if dst_idx >= bits.len() {
break 'outer;
}
bits[dst_idx] = word;
dst_idx += 1;
word = u32::MAX;
bitpos += 32;
delta -= 32;
}
word &= xbsceil((delta + 32) as usize);
}
go_to_v3 = true;
if src_idx + 1 >= column.len() {
break;
}
delta = i32::from(column[src_idx + 1]) - bitpos;
while delta >= 0 {
if dst_idx >= bits.len() {
break 'outer;
}
bits[dst_idx] = word;
dst_idx += 1;
word = 0;
bitpos += 32;
delta -= 32;
}
word |= xbsflor((delta + 32) as usize);
next_len = i32::from(column[src_idx]);
if next_len == 0 {
break;
}
src_idx += (next_len as usize) * 4;
}
if bitpos <= nbits {
while dst_idx < bits.len() {
bits[dst_idx] = word;
dst_idx += 1;
word = u32::MAX;
}
}
}
#[allow(dead_code)] pub(crate) struct EstNormCache {
bits: Vec<[u32; 8]>,
origin_x: i32,
origin_y: i32,
width: usize,
#[allow(dead_code)]
height: usize,
fsqrecip: Vec<f32>,
vsid: i32,
}
fn build_fsqrecip() -> Vec<f32> {
const N: usize = 5860;
let mut t = vec![0.0_f32; N];
t[0] = 0.0;
t[1] = 1.0;
t[2] = (1.0_f32 / 2.0_f32.sqrt()) as f32;
t[3] = 1.0 / 3.0_f32.sqrt();
let mut i = 3usize;
let mut z = 4usize;
while z < N {
if z + 5 >= N {
break;
}
t[z] = t[z >> 1] * t[2];
t[z + 2] = t[(z + 2) >> 1] * t[2];
t[z + 4] = t[(z + 4) >> 1] * t[2];
t[z + 5] = t[i] * t[3];
i += 2;
let mut f = (t[z] + t[z + 2]) * 0.5_f32;
if z <= 22 {
f = (1.5 - 0.5 * ((z + 1) as f32) * f * f) * f;
}
t[z + 1] = (1.5 - 0.5 * ((z + 1) as f32) * f * f) * f;
let mut f = (t[z + 2] + t[z + 4]) * 0.5_f32;
if z <= 22 {
f = (1.5 - 0.5 * ((z + 3) as f32) * f * f) * f;
}
t[z + 3] = (1.5 - 0.5 * ((z + 3) as f32) * f * f) * f;
z += 6;
}
t
}
impl EstNormCache {
#[must_use]
pub fn build(
world_data: &[u8],
column_offsets: &[u32],
vsid: u32,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> Self {
let rad = ESTNORMRAD;
let pad_x0 = x0 - rad;
let pad_y0 = y0 - rad;
let pad_x1 = x1 + rad;
let pad_y1 = y1 + rad;
let width = (pad_x1 - pad_x0) as usize;
let height = (pad_y1 - pad_y0) as usize;
let mut bits = vec![[0u32; 8]; width * height];
let vsid_i = vsid as i32;
for yi in 0..height {
let y = pad_y0 + yi as i32;
for xi in 0..width {
let x = pad_x0 + xi as i32;
if (x | y) < 0 || x >= vsid_i || y >= vsid_i {
continue;
}
let col_idx = (y as u32) * vsid + (x as u32);
let off_start = column_offsets[col_idx as usize] as usize;
let column = &world_data[off_start..];
expandbit256(column, &mut bits[yi * width + xi]);
}
}
Self {
bits,
origin_x: pad_x0,
origin_y: pad_y0,
width,
height,
fsqrecip: build_fsqrecip(),
vsid: vsid_i,
}
}
#[inline]
fn extract_bits5(&self, xi: usize, yi: usize, z: i32) -> u32 {
let col = &self.bits[yi * self.width + xi];
if z >= MAXZDIM {
return 0x1f;
}
if z + 5 <= 0 {
return 0;
}
let z_bit = z;
let word_idx = z_bit.div_euclid(32);
let bit_off = z_bit.rem_euclid(32) as u32;
let lo = if (0..8).contains(&word_idx) {
col[word_idx as usize]
} else if word_idx < 0 {
0 } else {
u32::MAX };
let hi = if word_idx + 1 < 8 && word_idx >= -1 {
col[(word_idx + 1) as usize]
} else if word_idx + 1 < 0 {
0
} else {
u32::MAX
};
let combined = u64::from(lo) | (u64::from(hi) << 32);
((combined >> bit_off) & 0x1f) as u32
}
#[must_use]
pub fn estnorm(&self, x: i32, y: i32, z: i32) -> [f32; 3] {
let center_xi = (x - self.origin_x) as usize;
let center_yi = (y - self.origin_y) as usize;
let mut nx: i32 = 0;
let mut ny: i32 = 0;
let mut nz: i32 = 0;
let z_window = z - ESTNORMRAD;
for yy in -ESTNORMRAD..=ESTNORMRAD {
let yi = (center_yi as i32 + yy) as usize;
let b0 = self.extract_bits5(center_xi - 2, yi, z_window) as usize;
let b1 = self.extract_bits5(center_xi - 1, yi, z_window) as usize;
let b2 = self.extract_bits5(center_xi, yi, z_window) as usize;
let b3 = self.extract_bits5(center_xi + 1, yi, z_window) as usize;
let b4 = self.extract_bits5(center_xi + 2, yi, z_window) as usize;
nx += ((i32::from(BITNUM[b4]) - i32::from(BITNUM[b0])) << 1) + i32::from(BITNUM[b3])
- i32::from(BITNUM[b1]);
let j = BITSNUM[b0]
.wrapping_add(BITSNUM[b1])
.wrapping_add(BITSNUM[b2])
.wrapping_add(BITSNUM[b3])
.wrapping_add(BITSNUM[b4]);
nz = nz.wrapping_add(j);
let j_lo16 = (j as i16) as i32;
ny = ny.wrapping_add(j_lo16 * yy);
}
nz >>= 16;
let len_sq = (nx * nx + ny * ny + nz * nz) as usize;
let f = if len_sq < self.fsqrecip.len() {
self.fsqrecip[len_sq]
} else {
0.0
};
[(nx as f32) * f, (ny as f32) * f, (nz as f32) * f]
}
#[must_use]
#[allow(dead_code)] pub(crate) fn vsid(&self) -> i32 {
self.vsid
}
}
pub fn update_lighting(
world_data: &mut [u8],
column_offsets: &[u32],
vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
lightmode: u32,
lights: &[LightSrc],
) {
if lightmode == 0 {
return;
}
let vsid_i = vsid as i32;
let x0p = (x0 - ESTNORMRAD).max(0);
let y0p = (y0 - ESTNORMRAD).max(0);
let z0p = (z0 - ESTNORMRAD).max(0);
let x1p = (x1 + ESTNORMRAD).min(vsid_i);
let y1p = (y1 + ESTNORMRAD).min(vsid_i);
let z1p = (z1 + ESTNORMRAD).min(MAXZDIM);
if x0p >= x1p || y0p >= y1p || z0p >= z1p {
return;
}
let cache = EstNormCache::build(world_data, column_offsets, vsid, x0p, y0p, x1p, y1p);
let lightsub: Vec<f32> = lights.iter().map(|l| 1.0 / (l.r2.sqrt() * l.r2)).collect();
let n_cols = (vsid as usize) * (vsid as usize);
let column_extents: Vec<(usize, usize)> = (0..n_cols)
.map(|col_idx| {
let start = column_offsets[col_idx] as usize;
let end = start + roxlap_formats::vxl::slng(&world_data[start..]);
(start, end)
})
.collect();
let world_view = WorldDataMutView::new(world_data);
let row_body = |y: i32| {
for x in x0p..x1p {
let col_idx = (y as u32) * vsid + (x as u32);
let (off_start, off_end) = column_extents[col_idx as usize];
let column = unsafe { world_view.column_slice(off_start, off_end) };
shade_column(column, x, y, z0p, z1p, lightmode, lights, &lightsub, &cache);
}
};
(y0p..y1p).into_par_iter().for_each(row_body);
}
struct WorldDataMutView<'a> {
ptr: *mut u8,
len: usize,
_marker: std::marker::PhantomData<&'a mut [u8]>,
}
unsafe impl Send for WorldDataMutView<'_> {}
unsafe impl Sync for WorldDataMutView<'_> {}
impl<'a> WorldDataMutView<'a> {
fn new(buf: &'a mut [u8]) -> Self {
Self {
ptr: buf.as_mut_ptr(),
len: buf.len(),
_marker: std::marker::PhantomData,
}
}
unsafe fn column_slice(&self, off_start: usize, off_end: usize) -> &'a mut [u8] {
debug_assert!(off_start <= off_end, "column slice: start > end");
debug_assert!(off_end <= self.len, "column slice: end past buffer");
unsafe { std::slice::from_raw_parts_mut(self.ptr.add(off_start), off_end - off_start) }
}
}
#[allow(clippy::cast_lossless)]
fn shade_column(
column: &mut [u8],
x: i32,
y: i32,
z_lo: i32,
z_hi: i32,
lightmode: u32,
lights: &[LightSrc],
lightsub: &[f32],
cache: &EstNormCache,
) {
let mut v_off: usize = 0;
let mut cstat = false;
loop {
let (sz0, sz1, voxel_byte_offset_signed): (i32, i32, isize);
if !cstat {
if v_off + 2 >= column.len() {
break;
}
let v1 = i32::from(column[v_off + 1]);
let v2 = i32::from(column[v_off + 2]);
sz0 = v1;
sz1 = v2 + 1;
voxel_byte_offset_signed = (v_off as isize) + 7 - ((sz0 as isize) << 2);
cstat = true;
} else {
if v_off + 2 >= column.len() {
break;
}
let v0 = i32::from(column[v_off]);
let v1 = i32::from(column[v_off + 1]);
let v2 = i32::from(column[v_off + 2]);
let prev_offset = v2 - v1 - v0 + 2; if v0 == 0 {
break;
}
v_off += (v0 as usize) * 4;
if v_off + 3 >= column.len() {
break;
}
let v3 = i32::from(column[v_off + 3]);
sz1 = v3;
sz0 = prev_offset + sz1;
voxel_byte_offset_signed = (v_off as isize) + 3 - ((sz1 as isize) << 2);
cstat = false;
}
let lo = sz0.max(z_lo);
let hi = sz1.min(z_hi);
for z in lo..hi {
let normal = cache.estnorm(x, y, z);
let brightness = compute_brightness(x, y, z, normal, lightmode, lights, lightsub);
let byte_off = voxel_byte_offset_signed + ((z as isize) << 2);
if byte_off >= 0 && (byte_off as usize) < column.len() {
column[byte_off as usize] = brightness;
}
}
}
}
fn compute_brightness(
x: i32,
y: i32,
z: i32,
tp: [f32; 3],
lightmode: u32,
lights: &[LightSrc],
lightsub: &[f32],
) -> u8 {
if lightmode < 2 {
let f = (tp[1] * 0.5 + tp[2]) * 64.0 + 103.5;
clamp_to_byte(f)
} else {
let mut f = (tp[1] * 0.5 + tp[2]) * 16.0 + 47.5;
let xf = x as f32;
let yf = y as f32;
let zf = z as f32;
for (i, light) in lights.iter().enumerate() {
let fx = light.pos[0] - xf;
let fy = light.pos[1] - yf;
let fz = light.pos[2] - zf;
let h = tp[0] * fx + tp[1] * fy + tp[2] * fz;
if h >= 0.0 {
continue;
}
let g_sq = fx * fx + fy * fy + fz * fz;
if g_sq >= light.r2 {
continue;
}
let g = 1.0 / (g_sq * g_sq.sqrt()) - lightsub[i];
f -= g * h * light.sc;
}
clamp_to_byte(f)
}
}
#[inline]
fn clamp_to_byte(f: f32) -> u8 {
if f >= 255.0 {
255
} else if f <= 0.0 {
0
} else {
f as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xbsflor_xbsceil_known_values() {
assert_eq!(xbsflor(0), 0xffff_ffff);
assert_eq!(xbsflor(1), 0xffff_fffe);
assert_eq!(xbsflor(5), 0xffff_ffe0);
assert_eq!(xbsflor(31), 0x8000_0000);
assert_eq!(xbsflor(32), 0);
assert_eq!(xbsceil(0), 0);
assert_eq!(xbsceil(5), 0x1f);
assert_eq!(xbsceil(31), 0x7fff_ffff);
assert_eq!(xbsceil(32), 0xffff_ffff);
}
#[test]
fn single_slab_z10_to_14_sets_correct_bits() {
let mut col = vec![0u8, 10, 14, 0]; col.extend(vec![0u8; 5 * 4]);
let mut bits = [0u32; 8];
expandbit256(&col, &mut bits);
assert_eq!(
bits[0], 0xffff_fc00,
"word 0 want 0xffff_fc00 got 0x{:08x}",
bits[0]
);
for (i, w) in bits.iter().enumerate().skip(1) {
assert_eq!(*w, 0xffff_ffff, "word {i} want -1 got 0x{:08x}", *w);
}
}
#[test]
fn fsqrecip_matches_1_over_sqrt() {
let t = build_fsqrecip();
for k in 1..=100 {
let want = 1.0_f32 / (k as f32).sqrt();
let got = t[k];
let err = (got - want).abs();
assert!(err < 1e-3, "fsqrecip[{k}] = {got}, want {want}, err {err}");
}
for k in [500, 1000, 2000, 5000] {
let want = 1.0_f32 / (k as f32).sqrt();
let got = t[k];
let rel = (got / want - 1.0).abs();
assert!(
rel < 0.01,
"fsqrecip[{k}] = {got}, want {want}, rel-err {rel}"
);
}
}
#[test]
fn lightmode1_bakes_brightness_into_visible_voxels() {
let vsid: u32 = 4;
let mut col = vec![0u8, 20, 24, 0]; for _ in 20..=24 {
col.extend([0x10, 0x20, 0x30, 0xab]);
}
let col_len = col.len() as u32;
let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
data.extend_from_slice(&col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
assert_eq!(col_len as usize * (vsid * vsid) as usize, data.len());
update_lighting(
&mut data,
&offsets,
vsid,
1,
1,
0,
3,
3,
30, 1, &[],
);
let off1 = offsets[(1 * vsid + 1) as usize] as usize;
let alphas: Vec<u8> = (0..5).map(|i| data[off1 + 4 + i * 4 + 3]).collect();
for (i, &a) in alphas.iter().enumerate() {
assert_ne!(a, 0xab, "alpha[{i}] not rewritten");
}
for (i, &a) in alphas.iter().enumerate() {
assert!(
a > 100,
"alpha[{i}]={a} should be on the bright side for top-of-floor voxels"
);
}
}
#[test]
fn lightmode2_with_light_produces_per_column_variation() {
let vsid: u32 = 5;
let mut col = vec![0u8, 20, 24, 0];
for _ in 20..=24 {
col.extend([0x10, 0x20, 0x30, 0]);
}
let mut data = Vec::new();
let mut offsets = vec![0u32; (vsid * vsid + 1) as usize];
for i in 0..(vsid * vsid) {
offsets[i as usize] = data.len() as u32;
data.extend_from_slice(&col);
}
offsets[(vsid * vsid) as usize] = data.len() as u32;
let lights = [LightSrc {
pos: [4.0, 2.0, 20.0],
r2: 50.0 * 50.0,
sc: 64.0,
}];
update_lighting(&mut data, &offsets, vsid, 0, 0, 0, 5, 5, 30, 2, &lights);
let alpha_at = |x: u32, z_idx: usize| {
let off = offsets[(2 * vsid + x) as usize] as usize;
data[off + 4 + z_idx * 4 + 3]
};
let close = alpha_at(4, 0); let far = alpha_at(0, 0); assert!(
close >= far,
"column nearer the light should be ≥ as bright as the far one (close={close} far={far})"
);
}
#[test]
fn empty_column_all_air() {
let col = vec![0u8, 0, 0, 0]; let mut bits = [0u32; 8];
expandbit256(&col, &mut bits);
assert_eq!(
bits[0], 0xffff_ffff,
"empty column word 0 want all-1 got 0x{:08x}",
bits[0]
);
}
}