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>
18#include <source_location>
29struct PlannedWorldStamp {
30 std::uintptr_t shape_identity = 0;
31 std::uint64_t chunk_limit = 0;
34template <
typename Shape, std::u
int64_t ChunkLimit>
35[[nodiscard]]
inline auto planned_world_stamp() noexcept
36 -> const PlannedWorldStamp* {
37 static const auto stamp = PlannedWorldStamp{
38 tag_identity<Shape>(),
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>();
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;
57 if (stamp ==
nullptr || stamp->shape_identity != expected->shape_identity) {
58 return PlannedExecutionStatus::InvalidShape;
60 if (stamp->chunk_limit != expected->chunk_limit) {
61 return PlannedExecutionStatus::InvalidChunk;
63 return PlannedExecutionStatus::Executed;
70 std::uint64_t value = 0;
72 friend constexpr bool operator==(
OpId lhs,
OpId rhs)
noexcept =
default;
77 std::uint64_t value = 0;
79 friend constexpr bool operator==(
OpHandle lhs,
84enum class OperationKind : std::uint8_t {
95static_assert(
sizeof(OperationKind) ==
sizeof(std::uint8_t));
98enum class BackendEligibility : std::uint8_t {
104enum class ExactnessRequirement : std::uint8_t {
111 std::uint64_t world = 0;
112 std::uint64_t topology = 0;
113 std::uint64_t fields = 0;
114 std::uint64_t product = 0;
123 bool topology =
false;
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>);
146 detail::tag_identity<Item>()};
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>();
176 [[nodiscard]]
auto bound() const noexcept ->
bool {
177 return type_identity != 0;
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>);
211 "IntentPayloadView::as<T> on a payload that does not "
212 "hold T; check holds<T>() first");
216 return {
static_cast<const T*
>(data), count};
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;
226enum class Priority : std::uint8_t {
233static_assert(
sizeof(Priority) ==
sizeof(std::uint8_t));
236enum class BudgetPolicy : std::uint8_t {
242static_assert(
sizeof(BudgetPolicy) ==
sizeof(std::uint8_t));
245enum class OperationStatus : std::uint8_t {
253static_assert(
sizeof(OperationStatus) ==
sizeof(std::uint8_t));
256enum class OperationFailure : std::uint8_t {
260 InvalidWritePolicyValue,
261 ExplicitChunkOutOfRange,
265static_assert(
sizeof(OperationFailure) ==
sizeof(std::uint8_t));
268enum class ExecutionPhaseStatus : std::uint8_t {
270 UnsupportedWritePolicy,
272static_assert(
sizeof(ExecutionPhaseStatus) ==
sizeof(std::uint8_t));
275enum class DomainKind : std::uint8_t {
281static_assert(
sizeof(DomainKind) ==
sizeof(std::uint8_t));
290 [[nodiscard]]
static auto explicit_chunks(std::span<const ChunkKey> keys)
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(),
296 desc.explicit_chunks_.erase(
297 std::unique(desc.explicit_chunks_.begin(), desc.explicit_chunks_.end()),
298 desc.explicit_chunks_.end());
302 [[nodiscard]]
static constexpr auto dirty_chunks(
DirtyMask mask)
noexcept
304 DomainDesc desc{DomainKind::DirtyChunks};
305 desc.mask_ = mask.value;
309 [[nodiscard]]
static constexpr auto active_chunks(
ActiveMask mask)
noexcept
311 DomainDesc desc{DomainKind::ActiveChunks};
312 desc.mask_ = mask.value;
316 [[nodiscard]]
static constexpr auto resident_chunks()
noexcept -> DomainDesc {
317 return DomainDesc{DomainKind::ResidentChunks};
320 [[nodiscard]]
constexpr auto kind()
const noexcept -> DomainKind {
324 [[nodiscard]]
constexpr auto dirty_mask()
const noexcept ->
DirtyMask {
328 [[nodiscard]]
constexpr auto active_mask()
const noexcept ->
ActiveMask {
333 [[nodiscard]]
constexpr auto mask_bits() const noexcept -> std::uint32_t {
337 [[nodiscard]]
constexpr auto explicit_chunks() const noexcept
339 return {explicit_chunks_.data(), explicit_chunks_.size()};
343 constexpr explicit DomainDesc(DomainKind kind) noexcept : kind_(kind) {}
346 std::uint32_t mask_ = 0;
347 std::vector<ChunkKey> explicit_chunks_;
352 std::uint32_t read_mask = 0;
353 std::uint32_t write_mask = 0;
362 DomainDesc domain = DomainDesc::resident_chunks();
364 WritePolicy write_policy = WritePolicy::ReadOnly;
365 Priority priority = Priority::GameplayCritical;
366 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
369 BackendEligibility backend = BackendEligibility::CpuOnly;
370 ExactnessRequirement exactness = ExactnessRequirement::Exact;
375 template <
typename Request, std::
size_t Extent>
376 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
380 std::move(operation)};
389 template <
typename Request, std::
size_t Extent>
390 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
394 std::move(operation)};
403 template <
typename Request, std::
size_t Extent>
404 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
408 std::move(operation)};
417 template <
typename Request, std::
size_t Extent>
418 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
422 std::move(operation)};
436 template <
typename Request, std::
size_t Extent>
437 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
441 std::move(operation)};
450 DomainDesc domain = DomainDesc::resident_chunks();
456 template <
typename Request, std::
size_t Extent>
457 [[nodiscard]]
static auto from(std::span<Request, Extent> requests,
461 std::move(operation)};
485 OperationKind kind = OperationKind::UpdateField;
488 DomainDesc domain = DomainDesc::resident_chunks();
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();
497 BackendEligibility backend = BackendEligibility::CpuOnly;
498 ExactnessRequirement exactness = ExactnessRequirement::Exact;
503 WritePolicy write_policy = WritePolicy::ReadOnly;
504 DomainKind domain_kind = DomainKind::ResidentChunks;
505 std::uint32_t domain_mask = 0;
509enum class PlannedOperationCreateStatus : std::uint8_t {
513static_assert(
sizeof(PlannedOperationCreateStatus) ==
sizeof(std::uint8_t));
515struct PlannedOperationCreateResult;
516class ExecutionReport;
525class PlannedOperation {
527 OperationKind kind = OperationKind::UpdateField;
532 WritePolicy write_policy = WritePolicy::ReadOnly;
533 Priority priority = Priority::GameplayCritical;
534 BudgetPolicy budget_policy = BudgetPolicy::MustRun;
538 BackendEligibility backend = BackendEligibility::CpuOnly;
539 ExactnessRequirement exactness = ExactnessRequirement::Exact;
542 std::source_location source = std::source_location::current();
550 template <
typename World>
551 [[nodiscard]]
static auto create(
const World& world,
553 std::span<const ChunkKey>
chunks)
557 [[nodiscard]]
constexpr auto chunks() const noexcept
563 template <
typename World>
565 const World& )
const noexcept -> PlannedExecutionStatus {
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_);
578 std::vector<ChunkKey>&&
chunks,
579 const detail::PlannedWorldStamp* world_stamp) noexcept
580 : kind(operation.kind),
581 handle(operation.handle),
583 access(
OperationAccess{operation.write_policy, operation.domain.kind(),
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) {}
598 std::vector<ChunkKey> chunks_;
599 const detail::PlannedWorldStamp* world_stamp_ =
nullptr;
604 PlannedOperationCreateStatus status =
605 PlannedOperationCreateStatus::InvalidChunk;
606 std::optional<PlannedOperation> operation;
610template <
typename World>
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.");
619 for (
const auto key :
chunks) {
620 if (key.value >= World::chunk_count) {
622 PlannedOperationCreateStatus::InvalidChunk,
629 auto validated = std::vector<ChunkKey>{
chunks.begin(),
chunks.end()};
630 std::sort(validated.begin(), validated.end(),
632 validated.erase(std::unique(validated.begin(), validated.end()),
634 auto planned = PlannedOperation{
636 std::move(validated),
637 detail::planned_world_stamp<World>(),
640 PlannedOperationCreateStatus::Created,
641 std::optional<PlannedOperation>{std::move(planned)},
652 [[nodiscard]]
constexpr auto operations()
const&
noexcept
653 -> std::span<const PlannedOperation> {
654 return {operations_.data(), operations_.size()};
656 auto operations()
const&& -> std::span<const PlannedOperation> =
delete;
658 [[nodiscard]]
constexpr bool empty()
const noexcept {
659 return operations_.empty();
662 [[nodiscard]]
constexpr auto size()
const noexcept -> std::size_t {
663 return operations_.size();
667 friend class ExecutionReport;
668 friend class ExecutionPhase;
670 ExecutionPlan()
noexcept =
default;
671 ExecutionPlan(
const ExecutionPlan&) =
default;
672 ExecutionPlan(ExecutionPlan&&)
noexcept =
default;
677 auto operator=(
const ExecutionPlan& other) -> ExecutionPlan& {
678 if (
this != &other) {
683 operations_ = other.operations_;
688 auto operator=(ExecutionPlan&& other)
noexcept -> ExecutionPlan& {
689 if (
this != &other) {
690 operations_ = std::move(other.operations_);
696 constexpr void bump_generation()
noexcept { ++generation_; }
698 std::vector<PlannedOperation> operations_;
699 std::uint64_t generation_ = 0;
709class ExecutionPhase {
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;
716 [[nodiscard]]
constexpr auto first_operation()
const noexcept -> std::size_t {
717 return first_operation_;
720 [[nodiscard]]
constexpr auto operation_count()
const noexcept -> std::size_t {
721 return operation_count_;
727 return plan_ == &plan && plan_generation_ == plan.generation_;
731 template <
typename World>
733 const World& )
const noexcept -> PlannedExecutionStatus {
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_);
742 template <WritePolicy Policy>
744 static_assert(is_valid_write_policy(Policy));
745 return write_policy_mask_ == policy_bit(Policy);
751 [[nodiscard]]
static constexpr auto policy_bit(WritePolicy policy)
noexcept
753 return static_cast<std::uint8_t
>(std::uint8_t{1}
754 <<
static_cast<std::uint8_t
>(policy));
757 constexpr ExecutionPhase(
const ExecutionPlan& plan,
758 std::size_t first_operation,
759 std::size_t operation_count,
760 const PlannedOperation& operation) noexcept
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)) {}
768 constexpr void extend(
const PlannedOperation& operation)
noexcept {
770 write_policy_mask_ |= policy_bit(operation.write_policy);
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_;
782[[nodiscard]]
constexpr auto executor_phase_range(
785 phase.first_operation(),
786 phase.operation_count(),
793 [[nodiscard]]
constexpr auto phases()
const noexcept
794 -> std::span<const ExecutionPhase> {
795 return {phases_.data(), phases_.size()};
798 [[nodiscard]]
constexpr auto status()
const noexcept -> ExecutionPhaseStatus {
802 [[nodiscard]]
constexpr bool ok()
const noexcept {
803 return status_ == ExecutionPhaseStatus::Ready;
806 [[nodiscard]]
constexpr auto failed_operation_index()
const noexcept
808 return failed_operation_index_;
811 [[nodiscard]]
constexpr auto failed_write_policy()
const noexcept
813 return failed_write_policy_;
820 void reserve(std::size_t size) { phases_.reserve(size); }
822 void push_phase(
const ExecutionPlan& plan, std::size_t first_operation,
823 std::size_t operation_count,
826 ExecutionPhase{plan, first_operation, operation_count, operation});
830 phases_.back().extend(operation);
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;
841[[nodiscard]]
constexpr bool execution_phase_valid_for(
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;
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;
857 const auto world_status = phase.world_validation_status(world);
858 if (world_status != PlannedExecutionStatus::Executed) {
861 if (!phase.template policy_matches<Policy>()) {
862 return PlannedExecutionStatus::PolicyMismatch;
864 return PlannedExecutionStatus::Executed;
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);
873 TESS_DIAG_EVENT(queued_phase_failure);
886 OperationStatus status = OperationStatus::Planned;
887 OperationFailure failure = OperationFailure::None;
892 BackendEligibility backend = BackendEligibility::CpuOnly;
893 ExactnessRequirement exactness = ExactnessRequirement::Exact;
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();
912enum class PlannedDirtyRecordStatus : std::uint8_t {
918static_assert(
sizeof(PlannedDirtyRecordStatus) ==
sizeof(std::uint8_t));
921enum class PlannedDirtyMergeStatus : std::uint8_t {
927static_assert(
sizeof(PlannedDirtyMergeStatus) ==
sizeof(std::uint8_t));
930enum class PlannedDirtyCollectStatus : std::uint8_t {
936static_assert(
sizeof(PlannedDirtyCollectStatus) ==
sizeof(std::uint8_t));
940 PlannedDirtyCollectStatus status = PlannedDirtyCollectStatus::Collected;
941 std::size_t record_count = 0;
943 [[nodiscard]]
constexpr bool ok()
const noexcept {
944 return status == PlannedDirtyCollectStatus::Collected;
950 PlannedDirtyMergeStatus status = PlannedDirtyMergeStatus::Merged;
951 std::size_t merged_chunk_count = 0;
953 [[nodiscard]]
constexpr bool ok()
const noexcept {
954 return status == PlannedDirtyMergeStatus::Merged;
963template <
bool BindWorld, WritePolicy Policy,
typename World,
typename Fn>
964[[nodiscard]]
auto execute_validated_planned_operation_deferred_dirty(
968template <
typename World>
969[[nodiscard]]
auto merge_planned_dirty_after_exception(
975class PlannedDirtyPartitions;
980 void reserve(std::size_t count) { records_.reserve(count); }
982 void clear()
noexcept {
984 world_stamp_ =
nullptr;
988 template <
typename World>
991 -> PlannedDirtyRecordStatus {
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.");
997 return PlannedDirtyRecordStatus::IgnoredEmptyMask;
999 if (chunk.value >= World::chunk_count) {
1000 return PlannedDirtyRecordStatus::InvalidChunk;
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;
1009 if (validation == PlannedExecutionStatus::InvalidChunk) {
1010 return PlannedDirtyRecordStatus::InvalidChunk;
1015 world_stamp_ = detail::planned_world_stamp<World>();
1016 return PlannedDirtyRecordStatus::Recorded;
1019 [[nodiscard]]
auto records() const noexcept
1025 template <
typename World>
1027 -> PlannedDirtyMergeStatus {
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;
1035 const auto validation =
1036 detail::validate_planned_world_stamp<World>(world_stamp_);
1037 if (validation == PlannedExecutionStatus::InvalidShape) {
1038 return PlannedDirtyMergeStatus::InvalidShape;
1040 if (validation == PlannedExecutionStatus::InvalidChunk) {
1041 return PlannedDirtyMergeStatus::InvalidChunk;
1043 return PlannedDirtyMergeStatus::Merged;
1049 template <
bool BindWorld, WritePolicy Policy,
typename World,
typename Fn>
1050 friend auto detail::execute_validated_planned_operation_deferred_dirty(
1054 template <WritePolicy Policy,
typename World,
typename Fn>
1058 template <
typename World>
1062 template <
typename World>
1070 template <
typename World>
1071 void bind_validated_world(
const World& )
noexcept {
1072 if (world_stamp_ ==
nullptr) {
1073 world_stamp_ = detail::planned_world_stamp<World>();
1077 template <
typename World>
1078 void prepare_for_validated_world(
const World& )
noexcept {
1080 world_stamp_ = detail::planned_world_stamp<World>();
1083 void record_validated(ChunkKey chunk, DirtyMask dirty_mask, Box3 bounds) {
1087 records_.push_back(PlannedDirtyRecord{chunk, dirty_mask, bounds});
1090 std::vector<PlannedDirtyRecord> records_;
1091 const detail::PlannedWorldStamp* world_stamp_ =
nullptr;
1094template <
bool BindWorld, WritePolicy Policy,
typename World,
typename Fn>
1095auto detail::execute_validated_planned_operation_deferred_dirty(
1098 if constexpr (BindWorld) {
1099 if (operation.field_access.dirty_mask) {
1100 dirty.bind_validated_world(world);
1103 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
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,
1115 PlannedExecutionStatus::Executed,
1123 void reserve(std::size_t count) { partitions_.reserve(count); }
1125 void resize(std::size_t count) { partitions_.resize(count); }
1127 void clear()
noexcept { partitions_.clear(); }
1129 void clear_records()
noexcept {
1130 for (
auto& partition : partitions_) {
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);
1142 [[nodiscard]]
auto size()
const noexcept -> std::size_t {
1143 return partitions_.size();
1146 [[nodiscard]]
auto partition(std::size_t index)
noexcept
1148 return partitions_[index];
1151 [[nodiscard]]
auto partition(std::size_t index)
const noexcept
1153 return partitions_[index];
1156 [[nodiscard]]
auto partitions()
const noexcept
1157 -> std::span<const PlannedDirtyAccumulator> {
1165 friend class PlannedPhaseExecutionScratch;
1167 void prepare(std::size_t count) {
1168 partitions_.resize(count);
1169 for (
auto& partition : partitions_) {
1171 partition.reserve(records_per_partition_reserve_);
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_);
1184 std::vector<PlannedDirtyAccumulator> partitions_;
1185 std::size_t records_per_partition_reserve_ = 0;
1198 void reserve(std::size_t count) { records_.reserve(count); }
1200 void clear()
noexcept { records_.clear(); }
1205 TESS_ASSERT(records_.size() < records_.capacity());
1210 [[nodiscard]]
auto records()
const noexcept
1211 -> std::span<const PlannedDirtyRecord> {
1216 std::vector<PlannedDirtyRecord> records_;
1221template <WritePolicy Policy,
typename World>
1222using PlannedChunkView =
1225template <WritePolicy Policy,
typename World,
typename Fn>
1226inline constexpr bool planned_callback_is_nothrow =
1227 std::is_nothrow_invocable_v<Fn&, PlannedChunkView<Policy, World>&>;
1229template <WritePolicy Policy,
typename World,
typename Fn>
1230[[nodiscard]]
auto execute_validated_phase_operation_deferred_dirty(
1233 auto ctx = block_ctx<Policy>(world, chunk_domain(operation.chunks()));
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());
1243 return PlannedExecutionResult{
1244 PlannedExecutionStatus::Executed,
1273inline constexpr std::size_t index_max_chunks_per_operation = 64;
1275[[nodiscard]]
constexpr bool operation_is_indexable(
1276 std::span<const ChunkKey> chunks)
noexcept {
1277 return chunks.size() <= index_max_chunks_per_operation;
1284class ChunkOperationIndex {
1286 static constexpr auto npos = std::numeric_limits<std::uint32_t>::max();
1291 void clear() noexcept {
1293 if (++generation_ != 0) {
1300 for (
auto& slot : slots_) {
1306 void reserve(std::size_t nodes) {
1307 nodes_.reserve(nodes);
1308 rehash(capacity_for(nodes));
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()));
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));
1327 template <
typename Visit>
1328 void for_each_sharing(std::span<const ChunkKey> chunks, Visit&& visit)
const {
1329 if (slots_.empty()) {
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);
1344 std::uint32_t head = npos;
1345 std::uint32_t generation = 0;
1348 std::uint64_t key = 0;
1349 std::uint32_t op_index = 0;
1350 std::uint32_t next = npos;
1353 [[nodiscard]]
static constexpr auto mix(std::uint64_t key)
noexcept
1356 key *= 0xff51afd7ed558ccdULL;
1361 [[nodiscard]]
static constexpr auto capacity_for(std::size_t nodes)
noexcept
1363 auto capacity = std::size_t{16};
1364 while (capacity < nodes * 2) {
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);
1376 [[nodiscard]]
auto head_for(std::uint64_t key)
const noexcept
1378 const auto& slot = slots_[bucket(key)];
1379 return slot.generation == generation_ ? slot.head : npos;
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_;
1388 nodes_[node].next = slot.head;
1392 void rehash(std::size_t capacity) {
1393 if (capacity <= slots_.size()) {
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));
1403 std::vector<Slot> slots_;
1404 std::vector<Node> nodes_;
1405 std::uint32_t generation_ = 1;
1411template <
typename T>
1417 void reserve_operations(std::size_t count) {
1418 dirty_partitions_.reserve(count);
1419 results_.reserve(count);
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);
1429 void reserve_merged_dirty_records(std::size_t count) {
1430 merged_dirty_.reserve(count);
1433 void prepare_for_operation_count(std::size_t count) { prepare(count); }
1435 void clear()
noexcept {
1436 for (
auto& partition : dirty_partitions_) {
1440 merged_dirty_.clear();
1441 world_stamp_ =
nullptr;
1444 [[nodiscard]]
auto operation_count()
const noexcept -> std::size_t {
1445 return results_.size();
1448 [[nodiscard]]
auto dirty_partitions()
const noexcept
1449 -> std::span<const PhaseDirtyPartition> {
1450 return dirty_partitions_;
1454 template <WritePolicy Policy,
typename Executor,
typename World,
typename Fn>
1460 template <WritePolicy Policy,
typename Executor,
typename World,
typename T,
1467 template <
typename World>
1471 template <
typename World>
1472 friend auto detail::merge_planned_dirty_after_exception(
1476 void prepare(std::size_t operation_count) {
1477 prepare_partitions(operation_count);
1479 merged_dirty_.clear();
1480 world_stamp_ =
nullptr;
1483 template <
typename World>
1484 void prepare(
const World& , std::size_t operation_count) {
1485 prepare_partitions(operation_count);
1487 merged_dirty_.clear();
1488 world_stamp_ = detail::planned_world_stamp<World>();
1491 [[nodiscard]]
auto dirty_for_operation(std::size_t index)
noexcept
1493 return dirty_partitions_[index];
1496 void prepare_partitions(std::size_t operation_count) {
1497 dirty_partitions_.resize(operation_count);
1498 for (
auto& partition : dirty_partitions_) {
1500 partition.reserve(records_per_partition_reserve_);
1505 results_[index] = result;
1508 [[nodiscard]]
auto results()
const noexcept
1509 -> std::span<const PlannedExecutionResult> {
1513 std::vector<PhaseDirtyPartition> dirty_partitions_;
1514 std::vector<PlannedExecutionResult> results_;
1516 std::size_t records_per_partition_reserve_ = 0;
1517 const detail::PlannedWorldStamp* world_stamp_ =
nullptr;
1531 [[nodiscard]]
constexpr auto operations()
const&
noexcept
1532 -> std::span<const OperationReport> {
1533 return {operations_.data(), operations_.size()};
1535 auto operations()
const&& -> std::span<const OperationReport> =
delete;
1537 [[nodiscard]]
constexpr auto plan()
const&
noexcept ->
const ExecutionPlan& {
1544 [[nodiscard]]
constexpr auto find(
OpHandle handle)
const&
noexcept
1546 for (
const auto& op : operations_) {
1547 if (op.handle == handle) {
1554 [[nodiscard]]
constexpr bool ok()
const noexcept {
1555 return failed_count() == 0;
1558 [[nodiscard]]
constexpr bool failed()
const noexcept {
1559 return failed_count() != 0;
1562 [[nodiscard]]
constexpr auto planned_count()
const noexcept -> std::size_t {
1563 return plan_.size();
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) {
1580 plan_.bump_generation();
1581 for (
auto& planned : plan_.operations_) {
1582 planned.chunks_.clear();
1583 chunk_pool_.push_back(std::move(planned.chunks_));
1585 plan_.operations_.clear();
1586 operations_.clear();
1587 chunk_index_.clear();
1588 wide_operations_.clear();
1592 template <
typename World>
1594 std::span<const QueuedOperation> operations,
1598 void reserve(std::size_t size) {
1599 operations_.reserve(size);
1600 plan_.operations_.reserve(size);
1601 chunk_index_.reserve(size);
1604 void push_report(
OperationReport report) { operations_.push_back(report); }
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);
1611 wide_operations_.push_back(op_index);
1613 plan_.operations_.push_back(std::move(planned));
1616 [[nodiscard]]
auto chunk_index()
const noexcept
1617 ->
const detail::ChunkOperationIndex& {
1618 return chunk_index_;
1621 [[nodiscard]]
auto wide_operations()
const noexcept
1622 -> std::span<const std::uint32_t> {
1623 return {wide_operations_.data(), wide_operations_.size()};
1626 template <
typename World>
1628 std::vector<ChunkKey>&& chunks)
1633 detail::planned_world_stamp<World>(),
1637 [[nodiscard]]
auto acquire_chunks() -> std::vector<ChunkKey> {
1638 if (chunk_pool_.empty()) {
1641 auto chunks = std::move(chunk_pool_.back());
1642 chunk_pool_.pop_back();
1646 void recycle_chunks(std::vector<ChunkKey>&& chunks) {
1648 chunk_pool_.push_back(std::move(chunks));
1652 recycle_chunks(std::move(planned.chunks_));
1655 std::vector<OperationReport> operations_;
1657 std::vector<std::vector<ChunkKey>> chunk_pool_;
1661 detail::ChunkOperationIndex chunk_index_;
1664 std::vector<std::uint32_t> wide_operations_;
1670 void reserve_operations(std::size_t count) { operations_.reserve(count); }
1672 [[nodiscard]]
auto update_field(
1674 Priority priority = Priority::GameplayCritical,
1675 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1676 std::source_location source = std::source_location::current())
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);
1687 [[nodiscard]]
auto update_field(
1689 Priority priority = Priority::GameplayCritical,
1690 BudgetPolicy budget_policy = BudgetPolicy::MustRun,
1691 std::source_location source = std::source_location::current())
1693 return update_field(std::move(domain),
FieldAccessDesc{}, write_policy,
1694 priority, budget_policy, source);
1697 [[nodiscard]]
auto query_paths(
1699 std::source_location source = std::source_location::current())
1702 std::move(desc.operation), desc.requests,
1706 [[nodiscard]]
auto query_nearest(
1708 std::source_location source = std::source_location::current())
1711 std::move(desc.operation), desc.requests,
1715 [[nodiscard]]
auto build_field_product(
1717 std::source_location source = std::source_location::current())
1719 return enqueue(OperationKind::BuildFieldProduct, std::move(desc.operation),
1720 desc.requests, source);
1723 [[nodiscard]]
auto move_entities(
1725 std::source_location source = std::source_location::current())
1727 return enqueue(OperationKind::MoveEntities, std::move(desc.operation),
1728 desc.requests, source);
1731 [[nodiscard]]
auto rebuild_topology(
1733 std::source_location source = std::source_location::current())
1735 return enqueue(OperationKind::RebuildTopology, std::move(desc.operation),
1739 [[nodiscard]]
auto ensure_resident(
1741 std::source_location source = std::source_location::current())
1743 return enqueue(OperationKind::EnsureResident, std::move(desc.operation),
1744 desc.requests, source);
1747 [[nodiscard]]
auto mark_dirty(
1749 std::source_location source = std::source_location::current())
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);
1758 [[nodiscard]]
auto publish_render_deltas(
1760 std::source_location source = std::source_location::current())
1762 return enqueue(OperationKind::PublishRenderDeltas,
1763 std::move(desc.operation), desc.requests, source);
1766 [[nodiscard]]
constexpr auto operations()
const noexcept
1767 -> std::span<const QueuedOperation> {
1768 return {operations_.data(), operations_.size()};
1771 [[nodiscard]]
constexpr auto operation(
OpHandle handle)
const noexcept
1773 if (handle.value >= operations_.size()) {
1776 return &operations_[
static_cast<std::size_t
>(handle.value)];
1779 [[nodiscard]]
constexpr bool empty()
const noexcept {
1780 return operations_.empty();
1783 [[nodiscard]]
constexpr auto size()
const noexcept -> std::size_t {
1784 return operations_.size();
1791 void clear()
noexcept { operations_.clear(); }
1794 [[nodiscard]]
auto enqueue(OperationKind kind,
IntentMetadata metadata,
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};
1800 operation.kind = kind;
1801 operation.handle = handle;
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));
1818 std::vector<QueuedOperation> operations_;
1823[[nodiscard]]
constexpr auto operation_access(
1828 op.domain.mask_bits(),
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) {
1840template <
typename World>
1841[[nodiscard]]
auto validate_explicit_chunks(
const World& world,
1842 std::span<const ChunkKey> chunks,
1843 ChunkKey& invalid_chunk)
noexcept
1845 for (
const auto key : chunks) {
1846 if (world.try_chunk(key) ==
nullptr) {
1847 invalid_chunk = key;
1857template <
typename World>
1858[[nodiscard]]
auto expand_domain(
const World& world,
const DomainDesc& domain,
1859 std::vector<ChunkKey>& chunks,
1860 ChunkKey& invalid_chunk) ->
bool {
1862 switch (domain.kind()) {
1863 case DomainKind::ExplicitChunks:
1864 if (!validate_explicit_chunks(world, domain.explicit_chunks(),
1868 chunks.assign(domain.explicit_chunks().begin(),
1869 domain.explicit_chunks().end());
1871 case DomainKind::DirtyChunks:
1872 world.collect_dirty_chunks(domain.dirty_mask(), chunks);
1874 case DomainKind::ActiveChunks:
1875 world.collect_active_chunks(domain.active_mask(), chunks);
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});
1887[[nodiscard]]
constexpr auto hazard_mask(FieldAccessDesc earlier,
1888 FieldAccessDesc later)
noexcept
1890 return (earlier.write_mask & later.write_mask) |
1891 (earlier.write_mask & later.read_mask) |
1892 (earlier.read_mask & later.write_mask);
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) {
1905 if (lhs_key < rhs_key) {
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) {
1921 if (chunks_overlap(earlier.chunks(), later.chunks())) {
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* {
1942 if (!operation_is_indexable(later.chunks())) {
1943 return find_hazard(earlier_ops, later);
1946 auto earliest = ChunkOperationIndex::npos;
1947 index.for_each_sharing(later.chunks(), [&](std::uint32_t op_index) {
1948 if (op_index >= earliest) {
1951 if (hazard_mask(earlier_ops[op_index].field_access, later.field_access) ==
1955 earliest = op_index;
1960 for (
const auto op_index : wide_ops) {
1961 if (op_index >= earliest) {
1964 const auto& earlier = earlier_ops[op_index];
1965 if (hazard_mask(earlier.field_access, later.field_access) == 0) {
1968 if (chunks_overlap(earlier.chunks(), later.chunks())) {
1969 earliest = op_index;
1973 if (earliest == ChunkOperationIndex::npos) {
1976 return &earlier_ops[earliest];
1979[[nodiscard]]
constexpr bool is_parallel_supported_policy(
1980 WritePolicy policy)
noexcept {
1981 return policy == WritePolicy::ReadOnly ||
1982 policy == WritePolicy::UniquePerChunk;
1985[[nodiscard]]
constexpr bool is_mutating_policy(WritePolicy policy)
noexcept {
1986 return policy != WritePolicy::ReadOnly;
1991inline constexpr std::size_t phase_index_min_operations = 16;
1996[[nodiscard]]
constexpr bool parallel_phase_conflict_given_overlap(
1998 if (is_mutating_policy(lhs.write_policy) ||
1999 is_mutating_policy(rhs.write_policy)) {
2002 return hazard_mask(lhs.field_access, rhs.field_access) != 0;
2005[[nodiscard]]
constexpr bool parallel_phase_conflict(
2007 if (!chunks_overlap(lhs.chunks(), rhs.chunks())) {
2010 return parallel_phase_conflict_given_overlap(lhs, rhs);
2013[[nodiscard]]
constexpr auto dirty_axis_end(std::int64_t origin,
2014 std::uint64_t extent)
noexcept
2018 return detail::box_axis_end(origin, extent);
2021[[nodiscard]]
constexpr auto dirty_min(std::int64_t lhs,
2022 std::int64_t rhs)
noexcept
2024 return lhs < rhs ? lhs : rhs;
2027[[nodiscard]]
constexpr auto dirty_max(std::int64_t lhs,
2028 std::int64_t rhs)
noexcept
2030 return lhs < rhs ? rhs : lhs;
2033[[nodiscard]]
constexpr auto dirty_union_extent(std::int64_t origin,
2034 std::int64_t end)
noexcept
2039 return abs_delta(end, origin);
2042[[nodiscard]]
constexpr auto union_dirty_bounds(
Box3 lhs,
Box3 rhs)
noexcept
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));
2055 Coord3{min_x, min_y, min_z},
2057 dirty_union_extent(min_x, max_x),
2058 dirty_union_extent(min_y, max_y),
2059 dirty_union_extent(min_z, max_z),
2070template <
typename World>
2072 std::span<const QueuedOperation> operations,
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.");
2081 report.reserve(operations.size());
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)};
2091 OperationStatus::Planned,
2092 OperationFailure::None,
2093 detail::operation_access(op),
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);
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);
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);
2138 auto planned_chunks = report.acquire_chunks();
2140 if (!detail::expand_domain(world, op.domain, planned_chunks,
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);
2154 report.template make_planned<World>(op, std::move(planned_chunks));
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",
2170 report.recycle_chunks(std::move(planned));
2171 report.push_report(op_report);
2175 op_report.chunk_count = planned.chunks().size();
2176 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
"planned",
2178 report.push_planned(std::move(planned));
2179 report.push_report(op_report);
2185template <
typename World>
2187[[nodiscard]]
auto plan_operations(
const World& world,
2188 std::span<const QueuedOperation> operations)
2191 plan_operations(world, operations, report);
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);
2202template <
typename World>
2204[[nodiscard]]
auto plan_operations(
const World& world,
2205 const OperationBatch& ops)
2206 -> ExecutionReport {
2207 return plan_operations(world, ops.operations());
2211[[nodiscard]]
constexpr auto planned_chunk_domain(
2212 const PlannedOperation& operation)
noexcept -> ChunkDomain {
2213 return chunk_domain(operation.chunks());
2219 const auto operations = plan.operations();
2221 phases.reserve(operations.size());
2229 const auto indexed = operations.size() >= detail::phase_index_min_operations;
2230 auto index = detail::ChunkOperationIndex{};
2234 auto wide_operations = std::vector<std::uint32_t>{};
2236 index.reserve(operations.size());
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);
2253 auto conflicts =
true;
2254 if (!phases.phases_.empty()) {
2255 const auto phase_first = phases.phases_.back().first_operation();
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)) {
2265 index.for_each_sharing(operation.chunks(), [&](std::uint32_t j) {
2266 if (conflicts || j < phase_first) {
2269 conflicts = detail::parallel_phase_conflict_given_overlap(
2270 operations[j], operation);
2272 for (
const auto j : wide_operations) {
2276 if (j < phase_first) {
2279 conflicts = detail::parallel_phase_conflict(operations[j], operation);
2285 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
"new_phase",
2287 phases.push_phase(plan, i, 1, operation);
2289 TESS_DIAG_TRACE_VALUE(diagnostics::TraceCategory::Planner,
"merged", i);
2290 phases.extend_last_phase(operation);
2293 if (detail::operation_is_indexable(operation.chunks())) {
2294 index.insert(operation.chunks(),
static_cast<std::uint32_t
>(i));
2296 wide_operations.push_back(
static_cast<std::uint32_t
>(i));
2308template <
typename World>
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.");
2322 const auto validation = dirty.validation_status(world);
2323 if (validation != PlannedDirtyMergeStatus::Merged) {
2330 auto& records = dirty.records_;
2331 std::sort(records.begin(), records.end(),
2333 return lhs.chunk.value < rhs.chunk.value;
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;
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);
2352 world.mark_dirty(chunk, dirty_mask, bounds);
2356 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
2359 PlannedDirtyMergeStatus::Merged,
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) {
2374 if (world_stamp ==
nullptr) {
2375 world_stamp = partition_stamp;
2378 if (world_stamp->shape_identity != partition_stamp->shape_identity) {
2380 PlannedDirtyCollectStatus::InvalidShape,
2384 if (world_stamp->chunk_limit != partition_stamp->chunk_limit) {
2386 PlannedDirtyCollectStatus::InvalidChunk,
2392 const auto record_limit =
2393 detail::effective_capacity_limit(dirty.records_.max_size());
2394 if (dirty.records_.size() > record_limit) {
2396 PlannedDirtyCollectStatus::CapacityExceeded,
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,
2410 required_capacity += partition_size;
2411 record_count += partition_size;
2415 dirty.records_.reserve(required_capacity);
2417 for (
auto& partition : partitions.partitions_) {
2418 if (partition.world_stamp_ !=
nullptr) {
2419 dirty.world_stamp_ = partition.world_stamp_;
2421 dirty.records_.insert(dirty.records_.end(), partition.records_.begin(),
2422 partition.records_.end());
2425 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2427 PlannedDirtyCollectStatus::Collected,
2432template <
typename World>
2437[[nodiscard]]
auto merge_planned_dirty(
World& world,
2441 for (
const auto& partition : partitions.partitions()) {
2442 const auto validation = partition.validation_status(world);
2443 if (validation != PlannedDirtyMergeStatus::Merged) {
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,
2456 return PlannedDirtyMergeResult{
2457 collected.status == PlannedDirtyCollectStatus::InvalidShape
2458 ? PlannedDirtyMergeStatus::InvalidShape
2459 : PlannedDirtyMergeStatus::InvalidChunk,
2463 return merge_planned_dirty(world, dirty_scratch);
2466template <
typename World>
2479 if (scratch.world_stamp_ ==
nullptr) {
2481 PlannedDirtyMergeStatus::Merged,
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,
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,
2509 record_count += partition_size;
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(),
2522 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2528template <
typename World>
2529auto detail::merge_planned_dirty_after_exception(
2532 if (scratch.world_stamp_ ==
nullptr) {
2534 PlannedDirtyMergeStatus::Merged,
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,
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();
2560 record_count += partition_size;
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();
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
2577 : earlier_records.size();
2578 for (std::size_t earlier_index = 0; earlier_index < earlier_count;
2580 if (earlier_records[earlier_index].chunk == record.chunk) {
2581 appeared_earlier =
true;
2586 if (appeared_earlier) {
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
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);
2609 world.mark_dirty(record.chunk, dirty_mask, bounds);
2613 for (
auto& partition : scratch.dirty_partitions_) {
2616 TESS_DIAG_EVENT_VALUE(queued_dirty_collect, record_count);
2617 TESS_DIAG_EVENT_VALUE(queued_dirty_merge, merged_count);
2620 return PlannedDirtyMergeResult{
2621 PlannedDirtyMergeStatus::Merged,
2627template <WritePolicy Policy>
2628[[nodiscard]]
constexpr bool planned_policy_matches(
2629 const PlannedOperation& operation)
noexcept {
2630 return operation.write_policy == Policy;
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;
2642 if (!planned_policy_matches<Policy>(operation)) {
2643 return PlannedExecutionStatus::PolicyMismatch;
2645 return PlannedExecutionStatus::Executed;
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>> {
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;
2661 return block_ctx<Policy>(world, planned_chunk_domain(operation));
2665template <WritePolicy Policy,
typename World,
typename Fn>
2666[[nodiscard]]
auto execute_planned_operation(World& world,
2667 const PlannedOperation& operation,
2669 -> PlannedExecutionResult {
2670 const auto validation = validate_planned_operation<Policy>(world, operation);
2671 if (validation != PlannedExecutionStatus::Executed) {
2672 return PlannedExecutionResult{
2677 auto ctx = block_ctx<Policy>(world, planned_chunk_domain(operation));
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,
2690 return PlannedExecutionResult{
2691 PlannedExecutionStatus::Executed,
2697template <WritePolicy Policy,
typename World,
typename Fn>
2701 const auto validation = validate_planned_operation<Policy>(world, operation);
2702 if (validation != PlannedExecutionStatus::Executed) {
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,
2719 return detail::execute_validated_planned_operation_deferred_dirty<
true,
2721 world, operation, dirty, std::forward<Fn>(fn));
2728template <WritePolicy Policy,
typename World,
typename Fn>
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) {
2738 chunk_count + result.chunk_count,
2741 chunk_count += result.chunk_count;
2743 return PlannedExecutionResult{
2744 PlannedExecutionStatus::Executed,
2750template <WritePolicy Policy,
typename World,
typename Fn>
2751[[nodiscard]]
auto execute_plan_deferred_dirty(World& world,
2752 const ExecutionPlan& plan,
2753 PlannedDirtyAccumulator& dirty,
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{
2764 chunk_count + result.chunk_count,
2767 chunk_count += result.chunk_count;
2769 return PlannedExecutionResult{
2770 PlannedExecutionStatus::Executed,
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{
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,
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,
2812 world, operations[index], dirty, callback);
2813 if (operation_result.status == PlannedExecutionStatus::Executed) {
2814 chunk_count += operation_result.chunk_count;
2816 return operation_result;
2818 if (result.status != PlannedExecutionStatus::Executed) {
2819 TESS_DIAG_EVENT(queued_phase_failure);
2820 result.chunk_count = chunk_count;
2824 return PlannedExecutionResult{
2825 PlannedExecutionStatus::Executed,
2831template <WritePolicy Policy,
typename Executor,
typename World,
typename 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);
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
2858 : operations[index].chunks().size());
2860 auto result = execute_operation_index_range(
2861 std::forward<Executor>(executor), executor_phase_range(phase),
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),
2872 scratch.record_result(offset, operation_result);
2873 return operation_result;
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,
2885 chunk_count += operation_result.chunk_count;
2888 if (result.status != PlannedExecutionStatus::Executed) {
2889 TESS_DIAG_EVENT(queued_phase_failure);
2897 PlannedExecutionStatus::Executed,
2903template <WritePolicy Policy,
typename World,
typename Fn>
2904[[nodiscard]]
auto execute_phase_deferred_dirty(
World& world,
2911 return execute_phase_deferred_dirty_with<Policy>(executor, world, plan, phase,
2912 dirty, std::forward<Fn>(fn));
constexpr auto mask_bits() const noexcept -> std::uint32_t
Returns the stored selector bits for planner identity and diagnostics.
Definition queued.h:333
friend auto plan_parallel_execution_phases(const ExecutionPlan &plan) -> ExecutionPhasePlan
Definition queued.h:2217
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
friend auto plan_operations(const World &world, std::span< const QueuedOperation > operations, ExecutionReport &report) -> const ExecutionReport &
Definition queued.h:2071
Per-operation dirty-record buffer owned by a phase execution scratch.
Definition queued.h:1196
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
friend auto collect_planned_dirty(PlannedDirtyAccumulator &dirty, PlannedDirtyPartitions &partitions) -> PlannedDirtyCollectResult
Definition queued.h:2365
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
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 metadata_types.h:49
Definition metadata_types.h:12
Definition phase_executor.h:54
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 phase_executor.h:65
Definition phase_executor.h:97