Pumpkin/pumpkin-world/src/lighting/engine.rs
2026-05-10 11:09:38 +02:00

573 lines
20 KiB
Rust

use crate::chunk_system::Chunk;
use crate::chunk_system::generation_cache::Cache;
use crate::generation::height_limit::HeightLimitView;
use crate::generation::proto_chunk::GenerationCache;
use crate::lighting::storage::{get_block_light, get_sky_light, set_block_light, set_sky_light};
use pumpkin_config::lighting::LightingEngineConfig;
use pumpkin_data::BlockDirection;
use pumpkin_util::HeightMap;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use std::collections::{HashMap, HashSet, VecDeque};
//use std::time::Instant;
type FastHashSet<K> = HashSet<K>;
type FastHashMap<K, V> = HashMap<K, V>;
/// Trait to unify Block and Sky light logic
pub trait LightProvider {
fn get_light(cache: &Cache, pos: BlockPos) -> u8;
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8);
fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8;
}
pub struct BlockLightProvider;
impl LightProvider for BlockLightProvider {
#[inline(always)]
fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
get_block_light(cache, pos)
}
#[inline(always)]
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
set_block_light(cache, pos, level);
}
#[inline(always)]
fn propagate_level(current_level: u8, opacity: u8, _dir: BlockDirection) -> u8 {
current_level.saturating_sub(opacity.max(1))
}
}
pub struct SkyLightProvider;
impl LightProvider for SkyLightProvider {
#[inline(always)]
fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
get_sky_light(cache, pos)
}
#[inline(always)]
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
set_sky_light(cache, pos, level);
}
#[inline(always)]
fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8 {
if current_level == 15 && dir == BlockDirection::Down && opacity == 0 {
return 15;
}
current_level.saturating_sub(opacity.max(1))
}
}
#[derive(Clone, Copy)]
pub struct PropagationEntry {
pos: BlockPos,
skip_direction: Option<BlockDirection>, // direction from which the light came, used to prevent back-propagation
}
pub struct LightPropagator<P: LightProvider> {
pub(crate) queue: VecDeque<PropagationEntry>,
pub(crate) visited: FastHashSet<BlockPos>,
pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>,
// Batched updates
pending_updates: FastHashMap<(i32, i32), Vec<(BlockPos, u8)>>,
shadow_cache: FastHashMap<BlockPos, u8>,
_marker: std::marker::PhantomData<P>,
}
impl<P: LightProvider> LightPropagator<P> {
#[must_use]
pub fn new() -> Self {
Self {
queue: VecDeque::with_capacity(4096),
visited: FastHashSet::default(),
decrease_queue: VecDeque::new(),
pending_updates: FastHashMap::default(),
shadow_cache: FastHashMap::default(),
_marker: std::marker::PhantomData,
}
}
pub fn clear(&mut self) {
self.queue.clear();
self.visited.clear();
self.decrease_queue.clear();
self.pending_updates.clear();
self.shadow_cache.clear();
}
/// Flushes pending updates to chunk storage
fn apply_updates(&mut self, cache: &mut Cache) {
if self.pending_updates.is_empty() {
return;
}
for (_, updates) in self.pending_updates.drain() {
for (pos, val) in updates {
P::set_light(cache, pos, val);
}
}
}
/// Core Propagation Logic (BFS)
pub fn propagate(&mut self, cache: &mut Cache) {
self.shadow_cache.clear();
// Cache metadata for bounds checking
let cache_x = cache.x;
let cache_z = cache.z;
let cache_size = cache.size;
while let Some(entry) = self.queue.pop_front() {
let pos = entry.pos;
// Check shadow cache first, fall back to storage
let current_light = self
.shadow_cache
.get(&pos)
.copied()
.unwrap_or_else(|| P::get_light(cache, pos));
if current_light <= 1 {
continue;
}
for dir in BlockDirection::all() {
// Skip the direction we came from (if specified)
if let Some(skip_dir) = entry.skip_direction
&& dir == skip_dir
{
continue;
}
let neighbor_pos = pos.offset(dir.to_offset());
// Skip if already visited (critical early-exit optimization)
if self.visited.contains(&neighbor_pos) {
continue;
}
// Skip neighbor if it's outside world bounds
let min_y = cache.bottom_y() as i32;
let max_y = min_y + cache.height() as i32;
if neighbor_pos.0.y < min_y || neighbor_pos.0.y >= max_y {
continue;
}
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position();
let rel_x = cx.x - cache_x;
let rel_z = cx.y - cache_z;
if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size {
continue;
}
// Get block opacity
let state = cache.get_block_state(&neighbor_pos.0);
let opacity = state.to_state().opacity;
let new_level = P::propagate_level(current_light, opacity, dir);
// Check shadow cache first, fall back to storage
let neighbor_light = self
.shadow_cache
.get(&neighbor_pos)
.copied()
.unwrap_or_else(|| P::get_light(cache, neighbor_pos));
if new_level > neighbor_light {
// Update shadow cache for this propagation cycle
self.shadow_cache.insert(neighbor_pos, new_level);
// Queue for batch write
let chunk_x = neighbor_pos.0.x >> 4;
let chunk_z = neighbor_pos.0.z >> 4;
self.pending_updates
.entry((chunk_x, chunk_z))
.or_default()
.push((neighbor_pos, new_level));
// Add to propagation queue if bright enough
if new_level > 1 && self.visited.insert(neighbor_pos) {
self.queue.push_back(PropagationEntry {
pos: neighbor_pos,
skip_direction: Some(dir.opposite()),
});
}
}
}
// Batch write every 64 chunks worth of updates
if self.pending_updates.len() > 64 {
self.apply_updates(cache);
}
}
// Final flush of any remaining updates
self.apply_updates(cache);
}
/// Handle light removal
pub fn process_decrease_queue(&mut self, cache: &mut Cache) {
{
// Cache metadata for bounds checking
let cache_x = cache.x;
let cache_z = cache.z;
let cache_size = cache.size;
while let Some((pos, old_val)) = self.decrease_queue.pop_front() {
for dir in BlockDirection::all() {
let neighbor_pos = pos.offset(dir.to_offset());
// Bounds check
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position();
let rel_x = cx.x - cache_x;
let rel_z = cx.y - cache_z;
if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size {
continue;
}
let neighbor_light = P::get_light(cache, neighbor_pos);
if neighbor_light == 0 {
continue;
}
let state = cache.get_block_state(&neighbor_pos.0);
let opacity = state.to_state().opacity;
let predicted = P::propagate_level(old_val, opacity, dir);
if neighbor_light == predicted || neighbor_light < old_val {
// Darken
P::set_light(cache, neighbor_pos, 0);
self.decrease_queue
.push_back((neighbor_pos, neighbor_light));
} else if neighbor_light >= old_val {
// Re-illuminate from this bright neighbor
self.queue.push_back(PropagationEntry {
pos: neighbor_pos,
skip_direction: None,
});
self.visited.insert(neighbor_pos);
}
}
}
}
self.propagate(cache); // Re-propagate from survivors
}
}
pub type BlockLightPropagator = LightPropagator<BlockLightProvider>;
pub type SkyLightPropagator = LightPropagator<SkyLightProvider>;
impl<P: LightProvider> Default for LightPropagator<P> {
fn default() -> Self {
Self::new()
}
}
impl BlockLightPropagator {
pub fn propagate_light(&mut self, cache: &mut Cache) {
self.clear();
//let scan_start = Instant::now();
let min_y = cache.bottom_y() as i32;
let max_y = min_y + cache.height() as i32;
let center_x = cache.x + (cache.size / 2);
let center_z = cache.z + (cache.size / 2);
let start_x = center_x * 16 - 1;
let start_z = center_z * 16 - 1;
let end_x = start_x + 18;
let end_z = start_z + 18;
for y in min_y..max_y {
for z in start_z..end_z {
for x in start_x..end_x {
let pos_vec = Vector3::new(x, y, z);
let state = cache.get_block_state(&pos_vec);
let emission = state.to_state().luminance;
if emission > 0 {
let pos = BlockPos(pos_vec);
set_block_light(cache, pos, emission);
if self.visited.insert(pos) {
// Block light propagates in all directions
self.queue.push_back(PropagationEntry {
pos,
skip_direction: None,
});
}
}
}
}
}
//let scan_elapsed = scan_start.elapsed();
//let propagate_start = Instant::now();
self.propagate(cache);
//let propagate_elapsed = propagate_start.elapsed();
//log::info!("Block light timing - Scan: {:?}, Propagate: {:?}", scan_elapsed, propagate_elapsed);
}
}
impl SkyLightPropagator {
pub fn convert_light(&mut self, cache: &mut Cache) {
self.clear();
//let scan_start = Instant::now();
let center_x = cache.x + (cache.size / 2);
let center_z = cache.z + (cache.size / 2);
let start_x = center_x * 16 - 1;
let start_z = center_z * 16 - 1;
let end_x = start_x + 18;
let end_z = start_z + 18;
let bottom_y = cache.bottom_y() as i32;
let max_y = bottom_y + cache.height() as i32;
// Pre-allocate with exact size needed
let capacity = ((end_x - start_x) * (end_z - start_z)) as usize;
let mut surface_heights =
FastHashMap::with_capacity_and_hasher(capacity, Default::default());
// Process in Z-outer, X-inner order for better cache locality
for z in start_z..end_z {
let chunk_z = z >> 4;
let local_z = (z & 15) as usize;
for x in start_x..end_x {
let chunk_x = x >> 4;
let local_x = (x & 15) as usize;
// Get heightmap (top solid blocks)
let top_y = cache.get_top_y(&HeightMap::WorldSurface, x, z);
surface_heights.insert((x, z), top_y);
// Get chunk index once per column
let rel_x = chunk_x - cache.x;
let rel_z = chunk_z - cache.z;
if rel_x < 0 || rel_x >= cache.size || rel_z < 0 || rel_z >= cache.size {
continue;
}
let chunk_idx = (rel_x * cache.size + rel_z) as usize;
// Fill everything above heightmap with 15 immediately
for y in (top_y + 1)..max_y {
let section_idx = ((y - bottom_y) >> 4) as usize;
let local_y = (y & 15) as usize;
// Direct array access - skip all function call overhead
match &mut cache.chunks[chunk_idx] {
Chunk::Proto(c) => {
if section_idx < c.light.sky_light.len() {
c.light.sky_light[section_idx].set(local_x, local_y, local_z, 15);
}
}
Chunk::Level(c) => {
let mut light_engine = c
.light_engine
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if section_idx < light_engine.sky_light.len() {
light_engine.sky_light[section_idx]
.set(local_x, local_y, local_z, 15);
}
}
}
}
// Only iterate from top_y DOWN - not from max_y
let mut light: i32 = 15;
for y in (bottom_y..=top_y).rev() {
let section_idx = ((y - bottom_y) >> 4) as usize;
let local_y = (y & 15) as usize;
// Get block opacity
let opacity = {
let pos_vec = Vector3::new(x, y, z);
let state = cache.get_block_state(&pos_vec);
state.to_state().opacity
} as i32;
// Reduce light by opacity
light = light.saturating_sub(opacity);
// Set the light value directly
let light_val = if light <= 0 { 0 } else { light as u8 };
match &mut cache.chunks[chunk_idx] {
Chunk::Proto(c) => {
if section_idx < c.light.sky_light.len() {
c.light.sky_light[section_idx]
.set(local_x, local_y, local_z, light_val);
}
}
Chunk::Level(c) => {
let mut light_engine = c
.light_engine
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if section_idx < light_engine.sky_light.len() {
light_engine.sky_light[section_idx]
.set(local_x, local_y, local_z, light_val);
}
}
}
// Early exit when light hits 0
if light <= 0 {
break;
}
}
}
}
// Enqueue horizontal propagation
for z in start_z..end_z {
for x in start_x..end_x {
let top_y = surface_heights[&(x, z)];
let north_top = surface_heights.get(&(x, z - 1)).copied().unwrap_or(top_y);
let south_top = surface_heights.get(&(x, z + 1)).copied().unwrap_or(top_y);
let west_top = surface_heights.get(&(x - 1, z)).copied().unwrap_or(top_y);
let east_top = surface_heights.get(&(x + 1, z)).copied().unwrap_or(top_y);
// We must check up to the highest neighbor to catch the "air sources"
let max_check_y = top_y
.max(north_top)
.max(south_top)
.max(west_top)
.max(east_top);
for y in (bottom_y..=max_check_y).rev() {
let pos = BlockPos(Vector3::new(x, y, z));
let light = get_sky_light(cache, pos);
// Use continue, or only break if we are safely below all possible side-light
if light == 0 {
if y <= top_y {
break;
}
continue;
}
let is_at_surface = y == top_y;
let below_neighbor =
y < north_top || y < south_top || y < west_top || y < east_top;
if (is_at_surface || below_neighbor) && self.visited.insert(pos) {
let skip_dir = (y >= top_y).then_some(BlockDirection::Up);
self.queue.push_back(PropagationEntry {
pos,
skip_direction: skip_dir,
});
}
}
}
}
//let propagate_start = Instant::now();
self.propagate(cache);
//let propagate_elapsed = propagate_start.elapsed();
//let scan_elapsed = scan_start.elapsed();
//log::info!("Sky light timing - Scan: {:?}, Propagate: {:?}", scan_elapsed, propagate_elapsed);
}
}
pub struct LightEngine {
block_light: BlockLightPropagator,
sky_light: SkyLightPropagator,
}
impl LightEngine {
#[must_use]
pub fn new() -> Self {
Self {
block_light: BlockLightPropagator::new(),
sky_light: SkyLightPropagator::new(),
}
}
pub fn initialize_light(&mut self, cache: &mut Cache, config: &LightingEngineConfig) {
if *config != LightingEngineConfig::Default {
return;
}
let should_skip = {
let center_chunk = cache.get_center_chunk();
center_chunk.stage >= crate::chunk_system::chunk_state::StagedChunkEnum::Lighting
};
if should_skip {
return;
}
self.sky_light.convert_light(cache);
self.block_light.propagate_light(cache);
self.block_light.clear();
self.sky_light.clear();
}
pub fn update_block_light(
&mut self,
cache: &mut Cache,
pos: BlockPos,
old_luminance: u8,
new_luminance: u8,
) {
// Decrease Logic
if old_luminance > new_luminance {
let current_light = get_block_light(cache, pos);
if current_light > 0 {
self.block_light
.decrease_queue
.push_back((pos, current_light));
set_block_light(cache, pos, 0);
}
}
// Increase Logic
if new_luminance > 0 {
set_block_light(cache, pos, new_luminance);
if self.block_light.visited.insert(pos) {
self.block_light.queue.push_back(PropagationEntry {
pos,
skip_direction: None,
});
}
}
}
pub fn run_light_updates(&mut self, cache: &mut Cache) {
if !self.block_light.decrease_queue.is_empty() {
self.block_light.process_decrease_queue(cache);
}
if !self.block_light.queue.is_empty() {
self.block_light.propagate(cache);
self.block_light.visited.clear();
}
if !self.sky_light.decrease_queue.is_empty() {
self.sky_light.process_decrease_queue(cache);
}
if !self.sky_light.queue.is_empty() {
self.sky_light.propagate(cache);
self.sky_light.visited.clear();
}
}
}
impl Default for LightEngine {
fn default() -> Self {
Self::new()
}
}