tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
chunk_maintenance.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/experimental/registered_maintenance.h>
5#include <tess/storage/chunk_meta.h>
6#include <tess/storage/residency.h>
7#include <tess/storage/sparse_world.h>
8#include <tess/storage/world.h>
9
10#include <atomic>
11#include <concepts>
12#include <cstddef>
13#include <cstdint>
14#include <functional>
15#include <limits>
16#include <memory>
17#include <optional>
18#include <type_traits>
19#include <utility>
20
21namespace tess::experimental::maintenance {
22
24enum class ChunkMarkResult : std::uint8_t {
25 Accepted,
26 CapacityExhausted,
27 Stalled,
28 InvalidMask,
29 Missing,
30 Released,
31};
32
34enum class ChunkProductState : std::uint8_t {
35 Unavailable,
36 Stale,
37 Current,
38};
39
42 ChunkKey key{};
43 ContentVersion content_version{};
44 ResidencyGeneration residency_generation{};
45
46 friend constexpr auto operator==(ChunkProductToken,
47 ChunkProductToken) noexcept
48 -> bool = default;
49};
50
52template <typename Product>
54 const Product* value = nullptr;
55 ChunkProductToken token{};
56 ChunkProductState state = ChunkProductState::Unavailable;
57};
58
62enum class ChunkResidencyStatus : std::uint8_t {
63 Ready,
64 Missing,
65 NotIdle,
66 Released,
67};
68
71 ChunkResidencyStatus status = ChunkResidencyStatus::Missing;
72 ResidencyHandle handle{};
73};
74
76enum class ChunkEvictionResult : std::uint8_t {
77 Evicted,
78 Missing,
79 NotIdle,
80 Released,
81};
82
84enum class ChunkAdapterReleaseResult : std::uint8_t {
85 Released,
86 NotIdle,
87 AlreadyReleased,
88};
89
135template <typename World, typename Product, typename Rebuild,
136 typename Backend = DirtyBitScheduler>
137 requires std::default_initializable<Product> &&
138 std::invocable<Rebuild&, const World&, ChunkKey, DirtyObservation,
139 Product&>
141 static constexpr bool is_dense =
142 std::same_as<typename World::residency_type, AlwaysResident>;
143 static constexpr bool is_sparse =
144 std::same_as<typename World::residency_type, SparseResident>;
145 static_assert(is_dense || is_sparse,
146 "ChunkMaintenanceAdapter supports tess dense and sparse "
147 "worlds only");
148
149 struct Task final : MaintenanceTask {
150 ChunkMaintenanceAdapter* owner = nullptr;
151 std::size_t slot = 0;
152
153 void run(MaintenanceBudget& budget) override {
154 owner->run_slot(slot, budget);
155 }
156 };
157
158 struct Slot {
159 Product product{};
160 Task task{};
161 ChunkProductToken token{};
162 ChunkKey key{};
163 ResidencyGeneration residency_generation{};
164 bool bound = false;
165 bool built = false;
166 std::atomic<bool> retry_debt = false;
167 };
168
169 public:
177 ChunkMaintenanceAdapter(World& world, DirtyMask owned_dirty_mask,
178 Rebuild rebuild, std::size_t backend_capacity = 0)
179 : world_(&world),
180 owned_dirty_mask_(owned_dirty_mask),
181 rebuild_(std::move(rebuild)),
182 slot_count_(world_slot_count(world)),
183 slots_(std::make_unique<Slot[]>(slot_count_)),
184 scheduler_(slot_count_,
185 backend_capacity == 0 ? slot_count_ : backend_capacity),
186 handles_(std::make_unique<MaintenanceHandle[]>(slot_count_)) {
187 if (owned_dirty_mask_.empty()) {
188 ::tess::detail::fail_fast(
189 "ChunkMaintenanceAdapter requires a nonzero owned dirty mask");
190 }
191 for (std::size_t slot = 0; slot < slot_count_; ++slot) {
192 slots_[slot].task.owner = this;
193 slots_[slot].task.slot = slot;
194 const auto handle = scheduler_.register_task(slots_[slot].task);
195 if (!handle.has_value()) {
196 ::tess::detail::fail_fast(
197 "ChunkMaintenanceAdapter fixed registry capacity mismatch");
198 }
199 handles_[slot] = handle.value();
200 if constexpr (is_dense) {
201 bind_slot(slot, ChunkKey{slot}, {});
202 }
203 }
204 if constexpr (is_sparse) {
205 bind_current_sparse_residency();
206 }
207 scheduler_.seal();
208 }
209
211 auto operator=(const ChunkMaintenanceAdapter&)
212 -> ChunkMaintenanceAdapter& = delete;
214 auto operator=(ChunkMaintenanceAdapter&&)
215 -> ChunkMaintenanceAdapter& = delete;
216
217 ~ChunkMaintenanceAdapter() = default;
218
220 [[nodiscard]] auto mark_dirty(ChunkKey key, DirtyMask mask, Box3 bounds)
221 -> ChunkMarkResult {
222 if (released_) {
223 return ChunkMarkResult::Released;
224 }
225 if (mask.empty() || !(mask & ~owned_dirty_mask_).empty()) {
226 return ChunkMarkResult::InvalidMask;
227 }
228 const auto slot = bound_slot(key);
229 if (!slot.has_value()) {
230 return ChunkMarkResult::Missing;
231 }
232 world_->mark_dirty(key, mask, bounds);
233 return map_mark_result(schedule_slot(slot.value()));
234 }
235
242 [[nodiscard]] auto retry(ChunkKey key) -> std::optional<ScheduleResult> {
243 if (released_) {
244 return std::nullopt;
245 }
246 const auto slot = bound_slot(key);
247 if (!slot.has_value()) {
248 return std::nullopt;
249 }
250 return schedule_slot(slot.value());
251 }
252
260 [[nodiscard]] auto run_some(MaintenanceBudget budget) -> DrainResult {
261 transition_ready_.store(false, std::memory_order_release);
262#if TESS_HAS_EXCEPTIONS
263 try {
264 const auto backend_result = scheduler_.run_some(budget);
265 const auto result = finalize_drain(backend_result, {});
266 transition_ready_.store(result == DrainResult::Idle,
267 std::memory_order_release);
268 return result;
269 } catch (...) {
270 transition_ready_.store(false, std::memory_order_release);
271 throw;
272 }
273#else
274 const auto backend_result = scheduler_.run_some(budget);
275 const auto result = finalize_drain(backend_result, {});
276 transition_ready_.store(result == DrainResult::Idle,
277 std::memory_order_release);
278 return result;
279#endif
280 }
281
286 [[nodiscard]] auto flush() -> DrainResult {
287 transition_ready_.store(false, std::memory_order_release);
288#if TESS_HAS_EXCEPTIONS
289 try {
290 static_cast<void>(reoffer_retry_debt());
291 const auto backend_result = scheduler_.flush();
292 const auto follow_up = reoffer_retry_debt();
293 const auto result = finalize_drain(backend_result, follow_up);
294 transition_ready_.store(result == DrainResult::Idle,
295 std::memory_order_release);
296 return result;
297 } catch (...) {
298 transition_ready_.store(false, std::memory_order_release);
299 throw;
300 }
301#else
302 static_cast<void>(reoffer_retry_debt());
303 const auto backend_result = scheduler_.flush();
304 const auto follow_up = reoffer_retry_debt();
305 const auto result = finalize_drain(backend_result, follow_up);
306 transition_ready_.store(result == DrainResult::Idle,
307 std::memory_order_release);
308 return result;
309#endif
310 }
311
313 [[nodiscard]] auto product(ChunkKey key) const -> ChunkProductView<Product> {
314 const auto slot_index = bound_slot(key);
315 if (!slot_index.has_value()) {
316 return {};
317 }
318 const auto& slot = slots_[slot_index.value()];
319 if (!slot.built || slot.token.key != key) {
320 return {};
321 }
322 const auto state = token_current(slot.token, slot_index.value())
323 ? ChunkProductState::Current
324 : ChunkProductState::Stale;
325 return ChunkProductView<Product>{&slot.product, slot.token, state};
326 }
327
329 [[nodiscard]] auto current(ChunkProductToken token) const noexcept -> bool {
330 const auto slot = bound_slot(token.key);
331 return slot.has_value() && slots_[slot.value()].built &&
332 slots_[slot.value()].token == token &&
333 token_current(token, slot.value());
334 }
335
337 [[nodiscard]] auto metrics() const noexcept -> MaintenanceMetrics {
338 return scheduler_.metrics();
339 }
340
348 requires(is_sparse)
349 {
350 if (released_) {
351 return ChunkResidencyResult{ChunkResidencyStatus::Released, {}};
352 }
353 if (!transition_ready_.load(std::memory_order_acquire)) {
354 return ChunkResidencyResult{ChunkResidencyStatus::NotIdle, {}};
355 }
356 const auto handle = world_->ensure_resident(key);
357 if (!handle.generation.valid()) {
358 return ChunkResidencyResult{ChunkResidencyStatus::Missing, {}};
359 }
360 const auto ref = world_->resident_ref(key);
361 if (ref.meta == nullptr || ref.slot >= slot_count_ ||
362 ref.generation != handle.generation) {
363 ::tess::detail::fail_fast(
364 "ChunkMaintenanceAdapter sparse residency binding mismatch");
365 }
366 bind_slot(ref.slot, key, ref.generation);
367 return ChunkResidencyResult{ChunkResidencyStatus::Ready, handle};
368 }
369
371 [[nodiscard]] auto evict(ChunkKey key) -> ChunkEvictionResult
372 requires(is_sparse)
373 {
374 if (released_) {
375 return ChunkEvictionResult::Released;
376 }
377 if (!transition_ready_.load(std::memory_order_acquire)) {
378 return ChunkEvictionResult::NotIdle;
379 }
380 const auto ref = world_->resident_ref(key);
381 if (ref.meta == nullptr) {
382 return ChunkEvictionResult::Missing;
383 }
384 if (!world_->evict(key)) {
385 return ChunkEvictionResult::Missing;
386 }
387 unbind_slot(ref.slot);
388 return ChunkEvictionResult::Evicted;
389 }
390
399 [[nodiscard]] auto reconcile_residency() -> ChunkResidencyStatus
400 requires(is_sparse)
401 {
402 if (released_) {
403 return ChunkResidencyStatus::Released;
404 }
405 if (!transition_ready_.load(std::memory_order_acquire)) {
406 return ChunkResidencyStatus::NotIdle;
407 }
408 for (std::size_t slot = 0; slot < slot_count_; ++slot) {
409 unbind_slot(slot);
410 }
411 bind_current_sparse_residency();
412 return ChunkResidencyStatus::Ready;
413 }
414
421 [[nodiscard]] auto try_release() -> ChunkAdapterReleaseResult {
422 if (released_) {
423 return ChunkAdapterReleaseResult::AlreadyReleased;
424 }
425 if (!transition_ready_.load(std::memory_order_acquire)) {
426 return ChunkAdapterReleaseResult::NotIdle;
427 }
428 for (std::size_t slot = 0; slot < slot_count_; ++slot) {
429 const auto result = scheduler_.try_release(handles_[slot]);
430 if (result != ReleaseResult::Released) {
431 return ChunkAdapterReleaseResult::NotIdle;
432 }
433 }
434 released_ = true;
435 transition_ready_.store(false, std::memory_order_release);
436 return ChunkAdapterReleaseResult::Released;
437 }
438
439 private:
440 [[nodiscard]] static auto world_slot_count(const World& world)
441 -> std::size_t {
442 if constexpr (is_dense) {
443 static_assert(
444 World::chunk_count <=
445 static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max()));
446 return static_cast<std::size_t>(World::chunk_count);
447 } else {
448 return world.capacity();
449 }
450 }
451
452 [[nodiscard]] auto bound_slot(ChunkKey key) const noexcept
453 -> std::optional<std::size_t> {
454 if constexpr (is_dense) {
455 if (key.value >= World::chunk_count) {
456 return std::nullopt;
457 }
458 return static_cast<std::size_t>(key.value);
459 } else {
460 const auto ref = world_->resident_ref(key);
461 if (ref.meta == nullptr || ref.slot >= slot_count_) {
462 return std::nullopt;
463 }
464 const auto& slot = slots_[ref.slot];
465 if (!slot.bound || slot.key != key ||
466 slot.residency_generation != ref.generation) {
467 return std::nullopt;
468 }
469 return ref.slot;
470 }
471 }
472
473 void bind_slot(std::size_t slot_index, ChunkKey key,
474 ResidencyGeneration generation) noexcept {
475 auto& slot = slots_[slot_index];
476 if (!slot.bound || slot.key != key ||
477 slot.residency_generation != generation) {
478 slot.built = false;
479 slot.retry_debt.store(false, std::memory_order_release);
480 }
481 slot.bound = true;
482 slot.key = key;
483 slot.residency_generation = generation;
484 }
485
486 void unbind_slot(std::size_t slot_index) noexcept {
487 auto& slot = slots_[slot_index];
488 slot.bound = false;
489 slot.built = false;
490 slot.residency_generation = {};
491 slot.retry_debt.store(false, std::memory_order_release);
492 }
493
494 void bind_current_sparse_residency()
495 requires(is_sparse)
496 {
497 for (const auto key : world_->resident_chunk_keys()) {
498 const auto ref = world_->resident_ref(key);
499 if (ref.meta == nullptr || ref.slot >= slot_count_) {
500 ::tess::detail::fail_fast(
501 "ChunkMaintenanceAdapter sparse residency reconciliation "
502 "mismatch");
503 }
504 bind_slot(ref.slot, key, ref.generation);
505 }
506 }
507
508 [[nodiscard]] auto schedule_slot(std::size_t slot) -> ScheduleResult {
509 transition_ready_.store(false, std::memory_order_release);
510 const auto claimed_debt =
511 slots_[slot].retry_debt.exchange(false, std::memory_order_acq_rel);
512 const auto result = scheduler_.schedule(handles_[slot]);
513 if (claimed_debt && result != ScheduleResult::Accepted) {
514 slots_[slot].retry_debt.store(true, std::memory_order_release);
515 }
516 return result;
517 }
518
519 struct RetryOfferSummary {
520 bool offered = false;
521 bool stalled = false;
522 };
523
524 [[nodiscard]] auto reoffer_retry_debt() -> RetryOfferSummary {
525 auto summary = RetryOfferSummary{};
526 for (std::size_t slot = 0; slot < slot_count_; ++slot) {
527 if (!slots_[slot].retry_debt.load(std::memory_order_acquire)) {
528 continue;
529 }
530 summary.offered = true;
531 const auto result = schedule_slot(slot);
532 summary.stalled = summary.stalled || result == ScheduleResult::Stalled;
533 }
534 return summary;
535 }
536
537 [[nodiscard]] auto has_retry_debt() const noexcept -> bool {
538 for (std::size_t slot = 0; slot < slot_count_; ++slot) {
539 if (slots_[slot].retry_debt.load(std::memory_order_acquire)) {
540 return true;
541 }
542 }
543 return false;
544 }
545
546 [[nodiscard]] auto finalize_drain(DrainResult result,
547 RetryOfferSummary follow_up) const noexcept
548 -> DrainResult {
549 if (result == DrainResult::Stalled || follow_up.stalled) {
550 return DrainResult::Stalled;
551 }
552 if (follow_up.offered || has_retry_debt()) {
553 return DrainResult::BudgetExhausted;
554 }
555 return result;
556 }
557
558 [[nodiscard]] static auto map_mark_result(ScheduleResult result) noexcept
559 -> ChunkMarkResult {
560 switch (result) {
561 case ScheduleResult::Accepted:
562 return ChunkMarkResult::Accepted;
563 case ScheduleResult::CapacityExhausted:
564 return ChunkMarkResult::CapacityExhausted;
565 case ScheduleResult::Stalled:
566 return ChunkMarkResult::Stalled;
567 }
568 ::tess::detail::fail_fast(
569 "ChunkMaintenanceAdapter received an invalid schedule result");
570 }
571
572 [[nodiscard]] auto sparse_binding_current(
573 const Slot& slot, std::size_t slot_index) const noexcept -> bool {
574 if constexpr (is_dense) {
575 static_cast<void>(slot);
576 static_cast<void>(slot_index);
577 return true;
578 } else {
579 const auto ref = world_->resident_ref(slot.key);
580 return ref.meta != nullptr && ref.slot == slot_index &&
581 ref.generation == slot.residency_generation;
582 }
583 }
584
585 [[nodiscard]] auto token_current(ChunkProductToken token,
586 std::size_t slot_index) const noexcept
587 -> bool {
588 const auto& slot = slots_[slot_index];
589 if (!sparse_binding_current(slot, slot_index) || slot.key != token.key ||
590 slot.residency_generation != token.residency_generation) {
591 return false;
592 }
593 return world_->meta(token.key).content_version == token.content_version &&
594 (world_->dirty_mask(token.key) & owned_dirty_mask_).empty();
595 }
596
597 void run_slot(std::size_t slot_index, MaintenanceBudget& budget) {
598 auto& slot = slots_[slot_index];
599 if (!slot.bound || !budget.consume()) {
600 return;
601 }
602 if (!sparse_binding_current(slot, slot_index)) {
603 return;
604 }
605 const auto observed = world_->observe_dirty(slot.key, owned_dirty_mask_);
606 if (observed.mask.empty() &&
607 (!slot.built ||
608 slot.token.content_version == observed.content_version)) {
609 slot.retry_debt.store(false, std::memory_order_release);
610 return;
611 }
612 if (!sparse_binding_current(slot, slot_index)) {
613 return;
614 }
615 std::invoke(rebuild_, std::as_const(*world_), slot.key, observed,
616 slot.product);
617 if (!sparse_binding_current(slot, slot_index)) {
618 return;
619 }
620 slot.token = ChunkProductToken{slot.key, observed.content_version,
621 slot.residency_generation};
622 slot.built = true;
623 if (world_->clear_dirty_observed(slot.key, observed)) {
624 slot.retry_debt.store(false, std::memory_order_release);
625 return;
626 }
627 if (schedule_slot(slot_index) != ScheduleResult::Accepted) {
628 slot.retry_debt.store(true, std::memory_order_release);
629 }
630 }
631
632 World* world_;
633 DirtyMask owned_dirty_mask_;
634 Rebuild rebuild_;
635 std::size_t slot_count_;
636 std::unique_ptr<Slot[]> slots_;
637 RegisteredScheduler<Backend> scheduler_;
638 std::unique_ptr<MaintenanceHandle[]> handles_;
639 std::atomic<bool> transition_ready_ = false;
640 bool released_ = false;
641};
642
643} // namespace tess::experimental::maintenance
Definition world.h:22
auto mark_dirty(ChunkKey key, DirtyMask mask, Box3 bounds) -> ChunkMarkResult
Definition chunk_maintenance.h:220
auto current(ChunkProductToken token) const noexcept -> bool
Definition chunk_maintenance.h:329
auto metrics() const noexcept -> MaintenanceMetrics
Definition chunk_maintenance.h:337
auto product(ChunkKey key) const -> ChunkProductView< Product >
Definition chunk_maintenance.h:313
auto ensure_resident(ChunkKey key) -> ChunkResidencyResult requires(is_sparse)
Definition chunk_maintenance.h:347
auto retry(ChunkKey key) -> std::optional< ScheduleResult >
Definition chunk_maintenance.h:242
ChunkMaintenanceAdapter(World &world, DirtyMask owned_dirty_mask, Rebuild rebuild, std::size_t backend_capacity=0)
Definition chunk_maintenance.h:177
auto run_some(MaintenanceBudget budget) -> DrainResult
Definition chunk_maintenance.h:260
auto try_release() -> ChunkAdapterReleaseResult
Definition chunk_maintenance.h:421
auto reconcile_residency() -> ChunkResidencyStatus requires(is_sparse)
Definition chunk_maintenance.h:399
auto evict(ChunkKey key) -> ChunkEvictionResult requires(is_sparse)
Definition chunk_maintenance.h:371
auto flush() -> DrainResult
Definition chunk_maintenance.h:286
Shared unit budget passed through one maintenance drain.
Definition maintenance.h:20
Definition registered_maintenance.h:117
Long-lived derived-state maintenance operation.
Definition maintenance.h:44
Definition shape.h:94
Definition shape.h:86
Definition metadata_types.h:86
Definition metadata_types.h:12
Definition chunk_meta.h:46
Definition metadata_types.h:118
Definition residency.h:40
Definition chunk_maintenance.h:53
Scheduler observations used by experiments and diagnostics.
Definition maintenance.h:69