tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
queued.h
1#pragma once
2
3#include <tess/block/block.h>
4#include <tess/core/shape.h>
5#include <tess/core/tag_identity.h>
6#include <tess/diagnostics/diagnostics.h>
7#include <tess/diagnostics/trace.h>
8#include <tess/ops/phase_executor.h>
9#include <tess/storage/world.h>
10
11#include <algorithm>
12#include <cstddef>
13#include <cstdint>
14#include <limits>
15#include <optional>
16#include <source_location>
17#include <span>
18#include <stdexcept>
19#include <type_traits>
20#include <utility>
21#include <vector>
22
23namespace tess {
24
25namespace detail {
26
27struct PlannedWorldStamp {
28 std::uintptr_t shape_identity = 0;
29 std::uint64_t chunk_limit = 0;
30};
31
32template <typename Shape, std::uint64_t ChunkLimit>
33[[nodiscard]] inline auto planned_world_stamp() noexcept
34 -> const PlannedWorldStamp* {
35 static const auto stamp = PlannedWorldStamp{
36 tag_identity<Shape>(),
37 ChunkLimit,
38 };
39 return &stamp;
40}
41
42template <typename World>
43[[nodiscard]] inline auto planned_world_stamp() noexcept
44 -> const PlannedWorldStamp* {
45 return planned_world_stamp<typename World::shape_type, World::chunk_count>();
46}
47
48template <typename World>
49[[nodiscard]] inline auto validate_planned_world_stamp(
50 const PlannedWorldStamp* stamp) noexcept -> PlannedExecutionStatus {
51 const auto* expected = planned_world_stamp<World>();
52 if (stamp == expected) {
53 return PlannedExecutionStatus::Executed;
54 }
55 if (stamp == nullptr || stamp->shape_identity != expected->shape_identity) {
56 return PlannedExecutionStatus::InvalidShape;
57 }
58 if (stamp->chunk_limit != expected->chunk_limit) {
59 return PlannedExecutionStatus::InvalidChunk;
60 }
61 return PlannedExecutionStatus::Executed;
62}
63
64} // namespace detail
65
67struct OpId {
68 std::uint64_t value = 0;
69
70 friend constexpr bool operator==(OpId lhs, OpId rhs) noexcept = default;
71};
72
74struct OpHandle {
75 std::uint64_t value = 0;
76
77 friend constexpr bool operator==(OpHandle lhs,
78 OpHandle rhs) noexcept = default;
79};
80
82enum class OperationKind : std::uint8_t {
83 UpdateField,
84};
85static_assert(sizeof(OperationKind) == sizeof(std::uint8_t));
86
88enum class Priority : std::uint8_t {
89 Immediate,
90 GameplayCritical,
91 VisibleSoon,
92 Background,
93 Maintenance,
94};
95static_assert(sizeof(Priority) == sizeof(std::uint8_t));
96
98enum class BudgetPolicy : std::uint8_t {
99 MustRun,
100 CanDefer,
101 CanSkipIfSuperseded,
102 BudgetedIncremental,
103};
104static_assert(sizeof(BudgetPolicy) == sizeof(std::uint8_t));
105
107enum class OperationStatus : std::uint8_t {
108 Planned,
109 InvalidIdentity,
110 InvalidWritePolicy,
111 InvalidDomain,
112 InvalidFieldAccess,
113 HazardConflict,
114};
115static_assert(sizeof(OperationStatus) == sizeof(std::uint8_t));
116
118enum class OperationFailure : std::uint8_t {
119 None,
120 NonDenseHandle,
121 NonDenseId,
122 InvalidWritePolicyValue,
123 ExplicitChunkOutOfRange,
124 ReadOnlyWriteMask,
125 FieldHazardConflict,
126};
127static_assert(sizeof(OperationFailure) == sizeof(std::uint8_t));
128
130enum class ExecutionPhaseStatus : std::uint8_t {
131 Ready,
132 UnsupportedWritePolicy,
133};
134static_assert(sizeof(ExecutionPhaseStatus) == sizeof(std::uint8_t));
135
137enum class DomainKind : std::uint8_t {
138 ExplicitChunks,
139 DirtyChunks,
140 ActiveChunks,
141 ResidentChunks,
142};
143static_assert(sizeof(DomainKind) == sizeof(std::uint8_t));
144
146class DomainDesc {
147 public:
148 // Explicit chunk keys are stored sorted and deduplicated so a planned
149 // operation never visits one chunk twice: repeated keys under
150 // UniquePerChunk would otherwise defeat the per-chunk ownership rule
151 // that parallel phase planning relies on.
152 [[nodiscard]] static auto explicit_chunks(std::span<const ChunkKey> keys)
153 -> DomainDesc {
154 DomainDesc desc{DomainKind::ExplicitChunks};
155 desc.explicit_chunks_.assign(keys.begin(), keys.end());
156 std::sort(desc.explicit_chunks_.begin(), desc.explicit_chunks_.end(),
157 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
158 desc.explicit_chunks_.erase(
159 std::unique(desc.explicit_chunks_.begin(), desc.explicit_chunks_.end()),
160 desc.explicit_chunks_.end());
161 return desc;
162 }
163
164 [[nodiscard]] static constexpr auto dirty_chunks(std::uint32_t flags) noexcept
165 -> DomainDesc {
166 DomainDesc desc{DomainKind::DirtyChunks};
167 desc.mask_ = flags;
168 return desc;
169 }
170
171 [[nodiscard]] static constexpr auto active_chunks(
172 std::uint32_t flags) noexcept -> DomainDesc {
173 DomainDesc desc{DomainKind::ActiveChunks};
174 desc.mask_ = flags;
175 return desc;
176 }
177
178 [[nodiscard]] static constexpr auto resident_chunks() noexcept -> DomainDesc {
179 return DomainDesc{DomainKind::ResidentChunks};
180 }
181
182 [[nodiscard]] constexpr auto kind() const noexcept -> DomainKind {
183 return kind_;
184 }
185
186 [[nodiscard]] constexpr auto mask() const noexcept -> std::uint32_t {
187 return mask_;
188 }
189
190 [[nodiscard]] constexpr auto explicit_chunks() const noexcept
191 -> std::span<const ChunkKey> {
192 return {explicit_chunks_.data(), explicit_chunks_.size()};
193 }
194
195 private:
196 constexpr explicit DomainDesc(DomainKind kind) noexcept : kind_(kind) {}
197
198 DomainKind kind_;
199 std::uint32_t mask_ = 0;
200 std::vector<ChunkKey> explicit_chunks_;
201};
202
205 std::uint32_t read_mask = 0;
206 std::uint32_t write_mask = 0;
207 std::uint32_t dirty_mask = 0;
208
209 friend constexpr bool operator==(FieldAccessDesc lhs,
210 FieldAccessDesc rhs) noexcept = default;
211};
212
215 OperationKind kind = OperationKind::UpdateField;
216 OpHandle handle{};
217 OpId id{};
218 DomainDesc domain = DomainDesc::resident_chunks();
219 FieldAccessDesc field_access{};
220 WritePolicy write_policy = WritePolicy::ReadOnly;
221 Priority priority = Priority::GameplayCritical;
222 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
223 std::source_location source = std::source_location::current();
224};
225
228 WritePolicy write_policy = WritePolicy::ReadOnly;
229 DomainKind domain_kind = DomainKind::ResidentChunks;
230 std::uint32_t domain_mask = 0;
231};
232
234enum class PlannedOperationCreateStatus : std::uint8_t {
235 Created,
236 InvalidChunk,
237};
238static_assert(sizeof(PlannedOperationCreateStatus) == sizeof(std::uint8_t));
239
240struct PlannedOperationCreateResult;
241class ExecutionReport;
242class ExecutionPhase;
243
245class PlannedOperation {
246 public:
247 OperationKind kind = OperationKind::UpdateField;
248 OpHandle handle{};
249 OpId id{};
250 OperationAccess access{};
251 FieldAccessDesc field_access{};
252 WritePolicy write_policy = WritePolicy::ReadOnly;
253 Priority priority = Priority::GameplayCritical;
254 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
255 // Enqueue-site capture carried through planning so run-time completions
256 // (result channels) can report where the operation came from.
257 std::source_location source = std::source_location::current();
258
265 template <typename World>
266 [[nodiscard]] static auto create(const World& world,
267 const QueuedOperation& operation,
268 std::span<const ChunkKey> chunks)
270
272 [[nodiscard]] constexpr auto chunks() const noexcept
273 -> std::span<const ChunkKey> {
274 return chunks_;
275 }
276
278 template <typename World>
279 [[nodiscard]] auto world_validation_status(
280 const World& /*world*/) const noexcept -> PlannedExecutionStatus {
281 static_assert(
282 std::is_same_v<typename World::residency_type, AlwaysResident>,
283 "Queued-op validation requires an AlwaysResidentWorld; sparse "
284 "queued-ops support is deferred to a later slice.");
285 return detail::validate_planned_world_stamp<World>(world_stamp_);
286 }
287
288 private:
289 friend class ExecutionReport;
290 friend class ExecutionPhase;
291
292 PlannedOperation(const QueuedOperation& operation,
293 std::vector<ChunkKey>&& chunks,
294 const detail::PlannedWorldStamp* world_stamp) noexcept
295 : kind(operation.kind),
296 handle(operation.handle),
297 id(operation.id),
298 access(OperationAccess{operation.write_policy, operation.domain.kind(),
299 operation.domain.mask()}),
300 field_access(operation.field_access),
301 write_policy(operation.write_policy),
302 priority(operation.priority),
303 budget_policy(operation.budget_policy),
304 source(operation.source),
305 chunks_(std::move(chunks)),
306 world_stamp_(world_stamp) {}
307
308 std::vector<ChunkKey> chunks_;
309 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
310};
311
314 PlannedOperationCreateStatus status =
315 PlannedOperationCreateStatus::InvalidChunk;
316 std::optional<PlannedOperation> operation;
317 ChunkKey invalid_chunk{};
318};
319
320template <typename World>
321auto PlannedOperation::create(const World& /*world*/,
322 const QueuedOperation& operation,
323 std::span<const ChunkKey> chunks)
325 static_assert(
326 std::is_same_v<typename World::residency_type, AlwaysResident>,
327 "Queued operations require an AlwaysResidentWorld; sparse queued-ops "
328 "support is deferred to a later slice.");
329
330 for (const auto key : chunks) {
331 if (key.value >= World::chunk_count) {
333 PlannedOperationCreateStatus::InvalidChunk,
334 std::nullopt,
335 key,
336 };
337 }
338 }
339
340 auto validated = std::vector<ChunkKey>{chunks.begin(), chunks.end()};
341 std::sort(validated.begin(), validated.end(),
342 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
343 validated.erase(std::unique(validated.begin(), validated.end()),
344 validated.end());
345 auto planned = PlannedOperation{
346 operation,
347 std::move(validated),
348 detail::planned_world_stamp<World>(),
349 };
351 PlannedOperationCreateStatus::Created,
352 std::optional<PlannedOperation>{std::move(planned)},
353 {},
354 };
355}
356
358class ExecutionPlan {
359 public:
360 [[nodiscard]] constexpr auto operations() const noexcept
361 -> std::span<const PlannedOperation> {
362 return {operations_.data(), operations_.size()};
363 }
364
365 [[nodiscard]] constexpr bool empty() const noexcept {
366 return operations_.empty();
367 }
368
369 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
370 return operations_.size();
371 }
372
373 private:
374 friend class ExecutionReport;
375 friend class ExecutionPhase;
376
377 ExecutionPlan() noexcept = default;
378 ExecutionPlan(const ExecutionPlan&) = default;
379 ExecutionPlan(ExecutionPlan&&) noexcept = default;
380
381 // generation_ is this plan's capability epoch and must never be copied from
382 // another plan; assignment invalidates it through bump_generation instead.
383 // cppcheck-suppress operatorEqVarError
384 auto operator=(const ExecutionPlan& other) -> ExecutionPlan& {
385 if (this != &other) {
386 // Expire every issued capability before a potentially throwing vector
387 // copy. A failed assignment must not leave an old phase authorized for
388 // whatever state the vector copy preserved or partially replaced.
389 bump_generation();
390 operations_ = other.operations_;
391 }
392 return *this;
393 }
394
395 auto operator=(ExecutionPlan&& other) noexcept -> ExecutionPlan& {
396 if (this != &other) {
397 operations_ = std::move(other.operations_);
398 bump_generation();
399 }
400 return *this;
401 }
402
403 constexpr void bump_generation() noexcept { ++generation_; }
404
405 std::vector<PlannedOperation> operations_;
406 std::uint64_t generation_ = 0;
407};
408
416class ExecutionPhase {
417 public:
418 ExecutionPhase(const ExecutionPhase&) noexcept = default;
419 ExecutionPhase(ExecutionPhase&&) noexcept = default;
420 auto operator=(const ExecutionPhase&) noexcept -> ExecutionPhase& = default;
421 auto operator=(ExecutionPhase&&) noexcept -> ExecutionPhase& = default;
422
423 [[nodiscard]] constexpr auto first_operation() const noexcept -> std::size_t {
424 return first_operation_;
425 }
426
427 [[nodiscard]] constexpr auto operation_count() const noexcept -> std::size_t {
428 return operation_count_;
429 }
430
432 [[nodiscard]] constexpr bool belongs_to(
433 const ExecutionPlan& plan) const noexcept {
434 return plan_ == &plan && plan_generation_ == plan.generation_;
435 }
436
438 template <typename World>
439 [[nodiscard]] auto world_validation_status(
440 const World& /*world*/) const noexcept -> PlannedExecutionStatus {
441 static_assert(
442 std::is_same_v<typename World::residency_type, AlwaysResident>,
443 "Queued-op validation requires an AlwaysResidentWorld; sparse "
444 "queued-ops support is deferred to a later slice.");
445 return detail::validate_planned_world_stamp<World>(world_stamp_);
446 }
447
449 template <WritePolicy Policy>
450 [[nodiscard]] constexpr bool policy_matches() const noexcept {
451 static_assert(is_valid_write_policy(Policy));
452 return write_policy_mask_ == policy_bit(Policy);
453 }
454
455 private:
456 friend class ExecutionPhasePlan;
457
458 [[nodiscard]] static constexpr auto policy_bit(WritePolicy policy) noexcept
459 -> std::uint8_t {
460 return static_cast<std::uint8_t>(std::uint8_t{1}
461 << static_cast<std::uint8_t>(policy));
462 }
463
464 constexpr ExecutionPhase(const ExecutionPlan& plan,
465 std::size_t first_operation,
466 std::size_t operation_count,
467 const PlannedOperation& operation) noexcept
468 : plan_(&plan),
469 first_operation_(first_operation),
470 operation_count_(operation_count),
471 plan_generation_(plan.generation_),
472 world_stamp_(operation.world_stamp_),
473 write_policy_mask_(policy_bit(operation.write_policy)) {}
474
475 constexpr void extend(const PlannedOperation& operation) noexcept {
476 ++operation_count_;
477 write_policy_mask_ |= policy_bit(operation.write_policy);
478 }
479
480 const ExecutionPlan* plan_;
481 std::size_t first_operation_;
482 std::size_t operation_count_;
483 std::uint64_t plan_generation_;
484 const detail::PlannedWorldStamp* world_stamp_;
485 std::uint8_t write_policy_mask_;
486};
487
489[[nodiscard]] constexpr auto executor_phase_range(
490 const ExecutionPhase& phase) noexcept -> ExecutorPhaseRange {
491 return ExecutorPhaseRange{
492 phase.first_operation(),
493 phase.operation_count(),
494 };
495}
496
499 public:
500 [[nodiscard]] constexpr auto phases() const noexcept
501 -> std::span<const ExecutionPhase> {
502 return {phases_.data(), phases_.size()};
503 }
504
505 [[nodiscard]] constexpr auto status() const noexcept -> ExecutionPhaseStatus {
506 return status_;
507 }
508
509 [[nodiscard]] constexpr bool ok() const noexcept {
510 return status_ == ExecutionPhaseStatus::Ready;
511 }
512
513 [[nodiscard]] constexpr auto failed_operation_index() const noexcept
514 -> std::size_t {
515 return failed_operation_index_;
516 }
517
518 [[nodiscard]] constexpr auto failed_write_policy() const noexcept
519 -> WritePolicy {
520 return failed_write_policy_;
521 }
522
523 private:
524 friend auto plan_parallel_execution_phases(const ExecutionPlan& plan)
526
527 void reserve(std::size_t size) { phases_.reserve(size); }
528
529 void push_phase(const ExecutionPlan& plan, std::size_t first_operation,
530 std::size_t operation_count,
531 const PlannedOperation& operation) {
532 phases_.push_back(
533 ExecutionPhase{plan, first_operation, operation_count, operation});
534 }
535
536 void extend_last_phase(const PlannedOperation& operation) {
537 phases_.back().extend(operation);
538 }
539
540 std::vector<ExecutionPhase> phases_;
541 ExecutionPhaseStatus status_ = ExecutionPhaseStatus::Ready;
542 std::size_t failed_operation_index_ = 0;
543 WritePolicy failed_write_policy_ = WritePolicy::ReadOnly;
544};
545
546namespace detail {
547
548[[nodiscard]] constexpr bool execution_phase_valid_for(
549 const ExecutionPlan& plan, const ExecutionPhase& phase) noexcept {
550 const auto operations = plan.operations();
551 const auto first = phase.first_operation();
552 const auto count = phase.operation_count();
553 return phase.belongs_to(plan) && first <= operations.size() &&
554 count <= operations.size() - first;
555}
556
557template <WritePolicy Policy, typename World>
558[[nodiscard]] auto execution_phase_validation_status(
559 const World& world, const ExecutionPlan& plan,
560 const ExecutionPhase& phase) noexcept -> PlannedExecutionStatus {
561 if (!execution_phase_valid_for(plan, phase)) {
562 return PlannedExecutionStatus::InvalidPhase;
563 }
564 const auto world_status = phase.world_validation_status(world);
565 if (world_status != PlannedExecutionStatus::Executed) {
566 return world_status;
567 }
568 if (!phase.template policy_matches<Policy>()) {
569 return PlannedExecutionStatus::PolicyMismatch;
570 }
571 return PlannedExecutionStatus::Executed;
572}
573
574inline void record_execution_phase_validation_failure(
575 PlannedExecutionStatus status) noexcept {
576#if TESS_DIAGNOSTICS_ENABLED
577 if (status == PlannedExecutionStatus::InvalidPhase) {
578 TESS_DIAG_EVENT(queued_phase_invalid_range);
579 } else {
580 TESS_DIAG_EVENT(queued_phase_failure);
581 }
582#else
583 (void)status;
584#endif
585}
586
587} // namespace detail
588
591 OpHandle handle{};
592 OpId id{};
593 OperationStatus status = OperationStatus::Planned;
594 OperationFailure failure = OperationFailure::None;
595 OperationAccess access{};
596 FieldAccessDesc field_access{};
597 ChunkKey detail_chunk{};
598 OpHandle conflict_handle{};
599 OpId conflict_id{};
600 std::uint32_t conflict_mask = 0;
601 bool has_detail_chunk = false;
602 bool has_conflict = false;
603 std::size_t chunk_count = 0;
604 std::source_location source = std::source_location::current();
605};
606
609 ChunkKey chunk{};
610 std::uint32_t dirty_mask = 0;
611 Box3 bounds{};
612};
613
615enum class PlannedDirtyRecordStatus : std::uint8_t {
616 Recorded,
617 IgnoredEmptyMask,
618 InvalidShape,
619 InvalidChunk,
620};
621static_assert(sizeof(PlannedDirtyRecordStatus) == sizeof(std::uint8_t));
622
624enum class PlannedDirtyMergeStatus : std::uint8_t {
625 Merged,
626 InvalidShape,
627 InvalidChunk,
628};
629static_assert(sizeof(PlannedDirtyMergeStatus) == sizeof(std::uint8_t));
630
632enum class PlannedDirtyCollectStatus : std::uint8_t {
633 Collected,
634 InvalidShape,
635 InvalidChunk,
636};
637static_assert(sizeof(PlannedDirtyCollectStatus) == sizeof(std::uint8_t));
638
641 PlannedDirtyCollectStatus status = PlannedDirtyCollectStatus::Collected;
642 std::size_t record_count = 0;
643
644 [[nodiscard]] constexpr bool ok() const noexcept {
645 return status == PlannedDirtyCollectStatus::Collected;
646 }
647};
648
651 PlannedDirtyMergeStatus status = PlannedDirtyMergeStatus::Merged;
652 std::size_t merged_chunk_count = 0;
653
654 [[nodiscard]] constexpr bool ok() const noexcept {
655 return status == PlannedDirtyMergeStatus::Merged;
656 }
657};
658
661
662namespace detail {
663
664template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
665auto execute_validated_planned_operation_deferred_dirty(
666 World& world, const PlannedOperation& operation,
668
669template <typename World>
670auto merge_planned_dirty_after_exception(
671 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
673
674} // namespace detail
675
676class PlannedDirtyPartitions;
677
680 public:
681 void reserve(std::size_t count) { records_.reserve(count); }
682
683 void clear() noexcept {
684 records_.clear();
685 world_stamp_ = nullptr;
686 }
687
689 template <typename World>
690 auto record(const World& /*world*/, ChunkKey chunk, std::uint32_t dirty_mask,
691 Box3 bounds) -> PlannedDirtyRecordStatus {
692 static_assert(
693 std::is_same_v<typename World::residency_type, AlwaysResident>,
694 "Queued-op dirty recording requires an AlwaysResidentWorld; sparse "
695 "queued-ops support is deferred to a later slice.");
696 if (dirty_mask == 0) {
697 return PlannedDirtyRecordStatus::IgnoredEmptyMask;
698 }
699 if (chunk.value >= World::chunk_count) {
700 return PlannedDirtyRecordStatus::InvalidChunk;
701 }
702
703 if (world_stamp_ != nullptr) {
704 const auto validation =
705 detail::validate_planned_world_stamp<World>(world_stamp_);
706 if (validation == PlannedExecutionStatus::InvalidShape) {
707 return PlannedDirtyRecordStatus::InvalidShape;
708 }
709 if (validation == PlannedExecutionStatus::InvalidChunk) {
710 return PlannedDirtyRecordStatus::InvalidChunk;
711 }
712 }
713
714 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
715 world_stamp_ = detail::planned_world_stamp<World>();
716 return PlannedDirtyRecordStatus::Recorded;
717 }
718
719 [[nodiscard]] auto records() const noexcept
720 -> std::span<const PlannedDirtyRecord> {
721 return records_;
722 }
723
725 template <typename World>
726 [[nodiscard]] auto validation_status(const World& /*world*/) const noexcept
727 -> PlannedDirtyMergeStatus {
728 static_assert(
729 std::is_same_v<typename World::residency_type, AlwaysResident>,
730 "Queued-op dirty validation requires an AlwaysResidentWorld; sparse "
731 "queued-ops support is deferred to a later slice.");
732 if (world_stamp_ == nullptr) {
733 return PlannedDirtyMergeStatus::Merged;
734 }
735 const auto validation =
736 detail::validate_planned_world_stamp<World>(world_stamp_);
737 if (validation == PlannedExecutionStatus::InvalidShape) {
738 return PlannedDirtyMergeStatus::InvalidShape;
739 }
740 if (validation == PlannedExecutionStatus::InvalidChunk) {
741 return PlannedDirtyMergeStatus::InvalidChunk;
742 }
743 return PlannedDirtyMergeStatus::Merged;
744 }
745
746 private:
747 friend class PlannedDirtyPartitions;
748
749 template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
750 friend auto detail::execute_validated_planned_operation_deferred_dirty(
751 World& world, const PlannedOperation& operation,
753
754 template <WritePolicy Policy, typename World, typename Fn>
756 World& world, const PlannedOperation& operation,
758 template <typename World>
759 friend auto merge_planned_dirty(World& world,
760 PlannedDirtyAccumulator& dirty) noexcept
762 template <typename World>
763 friend auto merge_planned_dirty(World& world,
767 PlannedDirtyPartitions& partitions)
769
770 template <typename World>
771 void bind_validated_world(const World& /*world*/) noexcept {
772 if (world_stamp_ == nullptr) {
773 world_stamp_ = detail::planned_world_stamp<World>();
774 }
775 }
776
777 template <typename World>
778 void prepare_for_validated_world(const World& /*world*/) noexcept {
779 records_.clear();
780 world_stamp_ = detail::planned_world_stamp<World>();
781 }
782
783 void record_validated(ChunkKey chunk, std::uint32_t dirty_mask, Box3 bounds) {
784 if (dirty_mask == 0) {
785 return;
786 }
787 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
788 }
789
790 std::vector<PlannedDirtyRecord> records_;
791 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
792};
793
794template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
795auto detail::execute_validated_planned_operation_deferred_dirty(
796 World& world, const PlannedOperation& operation,
798 if constexpr (BindWorld) {
799 if (operation.field_access.dirty_mask != 0) {
800 dirty.bind_validated_world(world);
801 }
802 }
803 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
804
805 std::size_t chunk_count = 0;
806 auto&& callback = fn;
807 ctx.for_each_chunk([&](auto view) {
808 dirty.record_validated(view.key(), operation.field_access.dirty_mask,
809 view.bounds());
810 callback(view);
811 ++chunk_count;
812 });
813
815 PlannedExecutionStatus::Executed,
816 chunk_count,
817 };
818}
819
822 public:
823 void reserve(std::size_t count) { partitions_.reserve(count); }
824
825 void resize(std::size_t count) { partitions_.resize(count); }
826
827 void clear() noexcept { partitions_.clear(); }
828
829 void clear_records() noexcept {
830 for (auto& partition : partitions_) {
831 partition.clear();
832 }
833 }
834
835 void reserve_records_per_partition(std::size_t count) {
836 records_per_partition_reserve_ = count;
837 for (auto& partition : partitions_) {
838 partition.reserve(count);
839 }
840 }
841
842 [[nodiscard]] auto size() const noexcept -> std::size_t {
843 return partitions_.size();
844 }
845
846 [[nodiscard]] auto partition(std::size_t index) noexcept
848 return partitions_[index];
849 }
850
851 [[nodiscard]] auto partition(std::size_t index) const noexcept
852 -> const PlannedDirtyAccumulator& {
853 return partitions_[index];
854 }
855
856 [[nodiscard]] auto partitions() const noexcept
857 -> std::span<const PlannedDirtyAccumulator> {
858 return partitions_;
859 }
860
861 private:
863 PlannedDirtyPartitions& partitions)
865 friend class PlannedPhaseExecutionScratch;
866
867 void prepare(std::size_t count) {
868 partitions_.resize(count);
869 for (auto& partition : partitions_) {
870 partition.clear();
871 partition.reserve(records_per_partition_reserve_);
872 }
873 }
874
875 template <typename World>
876 void prepare(const World& world, std::size_t count) {
877 partitions_.resize(count);
878 for (auto& partition : partitions_) {
879 partition.prepare_for_validated_world(world);
880 partition.reserve(records_per_partition_reserve_);
881 }
882 }
883
884 std::vector<PlannedDirtyAccumulator> partitions_;
885 std::size_t records_per_partition_reserve_ = 0;
886};
887
888namespace detail {
889
890// Scratch-owned phase partitions carry no independent world stamp. The
891// enclosing scratch object owns one capability stamp, and this record-only
892// type cannot be passed to the public dirty-merge APIs for another world.
893class PhaseDirtyPartition {
894 public:
895 void reserve(std::size_t count) { records_.reserve(count); }
896
897 void clear() noexcept { records_.clear(); }
898
899 void record(ChunkKey chunk, std::uint32_t dirty_mask, Box3 bounds) {
900 if (dirty_mask != 0) {
901 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
902 }
903 }
904
905 [[nodiscard]] auto records() const noexcept
906 -> std::span<const PlannedDirtyRecord> {
907 return records_;
908 }
909
910 private:
911 std::vector<PlannedDirtyRecord> records_;
912};
913
914template <WritePolicy Policy, typename World, typename Fn>
915auto execute_validated_phase_operation_deferred_dirty(
916 World& world, const PlannedOperation& operation, PhaseDirtyPartition& dirty,
917 Fn&& fn) -> PlannedExecutionResult {
918 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
919
920 std::size_t chunk_count = 0;
921 auto&& callback = fn;
922 ctx.for_each_chunk([&](auto view) {
923 dirty.record(view.key(), operation.field_access.dirty_mask, view.bounds());
924 callback(view);
925 ++chunk_count;
926 });
927
928 return PlannedExecutionResult{
929 PlannedExecutionStatus::Executed,
930 chunk_count,
931 };
932}
933
934} // namespace detail
935
937template <typename T>
938class ResultChannel;
939
942 public:
943 void reserve_operations(std::size_t count) {
944 dirty_partitions_.reserve(count);
945 results_.reserve(count);
946 }
947
948 void reserve_dirty_records_per_operation(std::size_t count) {
949 records_per_partition_reserve_ = count;
950 for (auto& partition : dirty_partitions_) {
951 partition.reserve(count);
952 }
953 }
954
955 void reserve_merged_dirty_records(std::size_t count) {
956 merged_dirty_.reserve(count);
957 }
958
959 void prepare_for_operation_count(std::size_t count) { prepare(count); }
960
961 void clear() noexcept {
962 for (auto& partition : dirty_partitions_) {
963 partition.clear();
964 }
965 results_.clear();
966 merged_dirty_.clear();
967 world_stamp_ = nullptr;
968 }
969
970 [[nodiscard]] auto operation_count() const noexcept -> std::size_t {
971 return results_.size();
972 }
973
974 [[nodiscard]] auto dirty_partitions() const noexcept
975 -> std::span<const detail::PhaseDirtyPartition> {
976 return dirty_partitions_;
977 }
978
979 private:
980 template <WritePolicy Policy, typename Executor, typename World, typename Fn>
982 Executor&& executor, World& world, const ExecutionPlan& plan,
983 const ExecutionPhase& phase, PlannedPhaseExecutionScratch& scratch,
984 Fn&& fn) -> PlannedExecutionResult;
985
986 template <WritePolicy Policy, typename Executor, typename World, typename T,
987 typename Fn>
989 Executor&& executor, World& world, const ExecutionPlan& plan,
990 const ExecutionPhase& phase, PlannedPhaseExecutionScratch& scratch,
991 ResultChannel<T>& channel, Fn&& fn) -> PlannedExecutionResult;
992
993 template <typename World>
994 friend auto merge_planned_dirty(World& world,
997 template <typename World>
998 friend auto detail::merge_planned_dirty_after_exception(
999 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
1001
1002 void prepare(std::size_t operation_count) {
1003 prepare_partitions(operation_count);
1004 results_.assign(operation_count, PlannedExecutionResult{});
1005 merged_dirty_.clear();
1006 world_stamp_ = nullptr;
1007 }
1008
1009 template <typename World>
1010 void prepare(const World& /*world*/, std::size_t operation_count) {
1011 prepare_partitions(operation_count);
1012 results_.assign(operation_count, PlannedExecutionResult{});
1013 merged_dirty_.clear();
1014 world_stamp_ = detail::planned_world_stamp<World>();
1015 }
1016
1017 [[nodiscard]] auto dirty_for_operation(std::size_t index) noexcept
1018 -> detail::PhaseDirtyPartition& {
1019 return dirty_partitions_[index];
1020 }
1021
1022 void prepare_partitions(std::size_t operation_count) {
1023 dirty_partitions_.resize(operation_count);
1024 for (auto& partition : dirty_partitions_) {
1025 partition.clear();
1026 partition.reserve(records_per_partition_reserve_);
1027 }
1028 }
1029
1030 void record_result(std::size_t index, PlannedExecutionResult result) {
1031 results_[index] = result;
1032 }
1033
1034 [[nodiscard]] auto results() const noexcept
1035 -> std::span<const PlannedExecutionResult> {
1036 return results_;
1037 }
1038
1039 std::vector<detail::PhaseDirtyPartition> dirty_partitions_;
1040 std::vector<PlannedExecutionResult> results_;
1041 PlannedDirtyAccumulator merged_dirty_;
1042 std::size_t records_per_partition_reserve_ = 0;
1043 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
1044};
1045
1048 public:
1049 [[nodiscard]] constexpr auto operations() const noexcept
1050 -> std::span<const OperationReport> {
1051 return {operations_.data(), operations_.size()};
1052 }
1053
1054 [[nodiscard]] constexpr auto plan() const noexcept -> const ExecutionPlan& {
1055 return plan_;
1056 }
1057
1058 [[nodiscard]] constexpr auto find(OpHandle handle) const noexcept
1059 -> const OperationReport* {
1060 for (const auto& op : operations_) {
1061 if (op.handle == handle) {
1062 return &op;
1063 }
1064 }
1065 return nullptr;
1066 }
1067
1068 [[nodiscard]] constexpr bool ok() const noexcept {
1069 return failed_count() == 0;
1070 }
1071
1072 [[nodiscard]] constexpr bool failed() const noexcept {
1073 return failed_count() != 0;
1074 }
1075
1076 [[nodiscard]] constexpr auto planned_count() const noexcept -> std::size_t {
1077 return plan_.size();
1078 }
1079
1080 [[nodiscard]] constexpr auto failed_count() const noexcept -> std::size_t {
1081 std::size_t count = 0;
1082 for (const auto& op : operations_) {
1083 if (op.status != OperationStatus::Planned) {
1084 ++count;
1085 }
1086 }
1087 return count;
1088 }
1089
1090 // Clears all results while keeping every allocation -- report rows,
1091 // planned operations, and their chunk lists (parked in a pool) -- so a
1092 // caller-owned report makes steady-state planning allocation-free
1093 // (audit 2026-07-11 M4).
1094 void reset() {
1095 plan_.bump_generation();
1096 for (auto& planned : plan_.operations_) {
1097 planned.chunks_.clear();
1098 chunk_pool_.push_back(std::move(planned.chunks_));
1099 }
1100 plan_.operations_.clear();
1101 operations_.clear();
1102 }
1103
1104 private:
1105 template <typename World>
1106 friend auto plan_operations(const World& world,
1107 std::span<const QueuedOperation> operations,
1108 ExecutionReport& report)
1109 -> const ExecutionReport&;
1110
1111 void reserve(std::size_t size) {
1112 operations_.reserve(size);
1113 plan_.operations_.reserve(size);
1114 }
1115
1116 void push_report(OperationReport report) { operations_.push_back(report); }
1117
1118 void push_planned(PlannedOperation planned) {
1119 plan_.operations_.push_back(std::move(planned));
1120 }
1121
1122 template <typename World>
1123 [[nodiscard]] auto make_planned(const QueuedOperation& operation,
1124 std::vector<ChunkKey>&& chunks)
1125 -> PlannedOperation {
1126 return PlannedOperation{
1127 operation,
1128 std::move(chunks),
1129 detail::planned_world_stamp<World>(),
1130 };
1131 }
1132
1133 [[nodiscard]] auto acquire_chunks() -> std::vector<ChunkKey> {
1134 if (chunk_pool_.empty()) {
1135 return {};
1136 }
1137 auto chunks = std::move(chunk_pool_.back());
1138 chunk_pool_.pop_back();
1139 return chunks;
1140 }
1141
1142 void recycle_chunks(std::vector<ChunkKey>&& chunks) {
1143 chunks.clear();
1144 chunk_pool_.push_back(std::move(chunks));
1145 }
1146
1147 void recycle_chunks(PlannedOperation&& planned) {
1148 recycle_chunks(std::move(planned.chunks_));
1149 }
1150
1151 std::vector<OperationReport> operations_;
1152 ExecutionPlan plan_;
1153 std::vector<std::vector<ChunkKey>> chunk_pool_;
1154};
1155
1158 public:
1159 [[nodiscard]] auto update_field(
1160 DomainDesc domain, FieldAccessDesc field_access, WritePolicy write_policy,
1161 Priority priority = Priority::GameplayCritical,
1162 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1163 std::source_location source = std::source_location::current())
1164 -> OpHandle {
1165 const auto id = OpId{static_cast<std::uint64_t>(operations_.size())};
1166 const auto handle = OpHandle{id.value};
1167 operations_.push_back(QueuedOperation{
1168 OperationKind::UpdateField,
1169 handle,
1170 id,
1171 std::move(domain),
1172 field_access,
1173 write_policy,
1174 priority,
1175 budget_policy,
1176 source,
1177 });
1178 return handle;
1179 }
1180
1181 [[nodiscard]] auto update_field(
1182 DomainDesc domain, WritePolicy write_policy,
1183 Priority priority = Priority::GameplayCritical,
1184 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1185 std::source_location source = std::source_location::current())
1186 -> OpHandle {
1187 return update_field(std::move(domain), FieldAccessDesc{}, write_policy,
1188 priority, budget_policy, source);
1189 }
1190
1191 [[nodiscard]] constexpr auto operations() const noexcept
1192 -> std::span<const QueuedOperation> {
1193 return {operations_.data(), operations_.size()};
1194 }
1195
1196 [[nodiscard]] constexpr auto operation(OpHandle handle) const noexcept
1197 -> const QueuedOperation* {
1198 if (handle.value >= operations_.size()) {
1199 return nullptr;
1200 }
1201 return &operations_[static_cast<std::size_t>(handle.value)];
1202 }
1203
1204 [[nodiscard]] constexpr bool empty() const noexcept {
1205 return operations_.empty();
1206 }
1207
1208 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
1209 return operations_.size();
1210 }
1211
1212 // Clears queued operations for per-frame reuse while keeping the enqueue
1213 // vector's capacity, so warm frame loops re-enqueue without allocating.
1214 // Previously returned handles are invalidated; handle and id assignment
1215 // restarts at zero on the next enqueue.
1216 void clear() noexcept { operations_.clear(); }
1217
1218 private:
1219 std::vector<QueuedOperation> operations_;
1220};
1221
1222namespace detail {
1223
1224[[nodiscard]] constexpr auto operation_access(
1225 const QueuedOperation& op) noexcept -> OperationAccess {
1226 return OperationAccess{
1227 op.write_policy,
1228 op.domain.kind(),
1229 op.domain.mask(),
1230 };
1231}
1232
1233[[nodiscard]] constexpr bool is_valid_field_access(
1234 WritePolicy write_policy, FieldAccessDesc field_access) noexcept {
1235 if (write_policy == WritePolicy::ReadOnly && field_access.write_mask != 0) {
1236 return false;
1237 }
1238 return true;
1239}
1240
1241template <typename World>
1242[[nodiscard]] auto validate_explicit_chunks(const World& world,
1243 std::span<const ChunkKey> chunks,
1244 ChunkKey& invalid_chunk) noexcept
1245 -> bool {
1246 for (const auto key : chunks) {
1247 if (world.try_chunk(key) == nullptr) {
1248 invalid_chunk = key;
1249 return false;
1250 }
1251 }
1252 return true;
1253}
1254
1255// Fills `chunks` in place (clearing it first) so a caller-supplied vector
1256// keeps its capacity across plans instead of being replaced by a fresh
1257// allocation per operation (audit 2026-07-11 M4).
1258template <typename World>
1259[[nodiscard]] auto expand_domain(const World& world, const DomainDesc& domain,
1260 std::vector<ChunkKey>& chunks,
1261 ChunkKey& invalid_chunk) -> bool {
1262 chunks.clear();
1263 switch (domain.kind()) {
1264 case DomainKind::ExplicitChunks:
1265 if (!validate_explicit_chunks(world, domain.explicit_chunks(),
1266 invalid_chunk)) {
1267 return false;
1268 }
1269 chunks.assign(domain.explicit_chunks().begin(),
1270 domain.explicit_chunks().end());
1271 return true;
1272 case DomainKind::DirtyChunks:
1273 world.collect_dirty_chunks(domain.mask(), chunks);
1274 return true;
1275 case DomainKind::ActiveChunks:
1276 world.collect_active_chunks(domain.mask(), chunks);
1277 return true;
1278 case DomainKind::ResidentChunks:
1279 chunks.reserve(static_cast<std::size_t>(World::chunk_count));
1280 for (std::uint64_t key = 0; key < World::chunk_count; ++key) {
1281 chunks.push_back(ChunkKey{key});
1282 }
1283 return true;
1284 }
1285 return false;
1286}
1287
1288[[nodiscard]] constexpr auto hazard_mask(FieldAccessDesc earlier,
1289 FieldAccessDesc later) noexcept
1290 -> std::uint32_t {
1291 return (earlier.write_mask & later.write_mask) |
1292 (earlier.write_mask & later.read_mask) |
1293 (earlier.read_mask & later.write_mask);
1294}
1295
1296[[nodiscard]] constexpr bool chunks_overlap(
1297 std::span<const ChunkKey> lhs, std::span<const ChunkKey> rhs) noexcept {
1298 std::size_t lhs_index = 0;
1299 std::size_t rhs_index = 0;
1300 while (lhs_index < lhs.size() && rhs_index < rhs.size()) {
1301 const auto lhs_key = lhs[lhs_index].value;
1302 const auto rhs_key = rhs[rhs_index].value;
1303 if (lhs_key == rhs_key) {
1304 return true;
1305 }
1306 if (lhs_key < rhs_key) {
1307 ++lhs_index;
1308 } else {
1309 ++rhs_index;
1310 }
1311 }
1312 return false;
1313}
1314
1315[[nodiscard]] constexpr auto find_hazard(
1316 std::span<const PlannedOperation> earlier_ops,
1317 const PlannedOperation& later) noexcept -> const PlannedOperation* {
1318 for (const auto& earlier : earlier_ops) {
1319 if (hazard_mask(earlier.field_access, later.field_access) == 0) {
1320 continue;
1321 }
1322 if (chunks_overlap(earlier.chunks(), later.chunks())) {
1323 // cppcheck-suppress returnDanglingLifetime
1324 return &earlier;
1325 }
1326 }
1327 return nullptr;
1328}
1329
1330[[nodiscard]] constexpr bool is_parallel_supported_policy(
1331 WritePolicy policy) noexcept {
1332 return policy == WritePolicy::ReadOnly ||
1333 policy == WritePolicy::UniquePerChunk;
1334}
1335
1336[[nodiscard]] constexpr bool is_mutating_policy(WritePolicy policy) noexcept {
1337 return policy != WritePolicy::ReadOnly;
1338}
1339
1340[[nodiscard]] constexpr bool parallel_phase_conflict(
1341 const PlannedOperation& lhs, const PlannedOperation& rhs) noexcept {
1342 if (!chunks_overlap(lhs.chunks(), rhs.chunks())) {
1343 return false;
1344 }
1345 if (is_mutating_policy(lhs.write_policy) ||
1346 is_mutating_policy(rhs.write_policy)) {
1347 return true;
1348 }
1349 return hazard_mask(lhs.field_access, rhs.field_access) != 0;
1350}
1351
1352[[nodiscard]] constexpr auto dirty_axis_end(std::int64_t origin,
1353 std::uint64_t extent) noexcept
1354 -> std::int64_t {
1355 // Saturating: an unguarded origin + int64(extent) is UB for huge
1356 // caller-supplied extents (audit 2026-07-11 C1); share chunk_meta's
1357 // guarded helper.
1358 return detail::box_axis_end(origin, extent);
1359}
1360
1361[[nodiscard]] constexpr auto dirty_min(std::int64_t lhs,
1362 std::int64_t rhs) noexcept
1363 -> std::int64_t {
1364 return lhs < rhs ? lhs : rhs;
1365}
1366
1367[[nodiscard]] constexpr auto dirty_max(std::int64_t lhs,
1368 std::int64_t rhs) noexcept
1369 -> std::int64_t {
1370 return lhs < rhs ? rhs : lhs;
1371}
1372
1373[[nodiscard]] constexpr auto dirty_union_extent(std::int64_t origin,
1374 std::int64_t end) noexcept
1375 -> std::uint64_t {
1376 // end >= origin, but a saturated INT64_MAX end paired with a negative
1377 // origin spans more than int64 can hold, so the subtraction must happen
1378 // in unsigned space (mirrors chunk_meta's union; audit 2026-07-11 C1).
1379 return abs_delta(end, origin);
1380}
1381
1382[[nodiscard]] constexpr auto union_dirty_bounds(Box3 lhs, Box3 rhs) noexcept
1383 -> Box3 {
1384 const auto min_x = dirty_min(lhs.origin.x, rhs.origin.x);
1385 const auto min_y = dirty_min(lhs.origin.y, rhs.origin.y);
1386 const auto min_z = dirty_min(lhs.origin.z, rhs.origin.z);
1387 const auto max_x = dirty_max(dirty_axis_end(lhs.origin.x, lhs.extent.x),
1388 dirty_axis_end(rhs.origin.x, rhs.extent.x));
1389 const auto max_y = dirty_max(dirty_axis_end(lhs.origin.y, lhs.extent.y),
1390 dirty_axis_end(rhs.origin.y, rhs.extent.y));
1391 const auto max_z = dirty_max(dirty_axis_end(lhs.origin.z, lhs.extent.z),
1392 dirty_axis_end(rhs.origin.z, rhs.extent.z));
1393
1394 return Box3{
1395 Coord3{min_x, min_y, min_z},
1396 Extent3{
1397 dirty_union_extent(min_x, max_x),
1398 dirty_union_extent(min_y, max_y),
1399 dirty_union_extent(min_z, max_z),
1400 },
1401 };
1402}
1403
1404} // namespace detail
1405
1410template <typename World>
1411auto plan_operations(const World& world,
1412 std::span<const QueuedOperation> operations,
1413 ExecutionReport& report) -> const ExecutionReport& {
1414 // Queued operations are not yet sparse-aware: expand_domain's ResidentChunks
1415 // case enumerates 0..chunk_count (an OOM on a huge sparse world, and it would
1416 // yield non-resident keys the executor then writes through), so restrict the
1417 // whole planner to always-resident worlds until the sparse queued-ops slice
1418 // ports it. This fails loudly at compile time rather than silently OOMing --
1419 // matching how every other deferred sparse-unsafe family is guarded.
1420 static_assert(
1421 std::is_same_v<typename World::residency_type, AlwaysResident>,
1422 "Queued operations require an AlwaysResidentWorld; sparse queued-ops "
1423 "support is deferred to a later slice.");
1424 report.reset();
1425 report.reserve(operations.size());
1426
1427 for (std::size_t op_index = 0; op_index < operations.size(); ++op_index) {
1428 const auto& op = operations[op_index];
1429 const auto canonical_handle =
1430 OpHandle{static_cast<std::uint64_t>(op_index)};
1431 const auto canonical_id = OpId{static_cast<std::uint64_t>(op_index)};
1432 OperationReport op_report{
1433 canonical_handle,
1434 canonical_id,
1435 OperationStatus::Planned,
1436 OperationFailure::None,
1437 detail::operation_access(op),
1438 op.field_access,
1439 {},
1440 {},
1441 {},
1442 0,
1443 false,
1444 false,
1445 0,
1446 op.source,
1447 };
1448
1449 if (op.handle != canonical_handle || op.id != canonical_id) {
1450 op_report.status = OperationStatus::InvalidIdentity;
1451 op_report.failure = op.handle != canonical_handle
1452 ? OperationFailure::NonDenseHandle
1453 : OperationFailure::NonDenseId;
1454 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
1455 "invalid_identity", op_index);
1456 report.push_report(op_report);
1457 continue;
1458 }
1459
1460 if (!is_valid_write_policy(op.write_policy)) {
1461 op_report.status = OperationStatus::InvalidWritePolicy;
1462 op_report.failure = OperationFailure::InvalidWritePolicyValue;
1463 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
1464 "invalid_write_policy", op_index);
1465 report.push_report(op_report);
1466 continue;
1467 }
1468
1469 if (!detail::is_valid_field_access(op.write_policy, op.field_access)) {
1470 op_report.status = OperationStatus::InvalidFieldAccess;
1471 op_report.failure = OperationFailure::ReadOnlyWriteMask;
1472 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
1473 "invalid_field_access", op_index);
1474 report.push_report(op_report);
1475 continue;
1476 }
1477
1478 auto planned_chunks = report.acquire_chunks();
1479 ChunkKey invalid_chunk{};
1480 if (!detail::expand_domain(world, op.domain, planned_chunks,
1481 invalid_chunk)) {
1482 op_report.status = OperationStatus::InvalidDomain;
1483 op_report.failure = OperationFailure::ExplicitChunkOutOfRange;
1484 op_report.detail_chunk = invalid_chunk;
1485 op_report.has_detail_chunk = true;
1486 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
1487 "invalid_domain", op_index);
1488 report.recycle_chunks(std::move(planned_chunks));
1489 report.push_report(op_report);
1490 continue;
1491 }
1492
1493 auto planned =
1494 report.template make_planned<World>(op, std::move(planned_chunks));
1495
1496 if (const auto* conflict =
1497 detail::find_hazard(report.plan().operations(), planned);
1498 conflict != nullptr) {
1499 op_report.status = OperationStatus::HazardConflict;
1500 op_report.failure = OperationFailure::FieldHazardConflict;
1501 op_report.conflict_handle = conflict->handle;
1502 op_report.conflict_id = conflict->id;
1503 op_report.conflict_mask =
1504 detail::hazard_mask(conflict->field_access, planned.field_access);
1505 op_report.has_conflict = true;
1506 op_report.chunk_count = planned.chunks().size();
1507 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "conflict",
1508 op_index);
1509 report.recycle_chunks(std::move(planned));
1510 report.push_report(op_report);
1511 continue;
1512 }
1513
1514 op_report.chunk_count = planned.chunks().size();
1515 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "planned",
1516 op_index);
1517 report.push_planned(std::move(planned));
1518 report.push_report(op_report);
1519 }
1520
1521 return report;
1522}
1523
1524template <typename World>
1526[[nodiscard]] auto plan_operations(const World& world,
1527 std::span<const QueuedOperation> operations)
1528 -> ExecutionReport {
1529 ExecutionReport report;
1530 plan_operations(world, operations, report);
1531 return report;
1532}
1533
1534template <typename World>
1536auto plan_operations(const World& world, const FrameOps& ops,
1537 ExecutionReport& report) -> const ExecutionReport& {
1538 return plan_operations(world, ops.operations(), report);
1539}
1540
1541template <typename World>
1543[[nodiscard]] auto plan_operations(const World& world, const FrameOps& ops)
1544 -> ExecutionReport {
1545 return plan_operations(world, ops.operations());
1546}
1547
1549[[nodiscard]] constexpr auto planned_chunk_domain(
1550 const PlannedOperation& operation) noexcept -> ChunkDomain {
1551 return chunk_domain(operation.chunks());
1552}
1553
1555[[nodiscard]] inline auto plan_parallel_execution_phases(
1556 const ExecutionPlan& plan) -> ExecutionPhasePlan {
1557 const auto operations = plan.operations();
1558 auto phases = ExecutionPhasePlan{};
1559 phases.reserve(operations.size());
1560
1561 for (std::size_t i = 0; i < operations.size(); ++i) {
1562 const auto& operation = operations[i];
1563 if (!detail::is_parallel_supported_policy(operation.write_policy)) {
1564 phases.status_ = ExecutionPhaseStatus::UnsupportedWritePolicy;
1565 phases.failed_operation_index_ = i;
1566 phases.failed_write_policy_ = operation.write_policy;
1567 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
1568 "unsupported_write_policy", i);
1569 return phases;
1570 }
1571
1572 if (phases.phases_.empty()) {
1573 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "new_phase",
1574 i);
1575 phases.push_phase(plan, i, 1, operation);
1576 continue;
1577 }
1578
1579 const auto& phase = phases.phases_.back();
1580 auto conflicts = false;
1581 const auto end = phase.first_operation() + phase.operation_count();
1582 for (std::size_t j = phase.first_operation(); j < end; ++j) {
1583 if (detail::parallel_phase_conflict(operations[j], operation)) {
1584 conflicts = true;
1585 break;
1586 }
1587 }
1588
1589 if (conflicts) {
1590 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "new_phase",
1591 i);
1592 phases.push_phase(plan, i, 1, operation);
1593 } else {
1594 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "merged", i);
1595 phases.extend_last_phase(operation);
1596 }
1597 }
1598
1599 return phases;
1600}
1601
1606template <typename World>
1609 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
1610 "Queued-op dirty merge requires an AlwaysResidentWorld; sparse "
1611 "queued-ops support is deferred to a later slice.");
1612
1613 const auto validation = dirty.validation_status(world);
1614 if (validation != PlannedDirtyMergeStatus::Merged) {
1616 validation,
1617 0,
1618 };
1619 }
1620
1621 auto& records = dirty.records_;
1622 std::sort(records.begin(), records.end(),
1624 return lhs.chunk.value < rhs.chunk.value;
1625 });
1626
1627 // Sorting makes duplicate records adjacent so they can be coalesced without
1628 // allocating. This noexcept path cannot replace a callback exception while
1629 // AutoExec unwinds.
1630 auto merged_count = std::size_t{0};
1631 for (std::size_t i = 0; i < records.size();) {
1632 auto chunk = records[i].chunk;
1633 auto dirty_mask = records[i].dirty_mask;
1634 auto bounds = records[i].bounds;
1635 ++i;
1636
1637 while (i < records.size() && records[i].chunk == chunk) {
1638 dirty_mask |= records[i].dirty_mask;
1639 bounds = detail::union_dirty_bounds(bounds, records[i].bounds);
1640 ++i;
1641 }
1642
1643 world.mark_dirty(chunk, dirty_mask, bounds);
1644 ++merged_count;
1645 }
1646
1647 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
1648 dirty.clear();
1650 PlannedDirtyMergeStatus::Merged,
1651 merged_count,
1652 };
1653}
1654
1657 PlannedDirtyPartitions& partitions)
1659 auto* world_stamp = dirty.world_stamp_;
1660 for (const auto& partition : partitions.partitions_) {
1661 const auto* partition_stamp = partition.world_stamp_;
1662 if (partition_stamp == nullptr) {
1663 continue;
1664 }
1665 if (world_stamp == nullptr) {
1666 world_stamp = partition_stamp;
1667 continue;
1668 }
1669 if (world_stamp->shape_identity != partition_stamp->shape_identity) {
1671 PlannedDirtyCollectStatus::InvalidShape,
1672 0,
1673 };
1674 }
1675 if (world_stamp->chunk_limit != partition_stamp->chunk_limit) {
1677 PlannedDirtyCollectStatus::InvalidChunk,
1678 0,
1679 };
1680 }
1681 }
1682
1683 auto required_capacity = dirty.records_.size();
1684 auto record_count = std::size_t{0};
1685 for (const auto& partition : partitions.partitions_) {
1686 const auto partition_size = partition.records_.size();
1687 if (partition_size > dirty.records_.max_size() - required_capacity) {
1688 throw std::length_error{"planned dirty record count exceeds max_size"};
1689 }
1690 required_capacity += partition_size;
1691 record_count += partition_size;
1692 }
1693 // Complete the only potentially allocating step before clearing a source;
1694 // a failed reserve therefore preserves both destination and partitions.
1695 dirty.records_.reserve(required_capacity);
1696
1697 for (auto& partition : partitions.partitions_) {
1698 if (partition.world_stamp_ != nullptr) {
1699 dirty.world_stamp_ = partition.world_stamp_;
1700 }
1701 dirty.records_.insert(dirty.records_.end(), partition.records_.begin(),
1702 partition.records_.end());
1703 partition.clear();
1704 }
1705 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
1707 PlannedDirtyCollectStatus::Collected,
1708 record_count,
1709 };
1710}
1711
1712template <typename World>
1714auto merge_planned_dirty(World& world, PlannedDirtyPartitions& partitions,
1715 PlannedDirtyAccumulator& dirty_scratch)
1717 for (const auto& partition : partitions.partitions()) {
1718 const auto validation = partition.validation_status(world);
1719 if (validation != PlannedDirtyMergeStatus::Merged) {
1720 return PlannedDirtyMergeResult{validation, 0};
1721 }
1722 }
1723 dirty_scratch.clear();
1724 const auto collected = collect_planned_dirty(dirty_scratch, partitions);
1725 if (!collected.ok()) {
1727 collected.status == PlannedDirtyCollectStatus::InvalidShape
1728 ? PlannedDirtyMergeStatus::InvalidShape
1729 : PlannedDirtyMergeStatus::InvalidChunk,
1730 0,
1731 };
1732 }
1733 return merge_planned_dirty(world, dirty_scratch);
1734}
1735
1736template <typename World>
1740 if (scratch.world_stamp_ == nullptr) {
1742 PlannedDirtyMergeStatus::Merged,
1743 0,
1744 };
1745 }
1746 const auto validation =
1747 detail::validate_planned_world_stamp<World>(scratch.world_stamp_);
1748 if (validation != PlannedExecutionStatus::Executed) {
1750 validation == PlannedExecutionStatus::InvalidShape
1751 ? PlannedDirtyMergeStatus::InvalidShape
1752 : PlannedDirtyMergeStatus::InvalidChunk,
1753 0,
1754 };
1755 }
1756
1757 auto& merged = scratch.merged_dirty_;
1758 auto record_count = std::size_t{0};
1759 for (const auto& partition : scratch.dirty_partitions_) {
1760 const auto partition_size = partition.records().size();
1761 if (partition_size > merged.records_.max_size() - record_count) {
1762 throw std::length_error{"planned dirty record count exceeds max_size"};
1763 }
1764 record_count += partition_size;
1765 }
1766 merged.clear();
1767 // Reserve before transferring anything, so allocation failure leaves every
1768 // phase partition available to the caller.
1769 merged.records_.reserve(record_count);
1770 merged.world_stamp_ = scratch.world_stamp_;
1771 for (auto& partition : scratch.dirty_partitions_) {
1772 const auto records = partition.records();
1773 merged.records_.insert(merged.records_.end(), records.begin(),
1774 records.end());
1775 partition.clear();
1776 }
1777 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
1778 (void)record_count;
1779 return merge_planned_dirty(world, merged);
1780}
1781
1783template <typename World>
1784auto detail::merge_planned_dirty_after_exception(
1785 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
1787 if (scratch.world_stamp_ == nullptr) {
1789 PlannedDirtyMergeStatus::Merged,
1790 0,
1791 };
1792 }
1793 const auto validation =
1794 detail::validate_planned_world_stamp<World>(scratch.world_stamp_);
1795 if (validation != PlannedExecutionStatus::Executed) {
1797 validation == PlannedExecutionStatus::InvalidShape
1798 ? PlannedDirtyMergeStatus::InvalidShape
1799 : PlannedDirtyMergeStatus::InvalidChunk,
1800 0,
1801 };
1802 }
1803
1804 // This cold path must preserve the original callback exception. Coalesce
1805 // with an allocation-free quadratic scan: exceptions are rare, and normal
1806 // phase sizes should not dictate whether failed work remains observable.
1807 auto record_count = std::size_t{0};
1808 for (const auto& partition : scratch.dirty_partitions_) {
1809 const auto partition_size = partition.records().size();
1810 if (partition_size >
1811 std::numeric_limits<std::size_t>::max() - record_count) {
1812 record_count = std::numeric_limits<std::size_t>::max();
1813 break;
1814 }
1815 record_count += partition_size;
1816 }
1817 auto merged_count = std::size_t{0};
1818 for (std::size_t partition_index = 0;
1819 partition_index < scratch.dirty_partitions_.size(); ++partition_index) {
1820 const auto records = scratch.dirty_partitions_[partition_index].records();
1821 for (std::size_t record_index = 0; record_index < records.size();
1822 ++record_index) {
1823 const auto record = records[record_index];
1824 auto appeared_earlier = false;
1825 for (std::size_t earlier_partition = 0;
1826 earlier_partition <= partition_index && !appeared_earlier;
1827 ++earlier_partition) {
1828 const auto earlier_records =
1829 scratch.dirty_partitions_[earlier_partition].records();
1830 const auto earlier_count = earlier_partition == partition_index
1831 ? record_index
1832 : earlier_records.size();
1833 for (std::size_t earlier_index = 0; earlier_index < earlier_count;
1834 ++earlier_index) {
1835 if (earlier_records[earlier_index].chunk == record.chunk) {
1836 appeared_earlier = true;
1837 break;
1838 }
1839 }
1840 }
1841 if (appeared_earlier) {
1842 continue;
1843 }
1844
1845 auto dirty_mask = record.dirty_mask;
1846 auto bounds = record.bounds;
1847 for (std::size_t later_partition = partition_index;
1848 later_partition < scratch.dirty_partitions_.size();
1849 ++later_partition) {
1850 const auto later_records =
1851 scratch.dirty_partitions_[later_partition].records();
1852 const auto first_later = later_partition == partition_index
1853 ? record_index + 1
1854 : std::size_t{0};
1855 for (std::size_t later_index = first_later;
1856 later_index < later_records.size(); ++later_index) {
1857 const auto later = later_records[later_index];
1858 if (later.chunk == record.chunk) {
1859 dirty_mask |= later.dirty_mask;
1860 bounds = detail::union_dirty_bounds(bounds, later.bounds);
1861 }
1862 }
1863 }
1864 world.mark_dirty(record.chunk, dirty_mask, bounds);
1865 ++merged_count;
1866 }
1867 }
1868 for (auto& partition : scratch.dirty_partitions_) {
1869 partition.clear();
1870 }
1871 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
1872 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
1873 (void)record_count;
1874 (void)merged_count;
1876 PlannedDirtyMergeStatus::Merged,
1877 merged_count,
1878 };
1879}
1880
1882template <WritePolicy Policy>
1883[[nodiscard]] constexpr bool planned_policy_matches(
1884 const PlannedOperation& operation) noexcept {
1885 return operation.write_policy == Policy;
1886}
1887
1889template <WritePolicy Policy, typename World>
1890[[nodiscard]] auto validate_planned_operation(
1891 const World& world, const PlannedOperation& operation) noexcept
1892 -> PlannedExecutionStatus {
1893 const auto world_status = operation.world_validation_status(world);
1894 if (world_status != PlannedExecutionStatus::Executed) {
1895 return world_status;
1896 }
1897 if (!planned_policy_matches<Policy>(operation)) {
1898 return PlannedExecutionStatus::PolicyMismatch;
1899 }
1900 return PlannedExecutionStatus::Executed;
1901}
1902
1904template <WritePolicy Policy, typename World>
1905[[nodiscard]] constexpr auto try_planned_block_ctx(
1906 World& world, const PlannedOperation& operation) noexcept
1907 -> std::optional<BlockCtx<World, Policy>> {
1908 static_assert(
1909 std::is_same_v<typename World::residency_type, AlwaysResident>,
1910 "Queued-op execution requires an AlwaysResidentWorld; sparse queued-ops "
1911 "support is deferred to a later slice.");
1912 if (validate_planned_operation<Policy>(world, operation) !=
1913 PlannedExecutionStatus::Executed) {
1914 return std::nullopt;
1915 }
1916 return block_ctx<Policy>(world, planned_chunk_domain(operation));
1917}
1918
1920template <WritePolicy Policy, typename World, typename Fn>
1921auto execute_planned_operation(World& world, const PlannedOperation& operation,
1922 Fn&& fn) -> PlannedExecutionResult {
1923 const auto validation = validate_planned_operation<Policy>(world, operation);
1924 if (validation != PlannedExecutionStatus::Executed) {
1926 validation,
1927 0,
1928 };
1929 }
1930 auto ctx = block_ctx<Policy>(world, planned_chunk_domain(operation));
1931
1932 std::size_t chunk_count = 0;
1933 auto&& callback = fn;
1934 ctx.for_each_chunk([&](auto view) {
1935 if (operation.field_access.dirty_mask != 0) {
1936 world.mark_dirty(view.key(), operation.field_access.dirty_mask,
1937 view.bounds());
1938 }
1939 callback(view);
1940 ++chunk_count;
1941 });
1942
1944 PlannedExecutionStatus::Executed,
1945 chunk_count,
1946 };
1947}
1948
1950template <WritePolicy Policy, typename World, typename Fn>
1952 const PlannedOperation& operation,
1954 Fn&& fn)
1956 const auto validation = validate_planned_operation<Policy>(world, operation);
1957 if (validation != PlannedExecutionStatus::Executed) {
1959 validation,
1960 0,
1961 };
1962 }
1963 if (operation.field_access.dirty_mask != 0) {
1964 const auto dirty_validation = dirty.validation_status(world);
1965 if (dirty_validation != PlannedDirtyMergeStatus::Merged) {
1967 dirty_validation == PlannedDirtyMergeStatus::InvalidShape
1968 ? PlannedExecutionStatus::InvalidShape
1969 : PlannedExecutionStatus::InvalidChunk,
1970 0,
1971 };
1972 }
1973 }
1974 return detail::execute_validated_planned_operation_deferred_dirty<true,
1975 Policy>(
1976 world, operation, dirty, std::forward<Fn>(fn));
1977}
1978
1983template <WritePolicy Policy, typename World, typename Fn>
1984auto execute_plan(World& world, const ExecutionPlan& plan, Fn&& fn)
1986 std::size_t chunk_count = 0;
1987 auto&& callback = fn;
1988 for (const auto& operation : plan.operations()) {
1989 auto result = execute_planned_operation<Policy>(world, operation, callback);
1990 if (result.status != PlannedExecutionStatus::Executed) {
1992 result.status,
1993 chunk_count + result.chunk_count,
1994 };
1995 }
1996 chunk_count += result.chunk_count;
1997 }
1999 PlannedExecutionStatus::Executed,
2000 chunk_count,
2001 };
2002}
2003
2005template <WritePolicy Policy, typename World, typename Fn>
2006auto execute_plan_deferred_dirty(World& world, const ExecutionPlan& plan,
2007 PlannedDirtyAccumulator& dirty, Fn&& fn)
2009 std::size_t chunk_count = 0;
2010 auto&& callback = fn;
2011 for (const auto& operation : plan.operations()) {
2012 auto result = execute_planned_operation_deferred_dirty<Policy>(
2013 world, operation, dirty, callback);
2014 if (result.status != PlannedExecutionStatus::Executed) {
2016 result.status,
2017 chunk_count + result.chunk_count,
2018 };
2019 }
2020 chunk_count += result.chunk_count;
2021 }
2023 PlannedExecutionStatus::Executed,
2024 chunk_count,
2025 };
2026}
2027
2028template <WritePolicy Policy, typename Executor, typename World, typename Fn>
2031auto execute_phase_deferred_dirty_with(Executor&& executor, World& world,
2032 const ExecutionPlan& plan,
2033 const ExecutionPhase& phase,
2034 PlannedDirtyAccumulator& dirty, Fn&& fn)
2036 const auto operations = plan.operations();
2037 const auto phase_validation =
2038 detail::execution_phase_validation_status<Policy>(world, plan, phase);
2039 if (phase_validation != PlannedExecutionStatus::Executed) {
2040 detail::record_execution_phase_validation_failure(phase_validation);
2042 phase_validation,
2043 0,
2044 };
2045 }
2046 const auto dirty_validation = dirty.validation_status(world);
2047 if (dirty_validation != PlannedDirtyMergeStatus::Merged) {
2048 TESS_DIAG_EVENT(queued_phase_failure);
2050 dirty_validation == PlannedDirtyMergeStatus::InvalidShape
2051 ? PlannedExecutionStatus::InvalidShape
2052 : PlannedExecutionStatus::InvalidChunk,
2053 0,
2054 };
2055 }
2056
2057 TESS_DIAG_EVENT_VALUE(queued_phase_execute, phase.operation_count());
2058 std::size_t chunk_count = 0;
2059 auto&& callback = fn;
2060 auto result = execute_operation_index_range(
2061 std::forward<Executor>(executor), executor_phase_range(phase),
2062 [&](std::size_t index) {
2063 auto operation_result =
2064 detail::execute_validated_planned_operation_deferred_dirty<true,
2065 Policy>(
2066 world, operations[index], dirty, callback);
2067 if (operation_result.status == PlannedExecutionStatus::Executed) {
2068 chunk_count += operation_result.chunk_count;
2069 }
2070 return operation_result;
2071 });
2072 if (result.status != PlannedExecutionStatus::Executed) {
2073 TESS_DIAG_EVENT(queued_phase_failure);
2074 result.chunk_count = chunk_count;
2075 return result;
2076 }
2077
2079 PlannedExecutionStatus::Executed,
2080 chunk_count,
2081 };
2082}
2083
2085template <WritePolicy Policy, typename Executor, typename World, typename Fn>
2086auto execute_phase_partitioned_dirty_with(Executor&& executor, World& world,
2087 const ExecutionPlan& plan,
2088 const ExecutionPhase& phase,
2090 Fn&& fn) -> PlannedExecutionResult {
2091 const auto operations = plan.operations();
2092 const auto phase_validation =
2093 detail::execution_phase_validation_status<Policy>(world, plan, phase);
2094 if (phase_validation != PlannedExecutionStatus::Executed) {
2095 detail::record_execution_phase_validation_failure(phase_validation);
2097 phase_validation,
2098 0,
2099 };
2100 }
2101
2102 TESS_DIAG_EVENT_VALUE(queued_phase_execute, phase.operation_count());
2103 TESS_DIAG_EVENT_VALUE(queued_partitioned_phase, phase.operation_count());
2104 scratch.prepare(world, phase.operation_count());
2105 auto&& callback = fn;
2106 auto result = execute_operation_index_range(
2107 std::forward<Executor>(executor), executor_phase_range(phase),
2108 [&](std::size_t index) {
2109 const auto offset = index - phase.first_operation();
2110 auto operation_result =
2111 detail::execute_validated_phase_operation_deferred_dirty<Policy>(
2112 world, operations[index], scratch.dirty_for_operation(offset),
2113 callback);
2114 scratch.record_result(offset, operation_result);
2115 return operation_result;
2116 });
2117
2118 std::size_t chunk_count = 0;
2119 for (const auto operation_result : scratch.results()) {
2120 if (operation_result.status != PlannedExecutionStatus::Executed) {
2121 TESS_DIAG_EVENT(queued_phase_failure);
2123 operation_result.status,
2124 chunk_count,
2125 };
2126 }
2127 chunk_count += operation_result.chunk_count;
2128 }
2129
2130 if (result.status != PlannedExecutionStatus::Executed) {
2131 TESS_DIAG_EVENT(queued_phase_failure);
2133 result.status,
2134 chunk_count,
2135 };
2136 }
2137
2139 PlannedExecutionStatus::Executed,
2140 chunk_count,
2141 };
2142}
2143
2145template <WritePolicy Policy, typename World, typename Fn>
2146auto execute_phase_deferred_dirty(World& world, const ExecutionPlan& plan,
2147 const ExecutionPhase& phase,
2148 PlannedDirtyAccumulator& dirty, Fn&& fn)
2150 const SerialPhaseExecutor executor;
2151 return execute_phase_deferred_dirty_with<Policy>(executor, world, plan, phase,
2152 dirty, std::forward<Fn>(fn));
2153}
2154
2155} // namespace tess
Definition block.h:170
Definition queued.h:146
Definition queued.h:498
friend auto plan_parallel_execution_phases(const ExecutionPlan &plan) -> ExecutionPhasePlan
Definition queued.h:1555
Definition queued.h:416
constexpr bool policy_matches() const noexcept
Definition queued.h:450
auto world_validation_status(const World &) const noexcept -> PlannedExecutionStatus
Definition queued.h:439
constexpr bool belongs_to(const ExecutionPlan &plan) const noexcept
Definition queued.h:432
Definition queued.h:358
Definition queued.h:1047
friend auto plan_operations(const World &world, std::span< const QueuedOperation > operations, ExecutionReport &report) -> const ExecutionReport &
Definition queued.h:1411
Definition queued.h:1157
Definition queued.h:679
friend auto collect_planned_dirty(PlannedDirtyAccumulator &dirty, PlannedDirtyPartitions &partitions) -> PlannedDirtyCollectResult
Definition queued.h:1656
auto validation_status(const World &) const noexcept -> PlannedDirtyMergeStatus
Definition queued.h:726
friend auto merge_planned_dirty(World &world, PlannedDirtyAccumulator &dirty) noexcept -> PlannedDirtyMergeResult
Definition queued.h:1607
auto record(const World &, ChunkKey chunk, std::uint32_t dirty_mask, Box3 bounds) -> PlannedDirtyRecordStatus
Definition queued.h:690
friend auto execute_planned_operation_deferred_dirty(World &world, const PlannedOperation &operation, PlannedDirtyAccumulator &dirty, Fn &&fn) -> PlannedExecutionResult
Definition queued.h:1951
Definition queued.h:821
friend auto collect_planned_dirty(PlannedDirtyAccumulator &dirty, PlannedDirtyPartitions &partitions) -> PlannedDirtyCollectResult
Definition queued.h:1656
Definition queued.h:245
constexpr auto chunks() const noexcept -> std::span< const ChunkKey >
Definition queued.h:272
auto world_validation_status(const World &) const noexcept -> PlannedExecutionStatus
Definition queued.h:279
static auto create(const World &world, const QueuedOperation &operation, std::span< const ChunkKey > chunks) -> PlannedOperationCreateResult
Definition queued.h:321
Definition queued.h:941
friend auto execute_phase_partitioned_dirty_with_results(Executor &&executor, World &world, const ExecutionPlan &plan, const ExecutionPhase &phase, PlannedPhaseExecutionScratch &scratch, ResultChannel< T > &channel, Fn &&fn) -> PlannedExecutionResult
Executes one phase while publishing per-operation payloads and completions.
Definition result_channel.h:293
friend auto execute_phase_partitioned_dirty_with(Executor &&executor, World &world, const ExecutionPlan &plan, const ExecutionPhase &phase, PlannedPhaseExecutionScratch &scratch, Fn &&fn) -> PlannedExecutionResult
Definition queued.h:2086
friend auto merge_planned_dirty(World &world, PlannedPhaseExecutionScratch &scratch) -> PlannedDirtyMergeResult
Definition queued.h:1738
Dense per-operation completion and payload channel.
Definition result_channel.h:92
Definition world.h:22
Definition phase_executor.h:126
Definition shape.h:75
Definition shape.h:67
Definition phase_executor.h:52
Definition queued.h:204
Definition queued.h:74
Definition queued.h:67
Definition queued.h:227
Definition queued.h:590
Definition queued.h:640
Definition queued.h:650
Definition queued.h:608
Definition phase_executor.h:63
Definition queued.h:313
Definition queued.h:214
Definition phase_executor.h:95