1use std::{
4 mem,
5 sync::atomic::{AtomicU64, Ordering},
6 time::Duration,
7};
8
9use steel_utils::{ChunkPos, locks::SyncMutex};
10
11use crate::chunk::chunk_ticket_manager::{ChunkTicket, ChunkTicketManager, LevelChange};
12use crate::chunk::gameplay_chunk_lookup_cache::GameplayChunkLookupCacheStats;
13
14#[derive(Debug, Default)]
16pub(crate) struct ChunkMapSchedulingTimings {
17 pub(crate) ticket_updates: Duration,
19 pub(crate) block_entity_unloads: Duration,
21 pub(crate) readiness_demotions: Duration,
23 pub(crate) lifecycle_commit: Duration,
25 pub(crate) readiness_reconcile: Duration,
27 pub(crate) post_process_generation: Duration,
29 pub(crate) post_process_chunk_count: usize,
31 pub(crate) post_process_position_count: usize,
33 pub(crate) readiness_candidate_count: usize,
35 pub(crate) ticking_snapshot_rebuild: Duration,
37 pub(crate) rebuilt_ticking_chunk_count: usize,
39 pub(crate) lookup_cache: GameplayChunkLookupCacheStats,
41 pub(crate) schedule_generation: Duration,
43 pub(crate) scheduled_count: usize,
45 pub(crate) run_generation: Duration,
47 pub(crate) process_unloads: Duration,
49}
50
51#[derive(Debug, Default)]
56pub(crate) struct ChunkMapPreparationTimings {
57 pub(crate) ticket_updates: Duration,
58 pub(crate) schedule_generation: Duration,
59 pub(crate) scheduled_count: usize,
60 pub(crate) run_generation: Duration,
61 pub(crate) process_unloads: Duration,
62}
63
64impl ChunkMapPreparationTimings {
65 pub(crate) fn into_scheduling_timings(self) -> ChunkMapSchedulingTimings {
66 ChunkMapSchedulingTimings {
67 ticket_updates: self.ticket_updates,
68 schedule_generation: self.schedule_generation,
69 scheduled_count: self.scheduled_count,
70 run_generation: self.run_generation,
71 process_unloads: self.process_unloads,
72 ..ChunkMapSchedulingTimings::default()
73 }
74 }
75}
76
77#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
79pub(crate) struct ChunkTicketRevision(u64);
80
81impl ChunkTicketRevision {
82 const INITIAL: Self = Self(0);
83
84 fn next(self) -> Self {
85 assert_ne!(self.0, u64::MAX, "chunk ticket revision exhausted");
86 Self(self.0 + 1)
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub(crate) enum ChunkTicketOperation {
93 Add { pos: ChunkPos, ticket: ChunkTicket },
94 Remove { pos: ChunkPos, ticket: ChunkTicket },
95}
96
97impl ChunkTicketOperation {
98 fn apply(self, ticket_manager: &mut ChunkTicketManager) {
99 match self {
100 Self::Add { pos, ticket } => ticket_manager.add_ticket(pos, ticket),
101 Self::Remove { pos, ticket } => {
102 ticket_manager.remove_ticket(pos, ticket);
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy)]
109struct QueuedChunkTicketOperation {
110 revision: ChunkTicketRevision,
111 operation: ChunkTicketOperation,
112}
113
114#[derive(Debug)]
115struct PendingChunkTicketOperations {
116 next_revision: ChunkTicketRevision,
117 operations: Vec<QueuedChunkTicketOperation>,
118 recycled_operations: Vec<QueuedChunkTicketOperation>,
119}
120
121impl Default for PendingChunkTicketOperations {
122 fn default() -> Self {
123 Self {
124 next_revision: ChunkTicketRevision::INITIAL,
125 operations: Vec::new(),
126 recycled_operations: Vec::new(),
127 }
128 }
129}
130
131impl PendingChunkTicketOperations {
132 fn push(&mut self, operation: ChunkTicketOperation) -> ChunkTicketRevision {
133 let revision = self.next_revision.next();
134 self.next_revision = revision;
135 self.operations.push(QueuedChunkTicketOperation {
136 revision,
137 operation,
138 });
139 revision
140 }
141
142 fn push_batch(
143 &mut self,
144 operations: impl IntoIterator<Item = ChunkTicketOperation>,
145 ) -> Option<ChunkTicketRevision> {
146 let mut operations = operations.into_iter();
147 let first = operations.next()?;
148 let revision = self.next_revision.next();
149 self.next_revision = revision;
150 self.operations.push(QueuedChunkTicketOperation {
151 revision,
152 operation: first,
153 });
154 self.operations
155 .extend(operations.map(|operation| QueuedChunkTicketOperation {
156 revision,
157 operation,
158 }));
159 Some(revision)
160 }
161
162 fn take(&mut self) -> Vec<QueuedChunkTicketOperation> {
163 mem::replace(
164 &mut self.operations,
165 mem::take(&mut self.recycled_operations),
166 )
167 }
168
169 fn recycle(&mut self, mut operations: Vec<QueuedChunkTicketOperation>) {
170 operations.clear();
171 self.recycled_operations = operations;
172 }
173}
174
175pub(crate) struct PreparedChunkSchedulingEpoch {
176 pub ticket_manager: ChunkTicketManager,
177 pub applied_revision: ChunkTicketRevision,
178 pub changes: Vec<LevelChange>,
179 pub timings: ChunkMapPreparationTimings,
180}
181
182enum ChunkSchedulingState {
183 Idle {
184 ticket_manager: ChunkTicketManager,
185 applied_revision: ChunkTicketRevision,
186 },
187 Running,
188 Ready(PreparedChunkSchedulingEpoch),
189}
190
191pub(crate) enum ChunkSchedulingBoundaryStep {
192 Running,
193 Start {
194 ticket_manager: ChunkTicketManager,
195 applied_revision: ChunkTicketRevision,
196 },
197 Commit(PreparedChunkSchedulingEpoch),
198}
199
200pub(crate) struct ChunkSchedulingCoordinator {
204 pending_ticket_operations: SyncMutex<PendingChunkTicketOperations>,
205 state: SyncMutex<ChunkSchedulingState>,
206 committed_revision: AtomicU64,
207}
208
209impl ChunkSchedulingCoordinator {
210 pub fn new(ticket_manager: ChunkTicketManager) -> Self {
211 Self {
212 pending_ticket_operations: SyncMutex::new(PendingChunkTicketOperations::default()),
213 state: SyncMutex::new(ChunkSchedulingState::Idle {
214 ticket_manager,
215 applied_revision: ChunkTicketRevision::INITIAL,
216 }),
217 committed_revision: AtomicU64::new(ChunkTicketRevision::INITIAL.0),
218 }
219 }
220
221 pub fn queue_ticket_operation(&self, operation: ChunkTicketOperation) -> ChunkTicketRevision {
222 self.pending_ticket_operations.lock().push(operation)
223 }
224
225 pub fn queue_ticket_operations(
226 &self,
227 operations: impl IntoIterator<Item = ChunkTicketOperation>,
228 ) -> Option<ChunkTicketRevision> {
229 self.pending_ticket_operations.lock().push_batch(operations)
230 }
231
232 pub fn apply_pending_ticket_operations(
233 &self,
234 ticket_manager: &mut ChunkTicketManager,
235 applied_revision: ChunkTicketRevision,
236 ) -> ChunkTicketRevision {
237 let mut operations = self.pending_ticket_operations.lock().take();
238 let mut latest_revision = applied_revision;
239 for queued in operations.drain(..) {
240 queued.operation.apply(ticket_manager);
241 latest_revision = queued.revision;
242 }
243 self.pending_ticket_operations.lock().recycle(operations);
244 latest_revision
245 }
246
247 pub fn take_boundary_step(&self) -> ChunkSchedulingBoundaryStep {
248 let mut state = self.state.lock();
249 match mem::replace(&mut *state, ChunkSchedulingState::Running) {
250 ChunkSchedulingState::Idle {
251 ticket_manager,
252 applied_revision,
253 } => ChunkSchedulingBoundaryStep::Start {
254 ticket_manager,
255 applied_revision,
256 },
257 ChunkSchedulingState::Running => ChunkSchedulingBoundaryStep::Running,
258 ChunkSchedulingState::Ready(epoch) => ChunkSchedulingBoundaryStep::Commit(epoch),
259 }
260 }
261
262 pub fn finish_epoch(&self, epoch: PreparedChunkSchedulingEpoch) {
263 let mut state = self.state.lock();
264 assert!(
265 matches!(*state, ChunkSchedulingState::Running),
266 "chunk scheduling epoch finished while another epoch was not running"
267 );
268 *state = ChunkSchedulingState::Ready(epoch);
269 }
270
271 pub fn publish_committed_revision(&self, revision: ChunkTicketRevision) {
272 self.committed_revision.store(revision.0, Ordering::Release);
273 }
274
275 #[must_use]
276 pub fn is_revision_committed(&self, revision: ChunkTicketRevision) -> bool {
277 self.committed_revision.load(Ordering::Acquire) >= revision.0
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn ticket_batches_apply_in_submission_order() {
287 let coordinator = ChunkSchedulingCoordinator::new(ChunkTicketManager::new());
288 let pos = ChunkPos::new(3, -2);
289 let ticket = ChunkTicket::full_chunks(0);
290 let add_revision = coordinator
291 .queue_ticket_operations([ChunkTicketOperation::Add { pos, ticket }])
292 .expect("non-empty batch should receive a revision");
293 let remove_revision = coordinator
294 .queue_ticket_operations([ChunkTicketOperation::Remove { pos, ticket }])
295 .expect("non-empty batch should receive a revision");
296 let mut manager = ChunkTicketManager::new();
297
298 let applied_revision =
299 coordinator.apply_pending_ticket_operations(&mut manager, ChunkTicketRevision::INITIAL);
300
301 assert!(add_revision < remove_revision);
302 assert_eq!(applied_revision, remove_revision);
303 assert_eq!(manager.ticket_count(), 0);
304
305 let next_revision =
306 coordinator.queue_ticket_operation(ChunkTicketOperation::Add { pos, ticket });
307 let applied_revision =
308 coordinator.apply_pending_ticket_operations(&mut manager, applied_revision);
309
310 assert_eq!(applied_revision, next_revision);
311 assert_eq!(manager.ticket_count(), 1);
312 }
313
314 #[test]
315 fn prepared_revision_is_not_visible_before_boundary_publication() {
316 let coordinator = ChunkSchedulingCoordinator::new(ChunkTicketManager::new());
317 let revision = coordinator
318 .queue_ticket_operations([ChunkTicketOperation::Add {
319 pos: ChunkPos::new(0, 0),
320 ticket: ChunkTicket::full_chunks(0),
321 }])
322 .expect("non-empty batch should receive a revision");
323 let ChunkSchedulingBoundaryStep::Start {
324 mut ticket_manager,
325 applied_revision,
326 } = coordinator.take_boundary_step()
327 else {
328 panic!("idle coordinator should start its first epoch");
329 };
330 assert!(matches!(
331 coordinator.take_boundary_step(),
332 ChunkSchedulingBoundaryStep::Running
333 ));
334 let applied =
335 coordinator.apply_pending_ticket_operations(&mut ticket_manager, applied_revision);
336 ticket_manager.run_all_updates();
337 let changes = ticket_manager.take_changes();
338 coordinator.finish_epoch(PreparedChunkSchedulingEpoch {
339 ticket_manager,
340 applied_revision: applied,
341 changes,
342 timings: ChunkMapPreparationTimings::default(),
343 });
344
345 assert_eq!(applied, revision);
346 assert!(!coordinator.is_revision_committed(revision));
347
348 let ChunkSchedulingBoundaryStep::Commit(epoch) = coordinator.take_boundary_step() else {
349 panic!("finished epoch should be committed at the next boundary");
350 };
351 coordinator.publish_committed_revision(epoch.applied_revision);
352
353 assert!(coordinator.is_revision_committed(revision));
354 }
355}