#![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 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 struct EstNormCache {
bits: Vec<[u32; 8]>,
origin_x: i32,
origin_y: i32,
width: usize,
#[allow(dead_code)]
height: usize,
vsid: i32,
}
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 vsid_i = vsid as i32;
let reader = |x: i32, y: i32| -> Option<&[u8]> {
if (x | y) < 0 || x >= vsid_i || y >= vsid_i {
return None;
}
let col_idx = (y as u32) * vsid + (x as u32);
let off_start = column_offsets[col_idx as usize] as usize;
Some(&world_data[off_start..])
};
let mut cache = Self::build_with_reader(reader, x0, y0, x1, y1);
cache.vsid = vsid_i;
cache
}
#[must_use]
pub fn build_with_reader<'r>(
column_reader: impl Fn(i32, i32) -> Option<&'r [u8]>,
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];
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 let Some(column) = column_reader(x, y) {
expandbit256(column, &mut bits[yi * width + xi]);
}
}
}
Self {
bits,
origin_x: pad_x0,
origin_y: pad_y0,
width,
height,
vsid: 0,
}
}
#[inline]
fn solid(&self, xi: usize, yi: usize, z: i32) -> bool {
if z < 0 {
return false;
}
if z >= MAXZDIM {
return true;
}
let col = &self.bits[yi * self.width + xi];
let z = z as usize;
(col[z >> 5] >> (z & 31)) & 1 != 0
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn estnorm(&self, x: i32, y: i32, z: i32) -> [f32; 3] {
let cx = (x - self.origin_x) as i32;
let cy = (y - self.origin_y) as i32;
let mut nx = 0i32;
let mut ny = 0i32;
let mut nz = 0i32;
for dy in -ESTNORMRAD..=ESTNORMRAD {
let yi = (cy + dy) as usize;
for dx in -ESTNORMRAD..=ESTNORMRAD {
let xi = (cx + dx) as usize;
for dz in -ESTNORMRAD..=ESTNORMRAD {
if self.solid(xi, yi, z + dz) {
nx += dx;
ny += dy;
nz += dz;
}
}
}
}
let len_sq = nx * nx + ny * ny + nz * nz;
if len_sq == 0 {
return [0.0, 0.0, 0.0];
}
let inv = 1.0 / (len_sq as f32).sqrt();
[nx as f32 * inv, ny as f32 * inv, nz as f32 * inv]
}
#[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();
#[allow(clippy::cast_sign_loss)]
let region_w = (x1p - x0p) as usize;
#[allow(clippy::cast_sign_loss)]
let region_h = (y1p - y0p) as usize;
let mut column_extents: Vec<(usize, usize)> = Vec::with_capacity(region_w * region_h);
for yi in 0..region_h {
#[allow(clippy::cast_possible_wrap)]
let y = y0p + yi as i32;
for xi in 0..region_w {
#[allow(clippy::cast_possible_wrap)]
let x = x0p + xi as i32;
#[allow(clippy::cast_sign_loss)]
let col_idx = (y as u32) * vsid + (x as u32);
let start = column_offsets[col_idx as usize] as usize;
let end = start + roxlap_formats::vxl::slng(&world_data[start..]);
column_extents.push((start, end));
}
}
let world_view = WorldDataMutView::new(world_data);
let row_body = |y: i32| {
#[allow(clippy::cast_sign_loss)]
let yi = (y - y0p) as usize;
for x in x0p..x1p {
#[allow(clippy::cast_sign_loss)]
let xi = (x - x0p) as usize;
let (off_start, off_end) = column_extents[yi * region_w + xi];
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);
}
#[allow(clippy::too_many_arguments)]
pub fn update_lighting_chunk<'r>(
target_data: &mut [u8],
target_column_offsets: &[u32],
target_vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
column_reader: impl Fn(i32, i32) -> Option<&'r [u8]>,
lightmode: u32,
lights: &[LightSrc],
) {
if lightmode == 0 {
return;
}
let target_vsid_i = target_vsid as i32;
let z0p = (z0 - ESTNORMRAD).max(0);
let z1p = (z1 + ESTNORMRAD).min(MAXZDIM);
let wx0 = x0.max(0);
let wy0 = y0.max(0);
let wx1 = x1.min(target_vsid_i);
let wy1 = y1.min(target_vsid_i);
if wx0 >= wx1 || wy0 >= wy1 || z0p >= z1p {
return;
}
let cache = EstNormCache::build_with_reader(column_reader, x0, y0, x1, y1);
apply_lighting_with_cache(
target_data,
target_column_offsets,
target_vsid,
wx0,
wy0,
z0p,
wx1,
wy1,
z1p,
&cache,
lightmode,
lights,
);
}
#[allow(clippy::too_many_arguments)]
pub fn apply_lighting_with_cache(
target_data: &mut [u8],
target_column_offsets: &[u32],
target_vsid: u32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
cache: &EstNormCache,
lightmode: u32,
lights: &[LightSrc],
) {
if lightmode == 0 || x0 >= x1 || y0 >= y1 || z0 >= z1 {
return;
}
let lightsub: Vec<f32> = lights.iter().map(|l| 1.0 / (l.r2.sqrt() * l.r2)).collect();
let region_w = (x1 - x0) as usize;
let region_h = (y1 - y0) as usize;
let mut column_extents: Vec<(usize, usize)> = Vec::with_capacity(region_w * region_h);
for yi in 0..region_h {
let y = y0 + yi as i32;
for xi in 0..region_w {
let x = x0 + xi as i32;
let col_idx = (y as u32) * target_vsid + (x as u32);
let start = target_column_offsets[col_idx as usize] as usize;
let end = start + roxlap_formats::vxl::slng(&target_data[start..]);
column_extents.push((start, end));
}
}
let world_view = WorldDataMutView::new(target_data);
let row_body = |y: i32| {
let yi = (y - y0) as usize;
for x in x0..x1 {
let xi = (x - x0) as usize;
let (off_start, off_end) = column_extents[yi * region_w + xi];
let column = unsafe { world_view.column_slice(off_start, off_end) };
shade_column(column, x, y, z0, z1, lightmode, lights, &lightsub, cache);
}
};
(y0..y1).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 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]
);
}
}