Skip to main content

steel_core/world/explosion/server/block_rays/
cache.rs

1//! Bounded block-ray caches for immutable explosion calculators.
2
3use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_utils::{BlockPos, BlockStateId, PackedBlockPos};
5
6use crate::world::BlockRegionBounds;
7use crate::world::explosion::{ExplosionBlockReader, ImmutableExplosionBlockCalculator};
8
9use super::super::ServerExplosion;
10use super::{
11    EqualHashOrder, ExplosionRay, ExplosionRayContext, ExplosionWorldBounds, JavaBlockPosSet,
12    RAY_POWER_DECAY, RAY_STEPS, ray_power_loss_from_resistance,
13};
14
15const BLOCK_CACHE_BITS: u32 = 9;
16const BLOCK_CACHE_SIZE: usize = 1 << BLOCK_CACHE_BITS;
17const BLOCK_CACHE_MASK: usize = BLOCK_CACHE_SIZE - 1;
18/// Bounds the temporary dense-cache allocations while covering standard radius-four explosions.
19const MAX_DENSE_BLOCK_CACHE_CELLS: usize = 8_192;
20const EMPTY_DENSE_BLOCK_CACHE_SLOT: u16 = u16::MAX;
21const DENSE_BLOCK_CACHE_HAS_RESISTANCE: u8 = 1;
22const DENSE_BLOCK_CACHE_AFFECTED: u8 = 1 << 1;
23const F64_INTEGER_MANTISSA_BIAS: f64 = 6_755_399_441_055_744.0;
24const LONG_HASH_PHI: u64 = 0x9e37_79b9_7f4a_7c15;
25
26#[derive(Clone, Copy)]
27struct ExplosionBlockCacheEntry {
28    tag: i64,
29    state: BlockStateId,
30    resistance: Option<f32>,
31    occupied: bool,
32    affected: bool,
33}
34
35impl ExplosionBlockCacheEntry {
36    const EMPTY: Self = Self {
37        tag: 0,
38        state: BlockStateId(0),
39        resistance: None,
40        occupied: false,
41        affected: false,
42    };
43}
44
45pub(super) struct ExplosionBlockCache {
46    entries: [ExplosionBlockCacheEntry; BLOCK_CACHE_SIZE],
47}
48
49#[derive(Clone, Copy)]
50struct DenseExplosionBlockCacheEntry {
51    resistance: f32,
52    state: BlockStateId,
53    flags: u8,
54}
55
56pub(super) struct DenseExplosionBlockCache {
57    min: BlockPos,
58    size_x: usize,
59    size_y: usize,
60    size_z: usize,
61    slots: Vec<u16>,
62    entries: Vec<DenseExplosionBlockCacheEntry>,
63}
64
65#[derive(Clone, Copy)]
66pub(super) enum ExplosionBlockCacheLookup<Miss> {
67    Hit(usize),
68    Miss(Miss),
69}
70
71pub(super) trait ExplosionRayBlockCache {
72    type Miss: Copy;
73
74    fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>>;
75    fn state(&self, entry_index: usize) -> BlockStateId;
76    fn resistance(&self, entry_index: usize) -> Option<f32>;
77    fn affected(&self, entry_index: usize) -> bool;
78    fn insert(&mut self, miss: Self::Miss, state: BlockStateId, resistance: Option<f32>) -> usize;
79    fn mark_affected(&mut self, entry_index: usize);
80}
81
82#[derive(Clone, Copy)]
83pub(super) struct ImmutableRayCachePolicy {
84    pub(super) resistance: bool,
85    pub(super) always_allows_block_explosion: bool,
86}
87
88impl Default for ExplosionBlockCache {
89    fn default() -> Self {
90        Self {
91            entries: [ExplosionBlockCacheEntry::EMPTY; BLOCK_CACHE_SIZE],
92        }
93    }
94}
95
96impl ExplosionRayBlockCache for ExplosionBlockCache {
97    type Miss = (usize, i64);
98
99    #[inline]
100    fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>> {
101        let tag = PackedBlockPos::from(pos).as_raw();
102        let cache_index = explosion_block_cache_index(tag);
103        let entry = self.entries[cache_index];
104        Some(if entry.occupied && entry.tag == tag {
105            ExplosionBlockCacheLookup::Hit(cache_index)
106        } else {
107            ExplosionBlockCacheLookup::Miss((cache_index, tag))
108        })
109    }
110
111    #[inline]
112    fn state(&self, entry_index: usize) -> BlockStateId {
113        self.entries[entry_index].state
114    }
115
116    #[inline]
117    fn resistance(&self, entry_index: usize) -> Option<f32> {
118        self.entries[entry_index].resistance
119    }
120
121    #[inline]
122    fn affected(&self, entry_index: usize) -> bool {
123        self.entries[entry_index].affected
124    }
125
126    #[inline]
127    fn insert(
128        &mut self,
129        (cache_index, tag): Self::Miss,
130        state: BlockStateId,
131        resistance: Option<f32>,
132    ) -> usize {
133        self.entries[cache_index] = ExplosionBlockCacheEntry {
134            tag,
135            state,
136            resistance,
137            occupied: true,
138            affected: false,
139        };
140        cache_index
141    }
142
143    #[inline]
144    fn mark_affected(&mut self, entry_index: usize) {
145        self.entries[entry_index].affected = true;
146    }
147}
148
149impl DenseExplosionBlockCache {
150    pub(super) fn try_new(bounds: BlockRegionBounds) -> Option<Self> {
151        let (min, max) = bounds.corners();
152        let size_x = inclusive_block_count(min.x(), max.x())?;
153        let size_y = inclusive_block_count(min.y(), max.y())?;
154        let size_z = inclusive_block_count(min.z(), max.z())?;
155        let volume = size_x.checked_mul(size_y)?.checked_mul(size_z)?;
156        if volume > MAX_DENSE_BLOCK_CACHE_CELLS
157            || volume >= usize::from(EMPTY_DENSE_BLOCK_CACHE_SLOT)
158        {
159            return None;
160        }
161
162        Some(Self {
163            min,
164            size_x,
165            size_y,
166            size_z,
167            slots: vec![EMPTY_DENSE_BLOCK_CACHE_SLOT; volume],
168            entries: Vec::with_capacity(volume),
169        })
170    }
171
172    #[inline]
173    pub(super) fn cell_index(&self, pos: BlockPos) -> Option<usize> {
174        let x = usize::try_from(i64::from(pos.x()) - i64::from(self.min.x())).ok()?;
175        let y = usize::try_from(i64::from(pos.y()) - i64::from(self.min.y())).ok()?;
176        let z = usize::try_from(i64::from(pos.z()) - i64::from(self.min.z())).ok()?;
177        if x >= self.size_x || y >= self.size_y || z >= self.size_z {
178            return None;
179        }
180        Some((y * self.size_z + z) * self.size_x + x)
181    }
182}
183
184impl ExplosionRayBlockCache for DenseExplosionBlockCache {
185    type Miss = usize;
186
187    #[inline]
188    fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>> {
189        let slot_index = self.cell_index(pos)?;
190        let entry_index = self.slots[slot_index];
191        Some(if entry_index == EMPTY_DENSE_BLOCK_CACHE_SLOT {
192            ExplosionBlockCacheLookup::Miss(slot_index)
193        } else {
194            ExplosionBlockCacheLookup::Hit(usize::from(entry_index))
195        })
196    }
197
198    #[inline]
199    fn state(&self, entry_index: usize) -> BlockStateId {
200        self.entries[entry_index].state
201    }
202
203    #[inline]
204    fn resistance(&self, entry_index: usize) -> Option<f32> {
205        let entry = self.entries[entry_index];
206        (entry.flags & DENSE_BLOCK_CACHE_HAS_RESISTANCE != 0).then_some(entry.resistance)
207    }
208
209    #[inline]
210    fn affected(&self, entry_index: usize) -> bool {
211        self.entries[entry_index].flags & DENSE_BLOCK_CACHE_AFFECTED != 0
212    }
213
214    #[inline]
215    fn insert(
216        &mut self,
217        slot_index: Self::Miss,
218        state: BlockStateId,
219        resistance: Option<f32>,
220    ) -> usize {
221        let entry_index = self.entries.len();
222        debug_assert!(entry_index < usize::from(EMPTY_DENSE_BLOCK_CACHE_SLOT));
223        let (resistance, flags) = resistance.map_or((0.0, 0), |resistance| {
224            (resistance, DENSE_BLOCK_CACHE_HAS_RESISTANCE)
225        });
226        self.entries.push(DenseExplosionBlockCacheEntry {
227            resistance,
228            state,
229            flags,
230        });
231        self.slots[slot_index] = entry_index as u16;
232        entry_index
233    }
234
235    #[inline]
236    fn mark_affected(&mut self, entry_index: usize) {
237        self.entries[entry_index].flags |= DENSE_BLOCK_CACHE_AFFECTED;
238    }
239}
240
241fn inclusive_block_count(min: i32, max: i32) -> Option<usize> {
242    usize::try_from(i64::from(max) - i64::from(min) + 1).ok()
243}
244
245impl ServerExplosion<'_> {
246    pub(super) fn calculate_immutable_ray_powers_with_reader<R: ExplosionBlockReader>(
247        &self,
248        powers: &[f32; super::RAY_COUNT],
249        calculator: &dyn ImmutableExplosionBlockCalculator,
250        reader: &R,
251        bounds: BlockRegionBounds,
252    ) -> Option<Vec<BlockPos>> {
253        let context = ExplosionRayContext {
254            center: self.center,
255            bounds: ExplosionWorldBounds::from_world(self.world),
256        };
257        let cache_policy = ImmutableRayCachePolicy {
258            resistance: calculator.can_cache_explosion_resistance(),
259            always_allows_block_explosion: calculator.always_allows_block_explosion(),
260        };
261        let use_bounded_floor = context.can_use_bounded_floor(bounds);
262
263        if cache_policy.resistance
264            && cache_policy.always_allows_block_explosion
265            && let Some(cache) = DenseExplosionBlockCache::try_new(bounds)
266        {
267            return Self::calculate_immutable_ray_powers_with_cache(
268                powers,
269                calculator,
270                reader,
271                context,
272                cache_policy,
273                cache,
274                use_bounded_floor,
275            );
276        }
277
278        Self::calculate_immutable_ray_powers_with_cache(
279            powers,
280            calculator,
281            reader,
282            context,
283            cache_policy,
284            ExplosionBlockCache::default(),
285            use_bounded_floor,
286        )
287    }
288
289    fn calculate_immutable_ray_powers_with_cache<
290        R: ExplosionBlockReader,
291        C: ExplosionRayBlockCache,
292    >(
293        powers: &[f32; super::RAY_COUNT],
294        calculator: &dyn ImmutableExplosionBlockCalculator,
295        reader: &R,
296        context: ExplosionRayContext,
297        cache_policy: ImmutableRayCachePolicy,
298        cache: C,
299        use_bounded_floor: bool,
300    ) -> Option<Vec<BlockPos>> {
301        if use_bounded_floor {
302            return Self::calculate_immutable_ray_powers_with_cache_mode::<R, C, true>(
303                powers,
304                calculator,
305                reader,
306                context,
307                cache_policy,
308                cache,
309            );
310        }
311        Self::calculate_immutable_ray_powers_with_cache_mode::<R, C, false>(
312            powers,
313            calculator,
314            reader,
315            context,
316            cache_policy,
317            cache,
318        )
319    }
320
321    fn calculate_immutable_ray_powers_with_cache_mode<
322        R: ExplosionBlockReader,
323        C: ExplosionRayBlockCache,
324        const USE_BOUNDED_FLOOR: bool,
325    >(
326        powers: &[f32; super::RAY_COUNT],
327        calculator: &dyn ImmutableExplosionBlockCalculator,
328        reader: &R,
329        context: ExplosionRayContext,
330        cache_policy: ImmutableRayCachePolicy,
331        mut cache: C,
332    ) -> Option<Vec<BlockPos>> {
333        let mut affected = JavaBlockPosSet::new(EqualHashOrder::Insertion);
334        for (&step, &initial_power) in RAY_STEPS.iter().zip(powers) {
335            if !visit_immutable_ray_positions_cached::<R, C, USE_BOUNDED_FLOOR>(
336                ExplosionRay {
337                    step,
338                    initial_power,
339                },
340                context,
341                reader,
342                calculator,
343                cache_policy,
344                &mut cache,
345                &mut affected,
346            ) {
347                return None;
348            }
349        }
350        Some(affected.into_iter().collect())
351    }
352}
353
354pub(super) fn visit_immutable_ray_positions_cached<
355    R: ExplosionBlockReader,
356    C: ExplosionRayBlockCache,
357    const USE_BOUNDED_FLOOR: bool,
358>(
359    ray: ExplosionRay,
360    context: ExplosionRayContext,
361    reader: &R,
362    calculator: &dyn ImmutableExplosionBlockCalculator,
363    cache_policy: ImmutableRayCachePolicy,
364    cache: &mut C,
365    affected: &mut JavaBlockPosSet,
366) -> bool {
367    let mut remaining_power = ray.initial_power;
368    let mut ray_pos = context.center;
369    let mut previous_cell: Option<(BlockPos, usize)> = None;
370    while remaining_power > 0.0 {
371        let pos = ray_block_pos::<USE_BOUNDED_FLOOR>(ray_pos);
372        if let Some((previous, cache_index)) = previous_cell
373            && previous == pos
374            && cache_policy.resistance
375            && cache_policy.always_allows_block_explosion
376            && cache.affected(cache_index)
377        {
378            if let Some(resistance) = cache.resistance(cache_index) {
379                remaining_power -= ray_power_loss_from_resistance(resistance);
380            }
381            ray_pos += ray.step;
382            remaining_power -= RAY_POWER_DECAY;
383            continue;
384        }
385
386        let lookup = match previous_cell {
387            Some((previous, cache_index)) if previous == pos => {
388                ExplosionBlockCacheLookup::Hit(cache_index)
389            }
390            _ => {
391                let Some(lookup) = cache.lookup(pos) else {
392                    return false;
393                };
394                lookup
395            }
396        };
397        let state = match lookup {
398            ExplosionBlockCacheLookup::Hit(cache_index) => cache.state(cache_index),
399            ExplosionBlockCacheLookup::Miss(_) => {
400                let Some(state) = reader.block_state(pos) else {
401                    return false;
402                };
403                state
404            }
405        };
406        if !context.bounds.contains(pos) {
407            break;
408        }
409
410        let resistance = match lookup {
411            ExplosionBlockCacheLookup::Hit(cache_index) if cache_policy.resistance => {
412                cache.resistance(cache_index)
413            }
414            _ => {
415                let fluid = state.get_fluid_state();
416                calculator.explosion_resistance(reader, pos, state, fluid)
417            }
418        };
419        let cache_index = match lookup {
420            ExplosionBlockCacheLookup::Hit(cache_index) => cache_index,
421            ExplosionBlockCacheLookup::Miss(miss) => cache.insert(
422                miss,
423                state,
424                if cache_policy.resistance {
425                    resistance
426                } else {
427                    None
428                },
429            ),
430        };
431        previous_cell = Some((pos, cache_index));
432
433        if let Some(resistance) = resistance {
434            remaining_power -= ray_power_loss_from_resistance(resistance);
435        }
436
437        if remaining_power > 0.0 {
438            let already_affected = cache.affected(cache_index);
439            let should_explode = cache_policy.always_allows_block_explosion
440                || calculator.should_explode(reader, pos, state, remaining_power);
441            if should_explode && !already_affected {
442                affected.insert(pos);
443                cache.mark_affected(cache_index);
444            }
445        }
446
447        ray_pos += ray.step;
448        remaining_power -= RAY_POWER_DECAY;
449    }
450    true
451}
452
453#[inline]
454fn ray_block_pos<const USE_BOUNDED_FLOOR: bool>(position: glam::DVec3) -> BlockPos {
455    if USE_BOUNDED_FLOOR {
456        BlockPos::new(
457            bounded_floor_to_i32(position.x),
458            bounded_floor_to_i32(position.y),
459            bounded_floor_to_i32(position.z),
460        )
461    } else {
462        BlockPos::from(position)
463    }
464}
465
466/// Floors a finite in-range coordinate. Adding `1.5 * 2^52` places the integer's two's-complement
467/// representation in the low 32 mantissa bits, avoiding a saturating float-to-int conversion.
468#[inline]
469pub(super) fn bounded_floor_to_i32(value: f64) -> i32 {
470    debug_assert!(
471        value.is_finite() && value >= f64::from(i32::MIN) && value < f64::from(i32::MAX) + 1.0
472    );
473    ((value.floor() + F64_INTEGER_MANTISSA_BIAS).to_bits() as u32) as i32
474}
475
476#[inline]
477const fn explosion_block_cache_index(tag: i64) -> usize {
478    let mut mixed = (tag as u64).wrapping_mul(LONG_HASH_PHI);
479    mixed ^= mixed >> 32;
480    mixed ^= mixed >> 16;
481    (mixed as usize) & BLOCK_CACHE_MASK
482}