tess 1.0.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/assert.h>
5#include <tess/core/capacity.h>
6#include <tess/core/shape.h>
7#include <tess/core/tag_identity.h>
8#include <tess/diagnostics/diagnostics.h>
9#include <tess/diagnostics/trace.h>
10#include <tess/ops/phase_executor.h>
11#include <tess/storage/world.h>
12
13#include <algorithm>
14#include <cstddef>
15#include <cstdint>
16#include <limits>
17#include <optional>
18#include <source_location>
19#include <span>
20#include <stdexcept>
21#include <type_traits>
22#include <utility>
23#include <vector>
24
25namespace tess {
26
27namespace detail {
28
29struct PlannedWorldStamp {
30 std::uintptr_t shape_identity = 0;
31 std::uint64_t chunk_limit = 0;
32};
33
34template <typename Shape, std::uint64_t ChunkLimit>
35[[nodiscard]] inline auto planned_world_stamp() noexcept
36 -> const PlannedWorldStamp* {
37 static const auto stamp = PlannedWorldStamp{
38 tag_identity<Shape>(),
39 ChunkLimit,
40 };
41 return &stamp;
42}
43
44template <typename World>
45[[nodiscard]] inline auto planned_world_stamp() noexcept
46 -> const PlannedWorldStamp* {
47 return planned_world_stamp<typename World::shape_type, World::chunk_count>();
48}
49
50template <typename World>
51[[nodiscard]] inline auto validate_planned_world_stamp(
52 const PlannedWorldStamp* stamp) noexcept -> PlannedExecutionStatus {
53 const auto* expected = planned_world_stamp<World>();
54 if (stamp == expected) {
55 return PlannedExecutionStatus::Executed;
56 }
57 if (stamp == nullptr || stamp->shape_identity != expected->shape_identity) {
58 return PlannedExecutionStatus::InvalidShape;
59 }
60 if (stamp->chunk_limit != expected->chunk_limit) {
61 return PlannedExecutionStatus::InvalidChunk;
62 }
63 return PlannedExecutionStatus::Executed;
64}
65
66} // namespace detail
67
69struct OpId {
70 std::uint64_t value = 0;
71
72 friend constexpr bool operator==(OpId lhs, OpId rhs) noexcept = default;
73};
74
76struct OpHandle {
77 std::uint64_t value = 0;
78
79 friend constexpr bool operator==(OpHandle lhs,
80 OpHandle rhs) noexcept = default;
81};
82
84enum class OperationKind : std::uint8_t {
85 UpdateField,
86 QueryPaths,
87 QueryNearest,
88 BuildFieldProduct,
89 MoveEntities,
90 RebuildTopology,
91 EnsureResident,
92 MarkDirty,
93 PublishRenderDeltas,
94};
95static_assert(sizeof(OperationKind) == sizeof(std::uint8_t));
96
98enum class BackendEligibility : std::uint8_t {
99 CpuOnly,
100 CpuOrGpu,
101};
102
104enum class ExactnessRequirement : std::uint8_t {
105 Exact,
106 ApproximateAllowed,
107};
108
111 std::uint64_t world = 0;
112 std::uint64_t topology = 0;
113 std::uint64_t fields = 0;
114 std::uint64_t product = 0;
115
116 friend constexpr bool operator==(IntentVersions lhs,
117 IntentVersions rhs) noexcept = default;
118};
119
122 DirtyMask dirty_mask{};
123 bool topology = false;
124 bool paths = false;
125 bool render = false;
126
127 friend constexpr bool operator==(IntentInvalidations lhs,
128 IntentInvalidations rhs) noexcept = default;
129};
130
139 template <typename T, std::size_t Extent>
140 [[nodiscard]] static auto from(std::span<T, Extent> values) noexcept
142 using Item = std::remove_cv_t<T>;
143 static_assert(std::is_object_v<Item>);
144 static_assert(!std::is_volatile_v<T>);
145 return IntentPayloadView{values.data(), values.size(), sizeof(Item),
146 detail::tag_identity<Item>()};
147 }
148
160 template <typename T>
161 [[nodiscard]] auto holds() const noexcept -> bool {
162 using Item = std::remove_cv_t<T>;
163 static_assert(std::is_object_v<Item>);
164 return item_size == sizeof(Item) &&
165 type_identity == detail::tag_identity<Item>();
166 }
167
176 [[nodiscard]] auto bound() const noexcept -> bool {
177 return type_identity != 0;
178 }
179
206 template <typename T>
207 [[nodiscard]] auto as() const noexcept -> std::span<const T> {
208 using Item = std::remove_cv_t<T>;
209 static_assert(std::is_object_v<Item>);
210 TESS_ASSERT_MSG(holds<Item>(),
211 "IntentPayloadView::as<T> on a payload that does not "
212 "hold T; check holds<T>() first");
213 if (!holds<Item>()) {
214 return {};
215 }
216 return {static_cast<const T*>(data), count};
217 }
218
219 const void* data = nullptr;
220 std::size_t count = 0;
221 std::size_t item_size = 0;
222 std::uintptr_t type_identity = 0;
223};
224
226enum class Priority : std::uint8_t {
227 Immediate,
228 GameplayCritical,
229 VisibleSoon,
230 Background,
231 Maintenance,
232};
233static_assert(sizeof(Priority) == sizeof(std::uint8_t));
234
236enum class BudgetPolicy : std::uint8_t {
237 MustRun,
238 CanDefer,
239 CanSkipIfSuperseded,
240 BudgetedIncremental,
241};
242static_assert(sizeof(BudgetPolicy) == sizeof(std::uint8_t));
243
245enum class OperationStatus : std::uint8_t {
246 Planned,
247 InvalidIdentity,
248 InvalidWritePolicy,
249 InvalidDomain,
250 InvalidFieldAccess,
251 HazardConflict,
252};
253static_assert(sizeof(OperationStatus) == sizeof(std::uint8_t));
254
256enum class OperationFailure : std::uint8_t {
257 None,
258 NonDenseHandle,
259 NonDenseId,
260 InvalidWritePolicyValue,
261 ExplicitChunkOutOfRange,
262 ReadOnlyWriteMask,
263 FieldHazardConflict,
264};
265static_assert(sizeof(OperationFailure) == sizeof(std::uint8_t));
266
268enum class ExecutionPhaseStatus : std::uint8_t {
269 Ready,
270 UnsupportedWritePolicy,
271};
272static_assert(sizeof(ExecutionPhaseStatus) == sizeof(std::uint8_t));
273
275enum class DomainKind : std::uint8_t {
276 ExplicitChunks,
277 DirtyChunks,
278 ActiveChunks,
279 ResidentChunks,
280};
281static_assert(sizeof(DomainKind) == sizeof(std::uint8_t));
282
284class DomainDesc {
285 public:
286 // Explicit chunk keys are stored sorted and deduplicated so a planned
287 // operation never visits one chunk twice: repeated keys under
288 // UniquePerChunk would otherwise defeat the per-chunk ownership rule
289 // that parallel phase planning relies on.
290 [[nodiscard]] static auto explicit_chunks(std::span<const ChunkKey> keys)
291 -> DomainDesc {
292 DomainDesc desc{DomainKind::ExplicitChunks};
293 desc.explicit_chunks_.assign(keys.begin(), keys.end());
294 std::sort(desc.explicit_chunks_.begin(), desc.explicit_chunks_.end(),
295 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
296 desc.explicit_chunks_.erase(
297 std::unique(desc.explicit_chunks_.begin(), desc.explicit_chunks_.end()),
298 desc.explicit_chunks_.end());
299 return desc;
300 }
301
302 [[nodiscard]] static constexpr auto dirty_chunks(DirtyMask mask) noexcept
303 -> DomainDesc {
304 DomainDesc desc{DomainKind::DirtyChunks};
305 desc.mask_ = mask.value;
306 return desc;
307 }
308
309 [[nodiscard]] static constexpr auto active_chunks(ActiveMask mask) noexcept
310 -> DomainDesc {
311 DomainDesc desc{DomainKind::ActiveChunks};
312 desc.mask_ = mask.value;
313 return desc;
314 }
315
316 [[nodiscard]] static constexpr auto resident_chunks() noexcept -> DomainDesc {
317 return DomainDesc{DomainKind::ResidentChunks};
318 }
319
320 [[nodiscard]] constexpr auto kind() const noexcept -> DomainKind {
321 return kind_;
322 }
323
324 [[nodiscard]] constexpr auto dirty_mask() const noexcept -> DirtyMask {
325 return DirtyMask{mask_};
326 }
327
328 [[nodiscard]] constexpr auto active_mask() const noexcept -> ActiveMask {
329 return ActiveMask{mask_};
330 }
331
333 [[nodiscard]] constexpr auto mask_bits() const noexcept -> std::uint32_t {
334 return mask_;
335 }
336
337 [[nodiscard]] constexpr auto explicit_chunks() const noexcept
338 -> std::span<const ChunkKey> {
339 return {explicit_chunks_.data(), explicit_chunks_.size()};
340 }
341
342 private:
343 constexpr explicit DomainDesc(DomainKind kind) noexcept : kind_(kind) {}
344
345 DomainKind kind_;
346 std::uint32_t mask_ = 0;
347 std::vector<ChunkKey> explicit_chunks_;
348};
349
352 std::uint32_t read_mask = 0;
353 std::uint32_t write_mask = 0;
354 DirtyMask dirty_mask = {};
355
356 friend constexpr bool operator==(FieldAccessDesc lhs,
357 FieldAccessDesc rhs) noexcept = default;
358};
359
362 DomainDesc domain = DomainDesc::resident_chunks();
363 FieldAccessDesc field_access{};
364 WritePolicy write_policy = WritePolicy::ReadOnly;
365 Priority priority = Priority::GameplayCritical;
366 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
367 IntentVersions versions{};
368 IntentInvalidations invalidations{};
369 BackendEligibility backend = BackendEligibility::CpuOnly;
370 ExactnessRequirement exactness = ExactnessRequirement::Exact;
371};
372
375 template <typename Request, std::size_t Extent>
376 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
377 IntentMetadata operation = {})
378 -> PathBatchDesc {
379 return PathBatchDesc{IntentPayloadView::from(requests),
380 std::move(operation)};
381 }
382
383 IntentPayloadView requests{};
384 IntentMetadata operation{};
385};
386
389 template <typename Request, std::size_t Extent>
390 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
391 IntentMetadata operation = {})
393 return NearestBatchDesc{IntentPayloadView::from(requests),
394 std::move(operation)};
395 }
396
397 IntentPayloadView requests{};
398 IntentMetadata operation{};
399};
400
403 template <typename Request, std::size_t Extent>
404 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
405 IntentMetadata operation = {})
407 return FieldProductDesc{IntentPayloadView::from(requests),
408 std::move(operation)};
409 }
410
411 IntentPayloadView requests{};
412 IntentMetadata operation{};
413};
414
417 template <typename Request, std::size_t Extent>
418 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
419 IntentMetadata operation = {})
420 -> MoveBatchDesc {
421 return MoveBatchDesc{IntentPayloadView::from(requests),
422 std::move(operation)};
423 }
424
425 IntentPayloadView requests{};
426 IntentMetadata operation{};
427};
428
431 IntentMetadata operation{};
432};
433
436 template <typename Request, std::size_t Extent>
437 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
438 IntentMetadata operation = {})
439 -> ResidencyDesc {
440 return ResidencyDesc{IntentPayloadView::from(requests),
441 std::move(operation)};
442 }
443
444 IntentPayloadView requests{};
445 IntentMetadata operation{};
446};
447
450 DomainDesc domain = DomainDesc::resident_chunks();
451 DirtyMask dirty_mask = {};
452};
453
456 template <typename Request, std::size_t Extent>
457 [[nodiscard]] static auto from(std::span<Request, Extent> requests,
458 IntentMetadata operation = {})
459 -> RenderDeltaDesc {
460 return RenderDeltaDesc{IntentPayloadView::from(requests),
461 std::move(operation)};
462 }
463
464 IntentPayloadView requests{};
465 IntentMetadata operation{};
466};
467
470 OpHandle operation{};
471};
472
475 OpHandle operation{};
476};
477
485 OperationKind kind = OperationKind::UpdateField;
486 OpHandle handle{};
487 OpId id{};
488 DomainDesc domain = DomainDesc::resident_chunks();
489 FieldAccessDesc field_access{};
490 WritePolicy write_policy = WritePolicy::ReadOnly;
491 Priority priority = Priority::GameplayCritical;
492 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
493 std::source_location source = std::source_location::current();
494 IntentPayloadView payload{};
495 IntentVersions versions{};
496 IntentInvalidations invalidations{};
497 BackendEligibility backend = BackendEligibility::CpuOnly;
498 ExactnessRequirement exactness = ExactnessRequirement::Exact;
499};
500
503 WritePolicy write_policy = WritePolicy::ReadOnly;
504 DomainKind domain_kind = DomainKind::ResidentChunks;
505 std::uint32_t domain_mask = 0;
506};
507
509enum class PlannedOperationCreateStatus : std::uint8_t {
510 Created,
511 InvalidChunk,
512};
513static_assert(sizeof(PlannedOperationCreateStatus) == sizeof(std::uint8_t));
514
515struct PlannedOperationCreateResult;
516class ExecutionReport;
517class ExecutionPhase;
518
525class PlannedOperation {
526 public:
527 OperationKind kind = OperationKind::UpdateField;
528 OpHandle handle{};
529 OpId id{};
530 OperationAccess access{};
531 FieldAccessDesc field_access{};
532 WritePolicy write_policy = WritePolicy::ReadOnly;
533 Priority priority = Priority::GameplayCritical;
534 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
535 IntentPayloadView payload{};
536 IntentVersions versions{};
537 IntentInvalidations invalidations{};
538 BackendEligibility backend = BackendEligibility::CpuOnly;
539 ExactnessRequirement exactness = ExactnessRequirement::Exact;
540 // Enqueue-site capture carried through planning so run-time completions
541 // (result channels) can report where the operation came from.
542 std::source_location source = std::source_location::current();
543
550 template <typename World>
551 [[nodiscard]] static auto create(const World& world,
552 const QueuedOperation& operation,
553 std::span<const ChunkKey> chunks)
555
557 [[nodiscard]] constexpr auto chunks() const noexcept
558 -> std::span<const ChunkKey> {
559 return chunks_;
560 }
561
563 template <typename World>
564 [[nodiscard]] auto world_validation_status(
565 const World& /*world*/) const noexcept -> PlannedExecutionStatus {
566 static_assert(
567 std::is_same_v<typename World::residency_type, AlwaysResident>,
568 "Queued-op validation requires an AlwaysResidentWorld; use direct "
569 "sparse-world operations instead.");
570 return detail::validate_planned_world_stamp<World>(world_stamp_);
571 }
572
573 private:
574 friend class ExecutionReport;
575 friend class ExecutionPhase;
576
577 PlannedOperation(const QueuedOperation& operation,
578 std::vector<ChunkKey>&& chunks,
579 const detail::PlannedWorldStamp* world_stamp) noexcept
580 : kind(operation.kind),
581 handle(operation.handle),
582 id(operation.id),
583 access(OperationAccess{operation.write_policy, operation.domain.kind(),
584 operation.domain.mask_bits()}),
585 field_access(operation.field_access),
586 write_policy(operation.write_policy),
587 priority(operation.priority),
588 budget_policy(operation.budget_policy),
589 payload(operation.payload),
590 versions(operation.versions),
591 invalidations(operation.invalidations),
592 backend(operation.backend),
593 exactness(operation.exactness),
594 source(operation.source),
595 chunks_(std::move(chunks)),
596 world_stamp_(world_stamp) {}
597
598 std::vector<ChunkKey> chunks_;
599 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
600};
601
604 PlannedOperationCreateStatus status =
605 PlannedOperationCreateStatus::InvalidChunk;
606 std::optional<PlannedOperation> operation;
607 ChunkKey invalid_chunk{};
608};
609
610template <typename World>
611auto PlannedOperation::create(const World& /*world*/,
612 const QueuedOperation& operation,
613 std::span<const ChunkKey> chunks)
615 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
616 "Queued operations require an AlwaysResidentWorld; use direct "
617 "sparse-world operations instead.");
618
619 for (const auto key : chunks) {
620 if (key.value >= World::chunk_count) {
622 PlannedOperationCreateStatus::InvalidChunk,
623 std::nullopt,
624 key,
625 };
626 }
627 }
628
629 auto validated = std::vector<ChunkKey>{chunks.begin(), chunks.end()};
630 std::sort(validated.begin(), validated.end(),
631 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
632 validated.erase(std::unique(validated.begin(), validated.end()),
633 validated.end());
634 auto planned = PlannedOperation{
635 operation,
636 std::move(validated),
637 detail::planned_world_stamp<World>(),
638 };
640 PlannedOperationCreateStatus::Created,
641 std::optional<PlannedOperation>{std::move(planned)},
642 {},
643 };
644}
645
647class ExecutionPlan {
648 public:
649 // Lvalue-only for the same reason as ExecutionReport's: the span points
650 // into operations_, and a plan reached through a temporary report would
651 // outlive it.
652 [[nodiscard]] constexpr auto operations() const& noexcept
653 -> std::span<const PlannedOperation> {
654 return {operations_.data(), operations_.size()};
655 }
656 auto operations() const&& -> std::span<const PlannedOperation> = delete;
657
658 [[nodiscard]] constexpr bool empty() const noexcept {
659 return operations_.empty();
660 }
661
662 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
663 return operations_.size();
664 }
665
666 private:
667 friend class ExecutionReport;
668 friend class ExecutionPhase;
669
670 ExecutionPlan() noexcept = default;
671 ExecutionPlan(const ExecutionPlan&) = default;
672 ExecutionPlan(ExecutionPlan&&) noexcept = default;
673
674 // generation_ is this plan's capability epoch and must never be copied from
675 // another plan; assignment invalidates it through bump_generation instead.
676 // cppcheck-suppress operatorEqVarError
677 auto operator=(const ExecutionPlan& other) -> ExecutionPlan& {
678 if (this != &other) {
679 // Expire every issued capability before a potentially throwing vector
680 // copy. A failed assignment must not leave an old phase authorized for
681 // whatever state the vector copy preserved or partially replaced.
682 bump_generation();
683 operations_ = other.operations_;
684 }
685 return *this;
686 }
687
688 auto operator=(ExecutionPlan&& other) noexcept -> ExecutionPlan& {
689 if (this != &other) {
690 operations_ = std::move(other.operations_);
691 bump_generation();
692 }
693 return *this;
694 }
695
696 constexpr void bump_generation() noexcept { ++generation_; }
697
698 std::vector<PlannedOperation> operations_;
699 std::uint64_t generation_ = 0;
700};
701
709class ExecutionPhase {
710 public:
711 ExecutionPhase(const ExecutionPhase&) noexcept = default;
712 ExecutionPhase(ExecutionPhase&&) noexcept = default;
713 auto operator=(const ExecutionPhase&) noexcept -> ExecutionPhase& = default;
714 auto operator=(ExecutionPhase&&) noexcept -> ExecutionPhase& = default;
715
716 [[nodiscard]] constexpr auto first_operation() const noexcept -> std::size_t {
717 return first_operation_;
718 }
719
720 [[nodiscard]] constexpr auto operation_count() const noexcept -> std::size_t {
721 return operation_count_;
722 }
723
725 [[nodiscard]] constexpr bool belongs_to(
726 const ExecutionPlan& plan) const noexcept {
727 return plan_ == &plan && plan_generation_ == plan.generation_;
728 }
729
731 template <typename World>
732 [[nodiscard]] auto world_validation_status(
733 const World& /*world*/) const noexcept -> PlannedExecutionStatus {
734 static_assert(
735 std::is_same_v<typename World::residency_type, AlwaysResident>,
736 "Queued-op validation requires an AlwaysResidentWorld; use direct "
737 "sparse-world operations instead.");
738 return detail::validate_planned_world_stamp<World>(world_stamp_);
739 }
740
742 template <WritePolicy Policy>
743 [[nodiscard]] constexpr bool policy_matches() const noexcept {
744 static_assert(is_valid_write_policy(Policy));
745 return write_policy_mask_ == policy_bit(Policy);
746 }
747
748 private:
749 friend class ExecutionPhasePlan;
750
751 [[nodiscard]] static constexpr auto policy_bit(WritePolicy policy) noexcept
752 -> std::uint8_t {
753 return static_cast<std::uint8_t>(std::uint8_t{1}
754 << static_cast<std::uint8_t>(policy));
755 }
756
757 constexpr ExecutionPhase(const ExecutionPlan& plan,
758 std::size_t first_operation,
759 std::size_t operation_count,
760 const PlannedOperation& operation) noexcept
761 : plan_(&plan),
762 first_operation_(first_operation),
763 operation_count_(operation_count),
764 plan_generation_(plan.generation_),
765 world_stamp_(operation.world_stamp_),
766 write_policy_mask_(policy_bit(operation.write_policy)) {}
767
768 constexpr void extend(const PlannedOperation& operation) noexcept {
769 ++operation_count_;
770 write_policy_mask_ |= policy_bit(operation.write_policy);
771 }
772
773 const ExecutionPlan* plan_;
774 std::size_t first_operation_;
775 std::size_t operation_count_;
776 std::uint64_t plan_generation_;
777 const detail::PlannedWorldStamp* world_stamp_;
778 std::uint8_t write_policy_mask_;
779};
780
782[[nodiscard]] constexpr auto executor_phase_range(
783 const ExecutionPhase& phase) noexcept -> ExecutorPhaseRange {
784 return ExecutorPhaseRange{
785 phase.first_operation(),
786 phase.operation_count(),
787 };
788}
789
792 public:
793 [[nodiscard]] constexpr auto phases() const noexcept
794 -> std::span<const ExecutionPhase> {
795 return {phases_.data(), phases_.size()};
796 }
797
798 [[nodiscard]] constexpr auto status() const noexcept -> ExecutionPhaseStatus {
799 return status_;
800 }
801
802 [[nodiscard]] constexpr bool ok() const noexcept {
803 return status_ == ExecutionPhaseStatus::Ready;
804 }
805
806 [[nodiscard]] constexpr auto failed_operation_index() const noexcept
807 -> std::size_t {
808 return failed_operation_index_;
809 }
810
811 [[nodiscard]] constexpr auto failed_write_policy() const noexcept
812 -> WritePolicy {
813 return failed_write_policy_;
814 }
815
816 private:
817 friend auto plan_parallel_execution_phases(const ExecutionPlan& plan)
819
820 void reserve(std::size_t size) { phases_.reserve(size); }
821
822 void push_phase(const ExecutionPlan& plan, std::size_t first_operation,
823 std::size_t operation_count,
824 const PlannedOperation& operation) {
825 phases_.push_back(
826 ExecutionPhase{plan, first_operation, operation_count, operation});
827 }
828
829 void extend_last_phase(const PlannedOperation& operation) {
830 phases_.back().extend(operation);
831 }
832
833 std::vector<ExecutionPhase> phases_;
834 ExecutionPhaseStatus status_ = ExecutionPhaseStatus::Ready;
835 std::size_t failed_operation_index_ = 0;
836 WritePolicy failed_write_policy_ = WritePolicy::ReadOnly;
837};
838
839namespace detail {
840
841[[nodiscard]] constexpr bool execution_phase_valid_for(
842 const ExecutionPlan& plan, const ExecutionPhase& phase) noexcept {
843 const auto operations = plan.operations();
844 const auto first = phase.first_operation();
845 const auto count = phase.operation_count();
846 return phase.belongs_to(plan) && first <= operations.size() &&
847 count <= operations.size() - first;
848}
849
850template <WritePolicy Policy, typename World>
851[[nodiscard]] auto execution_phase_validation_status(
852 const World& world, const ExecutionPlan& plan,
853 const ExecutionPhase& phase) noexcept -> PlannedExecutionStatus {
854 if (!execution_phase_valid_for(plan, phase)) {
855 return PlannedExecutionStatus::InvalidPhase;
856 }
857 const auto world_status = phase.world_validation_status(world);
858 if (world_status != PlannedExecutionStatus::Executed) {
859 return world_status;
860 }
861 if (!phase.template policy_matches<Policy>()) {
862 return PlannedExecutionStatus::PolicyMismatch;
863 }
864 return PlannedExecutionStatus::Executed;
865}
866
867inline void record_execution_phase_validation_failure(
868 PlannedExecutionStatus status) noexcept {
869#if TESS_DIAGNOSTICS_ENABLED
870 if (status == PlannedExecutionStatus::InvalidPhase) {
871 TESS_DIAG_EVENT(queued_phase_invalid_range);
872 } else {
873 TESS_DIAG_EVENT(queued_phase_failure);
874 }
875#else
876 (void)status;
877#endif
878}
879
880} // namespace detail
881
884 OpHandle handle{};
885 OpId id{};
886 OperationStatus status = OperationStatus::Planned;
887 OperationFailure failure = OperationFailure::None;
888 OperationAccess access{};
889 FieldAccessDesc field_access{};
890 IntentVersions versions{};
891 IntentInvalidations invalidations{};
892 BackendEligibility backend = BackendEligibility::CpuOnly;
893 ExactnessRequirement exactness = ExactnessRequirement::Exact;
894 ChunkKey detail_chunk{};
895 OpHandle conflict_handle{};
896 OpId conflict_id{};
897 std::uint32_t conflict_mask = 0;
898 bool has_detail_chunk = false;
899 bool has_conflict = false;
900 std::size_t chunk_count = 0;
901 std::source_location source = std::source_location::current();
902};
903
906 ChunkKey chunk{};
907 DirtyMask dirty_mask = {};
908 Box3 bounds{};
909};
910
912enum class PlannedDirtyRecordStatus : std::uint8_t {
913 Recorded,
914 IgnoredEmptyMask,
915 InvalidShape,
916 InvalidChunk,
917};
918static_assert(sizeof(PlannedDirtyRecordStatus) == sizeof(std::uint8_t));
919
921enum class PlannedDirtyMergeStatus : std::uint8_t {
922 Merged,
923 InvalidShape,
924 InvalidChunk,
925 CapacityExceeded,
926};
927static_assert(sizeof(PlannedDirtyMergeStatus) == sizeof(std::uint8_t));
928
930enum class PlannedDirtyCollectStatus : std::uint8_t {
931 Collected,
932 InvalidShape,
933 InvalidChunk,
934 CapacityExceeded,
935};
936static_assert(sizeof(PlannedDirtyCollectStatus) == sizeof(std::uint8_t));
937
940 PlannedDirtyCollectStatus status = PlannedDirtyCollectStatus::Collected;
941 std::size_t record_count = 0;
942
943 [[nodiscard]] constexpr bool ok() const noexcept {
944 return status == PlannedDirtyCollectStatus::Collected;
945 }
946};
947
950 PlannedDirtyMergeStatus status = PlannedDirtyMergeStatus::Merged;
951 std::size_t merged_chunk_count = 0;
952
953 [[nodiscard]] constexpr bool ok() const noexcept {
954 return status == PlannedDirtyMergeStatus::Merged;
955 }
956};
957
960
961namespace detail {
962
963template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
964[[nodiscard]] auto execute_validated_planned_operation_deferred_dirty(
965 World& world, const PlannedOperation& operation,
967
968template <typename World>
969[[nodiscard]] auto merge_planned_dirty_after_exception(
970 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
972
973} // namespace detail
974
975class PlannedDirtyPartitions;
976
979 public:
980 void reserve(std::size_t count) { records_.reserve(count); }
981
982 void clear() noexcept {
983 records_.clear();
984 world_stamp_ = nullptr;
985 }
986
988 template <typename World>
989 [[nodiscard]] auto record(const World& /*world*/, ChunkKey chunk,
990 DirtyMask dirty_mask, Box3 bounds)
991 -> PlannedDirtyRecordStatus {
992 static_assert(
993 std::is_same_v<typename World::residency_type, AlwaysResident>,
994 "Queued-op dirty recording requires an AlwaysResidentWorld; use "
995 "direct sparse-world operations instead.");
996 if (!dirty_mask) {
997 return PlannedDirtyRecordStatus::IgnoredEmptyMask;
998 }
999 if (chunk.value >= World::chunk_count) {
1000 return PlannedDirtyRecordStatus::InvalidChunk;
1001 }
1002
1003 if (world_stamp_ != nullptr) {
1004 const auto validation =
1005 detail::validate_planned_world_stamp<World>(world_stamp_);
1006 if (validation == PlannedExecutionStatus::InvalidShape) {
1007 return PlannedDirtyRecordStatus::InvalidShape;
1008 }
1009 if (validation == PlannedExecutionStatus::InvalidChunk) {
1010 return PlannedDirtyRecordStatus::InvalidChunk;
1011 }
1012 }
1013
1014 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
1015 world_stamp_ = detail::planned_world_stamp<World>();
1016 return PlannedDirtyRecordStatus::Recorded;
1017 }
1018
1019 [[nodiscard]] auto records() const noexcept
1020 -> std::span<const PlannedDirtyRecord> {
1021 return records_;
1022 }
1023
1025 template <typename World>
1026 [[nodiscard]] auto validation_status(const World& /*world*/) const noexcept
1027 -> PlannedDirtyMergeStatus {
1028 static_assert(
1029 std::is_same_v<typename World::residency_type, AlwaysResident>,
1030 "Queued-op dirty validation requires an AlwaysResidentWorld; use "
1031 "direct sparse-world operations instead.");
1032 if (world_stamp_ == nullptr) {
1033 return PlannedDirtyMergeStatus::Merged;
1034 }
1035 const auto validation =
1036 detail::validate_planned_world_stamp<World>(world_stamp_);
1037 if (validation == PlannedExecutionStatus::InvalidShape) {
1038 return PlannedDirtyMergeStatus::InvalidShape;
1039 }
1040 if (validation == PlannedExecutionStatus::InvalidChunk) {
1041 return PlannedDirtyMergeStatus::InvalidChunk;
1042 }
1043 return PlannedDirtyMergeStatus::Merged;
1044 }
1045
1046 private:
1047 friend class PlannedDirtyPartitions;
1048
1049 template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
1050 friend auto detail::execute_validated_planned_operation_deferred_dirty(
1051 World& world, const PlannedOperation& operation,
1053
1054 template <WritePolicy Policy, typename World, typename Fn>
1056 World& world, const PlannedOperation& operation,
1058 template <typename World>
1059 friend auto merge_planned_dirty(World& world,
1060 PlannedDirtyAccumulator& dirty) noexcept
1062 template <typename World>
1063 friend auto merge_planned_dirty(World& world,
1067 PlannedDirtyPartitions& partitions)
1069
1070 template <typename World>
1071 void bind_validated_world(const World& /*world*/) noexcept {
1072 if (world_stamp_ == nullptr) {
1073 world_stamp_ = detail::planned_world_stamp<World>();
1074 }
1075 }
1076
1077 template <typename World>
1078 void prepare_for_validated_world(const World& /*world*/) noexcept {
1079 records_.clear();
1080 world_stamp_ = detail::planned_world_stamp<World>();
1081 }
1082
1083 void record_validated(ChunkKey chunk, DirtyMask dirty_mask, Box3 bounds) {
1084 if (!dirty_mask) {
1085 return;
1086 }
1087 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
1088 }
1089
1090 std::vector<PlannedDirtyRecord> records_;
1091 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
1092};
1093
1094template <bool BindWorld, WritePolicy Policy, typename World, typename Fn>
1095auto detail::execute_validated_planned_operation_deferred_dirty(
1096 World& world, const PlannedOperation& operation,
1098 if constexpr (BindWorld) {
1099 if (operation.field_access.dirty_mask) {
1100 dirty.bind_validated_world(world);
1101 }
1102 }
1103 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
1104
1105 std::size_t chunk_count = 0;
1106 auto&& callback = fn;
1107 ctx.for_each_chunk([&](auto view) {
1108 dirty.record_validated(view.key(), operation.field_access.dirty_mask,
1109 view.bounds());
1110 callback(view);
1111 ++chunk_count;
1112 });
1113
1115 PlannedExecutionStatus::Executed,
1116 chunk_count,
1117 };
1118}
1119
1122 public:
1123 void reserve(std::size_t count) { partitions_.reserve(count); }
1124
1125 void resize(std::size_t count) { partitions_.resize(count); }
1126
1127 void clear() noexcept { partitions_.clear(); }
1128
1129 void clear_records() noexcept {
1130 for (auto& partition : partitions_) {
1131 partition.clear();
1132 }
1133 }
1134
1135 void reserve_records_per_partition(std::size_t count) {
1136 records_per_partition_reserve_ = count;
1137 for (auto& partition : partitions_) {
1138 partition.reserve(count);
1139 }
1140 }
1141
1142 [[nodiscard]] auto size() const noexcept -> std::size_t {
1143 return partitions_.size();
1144 }
1145
1146 [[nodiscard]] auto partition(std::size_t index) noexcept
1148 return partitions_[index];
1149 }
1150
1151 [[nodiscard]] auto partition(std::size_t index) const noexcept
1152 -> const PlannedDirtyAccumulator& {
1153 return partitions_[index];
1154 }
1155
1156 [[nodiscard]] auto partitions() const noexcept
1157 -> std::span<const PlannedDirtyAccumulator> {
1158 return partitions_;
1159 }
1160
1161 private:
1163 PlannedDirtyPartitions& partitions)
1165 friend class PlannedPhaseExecutionScratch;
1166
1167 void prepare(std::size_t count) {
1168 partitions_.resize(count);
1169 for (auto& partition : partitions_) {
1170 partition.clear();
1171 partition.reserve(records_per_partition_reserve_);
1172 }
1173 }
1174
1175 template <typename World>
1176 void prepare(const World& world, std::size_t count) {
1177 partitions_.resize(count);
1178 for (auto& partition : partitions_) {
1179 partition.prepare_for_validated_world(world);
1180 partition.reserve(records_per_partition_reserve_);
1181 }
1182 }
1183
1184 std::vector<PlannedDirtyAccumulator> partitions_;
1185 std::size_t records_per_partition_reserve_ = 0;
1186};
1187
1188// Public because a public accessor returns a span of it:
1189// PhaseExecutionScratch::dirty_partitions() hands these out, so a caller
1190// reading per-operation dirty records had to name a `detail` type that
1191// docs/style.md says carries no source-compatibility guarantee.
1192// Scratch-owned phase partitions carry no independent world stamp. The
1193// enclosing scratch object owns one capability stamp, and this record-only
1194// type cannot be passed to the public dirty-merge APIs for another world.
1197 public:
1198 void reserve(std::size_t count) { records_.reserve(count); }
1199
1200 void clear() noexcept { records_.clear(); }
1201
1202 void record(ChunkKey chunk, DirtyMask dirty_mask, Box3 bounds) {
1203 if (dirty_mask) {
1204 // Phase setup reserves one record for every possible chunk visit.
1205 TESS_ASSERT(records_.size() < records_.capacity());
1206 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
1207 }
1208 }
1209
1210 [[nodiscard]] auto records() const noexcept
1211 -> std::span<const PlannedDirtyRecord> {
1212 return records_;
1213 }
1214
1215 private:
1216 std::vector<PlannedDirtyRecord> records_;
1217};
1218
1219namespace detail {
1220
1221template <WritePolicy Policy, typename World>
1222using PlannedChunkView =
1224
1225template <WritePolicy Policy, typename World, typename Fn>
1226inline constexpr bool planned_callback_is_nothrow =
1227 std::is_nothrow_invocable_v<Fn&, PlannedChunkView<Policy, World>&>;
1228
1229template <WritePolicy Policy, typename World, typename Fn>
1230[[nodiscard]] auto execute_validated_phase_operation_deferred_dirty(
1231 World& world, const PlannedOperation& operation, PhaseDirtyPartition& dirty,
1232 Fn&& fn) -> PlannedExecutionResult {
1233 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
1234
1235 std::size_t chunk_count = 0;
1236 auto&& callback = fn;
1237 ctx.for_each_chunk([&](auto view) {
1238 dirty.record(view.key(), operation.field_access.dirty_mask, view.bounds());
1239 callback(view);
1240 ++chunk_count;
1241 });
1242
1243 return PlannedExecutionResult{
1244 PlannedExecutionStatus::Executed,
1245 chunk_count,
1246 };
1247}
1248
1249// Chunk-keyed index over accepted operations, so hazard detection and
1250// parallel-phase grouping examine only the operations that actually share
1251// a chunk with the candidate instead of every operation accepted so far.
1252//
1253// Both scans were quadratic in the operation count: `find_hazard` walked
1254// every accepted operation per new operation, and phase grouping compared
1255// each operation against every member of the open phase. Per-chunk edits
1256// -- one operation per dirty chunk, the ordinary consumer shape -- are
1257// pairwise disjoint, so every one of those comparisons failed on chunk
1258// overlap only after paying for the check.
1259//
1260// An index only pays for itself when operations touch FEW chunks each.
1261// An operation over a whole domain -- `resident_chunks()`, the default
1262// selector -- would store one node per chunk and make every later lookup
1263// walk one chain per chunk, turning an O(n^2) scan into O(n^2 * chunks).
1264// Worse, that scan was the cheap case: read-only operations have a zero
1265// hazard mask, so it rejected each pair on the mask alone and never
1266// touched a chunk list. Measured at 64 read-only resident operations, the
1267// index cost 168 ms against the scan's 201 us before this bound existed.
1268//
1269// So operations wider than `index_max_chunks_per_operation` are kept OUT
1270// of the index and scanned linearly, and a candidate that wide skips the
1271// index and scans everything, exactly as the planner used to. Every
1272// lookup is then at worst the old cost and at best the indexed one.
1273inline constexpr std::size_t index_max_chunks_per_operation = 64;
1274
1275[[nodiscard]] constexpr bool operation_is_indexable(
1276 std::span<const ChunkKey> chunks) noexcept {
1277 return chunks.size() <= index_max_chunks_per_operation;
1278}
1279
1280// Open-addressed chunk key -> intrusive chain of nodes, power-of-two
1281// capacity, linear probing. No erase: a plan only grows, and the index is
1282// cleared wholesale per plan. Each node carries its key so a rehash can
1283// relink without consulting the operations.
1284class ChunkOperationIndex {
1285 public:
1286 static constexpr auto npos = std::numeric_limits<std::uint32_t>::max();
1287
1288 // Stamped, not rewritten: `slots_` keeps its high-water size across the
1289 // report reuse this index exists to serve, so clearing it per plan would
1290 // charge every small batch for the largest batch that came before.
1291 void clear() noexcept {
1292 nodes_.clear();
1293 if (++generation_ != 0) {
1294 return;
1295 }
1296 // Wrapped: stale slots would read as live under the new generation.
1297 // Reset in place rather than `assign`, which is not noexcept -- and
1298 // `clear` runs on the report-reuse path, where throwing would leave a
1299 // half-cleared index behind a plan that reported success.
1300 for (auto& slot : slots_) {
1301 slot = Slot{};
1302 }
1303 generation_ = 1;
1304 }
1305
1306 void reserve(std::size_t nodes) {
1307 nodes_.reserve(nodes);
1308 rehash(capacity_for(nodes));
1309 }
1310
1312 void insert(std::span<const ChunkKey> chunks, std::uint32_t op_index) {
1313 if (slots_.empty() ||
1314 (nodes_.size() + chunks.size()) * 2 >= slots_.size()) {
1315 rehash(capacity_for(nodes_.size() + chunks.size()));
1316 }
1317 for (const auto key : chunks) {
1318 nodes_.push_back(Node{key.value, op_index, npos});
1319 link(static_cast<std::uint32_t>(nodes_.size() - 1));
1320 }
1321 }
1322
1327 template <typename Visit>
1328 void for_each_sharing(std::span<const ChunkKey> chunks, Visit&& visit) const {
1329 if (slots_.empty()) {
1330 return;
1331 }
1332 for (const auto key : chunks) {
1333 for (auto node = head_for(key.value); node != npos;
1334 node = nodes_[node].next) {
1335 if (nodes_[node].key == key.value) {
1336 visit(nodes_[node].op_index);
1337 }
1338 }
1339 }
1340 }
1341
1342 private:
1343 struct Slot {
1344 std::uint32_t head = npos;
1345 std::uint32_t generation = 0;
1346 };
1347 struct Node {
1348 std::uint64_t key = 0;
1349 std::uint32_t op_index = 0;
1350 std::uint32_t next = npos;
1351 };
1352
1353 [[nodiscard]] static constexpr auto mix(std::uint64_t key) noexcept
1354 -> std::uint64_t {
1355 key ^= key >> 33U;
1356 key *= 0xff51afd7ed558ccdULL;
1357 key ^= key >> 33U;
1358 return key;
1359 }
1360
1361 [[nodiscard]] static constexpr auto capacity_for(std::size_t nodes) noexcept
1362 -> std::size_t {
1363 auto capacity = std::size_t{16};
1364 while (capacity < nodes * 2) {
1365 capacity *= 2;
1366 }
1367 return capacity;
1368 }
1369
1370 // Chains are keyed by bucket, and every node carries its key, so a
1371 // bucket may hold nodes for several keys; `for_each_sharing` filters.
1372 [[nodiscard]] auto bucket(std::uint64_t key) const noexcept -> std::size_t {
1373 return static_cast<std::size_t>(mix(key)) & (slots_.size() - 1);
1374 }
1375
1376 [[nodiscard]] auto head_for(std::uint64_t key) const noexcept
1377 -> std::uint32_t {
1378 const auto& slot = slots_[bucket(key)];
1379 return slot.generation == generation_ ? slot.head : npos;
1380 }
1381
1382 void link(std::uint32_t node) noexcept {
1383 auto& slot = slots_[bucket(nodes_[node].key)];
1384 if (slot.generation != generation_) {
1385 slot.generation = generation_;
1386 slot.head = npos;
1387 }
1388 nodes_[node].next = slot.head;
1389 slot.head = node;
1390 }
1391
1392 void rehash(std::size_t capacity) {
1393 if (capacity <= slots_.size()) {
1394 return;
1395 }
1396 slots_.assign(capacity, Slot{});
1397 for (std::size_t i = 0; i < nodes_.size(); ++i) {
1398 nodes_[i].next = npos;
1399 link(static_cast<std::uint32_t>(i));
1400 }
1401 }
1402
1403 std::vector<Slot> slots_;
1404 std::vector<Node> nodes_;
1405 std::uint32_t generation_ = 1;
1406};
1407
1408} // namespace detail
1409
1411template <typename T>
1412class ResultChannel;
1413
1416 public:
1417 void reserve_operations(std::size_t count) {
1418 dirty_partitions_.reserve(count);
1419 results_.reserve(count);
1420 }
1421
1422 void reserve_dirty_records_per_operation(std::size_t count) {
1423 records_per_partition_reserve_ = count;
1424 for (auto& partition : dirty_partitions_) {
1425 partition.reserve(count);
1426 }
1427 }
1428
1429 void reserve_merged_dirty_records(std::size_t count) {
1430 merged_dirty_.reserve(count);
1431 }
1432
1433 void prepare_for_operation_count(std::size_t count) { prepare(count); }
1434
1435 void clear() noexcept {
1436 for (auto& partition : dirty_partitions_) {
1437 partition.clear();
1438 }
1439 results_.clear();
1440 merged_dirty_.clear();
1441 world_stamp_ = nullptr;
1442 }
1443
1444 [[nodiscard]] auto operation_count() const noexcept -> std::size_t {
1445 return results_.size();
1446 }
1447
1448 [[nodiscard]] auto dirty_partitions() const noexcept
1449 -> std::span<const PhaseDirtyPartition> {
1450 return dirty_partitions_;
1451 }
1452
1453 private:
1454 template <WritePolicy Policy, typename Executor, typename World, typename Fn>
1456 Executor&& executor, World& world, const ExecutionPlan& plan,
1457 const ExecutionPhase& phase, PlannedPhaseExecutionScratch& scratch,
1458 Fn&& fn) -> PlannedExecutionResult;
1459
1460 template <WritePolicy Policy, typename Executor, typename World, typename T,
1461 typename Fn>
1463 Executor&& executor, World& world, const ExecutionPlan& plan,
1464 const ExecutionPhase& phase, PlannedPhaseExecutionScratch& scratch,
1465 ResultChannel<T>& channel, Fn&& fn) -> PlannedExecutionResult;
1466
1467 template <typename World>
1468 friend auto merge_planned_dirty(World& world,
1471 template <typename World>
1472 friend auto detail::merge_planned_dirty_after_exception(
1473 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
1475
1476 void prepare(std::size_t operation_count) {
1477 prepare_partitions(operation_count);
1478 results_.assign(operation_count, PlannedExecutionResult{});
1479 merged_dirty_.clear();
1480 world_stamp_ = nullptr;
1481 }
1482
1483 template <typename World>
1484 void prepare(const World& /*world*/, std::size_t operation_count) {
1485 prepare_partitions(operation_count);
1486 results_.assign(operation_count, PlannedExecutionResult{});
1487 merged_dirty_.clear();
1488 world_stamp_ = detail::planned_world_stamp<World>();
1489 }
1490
1491 [[nodiscard]] auto dirty_for_operation(std::size_t index) noexcept
1493 return dirty_partitions_[index];
1494 }
1495
1496 void prepare_partitions(std::size_t operation_count) {
1497 dirty_partitions_.resize(operation_count);
1498 for (auto& partition : dirty_partitions_) {
1499 partition.clear();
1500 partition.reserve(records_per_partition_reserve_);
1501 }
1502 }
1503
1504 void record_result(std::size_t index, PlannedExecutionResult result) {
1505 results_[index] = result;
1506 }
1507
1508 [[nodiscard]] auto results() const noexcept
1509 -> std::span<const PlannedExecutionResult> {
1510 return results_;
1511 }
1512
1513 std::vector<PhaseDirtyPartition> dirty_partitions_;
1514 std::vector<PlannedExecutionResult> results_;
1515 PlannedDirtyAccumulator merged_dirty_;
1516 std::size_t records_per_partition_reserve_ = 0;
1517 const detail::PlannedWorldStamp* world_stamp_ = nullptr;
1518};
1519
1522 public:
1523 // plan_operations returns an ExecutionReport BY VALUE, so every accessor
1524 // handing out a reference or a span into it is lvalue-only. The
1525 // idiomatic-looking `for (const auto& op :
1526 // plan_operations(world, ops).plan().operations())` iterated freed
1527 // memory: the report died at the end of the full expression while the
1528 // range-for held a span into its member vector. ExecutionPhase already
1529 // carried a generation check, which made these unprotected accessors
1530 // read as safe by comparison.
1531 [[nodiscard]] constexpr auto operations() const& noexcept
1532 -> std::span<const OperationReport> {
1533 return {operations_.data(), operations_.size()};
1534 }
1535 auto operations() const&& -> std::span<const OperationReport> = delete;
1536
1537 [[nodiscard]] constexpr auto plan() const& noexcept -> const ExecutionPlan& {
1538 return plan_;
1539 }
1540 auto plan() const&& -> const ExecutionPlan& = delete;
1541
1542 auto find(OpHandle handle) const&& -> const OperationReport* = delete;
1543
1544 [[nodiscard]] constexpr auto find(OpHandle handle) const& noexcept
1545 -> const OperationReport* {
1546 for (const auto& op : operations_) {
1547 if (op.handle == handle) {
1548 return &op;
1549 }
1550 }
1551 return nullptr;
1552 }
1553
1554 [[nodiscard]] constexpr bool ok() const noexcept {
1555 return failed_count() == 0;
1556 }
1557
1558 [[nodiscard]] constexpr bool failed() const noexcept {
1559 return failed_count() != 0;
1560 }
1561
1562 [[nodiscard]] constexpr auto planned_count() const noexcept -> std::size_t {
1563 return plan_.size();
1564 }
1565
1566 [[nodiscard]] constexpr auto failed_count() const noexcept -> std::size_t {
1567 std::size_t count = 0;
1568 for (const auto& op : operations_) {
1569 if (op.status != OperationStatus::Planned) {
1570 ++count;
1571 }
1572 }
1573 return count;
1574 }
1575
1576 // Clears all results while keeping every allocation -- report rows,
1577 // planned operations, and their chunk lists (parked in a pool) -- so a
1578 // caller-owned report makes steady-state planning allocation-free.
1579 void reset() {
1580 plan_.bump_generation();
1581 for (auto& planned : plan_.operations_) {
1582 planned.chunks_.clear();
1583 chunk_pool_.push_back(std::move(planned.chunks_));
1584 }
1585 plan_.operations_.clear();
1586 operations_.clear();
1587 chunk_index_.clear();
1588 wide_operations_.clear();
1589 }
1590
1591 private:
1592 template <typename World>
1593 friend auto plan_operations(const World& world,
1594 std::span<const QueuedOperation> operations,
1595 ExecutionReport& report)
1596 -> const ExecutionReport&;
1597
1598 void reserve(std::size_t size) {
1599 operations_.reserve(size);
1600 plan_.operations_.reserve(size);
1601 chunk_index_.reserve(size);
1602 }
1603
1604 void push_report(OperationReport report) { operations_.push_back(report); }
1605
1606 void push_planned(PlannedOperation planned) {
1607 const auto op_index = static_cast<std::uint32_t>(plan_.operations_.size());
1608 if (detail::operation_is_indexable(planned.chunks())) {
1609 chunk_index_.insert(planned.chunks(), op_index);
1610 } else {
1611 wide_operations_.push_back(op_index);
1612 }
1613 plan_.operations_.push_back(std::move(planned));
1614 }
1615
1616 [[nodiscard]] auto chunk_index() const noexcept
1617 -> const detail::ChunkOperationIndex& {
1618 return chunk_index_;
1619 }
1620
1621 [[nodiscard]] auto wide_operations() const noexcept
1622 -> std::span<const std::uint32_t> {
1623 return {wide_operations_.data(), wide_operations_.size()};
1624 }
1625
1626 template <typename World>
1627 [[nodiscard]] auto make_planned(const QueuedOperation& operation,
1628 std::vector<ChunkKey>&& chunks)
1629 -> PlannedOperation {
1630 return PlannedOperation{
1631 operation,
1632 std::move(chunks),
1633 detail::planned_world_stamp<World>(),
1634 };
1635 }
1636
1637 [[nodiscard]] auto acquire_chunks() -> std::vector<ChunkKey> {
1638 if (chunk_pool_.empty()) {
1639 return {};
1640 }
1641 auto chunks = std::move(chunk_pool_.back());
1642 chunk_pool_.pop_back();
1643 return chunks;
1644 }
1645
1646 void recycle_chunks(std::vector<ChunkKey>&& chunks) {
1647 chunks.clear();
1648 chunk_pool_.push_back(std::move(chunks));
1649 }
1650
1651 void recycle_chunks(PlannedOperation&& planned) {
1652 recycle_chunks(std::move(planned.chunks_));
1653 }
1654
1655 std::vector<OperationReport> operations_;
1656 ExecutionPlan plan_;
1657 std::vector<std::vector<ChunkKey>> chunk_pool_;
1658 // Chunk -> accepted-operation index, so hazard detection stays linear in
1659 // the operations that share a chunk rather than in the whole plan. Cleared,
1660 // not freed, by `reset`.
1661 detail::ChunkOperationIndex chunk_index_;
1662 // Accepted operations too wide to index, in plan order. Scanned the way
1663 // the planner always scanned, which for these is the cheaper path.
1664 std::vector<std::uint32_t> wide_operations_;
1665};
1666
1669 public:
1670 void reserve_operations(std::size_t count) { operations_.reserve(count); }
1671
1672 [[nodiscard]] auto update_field(
1673 DomainDesc domain, FieldAccessDesc field_access, WritePolicy write_policy,
1674 Priority priority = Priority::GameplayCritical,
1675 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1676 std::source_location source = std::source_location::current())
1677 -> OpHandle {
1678 auto metadata = IntentMetadata{};
1679 metadata.domain = std::move(domain);
1680 metadata.field_access = field_access;
1681 metadata.write_policy = write_policy;
1682 metadata.priority = priority;
1683 metadata.budget_policy = budget_policy;
1684 return enqueue(OperationKind::UpdateField, std::move(metadata), {}, source);
1685 }
1686
1687 [[nodiscard]] auto update_field(
1688 DomainDesc domain, WritePolicy write_policy,
1689 Priority priority = Priority::GameplayCritical,
1690 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1691 std::source_location source = std::source_location::current())
1692 -> OpHandle {
1693 return update_field(std::move(domain), FieldAccessDesc{}, write_policy,
1694 priority, budget_policy, source);
1695 }
1696
1697 [[nodiscard]] auto query_paths(
1698 PathBatchDesc desc,
1699 std::source_location source = std::source_location::current())
1700 -> PathBatchHandle {
1701 return PathBatchHandle{enqueue(OperationKind::QueryPaths,
1702 std::move(desc.operation), desc.requests,
1703 source)};
1704 }
1705
1706 [[nodiscard]] auto query_nearest(
1707 NearestBatchDesc desc,
1708 std::source_location source = std::source_location::current())
1710 return NearestBatchHandle{enqueue(OperationKind::QueryNearest,
1711 std::move(desc.operation), desc.requests,
1712 source)};
1713 }
1714
1715 [[nodiscard]] auto build_field_product(
1716 FieldProductDesc desc,
1717 std::source_location source = std::source_location::current())
1718 -> OpHandle {
1719 return enqueue(OperationKind::BuildFieldProduct, std::move(desc.operation),
1720 desc.requests, source);
1721 }
1722
1723 [[nodiscard]] auto move_entities(
1724 MoveBatchDesc desc,
1725 std::source_location source = std::source_location::current())
1726 -> OpHandle {
1727 return enqueue(OperationKind::MoveEntities, std::move(desc.operation),
1728 desc.requests, source);
1729 }
1730
1731 [[nodiscard]] auto rebuild_topology(
1733 std::source_location source = std::source_location::current())
1734 -> OpHandle {
1735 return enqueue(OperationKind::RebuildTopology, std::move(desc.operation),
1736 {}, source);
1737 }
1738
1739 [[nodiscard]] auto ensure_resident(
1740 ResidencyDesc desc,
1741 std::source_location source = std::source_location::current())
1742 -> OpHandle {
1743 return enqueue(OperationKind::EnsureResident, std::move(desc.operation),
1744 desc.requests, source);
1745 }
1746
1747 [[nodiscard]] auto mark_dirty(
1748 MarkDirtyDesc desc,
1749 std::source_location source = std::source_location::current())
1750 -> OpHandle {
1751 auto metadata = IntentMetadata{};
1752 metadata.domain = std::move(desc.domain);
1753 metadata.field_access.dirty_mask = desc.dirty_mask;
1754 metadata.invalidations.dirty_mask = desc.dirty_mask;
1755 return enqueue(OperationKind::MarkDirty, std::move(metadata), {}, source);
1756 }
1757
1758 [[nodiscard]] auto publish_render_deltas(
1759 RenderDeltaDesc desc,
1760 std::source_location source = std::source_location::current())
1761 -> OpHandle {
1762 return enqueue(OperationKind::PublishRenderDeltas,
1763 std::move(desc.operation), desc.requests, source);
1764 }
1765
1766 [[nodiscard]] constexpr auto operations() const noexcept
1767 -> std::span<const QueuedOperation> {
1768 return {operations_.data(), operations_.size()};
1769 }
1770
1771 [[nodiscard]] constexpr auto operation(OpHandle handle) const noexcept
1772 -> const QueuedOperation* {
1773 if (handle.value >= operations_.size()) {
1774 return nullptr;
1775 }
1776 return &operations_[static_cast<std::size_t>(handle.value)];
1777 }
1778
1779 [[nodiscard]] constexpr bool empty() const noexcept {
1780 return operations_.empty();
1781 }
1782
1783 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
1784 return operations_.size();
1785 }
1786
1787 // Clears queued operations for per-batch reuse while keeping the enqueue
1788 // vector's capacity, so warm batch loops re-enqueue without allocating.
1789 // Previously returned handles are invalidated; handle and id assignment
1790 // restarts at zero on the next enqueue.
1791 void clear() noexcept { operations_.clear(); }
1792
1793 private:
1794 [[nodiscard]] auto enqueue(OperationKind kind, IntentMetadata metadata,
1795 IntentPayloadView payload,
1796 std::source_location source) -> OpHandle {
1797 const auto id = OpId{static_cast<std::uint64_t>(operations_.size())};
1798 const auto handle = OpHandle{id.value};
1799 auto operation = QueuedOperation{};
1800 operation.kind = kind;
1801 operation.handle = handle;
1802 operation.id = id;
1803 operation.domain = std::move(metadata.domain);
1804 operation.field_access = metadata.field_access;
1805 operation.write_policy = metadata.write_policy;
1806 operation.priority = metadata.priority;
1807 operation.budget_policy = metadata.budget_policy;
1808 operation.source = source;
1809 operation.payload = payload;
1810 operation.versions = metadata.versions;
1811 operation.invalidations = metadata.invalidations;
1812 operation.backend = metadata.backend;
1813 operation.exactness = metadata.exactness;
1814 operations_.push_back(std::move(operation));
1815 return handle;
1816 }
1817
1818 std::vector<QueuedOperation> operations_;
1819};
1820
1821namespace detail {
1822
1823[[nodiscard]] constexpr auto operation_access(
1824 const QueuedOperation& op) noexcept -> OperationAccess {
1825 return OperationAccess{
1826 op.write_policy,
1827 op.domain.kind(),
1828 op.domain.mask_bits(),
1829 };
1830}
1831
1832[[nodiscard]] constexpr bool is_valid_field_access(
1833 WritePolicy write_policy, FieldAccessDesc field_access) noexcept {
1834 if (write_policy == WritePolicy::ReadOnly && field_access.write_mask != 0) {
1835 return false;
1836 }
1837 return true;
1838}
1839
1840template <typename World>
1841[[nodiscard]] auto validate_explicit_chunks(const World& world,
1842 std::span<const ChunkKey> chunks,
1843 ChunkKey& invalid_chunk) noexcept
1844 -> bool {
1845 for (const auto key : chunks) {
1846 if (world.try_chunk(key) == nullptr) {
1847 invalid_chunk = key;
1848 return false;
1849 }
1850 }
1851 return true;
1852}
1853
1854// Fills `chunks` in place (clearing it first) so a caller-supplied vector
1855// keeps its capacity across plans instead of being replaced by a fresh
1856// allocation per operation.
1857template <typename World>
1858[[nodiscard]] auto expand_domain(const World& world, const DomainDesc& domain,
1859 std::vector<ChunkKey>& chunks,
1860 ChunkKey& invalid_chunk) -> bool {
1861 chunks.clear();
1862 switch (domain.kind()) {
1863 case DomainKind::ExplicitChunks:
1864 if (!validate_explicit_chunks(world, domain.explicit_chunks(),
1865 invalid_chunk)) {
1866 return false;
1867 }
1868 chunks.assign(domain.explicit_chunks().begin(),
1869 domain.explicit_chunks().end());
1870 return true;
1871 case DomainKind::DirtyChunks:
1872 world.collect_dirty_chunks(domain.dirty_mask(), chunks);
1873 return true;
1874 case DomainKind::ActiveChunks:
1875 world.collect_active_chunks(domain.active_mask(), chunks);
1876 return true;
1877 case DomainKind::ResidentChunks:
1878 chunks.reserve(static_cast<std::size_t>(World::chunk_count));
1879 for (std::uint64_t key = 0; key < World::chunk_count; ++key) {
1880 chunks.push_back(ChunkKey{key});
1881 }
1882 return true;
1883 }
1884 return false;
1885}
1886
1887[[nodiscard]] constexpr auto hazard_mask(FieldAccessDesc earlier,
1888 FieldAccessDesc later) noexcept
1889 -> std::uint32_t {
1890 return (earlier.write_mask & later.write_mask) |
1891 (earlier.write_mask & later.read_mask) |
1892 (earlier.read_mask & later.write_mask);
1893}
1894
1895[[nodiscard]] constexpr bool chunks_overlap(
1896 std::span<const ChunkKey> lhs, std::span<const ChunkKey> rhs) noexcept {
1897 std::size_t lhs_index = 0;
1898 std::size_t rhs_index = 0;
1899 while (lhs_index < lhs.size() && rhs_index < rhs.size()) {
1900 const auto lhs_key = lhs[lhs_index].value;
1901 const auto rhs_key = rhs[rhs_index].value;
1902 if (lhs_key == rhs_key) {
1903 return true;
1904 }
1905 if (lhs_key < rhs_key) {
1906 ++lhs_index;
1907 } else {
1908 ++rhs_index;
1909 }
1910 }
1911 return false;
1912}
1913
1914[[nodiscard]] constexpr auto find_hazard(
1915 std::span<const PlannedOperation> earlier_ops,
1916 const PlannedOperation& later) noexcept -> const PlannedOperation* {
1917 for (const auto& earlier : earlier_ops) {
1918 if (hazard_mask(earlier.field_access, later.field_access) == 0) {
1919 continue;
1920 }
1921 if (chunks_overlap(earlier.chunks(), later.chunks())) {
1922 // cppcheck-suppress returnDanglingLifetime
1923 return &earlier;
1924 }
1925 }
1926 return nullptr;
1927}
1928
1929// Index-backed equivalent of `find_hazard`. It consults only the operations
1930// sharing a chunk with `later`, then keeps the earliest of those that also
1931// hazards on field access -- exactly the operation the linear scan returns,
1932// since that scan reports its first match in plan order. `conflict_handle`
1933// and `conflict_id` are observable, so picking any other match would be a
1934// behaviour change, not just a faster one.
1935[[nodiscard]] inline auto find_hazard_indexed(
1936 const ChunkOperationIndex& index, std::span<const std::uint32_t> wide_ops,
1937 std::span<const PlannedOperation> earlier_ops,
1938 const PlannedOperation& later) -> const PlannedOperation* {
1939 // A candidate this wide would walk one chain per chunk. The linear scan
1940 // it replaces rejects a non-hazarding pair on the mask alone, so for
1941 // these the scan is strictly cheaper and is what the planner uses.
1942 if (!operation_is_indexable(later.chunks())) {
1943 return find_hazard(earlier_ops, later);
1944 }
1945
1946 auto earliest = ChunkOperationIndex::npos;
1947 index.for_each_sharing(later.chunks(), [&](std::uint32_t op_index) {
1948 if (op_index >= earliest) {
1949 return;
1950 }
1951 if (hazard_mask(earlier_ops[op_index].field_access, later.field_access) ==
1952 0) {
1953 return;
1954 }
1955 earliest = op_index;
1956 });
1957 // Wide operations are not in the index, so they are scanned -- mask
1958 // first, exactly as `find_hazard` orders it. They are in plan order, so
1959 // the first match is the earliest among them.
1960 for (const auto op_index : wide_ops) {
1961 if (op_index >= earliest) {
1962 break;
1963 }
1964 const auto& earlier = earlier_ops[op_index];
1965 if (hazard_mask(earlier.field_access, later.field_access) == 0) {
1966 continue;
1967 }
1968 if (chunks_overlap(earlier.chunks(), later.chunks())) {
1969 earliest = op_index;
1970 break;
1971 }
1972 }
1973 if (earliest == ChunkOperationIndex::npos) {
1974 return nullptr;
1975 }
1976 return &earlier_ops[earliest];
1977}
1978
1979[[nodiscard]] constexpr bool is_parallel_supported_policy(
1980 WritePolicy policy) noexcept {
1981 return policy == WritePolicy::ReadOnly ||
1982 policy == WritePolicy::UniquePerChunk;
1983}
1984
1985[[nodiscard]] constexpr bool is_mutating_policy(WritePolicy policy) noexcept {
1986 return policy != WritePolicy::ReadOnly;
1987}
1988
1989// Plan length at or above which parallel-phase grouping builds an index
1990// instead of comparing every candidate against every open-phase member.
1991inline constexpr std::size_t phase_index_min_operations = 16;
1992
1993// Tail of `parallel_phase_conflict` for callers that already established
1994// the chunk overlap -- an index lookup does, so re-walking both chunk lists
1995// would repeat work the lookup just did.
1996[[nodiscard]] constexpr bool parallel_phase_conflict_given_overlap(
1997 const PlannedOperation& lhs, const PlannedOperation& rhs) noexcept {
1998 if (is_mutating_policy(lhs.write_policy) ||
1999 is_mutating_policy(rhs.write_policy)) {
2000 return true;
2001 }
2002 return hazard_mask(lhs.field_access, rhs.field_access) != 0;
2003}
2004
2005[[nodiscard]] constexpr bool parallel_phase_conflict(
2006 const PlannedOperation& lhs, const PlannedOperation& rhs) noexcept {
2007 if (!chunks_overlap(lhs.chunks(), rhs.chunks())) {
2008 return false;
2009 }
2010 return parallel_phase_conflict_given_overlap(lhs, rhs);
2011}
2012
2013[[nodiscard]] constexpr auto dirty_axis_end(std::int64_t origin,
2014 std::uint64_t extent) noexcept
2015 -> std::int64_t {
2016 // Saturating: an unguarded origin + int64(extent) is undefined for huge
2017 // caller-supplied extents, so share chunk_meta's guarded helper.
2018 return detail::box_axis_end(origin, extent);
2019}
2020
2021[[nodiscard]] constexpr auto dirty_min(std::int64_t lhs,
2022 std::int64_t rhs) noexcept
2023 -> std::int64_t {
2024 return lhs < rhs ? lhs : rhs;
2025}
2026
2027[[nodiscard]] constexpr auto dirty_max(std::int64_t lhs,
2028 std::int64_t rhs) noexcept
2029 -> std::int64_t {
2030 return lhs < rhs ? rhs : lhs;
2031}
2032
2033[[nodiscard]] constexpr auto dirty_union_extent(std::int64_t origin,
2034 std::int64_t end) noexcept
2035 -> std::uint64_t {
2036 // end >= origin, but a saturated INT64_MAX end paired with a negative
2037 // origin spans more than int64 can hold, so the subtraction must happen
2038 // in unsigned space, mirroring chunk_meta's union.
2039 return abs_delta(end, origin);
2040}
2041
2042[[nodiscard]] constexpr auto union_dirty_bounds(Box3 lhs, Box3 rhs) noexcept
2043 -> Box3 {
2044 const auto min_x = dirty_min(lhs.origin.x, rhs.origin.x);
2045 const auto min_y = dirty_min(lhs.origin.y, rhs.origin.y);
2046 const auto min_z = dirty_min(lhs.origin.z, rhs.origin.z);
2047 const auto max_x = dirty_max(dirty_axis_end(lhs.origin.x, lhs.extent.x),
2048 dirty_axis_end(rhs.origin.x, rhs.extent.x));
2049 const auto max_y = dirty_max(dirty_axis_end(lhs.origin.y, lhs.extent.y),
2050 dirty_axis_end(rhs.origin.y, rhs.extent.y));
2051 const auto max_z = dirty_max(dirty_axis_end(lhs.origin.z, lhs.extent.z),
2052 dirty_axis_end(rhs.origin.z, rhs.extent.z));
2053
2054 return Box3{
2055 Coord3{min_x, min_y, min_z},
2056 Extent3{
2057 dirty_union_extent(min_x, max_x),
2058 dirty_union_extent(min_y, max_y),
2059 dirty_union_extent(min_z, max_z),
2060 },
2061 };
2062}
2063
2064} // namespace detail
2065
2070template <typename World>
2071auto plan_operations(const World& world,
2072 std::span<const QueuedOperation> operations,
2073 ExecutionReport& report) -> const ExecutionReport& {
2074 // Queued operations require dense storage: ResidentChunks expands every
2075 // chunk key, and the executor writes through each expanded key. Reject sparse
2076 // worlds at compile time instead of enumerating non-resident chunks.
2077 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
2078 "Queued operations require an AlwaysResidentWorld; use direct "
2079 "sparse-world operations instead.");
2080 report.reset();
2081 report.reserve(operations.size());
2082
2083 for (std::size_t op_index = 0; op_index < operations.size(); ++op_index) {
2084 const auto& op = operations[op_index];
2085 const auto canonical_handle =
2086 OpHandle{static_cast<std::uint64_t>(op_index)};
2087 const auto canonical_id = OpId{static_cast<std::uint64_t>(op_index)};
2088 OperationReport op_report{
2089 canonical_handle,
2090 canonical_id,
2091 OperationStatus::Planned,
2092 OperationFailure::None,
2093 detail::operation_access(op),
2094 op.field_access,
2095 op.versions,
2096 op.invalidations,
2097 op.backend,
2098 op.exactness,
2099 {},
2100 {},
2101 {},
2102 0,
2103 false,
2104 false,
2105 0,
2106 op.source,
2107 };
2108
2109 if (op.handle != canonical_handle || op.id != canonical_id) {
2110 op_report.status = OperationStatus::InvalidIdentity;
2111 op_report.failure = op.handle != canonical_handle
2112 ? OperationFailure::NonDenseHandle
2113 : OperationFailure::NonDenseId;
2114 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
2115 "invalid_identity", op_index);
2116 report.push_report(op_report);
2117 continue;
2118 }
2119
2120 if (!is_valid_write_policy(op.write_policy)) {
2121 op_report.status = OperationStatus::InvalidWritePolicy;
2122 op_report.failure = OperationFailure::InvalidWritePolicyValue;
2123 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
2124 "invalid_write_policy", op_index);
2125 report.push_report(op_report);
2126 continue;
2127 }
2128
2129 if (!detail::is_valid_field_access(op.write_policy, op.field_access)) {
2130 op_report.status = OperationStatus::InvalidFieldAccess;
2131 op_report.failure = OperationFailure::ReadOnlyWriteMask;
2132 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
2133 "invalid_field_access", op_index);
2134 report.push_report(op_report);
2135 continue;
2136 }
2137
2138 auto planned_chunks = report.acquire_chunks();
2139 ChunkKey invalid_chunk{};
2140 if (!detail::expand_domain(world, op.domain, planned_chunks,
2141 invalid_chunk)) {
2142 op_report.status = OperationStatus::InvalidDomain;
2143 op_report.failure = OperationFailure::ExplicitChunkOutOfRange;
2144 op_report.detail_chunk = invalid_chunk;
2145 op_report.has_detail_chunk = true;
2146 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
2147 "invalid_domain", op_index);
2148 report.recycle_chunks(std::move(planned_chunks));
2149 report.push_report(op_report);
2150 continue;
2151 }
2152
2153 auto planned =
2154 report.template make_planned<World>(op, std::move(planned_chunks));
2155
2156 if (const auto* conflict = detail::find_hazard_indexed(
2157 report.chunk_index(), report.wide_operations(),
2158 report.plan().operations(), planned);
2159 conflict != nullptr) {
2160 op_report.status = OperationStatus::HazardConflict;
2161 op_report.failure = OperationFailure::FieldHazardConflict;
2162 op_report.conflict_handle = conflict->handle;
2163 op_report.conflict_id = conflict->id;
2164 op_report.conflict_mask =
2165 detail::hazard_mask(conflict->field_access, planned.field_access);
2166 op_report.has_conflict = true;
2167 op_report.chunk_count = planned.chunks().size();
2168 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "conflict",
2169 op_index);
2170 report.recycle_chunks(std::move(planned));
2171 report.push_report(op_report);
2172 continue;
2173 }
2174
2175 op_report.chunk_count = planned.chunks().size();
2176 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "planned",
2177 op_index);
2178 report.push_planned(std::move(planned));
2179 report.push_report(op_report);
2180 }
2181
2182 return report;
2183}
2184
2185template <typename World>
2187[[nodiscard]] auto plan_operations(const World& world,
2188 std::span<const QueuedOperation> operations)
2189 -> ExecutionReport {
2190 ExecutionReport report;
2191 plan_operations(world, operations, report);
2192 return report;
2193}
2194
2195template <typename World>
2197auto plan_operations(const World& world, const OperationBatch& ops,
2198 ExecutionReport& report) -> const ExecutionReport& {
2199 return plan_operations(world, ops.operations(), report);
2200}
2201
2202template <typename World>
2204[[nodiscard]] auto plan_operations(const World& world,
2205 const OperationBatch& ops)
2206 -> ExecutionReport {
2207 return plan_operations(world, ops.operations());
2208}
2209
2211[[nodiscard]] constexpr auto planned_chunk_domain(
2212 const PlannedOperation& operation) noexcept -> ChunkDomain {
2213 return chunk_domain(operation.chunks());
2214}
2215
2217[[nodiscard]] inline auto plan_parallel_execution_phases(
2218 const ExecutionPlan& plan) -> ExecutionPhasePlan {
2219 const auto operations = plan.operations();
2220 auto phases = ExecutionPhasePlan{};
2221 phases.reserve(operations.size());
2222 // Same index as hazard detection, rebuilt locally: this entry point takes
2223 // a plan rather than a report, so there is no long-lived one to reuse.
2224 // That rebuild costs two allocations, which a short plan never repays --
2225 // a handful of chunk-overlap checks is cheaper than the table they would
2226 // avoid -- so small plans keep the all-pairs comparison. No benchmark
2227 // covers small-plan grouping, so the cutoff is reasoned rather than
2228 // tuned; both paths are held to the same answer by a differential test.
2229 const auto indexed = operations.size() >= detail::phase_index_min_operations;
2230 auto index = detail::ChunkOperationIndex{};
2231 // Operations too wide to index are compared the way they always were.
2232 // See `index_max_chunks_per_operation`: a whole-domain operation would
2233 // otherwise cost one chain per chunk on every later comparison.
2234 auto wide_operations = std::vector<std::uint32_t>{};
2235 if (indexed) {
2236 index.reserve(operations.size());
2237 }
2238
2239 for (std::size_t i = 0; i < operations.size(); ++i) {
2240 const auto& operation = operations[i];
2241 if (!detail::is_parallel_supported_policy(operation.write_policy)) {
2242 phases.status_ = ExecutionPhaseStatus::UnsupportedWritePolicy;
2243 phases.failed_operation_index_ = i;
2244 phases.failed_write_policy_ = operation.write_policy;
2245 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
2246 "unsupported_write_policy", i);
2247 return phases;
2248 }
2249
2250 // Only members of the open phase matter, and the open phase is always
2251 // the suffix [first_operation, i), so filtering index hits on
2252 // `>= phase_first` is the same set the inner loop used to walk.
2253 auto conflicts = true;
2254 if (!phases.phases_.empty()) {
2255 const auto phase_first = phases.phases_.back().first_operation();
2256 conflicts = false;
2257 if (!indexed || !detail::operation_is_indexable(operation.chunks())) {
2258 for (std::size_t j = phase_first; j < i; ++j) {
2259 if (detail::parallel_phase_conflict(operations[j], operation)) {
2260 conflicts = true;
2261 break;
2262 }
2263 }
2264 } else {
2265 index.for_each_sharing(operation.chunks(), [&](std::uint32_t j) {
2266 if (conflicts || j < phase_first) {
2267 return;
2268 }
2269 conflicts = detail::parallel_phase_conflict_given_overlap(
2270 operations[j], operation);
2271 });
2272 for (const auto j : wide_operations) {
2273 if (conflicts) {
2274 break;
2275 }
2276 if (j < phase_first) {
2277 continue;
2278 }
2279 conflicts = detail::parallel_phase_conflict(operations[j], operation);
2280 }
2281 }
2282 }
2283
2284 if (conflicts) {
2285 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "new_phase",
2286 i);
2287 phases.push_phase(plan, i, 1, operation);
2288 } else {
2289 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner, "merged", i);
2290 phases.extend_last_phase(operation);
2291 }
2292 if (indexed) {
2293 if (detail::operation_is_indexable(operation.chunks())) {
2294 index.insert(operation.chunks(), static_cast<std::uint32_t>(i));
2295 } else {
2296 wide_operations.push_back(static_cast<std::uint32_t>(i));
2297 }
2298 }
2299 }
2300
2301 return phases;
2302}
2303
2308template <typename World>
2309// noexcept, and the only overload of this family that is: it merges an
2310// already-populated accumulator into the world and allocates nothing. The
2311// rule across the three is "noexcept iff the overload does not allocate",
2312// not an inconsistency -- marking the allocating ones noexcept would turn
2313// a std::bad_alloc into std::terminate. An -fno-exceptions consumer can
2314// therefore read the signature instead of the body.
2315[[nodiscard]] auto merge_planned_dirty(World& world,
2316 PlannedDirtyAccumulator& dirty) noexcept
2318 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
2319 "Queued-op dirty merge requires an AlwaysResidentWorld; sparse "
2320 "worlds must merge changes through direct operations.");
2321
2322 const auto validation = dirty.validation_status(world);
2323 if (validation != PlannedDirtyMergeStatus::Merged) {
2325 validation,
2326 0,
2327 };
2328 }
2329
2330 auto& records = dirty.records_;
2331 std::sort(records.begin(), records.end(),
2333 return lhs.chunk.value < rhs.chunk.value;
2334 });
2335
2336 // Sorting makes duplicate records adjacent so they can be coalesced without
2337 // allocating. This noexcept path cannot replace a callback exception while
2338 // AutoExec unwinds.
2339 auto merged_count = std::size_t{0};
2340 for (std::size_t i = 0; i < records.size();) {
2341 auto chunk = records[i].chunk;
2342 auto dirty_mask = records[i].dirty_mask;
2343 auto bounds = records[i].bounds;
2344 ++i;
2345
2346 while (i < records.size() && records[i].chunk == chunk) {
2347 dirty_mask |= records[i].dirty_mask;
2348 bounds = detail::union_dirty_bounds(bounds, records[i].bounds);
2349 ++i;
2350 }
2351
2352 world.mark_dirty(chunk, dirty_mask, bounds);
2353 ++merged_count;
2354 }
2355
2356 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
2357 dirty.clear();
2359 PlannedDirtyMergeStatus::Merged,
2360 merged_count,
2361 };
2362}
2363
2365[[nodiscard]] inline auto collect_planned_dirty(
2366 PlannedDirtyAccumulator& dirty, PlannedDirtyPartitions& partitions)
2368 auto* world_stamp = dirty.world_stamp_;
2369 for (const auto& partition : partitions.partitions_) {
2370 const auto* partition_stamp = partition.world_stamp_;
2371 if (partition_stamp == nullptr) {
2372 continue;
2373 }
2374 if (world_stamp == nullptr) {
2375 world_stamp = partition_stamp;
2376 continue;
2377 }
2378 if (world_stamp->shape_identity != partition_stamp->shape_identity) {
2380 PlannedDirtyCollectStatus::InvalidShape,
2381 0,
2382 };
2383 }
2384 if (world_stamp->chunk_limit != partition_stamp->chunk_limit) {
2386 PlannedDirtyCollectStatus::InvalidChunk,
2387 0,
2388 };
2389 }
2390 }
2391
2392 const auto record_limit =
2393 detail::effective_capacity_limit(dirty.records_.max_size());
2394 if (dirty.records_.size() > record_limit) {
2396 PlannedDirtyCollectStatus::CapacityExceeded,
2397 0,
2398 };
2399 }
2400 auto required_capacity = dirty.records_.size();
2401 auto record_count = std::size_t{0};
2402 for (const auto& partition : partitions.partitions_) {
2403 const auto partition_size = partition.records_.size();
2404 if (partition_size > record_limit - required_capacity) {
2406 PlannedDirtyCollectStatus::CapacityExceeded,
2407 0,
2408 };
2409 }
2410 required_capacity += partition_size;
2411 record_count += partition_size;
2412 }
2413 // Complete the only potentially allocating step before clearing a source;
2414 // a failed reserve therefore preserves both destination and partitions.
2415 dirty.records_.reserve(required_capacity);
2416
2417 for (auto& partition : partitions.partitions_) {
2418 if (partition.world_stamp_ != nullptr) {
2419 dirty.world_stamp_ = partition.world_stamp_;
2420 }
2421 dirty.records_.insert(dirty.records_.end(), partition.records_.begin(),
2422 partition.records_.end());
2423 partition.clear();
2424 }
2425 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2427 PlannedDirtyCollectStatus::Collected,
2428 record_count,
2429 };
2430}
2431
2432template <typename World>
2434// Deliberately NOT noexcept: collect_planned_dirty reserves the
2435// destination accumulator, so this can throw std::bad_alloc. See the
2436// accumulator overload for the rule.
2437[[nodiscard]] auto merge_planned_dirty(World& world,
2438 PlannedDirtyAccumulator& dirty_scratch,
2439 PlannedDirtyPartitions& partitions)
2441 for (const auto& partition : partitions.partitions()) {
2442 const auto validation = partition.validation_status(world);
2443 if (validation != PlannedDirtyMergeStatus::Merged) {
2444 return PlannedDirtyMergeResult{validation, 0};
2445 }
2446 }
2447 dirty_scratch.clear();
2448 const auto collected = collect_planned_dirty(dirty_scratch, partitions);
2449 if (!collected.ok()) {
2450 if (collected.status == PlannedDirtyCollectStatus::CapacityExceeded) {
2451 return PlannedDirtyMergeResult{
2452 PlannedDirtyMergeStatus::CapacityExceeded,
2453 0,
2454 };
2455 }
2456 return PlannedDirtyMergeResult{
2457 collected.status == PlannedDirtyCollectStatus::InvalidShape
2458 ? PlannedDirtyMergeStatus::InvalidShape
2459 : PlannedDirtyMergeStatus::InvalidChunk,
2460 0,
2461 };
2462 }
2463 return merge_planned_dirty(world, dirty_scratch);
2464}
2465
2466template <typename World>
2476[[nodiscard]] auto merge_planned_dirty(World& world,
2479 if (scratch.world_stamp_ == nullptr) {
2481 PlannedDirtyMergeStatus::Merged,
2482 0,
2483 };
2484 }
2485 const auto validation =
2486 detail::validate_planned_world_stamp<World>(scratch.world_stamp_);
2487 if (validation != PlannedExecutionStatus::Executed) {
2489 validation == PlannedExecutionStatus::InvalidShape
2490 ? PlannedDirtyMergeStatus::InvalidShape
2491 : PlannedDirtyMergeStatus::InvalidChunk,
2492 0,
2493 };
2494 }
2495
2496 auto& merged = scratch.merged_dirty_;
2497 const auto record_limit =
2498 detail::effective_capacity_limit(merged.records_.max_size());
2499 auto record_count = std::size_t{0};
2500 for (const auto& partition : scratch.dirty_partitions_) {
2501 const auto partition_size = partition.records().size();
2502 if (record_count > record_limit ||
2503 partition_size > record_limit - record_count) {
2505 PlannedDirtyMergeStatus::CapacityExceeded,
2506 0,
2507 };
2508 }
2509 record_count += partition_size;
2510 }
2511 merged.clear();
2512 // Reserve before transferring anything, so allocation failure leaves every
2513 // phase partition available to the caller.
2514 merged.records_.reserve(record_count);
2515 merged.world_stamp_ = scratch.world_stamp_;
2516 for (auto& partition : scratch.dirty_partitions_) {
2517 const auto records = partition.records();
2518 merged.records_.insert(merged.records_.end(), records.begin(),
2519 records.end());
2520 partition.clear();
2521 }
2522 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2523 (void)record_count;
2524 return merge_planned_dirty(world, merged);
2525}
2526
2528template <typename World>
2529auto detail::merge_planned_dirty_after_exception(
2530 World& world, PlannedPhaseExecutionScratch& scratch) noexcept
2532 if (scratch.world_stamp_ == nullptr) {
2534 PlannedDirtyMergeStatus::Merged,
2535 0,
2536 };
2537 }
2538 const auto validation =
2539 detail::validate_planned_world_stamp<World>(scratch.world_stamp_);
2540 if (validation != PlannedExecutionStatus::Executed) {
2541 return PlannedDirtyMergeResult{
2542 validation == PlannedExecutionStatus::InvalidShape
2543 ? PlannedDirtyMergeStatus::InvalidShape
2544 : PlannedDirtyMergeStatus::InvalidChunk,
2545 0,
2546 };
2547 }
2548
2549 // This cold path must preserve the original callback exception. Coalesce
2550 // with an allocation-free quadratic scan: exceptions are rare, and normal
2551 // phase sizes should not dictate whether failed work remains observable.
2552 auto record_count = std::size_t{0};
2553 for (const auto& partition : scratch.dirty_partitions_) {
2554 const auto partition_size = partition.records().size();
2555 if (partition_size >
2556 std::numeric_limits<std::size_t>::max() - record_count) {
2557 record_count = std::numeric_limits<std::size_t>::max();
2558 break;
2559 }
2560 record_count += partition_size;
2561 }
2562 auto merged_count = std::size_t{0};
2563 for (std::size_t partition_index = 0;
2564 partition_index < scratch.dirty_partitions_.size(); ++partition_index) {
2565 const auto records = scratch.dirty_partitions_[partition_index].records();
2566 for (std::size_t record_index = 0; record_index < records.size();
2567 ++record_index) {
2568 const auto record = records[record_index];
2569 auto appeared_earlier = false;
2570 for (std::size_t earlier_partition = 0;
2571 earlier_partition <= partition_index && !appeared_earlier;
2572 ++earlier_partition) {
2573 const auto earlier_records =
2574 scratch.dirty_partitions_[earlier_partition].records();
2575 const auto earlier_count = earlier_partition == partition_index
2576 ? record_index
2577 : earlier_records.size();
2578 for (std::size_t earlier_index = 0; earlier_index < earlier_count;
2579 ++earlier_index) {
2580 if (earlier_records[earlier_index].chunk == record.chunk) {
2581 appeared_earlier = true;
2582 break;
2583 }
2584 }
2585 }
2586 if (appeared_earlier) {
2587 continue;
2588 }
2589
2590 auto dirty_mask = record.dirty_mask;
2591 auto bounds = record.bounds;
2592 for (std::size_t later_partition = partition_index;
2593 later_partition < scratch.dirty_partitions_.size();
2594 ++later_partition) {
2595 const auto later_records =
2596 scratch.dirty_partitions_[later_partition].records();
2597 const auto first_later = later_partition == partition_index
2598 ? record_index + 1
2599 : std::size_t{0};
2600 for (std::size_t later_index = first_later;
2601 later_index < later_records.size(); ++later_index) {
2602 const auto later = later_records[later_index];
2603 if (later.chunk == record.chunk) {
2604 dirty_mask |= later.dirty_mask;
2605 bounds = detail::union_dirty_bounds(bounds, later.bounds);
2606 }
2607 }
2608 }
2609 world.mark_dirty(record.chunk, dirty_mask, bounds);
2610 ++merged_count;
2611 }
2612 }
2613 for (auto& partition : scratch.dirty_partitions_) {
2614 partition.clear();
2615 }
2616 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2617 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
2618 (void)record_count;
2619 (void)merged_count;
2620 return PlannedDirtyMergeResult{
2621 PlannedDirtyMergeStatus::Merged,
2622 merged_count,
2623 };
2624}
2625
2627template <WritePolicy Policy>
2628[[nodiscard]] constexpr bool planned_policy_matches(
2629 const PlannedOperation& operation) noexcept {
2630 return operation.write_policy == Policy;
2631}
2632
2634template <WritePolicy Policy, typename World>
2635[[nodiscard]] auto validate_planned_operation(
2636 const World& world, const PlannedOperation& operation) noexcept
2637 -> PlannedExecutionStatus {
2638 const auto world_status = operation.world_validation_status(world);
2639 if (world_status != PlannedExecutionStatus::Executed) {
2640 return world_status;
2641 }
2642 if (!planned_policy_matches<Policy>(operation)) {
2643 return PlannedExecutionStatus::PolicyMismatch;
2644 }
2645 return PlannedExecutionStatus::Executed;
2646}
2647
2649template <WritePolicy Policy, typename World>
2650[[nodiscard]] constexpr auto try_planned_block_ctx(
2651 World& world, const PlannedOperation& operation) noexcept
2652 -> std::optional<BlockCtx<World, Policy>> {
2653 static_assert(
2654 std::is_same_v<typename World::residency_type, AlwaysResident>,
2655 "Queued-op execution requires an AlwaysResidentWorld; use direct "
2656 "sparse-world operations instead.");
2657 if (validate_planned_operation<Policy>(world, operation) !=
2658 PlannedExecutionStatus::Executed) {
2659 return std::nullopt;
2660 }
2661 return block_ctx<Policy>(world, planned_chunk_domain(operation));
2662}
2663
2665template <WritePolicy Policy, typename World, typename Fn>
2666[[nodiscard]] auto execute_planned_operation(World& world,
2667 const PlannedOperation& operation,
2668 Fn&& fn)
2669 -> PlannedExecutionResult {
2670 const auto validation = validate_planned_operation<Policy>(world, operation);
2671 if (validation != PlannedExecutionStatus::Executed) {
2672 return PlannedExecutionResult{
2673 validation,
2674 0,
2675 };
2676 }
2677 auto ctx = block_ctx<Policy>(world, planned_chunk_domain(operation));
2678
2679 std::size_t chunk_count = 0;
2680 auto&& callback = fn;
2681 ctx.for_each_chunk([&](auto view) {
2682 if (operation.field_access.dirty_mask) {
2683 world.mark_dirty(view.key(), operation.field_access.dirty_mask,
2684 view.bounds());
2685 }
2686 callback(view);
2687 ++chunk_count;
2688 });
2689
2690 return PlannedExecutionResult{
2691 PlannedExecutionStatus::Executed,
2692 chunk_count,
2693 };
2694}
2695
2697template <WritePolicy Policy, typename World, typename Fn>
2699 World& world, const PlannedOperation& operation,
2701 const auto validation = validate_planned_operation<Policy>(world, operation);
2702 if (validation != PlannedExecutionStatus::Executed) {
2704 validation,
2705 0,
2706 };
2707 }
2708 if (operation.field_access.dirty_mask) {
2709 const auto dirty_validation = dirty.validation_status(world);
2710 if (dirty_validation != PlannedDirtyMergeStatus::Merged) {
2712 dirty_validation == PlannedDirtyMergeStatus::InvalidShape
2713 ? PlannedExecutionStatus::InvalidShape
2714 : PlannedExecutionStatus::InvalidChunk,
2715 0,
2716 };
2717 }
2718 }
2719 return detail::execute_validated_planned_operation_deferred_dirty<true,
2720 Policy>(
2721 world, operation, dirty, std::forward<Fn>(fn));
2722}
2723
2728template <WritePolicy Policy, typename World, typename Fn>
2729[[nodiscard]] auto execute_plan(World& world, const ExecutionPlan& plan,
2730 Fn&& fn) -> PlannedExecutionResult {
2731 std::size_t chunk_count = 0;
2732 auto&& callback = fn;
2733 for (const auto& operation : plan.operations()) {
2734 auto result = execute_planned_operation<Policy>(world, operation, callback);
2735 if (result.status != PlannedExecutionStatus::Executed) {
2737 result.status,
2738 chunk_count + result.chunk_count,
2739 };
2740 }
2741 chunk_count += result.chunk_count;
2742 }
2743 return PlannedExecutionResult{
2744 PlannedExecutionStatus::Executed,
2745 chunk_count,
2746 };
2747}
2748
2750template <WritePolicy Policy, typename World, typename Fn>
2751[[nodiscard]] auto execute_plan_deferred_dirty(World& world,
2752 const ExecutionPlan& plan,
2753 PlannedDirtyAccumulator& dirty,
2754 Fn&& fn)
2755 -> PlannedExecutionResult {
2756 std::size_t chunk_count = 0;
2757 auto&& callback = fn;
2758 for (const auto& operation : plan.operations()) {
2759 auto result = execute_planned_operation_deferred_dirty<Policy>(
2760 world, operation, dirty, callback);
2761 if (result.status != PlannedExecutionStatus::Executed) {
2762 return PlannedExecutionResult{
2763 result.status,
2764 chunk_count + result.chunk_count,
2765 };
2766 }
2767 chunk_count += result.chunk_count;
2768 }
2769 return PlannedExecutionResult{
2770 PlannedExecutionStatus::Executed,
2771 chunk_count,
2772 };
2773}
2774
2775template <WritePolicy Policy, typename Executor, typename World, typename Fn>
2776 requires SerialExecutor<Executor>
2778[[nodiscard]] auto execute_phase_deferred_dirty_with(
2779 Executor&& executor, World& world, const ExecutionPlan& plan,
2780 const ExecutionPhase& phase, PlannedDirtyAccumulator& dirty, Fn&& fn)
2781 -> PlannedExecutionResult {
2782 const auto operations = plan.operations();
2783 const auto phase_validation =
2784 detail::execution_phase_validation_status<Policy>(world, plan, phase);
2785 if (phase_validation != PlannedExecutionStatus::Executed) {
2786 detail::record_execution_phase_validation_failure(phase_validation);
2787 return PlannedExecutionResult{
2788 phase_validation,
2789 0,
2790 };
2791 }
2792 const auto dirty_validation = dirty.validation_status(world);
2793 if (dirty_validation != PlannedDirtyMergeStatus::Merged) {
2794 TESS_DIAG_EVENT(queued_phase_failure);
2795 return PlannedExecutionResult{
2796 dirty_validation == PlannedDirtyMergeStatus::InvalidShape
2797 ? PlannedExecutionStatus::InvalidShape
2798 : PlannedExecutionStatus::InvalidChunk,
2799 0,
2800 };
2801 }
2802
2803 TESS_DIAG_EVENT_VALUE(queued_phase_execute, phase.operation_count());
2804 std::size_t chunk_count = 0;
2805 auto&& callback = fn;
2806 auto result = execute_operation_index_range(
2807 std::forward<Executor>(executor), executor_phase_range(phase),
2808 [&](std::size_t index) {
2809 auto operation_result =
2810 detail::execute_validated_planned_operation_deferred_dirty<true,
2811 Policy>(
2812 world, operations[index], dirty, callback);
2813 if (operation_result.status == PlannedExecutionStatus::Executed) {
2814 chunk_count += operation_result.chunk_count;
2815 }
2816 return operation_result;
2817 });
2818 if (result.status != PlannedExecutionStatus::Executed) {
2819 TESS_DIAG_EVENT(queued_phase_failure);
2820 result.chunk_count = chunk_count;
2821 return result;
2822 }
2823
2824 return PlannedExecutionResult{
2825 PlannedExecutionStatus::Executed,
2826 chunk_count,
2827 };
2828}
2829
2831template <WritePolicy Policy, typename Executor, typename World, typename Fn>
2833 Executor&& executor, World& world, const ExecutionPlan& plan,
2834 const ExecutionPhase& phase, PlannedPhaseExecutionScratch& scratch, Fn&& fn)
2836 const auto operations = plan.operations();
2837 const auto phase_validation =
2838 detail::execution_phase_validation_status<Policy>(world, plan, phase);
2839 if (phase_validation != PlannedExecutionStatus::Executed) {
2840 detail::record_execution_phase_validation_failure(phase_validation);
2842 phase_validation,
2843 0,
2844 };
2845 }
2846
2847 TESS_DIAG_EVENT_VALUE(queued_phase_execute, phase.operation_count());
2848 TESS_DIAG_EVENT_VALUE(queued_partitioned_phase, phase.operation_count());
2849 scratch.prepare(world, phase.operation_count());
2850 auto&& callback = fn;
2851 constexpr auto no_throw_callback =
2852 detail::planned_callback_is_nothrow<Policy, World, decltype(callback)>;
2853 for (std::size_t offset = 0; offset < phase.operation_count(); ++offset) {
2854 const auto index = phase.first_operation() + offset;
2855 scratch.dirty_for_operation(offset).reserve(
2856 !operations[index].field_access.dirty_mask
2857 ? 0
2858 : operations[index].chunks().size());
2859 }
2860 auto result = execute_operation_index_range(
2861 std::forward<Executor>(executor), executor_phase_range(phase),
2862 // Each partition reserved one record per possible chunk visit above;
2863 // push_back therefore cannot allocate. Clang-tidy 22 does not carry
2864 // that capacity proof through std::vector.
2865 // NOLINTNEXTLINE(bugprone-exception-escape)
2866 [&](std::size_t index) noexcept(no_throw_callback) {
2867 const auto offset = index - phase.first_operation();
2868 auto operation_result =
2869 detail::execute_validated_phase_operation_deferred_dirty<Policy>(
2870 world, operations[index], scratch.dirty_for_operation(offset),
2871 callback);
2872 scratch.record_result(offset, operation_result);
2873 return operation_result;
2874 });
2875
2876 std::size_t chunk_count = 0;
2877 for (const auto operation_result : scratch.results()) {
2878 if (operation_result.status != PlannedExecutionStatus::Executed) {
2879 TESS_DIAG_EVENT(queued_phase_failure);
2881 operation_result.status,
2882 chunk_count,
2883 };
2884 }
2885 chunk_count += operation_result.chunk_count;
2886 }
2887
2888 if (result.status != PlannedExecutionStatus::Executed) {
2889 TESS_DIAG_EVENT(queued_phase_failure);
2891 result.status,
2892 chunk_count,
2893 };
2894 }
2895
2897 PlannedExecutionStatus::Executed,
2898 chunk_count,
2899 };
2900}
2901
2903template <WritePolicy Policy, typename World, typename Fn>
2904[[nodiscard]] auto execute_phase_deferred_dirty(World& world,
2905 const ExecutionPlan& plan,
2906 const ExecutionPhase& phase,
2908 Fn&& fn)
2910 const SerialPhaseExecutor executor;
2911 return execute_phase_deferred_dirty_with<Policy>(executor, world, plan, phase,
2912 dirty, std::forward<Fn>(fn));
2913}
2914
2915} // namespace tess
Definition block.h:330
Definition queued.h:284
constexpr auto mask_bits() const noexcept -> std::uint32_t
Returns the stored selector bits for planner identity and diagnostics.
Definition queued.h:333
Definition queued.h:791
friend auto plan_parallel_execution_phases(const ExecutionPlan &plan) -> ExecutionPhasePlan
Definition queued.h:2217
Definition queued.h:709
constexpr bool policy_matches() const noexcept
Definition queued.h:743
auto world_validation_status(const World &) const noexcept -> PlannedExecutionStatus
Definition queued.h:732
constexpr bool belongs_to(const ExecutionPlan &plan) const noexcept
Definition queued.h:725
Definition queued.h:647
Definition queued.h:1521
friend auto plan_operations(const World &world, std::span< const QueuedOperation > operations, ExecutionReport &report) -> const ExecutionReport &
Definition queued.h:2071
Definition queued.h:1668
Per-operation dirty-record buffer owned by a phase execution scratch.
Definition queued.h:1196
Definition queued.h:978
auto record(const World &, ChunkKey chunk, DirtyMask dirty_mask, Box3 bounds) -> PlannedDirtyRecordStatus
Definition queued.h:989
friend auto collect_planned_dirty(PlannedDirtyAccumulator &dirty, PlannedDirtyPartitions &partitions) -> PlannedDirtyCollectResult
Definition queued.h:2365
auto validation_status(const World &) const noexcept -> PlannedDirtyMergeStatus
Definition queued.h:1026
friend auto merge_planned_dirty(World &world, PlannedDirtyAccumulator &dirty) noexcept -> PlannedDirtyMergeResult
Definition queued.h:2315
friend auto execute_planned_operation_deferred_dirty(World &world, const PlannedOperation &operation, PlannedDirtyAccumulator &dirty, Fn &&fn) -> PlannedExecutionResult
Definition queued.h:2698
Definition queued.h:1121
friend auto collect_planned_dirty(PlannedDirtyAccumulator &dirty, PlannedDirtyPartitions &partitions) -> PlannedDirtyCollectResult
Definition queued.h:2365
Definition queued.h:525
constexpr auto chunks() const noexcept -> std::span< const ChunkKey >
Definition queued.h:557
auto world_validation_status(const World &) const noexcept -> PlannedExecutionStatus
Definition queued.h:564
static auto create(const World &world, const QueuedOperation &operation, std::span< const ChunkKey > chunks) -> PlannedOperationCreateResult
Definition queued.h:611
Definition queued.h:1415
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:315
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:2832
friend auto merge_planned_dirty(World &world, PlannedPhaseExecutionScratch &scratch) -> PlannedDirtyMergeResult
Definition queued.h:2476
Dense per-operation completion and payload channel.
Definition result_channel.h:92
Definition world.h:22
Definition metadata_types.h:49
Definition shape.h:94
Definition shape.h:86
Definition shape.h:46
Definition metadata_types.h:12
Definition phase_executor.h:54
Definition shape.h:14
Definition queued.h:351
Definition queued.h:402
Definition queued.h:121
Definition queued.h:361
Definition queued.h:138
auto holds() const noexcept -> bool
Definition queued.h:161
auto as() const noexcept -> std::span< const T >
Definition queued.h:207
auto bound() const noexcept -> bool
Definition queued.h:176
Definition queued.h:110
Definition queued.h:449
Definition queued.h:416
Definition queued.h:388
Definition queued.h:474
Definition queued.h:76
Definition queued.h:69
Definition queued.h:502
Definition queued.h:883
Definition queued.h:374
Definition queued.h:469
Definition queued.h:939
Definition queued.h:949
Definition queued.h:905
Definition phase_executor.h:65
Definition queued.h:603
Definition queued.h:484
Definition queued.h:455
Definition queued.h:435
Definition phase_executor.h:97
Definition queued.h:430