Skip to main content

steel_core/chunk/chunk_map/
light_updates.rs

1use super::{
2    Arc, BlockPos, ChunkHolder, ChunkMap, ChunkPos, ChunkStatus, InFlightLightUpdates,
3    LightCacheLayout, LightCacheSetupRadius, LightLayer, LightSectionEmptinessChange,
4    LightSectionRange, LightUpdateState, LightWorkset, PendingChunkLightUpdates, SectionPos,
5    propagate_block_light_changes_with_empty_sections,
6    propagate_sky_light_changes_with_empty_sections,
7};
8
9impl ChunkMap {
10    /// Records a block change at the given position.
11    /// This marks the chunk as having pending changes to broadcast.
12    pub fn block_changed(&self, pos: BlockPos) {
13        let chunk_pos = ChunkPos::new(
14            SectionPos::block_to_section_coord(pos.0.x),
15            SectionPos::block_to_section_coord(pos.0.z),
16        );
17
18        if let Some(holder) = self.lookup_active_holder(chunk_pos)
19            && holder.block_changed(pos)
20        {
21            // First change for this chunk - add to broadcast list
22            self.chunks_to_broadcast.lock().push(holder);
23        }
24    }
25
26    /// Marks client-visible chunk packet content as changed.
27    pub fn packet_content_changed(&self, chunk_pos: ChunkPos) {
28        if let Some(holder) = self.lookup_active_holder(chunk_pos) {
29            holder.mark_packet_content_changed();
30        }
31    }
32
33    /// Records a light-section change at the given position.
34    pub fn light_changed(&self, layer: LightLayer, section_pos: SectionPos) {
35        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
36
37        if let Some(holder) = self.lookup_active_holder(chunk_pos) {
38            if holder.light_changed(layer, section_pos) {
39                self.chunks_to_broadcast.lock().push(holder);
40            }
41            return;
42        }
43
44        if let Some(holder) = self
45            .unloading_chunks
46            .read_sync(&chunk_pos, |_, h| Arc::clone(h))
47        {
48            holder.mark_light_section_dirty(section_pos);
49        }
50    }
51
52    /// Queues a block or section light change for the next light propagation drain.
53    pub fn queue_light_change(
54        &self,
55        pos: BlockPos,
56        check_block: bool,
57        empty_section_change: Option<LightSectionEmptinessChange>,
58    ) {
59        if !check_block && empty_section_change.is_none() {
60            return;
61        }
62
63        let chunk_pos = ChunkPos::new(
64            SectionPos::block_to_section_coord(pos.0.x),
65            SectionPos::block_to_section_coord(pos.0.z),
66        );
67
68        let mut light_updates = self.light_updates.lock();
69        if !self.light_update_center_is_available(chunk_pos) {
70            return;
71        }
72
73        light_updates
74            .pending
75            .queue_change(chunk_pos, pos, check_block, empty_section_change);
76    }
77
78    /// Drains all queued light updates and runs one scoped propagation per changed chunk.
79    pub fn propagate_queued_light_changes(&self) {
80        let Some((tasks, in_flight_updates)) = self.drain_pending_light_updates() else {
81            return;
82        };
83
84        let mut blocked_tasks = Vec::new();
85        for (center, task) in tasks {
86            if task.is_empty() {
87                continue;
88            }
89            let Some(light_work_window_reservation) =
90                self.light_work_window_gate.try_reserve_centered(center)
91            else {
92                blocked_tasks.push((center, task));
93                continue;
94            };
95
96            self.propagate_queued_light_change(center, task);
97            drop(light_work_window_reservation);
98        }
99
100        if !blocked_tasks.is_empty() {
101            self.light_updates
102                .lock()
103                .pending
104                .prepend_drained(blocked_tasks);
105        }
106        drop(in_flight_updates);
107    }
108
109    pub(super) async fn flush_queued_light_changes_for_save(&self) {
110        loop {
111            let Some(center) = self.next_pending_light_update_center() else {
112                if !self.has_in_flight_light_updates() {
113                    return;
114                }
115                self.wait_for_in_flight_light_updates().await;
116                continue;
117            };
118
119            let light_work_window_reservation =
120                self.light_work_window_gate.reserve_centered(center).await;
121
122            let Some((task, in_flight_updates)) =
123                self.drain_pending_light_update_for_center(center)
124            else {
125                drop(light_work_window_reservation);
126                continue;
127            };
128
129            if task.is_empty() {
130                drop(light_work_window_reservation);
131                drop(in_flight_updates);
132                continue;
133            }
134
135            self.propagate_queued_light_change(center, task);
136            drop(light_work_window_reservation);
137            drop(in_flight_updates);
138        }
139    }
140
141    pub(super) fn drain_pending_light_updates(
142        &self,
143    ) -> Option<(
144        Vec<(ChunkPos, PendingChunkLightUpdates)>,
145        InFlightLightUpdates<'_>,
146    )> {
147        let mut light_updates = self.light_updates.lock();
148        if light_updates.pending.is_empty() {
149            return None;
150        }
151        let tasks = light_updates.pending.drain();
152        let centers = tasks
153            .iter()
154            .map(|(chunk_pos, _)| *chunk_pos)
155            .collect::<Vec<_>>();
156        let in_flight = self.track_in_flight_light_updates(&mut light_updates, centers);
157        Some((tasks, in_flight))
158    }
159
160    pub(super) fn next_pending_light_update_center(&self) -> Option<ChunkPos> {
161        self.light_updates.lock().pending.next_center()
162    }
163
164    pub(super) fn next_pending_light_update_center_touching_chunk(
165        &self,
166        chunk_pos: ChunkPos,
167    ) -> Option<ChunkPos> {
168        self.light_updates
169            .lock()
170            .pending
171            .next_center_touching_chunk(chunk_pos)
172    }
173
174    pub(super) fn drain_pending_light_update_for_center(
175        &self,
176        center: ChunkPos,
177    ) -> Option<(PendingChunkLightUpdates, InFlightLightUpdates<'_>)> {
178        let mut light_updates = self.light_updates.lock();
179        let task = light_updates.pending.drain_center(center)?;
180        let in_flight = self.track_in_flight_light_updates(&mut light_updates, vec![center]);
181        Some((task, in_flight))
182    }
183
184    pub(super) fn track_in_flight_light_updates(
185        &self,
186        light_updates: &mut LightUpdateState,
187        centers: Vec<ChunkPos>,
188    ) -> InFlightLightUpdates<'_> {
189        light_updates.track_in_flight(&centers);
190        InFlightLightUpdates {
191            centers,
192            light_updates: &self.light_updates,
193            progress_notify: &self.light_updates_progress_notify,
194        }
195    }
196
197    pub(super) fn has_in_flight_light_updates(&self) -> bool {
198        self.light_updates.lock().has_in_flight_updates()
199    }
200
201    pub(super) fn has_in_flight_light_update_touching_chunk(&self, chunk_pos: ChunkPos) -> bool {
202        self.light_updates
203            .lock()
204            .has_in_flight_update_touching_chunk(chunk_pos)
205    }
206
207    pub(super) async fn wait_for_in_flight_light_updates(&self) {
208        loop {
209            if !self.has_in_flight_light_updates() {
210                return;
211            }
212
213            let progress = self.light_updates_progress_notify.notified();
214            if !self.has_in_flight_light_updates() {
215                return;
216            }
217            progress.await;
218        }
219    }
220
221    pub(super) async fn wait_for_in_flight_light_update_touching_chunk(&self, chunk_pos: ChunkPos) {
222        loop {
223            if !self.has_in_flight_light_update_touching_chunk(chunk_pos) {
224                return;
225            }
226
227            let progress = self.light_updates_progress_notify.notified();
228            if !self.has_in_flight_light_update_touching_chunk(chunk_pos) {
229                return;
230            }
231            progress.await;
232        }
233    }
234
235    pub(super) async fn flush_queued_light_changes_touching_chunk_for_save(
236        &self,
237        chunk_pos: ChunkPos,
238    ) {
239        loop {
240            let Some(center) = self.next_pending_light_update_center_touching_chunk(chunk_pos)
241            else {
242                if !self.has_in_flight_light_update_touching_chunk(chunk_pos) {
243                    return;
244                }
245                self.wait_for_in_flight_light_update_touching_chunk(chunk_pos)
246                    .await;
247                continue;
248            };
249
250            let light_work_window_reservation =
251                self.light_work_window_gate.reserve_centered(center).await;
252
253            let Some((task, in_flight_updates)) =
254                self.drain_pending_light_update_for_center(center)
255            else {
256                drop(light_work_window_reservation);
257                continue;
258            };
259
260            if task.is_empty() {
261                drop(light_work_window_reservation);
262                drop(in_flight_updates);
263                continue;
264            }
265
266            self.propagate_queued_light_change(center, task);
267            drop(light_work_window_reservation);
268            drop(in_flight_updates);
269        }
270    }
271
272    #[cfg(test)]
273    pub(super) fn has_pending_light_updates(&self) -> bool {
274        !self.light_updates.lock().is_idle()
275    }
276
277    #[cfg(test)]
278    pub(super) fn light_update_touches_chunk(&self, chunk_pos: ChunkPos) -> bool {
279        self.light_updates.lock().touches_chunk(chunk_pos)
280    }
281
282    pub(super) fn light_update_center_is_available(&self, center: ChunkPos) -> bool {
283        self.light_update_holder(center)
284            .is_some_and(|holder| holder.try_chunk(ChunkStatus::Light).is_some())
285    }
286
287    pub(super) fn light_update_holder(&self, chunk_pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
288        self.chunks
289            .read_sync(&chunk_pos, |_, holder| Arc::clone(holder))
290            .or_else(|| {
291                self.unloading_chunks
292                    .read_sync(&chunk_pos, |_, holder| Arc::clone(holder))
293            })
294    }
295
296    pub(super) fn propagate_queued_light_change(
297        &self,
298        center: ChunkPos,
299        task: PendingChunkLightUpdates,
300    ) {
301        let Some(workset) = self.light_workset_for_change(center) else {
302            log::warn!("Failed to set up light workset for queued light update at {center:?}");
303            return;
304        };
305
306        let empty_sections = task.empty_section_changes();
307        let positions = task.changed_positions.into_iter().collect::<Vec<_>>();
308        let world = self.world_gen_context.world();
309
310        if world.dimension_type.has_skylight {
311            match propagate_sky_light_changes_with_empty_sections(
312                &workset,
313                positions.iter().copied(),
314                empty_sections.iter().copied(),
315            ) {
316                Ok(result) => {
317                    for section_pos in result.updated_sections {
318                        self.light_changed(LightLayer::Sky, section_pos);
319                    }
320                }
321                Err(error) => {
322                    log::warn!(
323                        "Failed to propagate queued sky-light change for {center:?}: {error:?}"
324                    );
325                }
326            }
327        }
328
329        let Ok(result) =
330            propagate_block_light_changes_with_empty_sections(&workset, positions, empty_sections)
331        else {
332            log::warn!("Failed to propagate queued block-light change for {center:?}");
333            return;
334        };
335
336        for section_pos in result.updated_sections {
337            self.light_changed(LightLayer::Block, section_pos);
338        }
339    }
340
341    pub(super) fn light_workset_for_change(&self, center: ChunkPos) -> Option<LightWorkset> {
342        let Ok(range) = LightSectionRange::from_world_height(
343            self.world_gen_context.min_y(),
344            self.world_gen_context.height(),
345        ) else {
346            return None;
347        };
348
349        let layout = LightCacheLayout::new(center, range);
350        LightWorkset::setup(
351            layout,
352            LightCacheSetupRadius::Full,
353            true,
354            |chunk_pos| {
355                let holder = self.light_update_holder(chunk_pos)?;
356                holder.try_chunk(ChunkStatus::Light)?;
357                Some(holder)
358            },
359            |_| true,
360        )
361        .ok()
362    }
363}