Skip to main content

steel_core/chunk/light/
work_gate.rs

1//! Scheduling gate for light work cache windows.
2
3use std::sync::Arc;
4
5use steel_utils::{ChunkPos, locks::SyncMutex};
6use tokio::sync::Notify;
7
8use super::LIGHT_CACHE_RADIUS;
9
10const LIGHT_WORK_CENTER_EXCLUSION_RADIUS: i32 = LIGHT_CACHE_RADIUS * 2;
11
12/// Shared gate used to exclude overlapping light-engine cache windows.
13///
14/// Light worksets can write light data across a full 5x5 cache window. Two
15/// worksets whose windows overlap must therefore not run at the same time:
16/// chunk-light status work must publish `ChunkStatus::Light` before overlapping
17/// work builds its light cache, and queued light updates must not interleave
18/// with either operation.
19#[derive(Debug)]
20pub(crate) struct LightWorkWindowGate {
21    active_centers: SyncMutex<Vec<ChunkPos>>,
22    released: Notify,
23}
24
25/// Reservation for one light-engine cache window.
26#[derive(Debug)]
27pub(crate) struct LightWorkWindowReservation {
28    gate: Arc<LightWorkWindowGate>,
29    center: ChunkPos,
30}
31
32impl LightWorkWindowGate {
33    /// Creates an empty light work gate.
34    #[must_use]
35    pub(crate) fn new() -> Self {
36        Self {
37            active_centers: SyncMutex::new(Vec::new()),
38            released: Notify::new(),
39        }
40    }
41
42    /// Reserves the radius-2 light cache window centered on `center`.
43    pub(crate) async fn reserve_centered(
44        self: &Arc<Self>,
45        center: ChunkPos,
46    ) -> LightWorkWindowReservation {
47        loop {
48            let released = self.released.notified();
49            if let Some(reservation) = self.try_reserve_centered(center) {
50                return reservation;
51            }
52            released.await;
53        }
54    }
55
56    /// Attempts to reserve the radius-2 light cache window centered on `center`.
57    pub(crate) fn try_reserve_centered(
58        self: &Arc<Self>,
59        center: ChunkPos,
60    ) -> Option<LightWorkWindowReservation> {
61        let mut active_centers = self.active_centers.lock();
62        if active_centers
63            .iter()
64            .any(|&active| Self::windows_overlap(center, active))
65        {
66            return None;
67        }
68
69        active_centers.push(center);
70        Some(LightWorkWindowReservation {
71            gate: Arc::clone(self),
72            center,
73        })
74    }
75
76    const fn windows_overlap(left: ChunkPos, right: ChunkPos) -> bool {
77        let dx = left.0.x.abs_diff(right.0.x);
78        let dz = left.0.y.abs_diff(right.0.y);
79        dx <= LIGHT_WORK_CENTER_EXCLUSION_RADIUS as u32
80            && dz <= LIGHT_WORK_CENTER_EXCLUSION_RADIUS as u32
81    }
82}
83
84impl Default for LightWorkWindowGate {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl Drop for LightWorkWindowReservation {
91    fn drop(&mut self) {
92        let removed = {
93            let mut active_centers = self.gate.active_centers.lock();
94            if let Some(index) = active_centers
95                .iter()
96                .position(|&center| center == self.center)
97            {
98                active_centers.swap_remove(index);
99                true
100            } else {
101                false
102            }
103        };
104
105        debug_assert!(removed, "light work reservation missing active center");
106        if removed {
107            self.gate.released.notify_waiters();
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn overlapping_windows_cannot_be_reserved_together() {
118        let gate = Arc::new(LightWorkWindowGate::new());
119        let _first = gate
120            .try_reserve_centered(ChunkPos::new(0, 0))
121            .expect("first light window should reserve");
122
123        assert!(gate.try_reserve_centered(ChunkPos::new(1, 0)).is_none());
124        assert!(gate.try_reserve_centered(ChunkPos::new(4, 4)).is_none());
125    }
126
127    #[test]
128    fn non_overlapping_windows_can_be_reserved_together() {
129        let gate = Arc::new(LightWorkWindowGate::new());
130        let _first = gate
131            .try_reserve_centered(ChunkPos::new(0, 0))
132            .expect("first light window should reserve");
133
134        assert!(gate.try_reserve_centered(ChunkPos::new(5, 0)).is_some());
135        assert!(gate.try_reserve_centered(ChunkPos::new(0, 5)).is_some());
136    }
137
138    #[test]
139    fn dropping_reservation_releases_window() {
140        let gate = Arc::new(LightWorkWindowGate::new());
141        let first = gate
142            .try_reserve_centered(ChunkPos::new(0, 0))
143            .expect("first light window should reserve");
144
145        assert!(gate.try_reserve_centered(ChunkPos::new(1, 0)).is_none());
146
147        drop(first);
148        assert!(gate.try_reserve_centered(ChunkPos::new(1, 0)).is_some());
149    }
150}