3#include <tess/core/tag_identity.h>
4#include <tess/path/path.h>
21 void reserve(std::size_t count) { goals_.reserve(count); }
23 void clear()
noexcept { goals_.clear(); }
25 void add(
Coord3 goal) { goals_.push_back(goal); }
27 [[nodiscard]]
auto empty()
const noexcept ->
bool {
return goals_.empty(); }
29 [[nodiscard]]
auto size()
const noexcept -> std::size_t {
33 [[nodiscard]]
auto goals()
const noexcept -> std::span<const Coord3> {
38 std::vector<Coord3> goals_;
43 PathStatus status = PathStatus::NotComputed;
44 std::uint32_t cost = 0;
46 std::size_t expanded_nodes = 0;
47 std::size_t reached_nodes = 0;
48 std::span<const Coord3> path;
49 std::uint32_t cost_scale = 1;
62 void reserve_goals(std::size_t count) { goals_.reserve(count); }
64 void reserve_nodes(std::size_t node_count) { distance_.reserve(node_count); }
69 void reserve_dependencies(std::size_t count) { dependencies_.reserve(count); }
71 void clear()
noexcept {
72 status_ = PathStatus::NotComputed;
77 local_tile_count_ = 0;
78 model_class_identity_ = 0;
79 model_lattice_identity_ = 0;
80 model_lattice_version_ = 0;
81 model_step_identity_ = 0;
82 model_cost_scale_ = 0;
83 model_provider_identity_ = 0;
84 model_provider_instance_identity_ =
nullptr;
85 model_provider_revision_ = 0;
88 dependencies_.clear();
93 template <
typename World>
94 [[nodiscard]]
auto is_valid(
const World& world)
const noexcept ->
bool {
95 return !dependencies_.empty() && dependencies_.is_valid(world);
98 [[nodiscard]]
auto status()
const noexcept -> PathStatus {
return status_; }
100 [[nodiscard]]
auto goals()
const noexcept -> std::span<const Coord3> {
104 [[nodiscard]]
auto dependencies()
const noexcept
105 -> std::span<const ContentVersionDependencies::ContentVersionDependency> {
106 return dependencies_.chunks();
109 [[nodiscard]]
auto expanded_nodes()
const noexcept -> std::size_t {
110 return expanded_nodes_;
113 [[nodiscard]]
auto reached_nodes()
const noexcept -> std::size_t {
114 return reached_nodes_;
119 std::numeric_limits<std::uint32_t>::max();
127 template <
typename World>
129 using Shape =
typename World::shape_type;
130 if (status_ != PathStatus::Found ||
131 tile_count_ != detail::tile_count<World>() ||
132 chunk_count_ != World::chunk_count ||
133 local_tile_count_ != World::local_tile_count ||
134 shape_size_ != ShapeTraits<Shape>::size ||
135 chunk_extent_ != ShapeTraits<Shape>::chunk || !contains<Shape>(coord)) {
139 static_cast<std::size_t
>(detail::tile_index<Shape>(coord));
140 if (index >= distance_.size()) {
143 return distance_[index];
146 [[nodiscard]]
auto byte_size() const noexcept -> std::
size_t {
147 return distance_.size() *
sizeof(std::uint32_t) +
148 goals_.size() *
sizeof(
Coord3) +
149 dependencies_.size() *
154 friend class FieldProductCache;
156 template <
typename World,
typename Tag>
158 const GoalSet& goals,
159 DistanceFieldProduct& product,
160 DistanceFieldScratch& scratch)
161 -> DistanceFieldResult;
163 template <
typename World,
typename Tag,
typename Prov
ider>
165 const GoalSet& goals,
166 DistanceFieldProduct& product,
167 DistanceFieldScratch& scratch,
168 const Provider& provider)
169 -> DistanceFieldResult;
171 template <
typename World,
typename Tag>
173 const DistanceFieldProduct& product,
174 DistanceFieldScratch& scratch)
177 template <
typename World,
typename Tag,
typename Prov
ider>
179 const DistanceFieldProduct& product,
180 DistanceFieldScratch& scratch,
181 const Provider& provider)
184 template <
typename World,
typename Tag>
186 const DistanceFieldProduct& product,
187 DistanceFieldScratch& scratch)
188 -> NearestTargetResult;
190 template <
typename World,
typename Tag,
typename Prov
ider>
192 const DistanceFieldProduct& product,
193 DistanceFieldScratch& scratch,
194 const Provider& provider) -> NearestTargetResult;
196 template <
typename World,
typename Class,
typename Prov
ider>
198 const World& world,
const GoalSet& goals, DistanceFieldProduct& product,
199 DistanceFieldScratch& scratch,
const Provider& provider)
200 -> DistanceFieldResult;
202 template <
typename World,
typename Class,
typename Prov
ider>
204 const World& world, Coord3 start,
const DistanceFieldProduct& product,
205 DistanceFieldScratch& scratch,
const Provider& provider) -> PathResult;
207 [[nodiscard]]
auto is_goal(Coord3 coord)
const noexcept ->
bool {
208 for (
const auto goal : goals_) {
216 PathStatus status_ = PathStatus::NotComputed;
217 std::size_t expanded_nodes_ = 0;
218 std::size_t reached_nodes_ = 0;
219 std::size_t tile_count_ = 0;
220 std::uint64_t chunk_count_ = 0;
221 std::uint64_t local_tile_count_ = 0;
222 Extent3 shape_size_{};
223 Extent3 chunk_extent_{};
224 std::uintptr_t model_class_identity_ = 0;
225 std::uint32_t model_lattice_identity_ = 0;
226 std::uint32_t model_lattice_version_ = 0;
227 std::uint32_t model_step_identity_ = 0;
228 std::uint32_t model_cost_scale_ = 0;
229 std::uintptr_t model_provider_identity_ = 0;
230 const void* model_provider_instance_identity_ =
nullptr;
231 std::uint64_t model_provider_revision_ = 0;
232 std::vector<Coord3> goals_;
233 std::vector<std::uint32_t> distance_;
234 ContentVersionDependencies dependencies_;
239 std::size_t entries = 0;
240 std::size_t bytes = 0;
241 std::size_t hits = 0;
242 std::size_t misses = 0;
243 std::size_t evictions = 0;
244 std::size_t stale_rejections = 0;
263class FieldProductCache {
265 std::uintptr_t movement_class = 0;
266 std::size_t tile_count = 0;
267 std::uint64_t chunk_count = 0;
268 std::uint64_t local_tile_count = 0;
271 std::uint32_t lattice_identity = 0;
272 std::uint32_t lattice_version = 0;
273 std::uint32_t step_identity = 0;
274 std::uint32_t cost_scale = 0;
275 std::uintptr_t provider_identity = 0;
276 const void* provider_instance_identity =
nullptr;
277 std::uint64_t provider_revision = 0;
278 std::vector<Coord3> goals;
282 explicit FieldProductCache(
283 std::size_t byte_budget =
284 std::numeric_limits<std::size_t>::max()) noexcept
285 : byte_budget_(byte_budget) {}
287 void set_byte_budget(std::size_t byte_budget) {
288 byte_budget_ = byte_budget;
292 void reserve_entries(std::size_t count) { entries_.reserve(count); }
294 [[nodiscard]]
auto can_fit_distance_storage(
295 std::size_t node_count)
const noexcept ->
bool {
298 return node_count <= byte_budget_ /
sizeof(std::uint32_t);
301 void clear()
noexcept {
306 void reset_stats()
noexcept {
310 stale_rejections_ = 0;
315 entries_.size(), bytes_, hits_, misses_, evictions_, stale_rejections_,
324 template <
typename World,
typename Tag>
325 [[nodiscard]]
auto lookup(
const World& world,
const GoalSet& goals)
327 return lookup<World, Tag, AdjacentTransitions>(world, goals,
332 template <
typename World,
typename Tag,
typename Prov
ider>
334 const Provider& provider)
336 for (std::size_t i = 0; i < entries_.size(); ++i) {
337 auto& entry = entries_[i];
338 if (!key_matches<World, Tag, Provider>(entry.key, goals.goals(),
342 if (!entry.product->is_valid(world)) {
348 entry.last_used = ++clock_;
349 return entry.product.get();
357 template <
typename World,
typename Class,
typename Prov
ider>
359 const Provider& provider)
361 static_assert(std::derived_from<Class, movement::movement_class_tag>,
362 "FieldProductCache::lookup_weighted requires Class to be "
364 for (std::size_t i = 0; i < entries_.size(); ++i) {
365 auto& entry = entries_[i];
366 if (!key_matches_class<World, Class, Provider>(entry.key, goals.goals(),
370 if (!entry.product->is_valid(world)) {
376 entry.last_used = ++clock_;
377 return entry.product.get();
384 template <
typename World,
typename Class>
399 template <
typename World,
typename Tag>
401 return store<World, Tag, AdjacentTransitions>(std::move(product),
407 [[nodiscard]]
static auto key_matches_product(
408 const Key& key,
const DistanceFieldProduct& product)
noexcept ->
bool {
409 return product.model_class_identity_ == key.movement_class &&
410 product.tile_count_ == key.tile_count &&
411 product.chunk_count_ == key.chunk_count &&
412 product.local_tile_count_ == key.local_tile_count &&
413 product.model_lattice_identity_ == key.lattice_identity &&
414 product.model_lattice_version_ == key.lattice_version &&
415 product.model_step_identity_ == key.step_identity &&
416 product.model_cost_scale_ == key.cost_scale &&
417 product.model_provider_identity_ == key.provider_identity &&
418 product.model_provider_instance_identity_ ==
419 key.provider_instance_identity &&
420 product.model_provider_revision_ == key.provider_revision;
424 template <
typename World,
typename Tag,
typename Prov
ider>
426 if (product.status() != PathStatus::Found) {
430 auto key = make_key<World, Tag, Provider>(product.goals(), provider);
431 if (!key_matches_product(key, product)) {
434 return store_with_key(product, std::move(key)) !=
nullptr;
439 template <
typename World,
typename Tag,
typename Prov
ider>
442 if (product.status() != PathStatus::Found) {
445 auto key = make_key<World, Tag, Provider>(product.goals(), provider);
446 if (!key_matches_product(key, product)) {
449 return store_with_key(product, std::move(key));
453 template <
typename World,
typename Tag>
461 template <
typename World,
typename Class,
typename Prov
ider>
464 static_assert(std::derived_from<Class, movement::movement_class_tag>,
465 "FieldProductCache::store_weighted requires Class to be a "
467 if (product.status() != PathStatus::Found) {
471 make_key_for_class<World, Class, Provider>(product.goals(), provider);
472 if (!key_matches_product(key, product)) {
475 return store_with_key(product, std::move(key)) !=
nullptr;
480 template <
typename World,
typename Class,
typename Prov
ider>
482 const Provider& provider)
485 std::derived_from<Class, movement::movement_class_tag>,
486 "FieldProductCache::store_weighted_reusing requires Class to be a "
488 if (product.status() != PathStatus::Found) {
492 make_key_for_class<World, Class, Provider>(product.goals(), provider);
493 if (!key_matches_product(key, product)) {
496 return store_with_key(product, std::move(key));
500 template <
typename World,
typename Class>
515 const auto bytes = entry_byte_size(key, product);
516 if (bytes > byte_budget_) {
520 for (std::size_t i = 0; i < entries_.size(); ++i) {
521 if (keys_equal(entries_[i].key, key)) {
522 bytes_ -= entries_[i].bytes;
524 std::swap(*entries_[i].product, product);
525 entries_[i].last_used = ++clock_;
526 entries_[i].bytes = bytes;
533 const auto* stored = entries_[i].product.get();
534 (void)evict_to_budget();
545 std::make_unique<DistanceFieldProduct>(std::move(product));
547 Entry{std::move(key), std::move(owned_product), clock_ + 1u, bytes};
551 entries_.push_back(std::move(candidate));
554 const auto* stored = entries_.back().product.get();
560 if (
auto recycled = evict_to_budget(); recycled !=
nullptr) {
563 product = std::move(*recycled);
573 std::unique_ptr<DistanceFieldProduct> product;
574 std::uint64_t last_used = 0;
575 std::size_t bytes = 0;
578 template <
typename World,
typename Tag,
typename Prov
ider>
579 [[nodiscard]]
static auto make_key(std::span<const Coord3> goals,
580 const Provider& provider) -> Key {
581 using Class = movement::movement_class_of<Tag>;
582 using UnitClass = movement::detail::UnitMovementClass<Class>;
583 return make_key_for_class<World, UnitClass, Provider>(goals, provider);
586 template <
typename World,
typename Class,
typename Prov
ider>
587 [[nodiscard]]
static auto make_key_for_class(std::span<const Coord3> goals,
588 const Provider& provider)
590 using Model = ResolvedTransitionModel<World, Class, Provider>;
591 const auto model = Model{provider};
593 detail::tag_identity<typename Model::class_type>(),
594 detail::tile_count<World>(),
596 World::local_tile_count,
597 ShapeTraits<typename World::shape_type>::size,
598 ShapeTraits<typename World::shape_type>::chunk,
599 static_cast<std::uint32_t
>(Model::lattice_identity),
600 Model::lattice_version,
601 static_cast<std::uint32_t
>(Model::step_policy_identity),
603 detail::tag_identity<Provider>(),
604 detail::transition_provider_instance_identity(provider),
606 std::vector<Coord3>{goals.begin(), goals.end()},
610 [[nodiscard]]
static auto keys_equal(
const Key& lhs,
const Key& rhs)
noexcept
612 return lhs.movement_class == rhs.movement_class &&
613 lhs.tile_count == rhs.tile_count &&
614 lhs.chunk_count == rhs.chunk_count &&
615 lhs.local_tile_count == rhs.local_tile_count &&
616 lhs.shape_size == rhs.shape_size &&
617 lhs.chunk_extent == rhs.chunk_extent &&
618 lhs.lattice_identity == rhs.lattice_identity &&
619 lhs.lattice_version == rhs.lattice_version &&
620 lhs.step_identity == rhs.step_identity &&
621 lhs.cost_scale == rhs.cost_scale &&
622 lhs.provider_identity == rhs.provider_identity &&
623 lhs.provider_instance_identity == rhs.provider_instance_identity &&
624 lhs.provider_revision == rhs.provider_revision &&
625 lhs.goals == rhs.goals;
628 template <
typename World,
typename Tag,
typename Prov
ider>
629 [[nodiscard]]
static auto key_matches(
const Key& key,
630 std::span<const Coord3> goals,
631 const Provider& provider)
noexcept
633 using Class = movement::movement_class_of<Tag>;
634 using UnitClass = movement::detail::UnitMovementClass<Class>;
635 return key_matches_class<World, UnitClass, Provider>(key, goals, provider);
638 template <
typename World,
typename Class,
typename Prov
ider>
639 [[nodiscard]]
static auto key_matches_class(
const Key& key,
640 std::span<const Coord3> goals,
641 const Provider& provider)
noexcept
643 using Model = ResolvedTransitionModel<World, Class, Provider>;
644 const auto model = Model{provider};
645 return key.movement_class ==
646 detail::tag_identity<typename Model::class_type>() &&
647 key.tile_count == detail::tile_count<World>() &&
648 key.chunk_count == World::chunk_count &&
649 key.local_tile_count == World::local_tile_count &&
650 key.shape_size == ShapeTraits<typename World::shape_type>::size &&
651 key.chunk_extent == ShapeTraits<typename World::shape_type>::chunk &&
652 key.lattice_identity ==
653 static_cast<std::uint32_t
>(Model::lattice_identity) &&
654 key.lattice_version == Model::lattice_version &&
656 static_cast<std::uint32_t
>(Model::step_policy_identity) &&
657 key.cost_scale == Model::cost_scale &&
658 key.provider_identity == detail::tag_identity<Provider>() &&
659 key.provider_instance_identity ==
660 detail::transition_provider_instance_identity(provider) &&
661 key.provider_revision == model.revision() &&
662 key.goals.size() == goals.size() &&
663 std::equal(key.goals.begin(), key.goals.end(), goals.begin());
666 [[nodiscard]]
static auto entry_byte_size(
667 const Key& key,
const DistanceFieldProduct& product)
noexcept
669 return sizeof(Entry) +
sizeof(DistanceFieldProduct) +
670 key.goals.size() *
sizeof(Coord3) + product.byte_size();
673 void erase_entry(std::size_t index)
noexcept {
674 bytes_ -= entries_[index].bytes;
675 entries_.erase(entries_.begin() +
static_cast<std::ptrdiff_t
>(index));
683 auto evict_to_budget() noexcept -> std::unique_ptr<DistanceFieldProduct> {
684 std::unique_ptr<DistanceFieldProduct> recycled;
685 while (bytes_ > byte_budget_ && !entries_.empty()) {
686 auto oldest = std::size_t{0};
687 for (std::size_t i = 1; i < entries_.size(); ++i) {
688 if (entries_[i].last_used < entries_[oldest].last_used) {
692 recycled = std::move(entries_[oldest].product);
699 std::vector<Entry> entries_;
700 std::size_t byte_budget_ = 0;
701 std::size_t bytes_ = 0;
702 std::size_t hits_ = 0;
703 std::size_t misses_ = 0;
704 std::size_t evictions_ = 0;
705 std::size_t stale_rejections_ = 0;
706 std::uint64_t clock_ = 0;
711template <
typename World,
typename Model>
712void capture_field_product_dependencies(
713 const World& world, std::span<const std::uint64_t> touched,
714 std::vector<std::uint8_t>& seen, ContentVersionDependencies& dependencies,
715 const Model& model) {
716 using Shape =
typename World::shape_type;
717 using Traits = ShapeTraits<Shape>;
719 seen.assign(
static_cast<std::size_t
>(World::chunk_count), 0);
720 if constexpr (Model::preserves_default_connectivity &&
721 std::is_same_v<
typename Model::step_policy,
722 movement::DefaultSteps>) {
726 const auto reached_chunks = dependencies.size();
727 for (std::size_t i = 0; i < reached_chunks; ++i) {
728 seen[
static_cast<std::size_t
>(dependencies.chunks()[i].key.value)] = 1;
730 for (std::size_t i = 0; i < reached_chunks; ++i) {
731 const auto center = chunk_coord<Shape>(dependencies.chunks()[i].key);
732 const auto add = [&](ChunkCoord3 neighbor) {
733 const auto key = chunk_key<Shape>(neighbor);
734 auto& mark = seen[
static_cast<std::size_t
>(key.value)];
737 dependencies.add_chunk_unique(world, key);
741 add(ChunkCoord3{center.x - 1, center.y, center.z});
743 if (center.x + 1 < Traits::chunk_count_x) {
744 add(ChunkCoord3{center.x + 1, center.y, center.z});
747 add(ChunkCoord3{center.x, center.y - 1, center.z});
749 if (center.y + 1 < Traits::chunk_count_y) {
750 add(ChunkCoord3{center.x, center.y + 1, center.z});
753 add(ChunkCoord3{center.x, center.y, center.z - 1});
755 if (center.z + 1 < Traits::chunk_count_z) {
756 add(ChunkCoord3{center.x, center.y, center.z + 1});
762 for (
const auto dependency : dependencies.chunks()) {
763 seen[
static_cast<std::size_t
>(dependency.key.value)] = 1;
765 for (
const auto index : touched) {
766 model.for_each_dependency_chunk(
767 world, tile_coord<Shape>(index), [&](ChunkKey key) {
768 auto& mark = seen[
static_cast<std::size_t
>(key.value)];
771 dependencies.add_chunk_unique(world, key);
776 if constexpr (Model::has_special_transitions) {
780 dependencies.capture_all(world);
790template <
typename World,
typename Tag,
typename Prov
ider>
795 const Provider& provider)
797 using Shape =
typename World::shape_type;
798 using Class = movement::movement_class_of<Tag>;
799 using UnitClass = movement::detail::UnitMovementClass<Class>;
808 std::is_same_v<typename World::residency_type, AlwaysResident>,
809 "build_distance_field_product requires an AlwaysResidentWorld; use "
810 "build_distance_field for sparse worlds.");
811 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
813 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
814 scratch.clear_build();
816 const auto model = Model{provider};
817 product.goals_.assign(goals.goals().begin(), goals.goals().end());
818 product.tile_count_ = detail::tile_count<World>();
819 product.chunk_count_ = World::chunk_count;
820 product.local_tile_count_ = World::local_tile_count;
821 product.shape_size_ = ShapeTraits<Shape>::size;
822 product.chunk_extent_ = ShapeTraits<Shape>::chunk;
823 product.model_class_identity_ =
824 detail::tag_identity<typename Model::class_type>();
825 product.model_lattice_identity_ =
826 static_cast<std::uint32_t
>(Model::lattice_identity);
827 product.model_lattice_version_ = Model::lattice_version;
828 product.model_step_identity_ =
829 static_cast<std::uint32_t
>(Model::step_policy_identity);
830 product.model_cost_scale_ = Model::cost_scale;
831 product.model_provider_identity_ = detail::tag_identity<Provider>();
832 product.model_provider_instance_identity_ =
833 detail::transition_provider_instance_identity(provider);
834 product.model_provider_revision_ = model.revision();
839 for (
const auto goal : goals.goals()) {
840 if (!contains<Shape>(goal)) {
843 TESS_DIAG_EVENT(path_goal_passability_check);
844 if (!detail::is_passable<World, Tag>(world, goal)) {
849 const auto node_count = detail::tile_count<World>();
850 if (scratch.distance_.size() != node_count) {
851 TESS_DIAG_EVENT(path_initialize);
852 scratch.generation_.assign(node_count, 0);
853 scratch.distance_.assign(node_count, infinite_distance);
856 for (
const auto goal : goals.goals()) {
857 const auto goal_index = detail::tile_index<Shape>(goal);
858 const auto goal_offset =
static_cast<std::size_t
>(goal_index);
859 if (scratch.is_current(goal_offset)) {
862 scratch.distance_[goal_offset] = 0;
863 scratch.touch_node(goal_index);
864 TESS_DIAG_EVENT(path_touch_node);
865 if constexpr (Model::cost_scale == 1 && !Model::has_special_transitions) {
866 scratch.frontier_.push_back(goal_index);
867 TESS_DIAG_EVENT(path_heap_push);
869 scratch.weighted_frontier_.push_back(
870 detail::PackedOpenNode::make(goal_index, 0, 0));
871 std::push_heap(scratch.weighted_frontier_.begin(),
872 scratch.weighted_frontier_.end(),
873 detail::packed_open_node_less);
874 TESS_DIAG_EVENT(path_heap_push);
878 std::size_t expanded_nodes = 0;
879 auto cost_overflow =
false;
880 if constexpr (Model::cost_scale == 1 && !Model::has_special_transitions) {
881 std::size_t head = 0;
882 while (head < scratch.frontier_.size()) {
883 const auto current = scratch.frontier_[head];
885 TESS_DIAG_EVENT(path_heap_pop);
888 const auto current_offset =
static_cast<std::size_t
>(current);
889 const auto current_distance =
890 scratch.distance_at(current_offset, infinite_distance);
891 const auto current_coord = detail::tile_coord<Shape>(current);
892 const auto visit_neighbor = [&](std::uint64_t neighbor_index) {
893 const auto neighbor_offset =
static_cast<std::size_t
>(neighbor_index);
894 if (scratch.is_current(neighbor_offset)) {
895 TESS_DIAG_EVENT(path_neighbor_closed);
899 scratch.distance_[neighbor_offset] = current_distance + 1;
900 scratch.touch_node(neighbor_index);
901 TESS_DIAG_EVENT(path_touch_node);
902 scratch.frontier_.push_back(neighbor_index);
903 TESS_DIAG_EVENT(path_heap_push);
905 if constexpr (Model::preserves_default_connectivity &&
906 std::is_same_v<
typename Model::step_policy,
908 detail::for_each_indexed_axis_neighbor<Shape>(
909 current_coord, current, [&](
Coord3, std::uint64_t neighbor_index) {
910 TESS_DIAG_EVENT(path_neighbor_candidate);
911 TESS_DIAG_EVENT(path_passability_check);
912 if (!detail::is_passable_index<World, Tag>(world,
914 TESS_DIAG_EVENT(path_neighbor_blocked);
917 visit_neighbor(neighbor_index);
920 model.for_each_reverse(world, current_coord, current, [&](
auto probe) {
921 TESS_DIAG_EVENT(path_neighbor_candidate);
922 if (probe.availability == TransitionAvailability::Legal) {
923 visit_neighbor(probe.to_index);
929 while (!scratch.weighted_frontier_.empty()) {
930 TESS_DIAG_EVENT(path_heap_pop);
931 std::pop_heap(scratch.weighted_frontier_.begin(),
932 scratch.weighted_frontier_.end(),
933 detail::packed_open_node_less);
934 const auto current = scratch.weighted_frontier_.back();
935 scratch.weighted_frontier_.pop_back();
936 const auto current_offset =
static_cast<std::size_t
>(current.index);
937 const auto current_distance =
938 scratch.distance_at(current_offset, infinite_distance);
939 if (current.g() != current_distance) {
940 TESS_DIAG_EVENT_VALUE(path_skip_pop,
false);
944 const auto current_coord = detail::tile_coord<Shape>(current.index);
945 model.for_each_reverse(
946 world, current_coord, current.index, [&](
auto probe) {
947 TESS_DIAG_EVENT(path_neighbor_candidate);
948 if (probe.availability != TransitionAvailability::Legal) {
951 if (probe.cost_overflow) {
952 cost_overflow = true;
955 const auto neighbor_offset =
956 static_cast<std::size_t
>(probe.to_index);
957 TESS_DIAG_EVENT(path_relax_attempt);
958 if (!scratch.is_current(neighbor_offset)) {
959 scratch.distance_[neighbor_offset] = infinite_distance;
960 scratch.touch_node(probe.to_index);
961 TESS_DIAG_EVENT(path_touch_node);
963 const auto next_distance =
964 detail::saturating_add(current_distance, probe.cost);
965 if (next_distance == infinite_distance) {
966 cost_overflow =
true;
969 if (next_distance < scratch.distance_[neighbor_offset]) {
970 TESS_DIAG_EVENT(path_relax_success);
971 scratch.distance_[neighbor_offset] = next_distance;
972 scratch.weighted_frontier_.push_back(detail::PackedOpenNode::make(
973 probe.to_index, next_distance, next_distance));
974 std::push_heap(scratch.weighted_frontier_.begin(),
975 scratch.weighted_frontier_.end(),
976 detail::packed_open_node_less);
977 TESS_DIAG_EVENT(path_heap_push);
983 product.distance_.assign(node_count, infinite_distance);
984 for (
const auto index : scratch.touched_) {
985 const auto offset =
static_cast<std::size_t
>(index);
986 product.distance_[offset] = scratch.distance_[offset];
987 const auto key = tile_key<Shape>(detail::tile_coord<Shape>(index));
988 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
992 detail::capture_field_product_dependencies<World, Model>(
993 world, scratch.touched_, scratch.chunk_seen_, product.dependencies_,
996 cost_overflow ? PathStatus::CostOverflow : PathStatus::Found;
997 product.expanded_nodes_ = expanded_nodes;
998 product.reached_nodes_ = scratch.touched_.size();
1000 return DistanceFieldResult{product.status_, product.expanded_nodes_,
1001 product.reached_nodes_};
1004template <
typename World,
typename Tag>
1015template <
typename World,
typename Class,
typename Prov
ider>
1020 static_assert(std::derived_from<Class, movement::movement_class_tag>,
1021 "build_weighted_distance_field_product requires Class to be a "
1023 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
1024 "weighted distance-field products are dense-only");
1025 using Shape =
typename World::shape_type;
1027 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1029 scratch.clear_build();
1031 const auto model = Model{provider};
1032 product.goals_.assign(goals.goals().begin(), goals.goals().end());
1033 product.tile_count_ = detail::tile_count<World>();
1034 product.chunk_count_ = World::chunk_count;
1035 product.local_tile_count_ = World::local_tile_count;
1036 product.shape_size_ = ShapeTraits<Shape>::size;
1037 product.chunk_extent_ = ShapeTraits<Shape>::chunk;
1038 product.model_class_identity_ = detail::tag_identity<Class>();
1039 product.model_lattice_identity_ =
1040 static_cast<std::uint32_t
>(Model::lattice_identity);
1041 product.model_lattice_version_ = Model::lattice_version;
1042 product.model_step_identity_ =
1043 static_cast<std::uint32_t
>(Model::step_policy_identity);
1044 product.model_cost_scale_ = Model::cost_scale;
1045 product.model_provider_identity_ = detail::tag_identity<Provider>();
1046 product.model_provider_instance_identity_ =
1047 detail::transition_provider_instance_identity(provider);
1048 product.model_provider_revision_ = model.revision();
1050 if (goals.empty()) {
1051 return {PathStatus::InvalidGoal, 0, 0};
1053 for (
const auto goal : goals.goals()) {
1054 if (!contains<Shape>(goal) ||
1055 !detail::is_passable<World, Class>(world, goal)) {
1056 return {PathStatus::InvalidGoal, 0, 0};
1058 const auto index = detail::tile_index<Shape>(goal);
1059 if (detail::tile_entry_cost_index<World, Class>(world, index) == 0) {
1060 return {PathStatus::InvalidGoal, 0, 0};
1064 const auto node_count = detail::tile_count<World>();
1065 if (scratch.distance_.size() != node_count) {
1066 scratch.generation_.assign(node_count, 0);
1067 scratch.distance_.assign(node_count, infinite_distance);
1069 for (
const auto goal : goals.goals()) {
1070 const auto index = detail::tile_index<Shape>(goal);
1071 const auto offset =
static_cast<std::size_t
>(index);
1072 if (scratch.is_current(offset)) {
1075 scratch.distance_[offset] = 0;
1076 scratch.touch_node(index);
1077 scratch.weighted_frontier_.push_back(
1078 detail::PackedOpenNode::make(index, 0, 0));
1079 std::push_heap(scratch.weighted_frontier_.begin(),
1080 scratch.weighted_frontier_.end(),
1081 detail::packed_open_node_less);
1084 auto expanded_nodes = std::size_t{0};
1085 auto cost_overflow =
false;
1086 while (!scratch.weighted_frontier_.empty()) {
1087 std::pop_heap(scratch.weighted_frontier_.begin(),
1088 scratch.weighted_frontier_.end(),
1089 detail::packed_open_node_less);
1090 const auto current = scratch.weighted_frontier_.back();
1091 scratch.weighted_frontier_.pop_back();
1092 const auto current_offset =
static_cast<std::size_t
>(current.index);
1093 const auto current_distance =
1094 scratch.distance_at(current_offset, infinite_distance);
1095 if (current.g() != current_distance) {
1099 model.for_each_reverse(
1100 world, detail::tile_coord<Shape>(current.index), current.index,
1102 if (probe.availability != TransitionAvailability::Legal) {
1105 if (probe.cost_overflow) {
1106 cost_overflow = true;
1109 const auto offset =
static_cast<std::size_t
>(probe.to_index);
1110 if (!scratch.is_current(offset)) {
1111 scratch.distance_[offset] = infinite_distance;
1112 scratch.touch_node(probe.to_index);
1115 detail::saturating_add(current_distance, probe.cost);
1116 if (next == infinite_distance) {
1117 cost_overflow =
true;
1120 if (next < scratch.distance_[offset]) {
1121 scratch.distance_[offset] = next;
1122 scratch.weighted_frontier_.push_back(
1123 detail::PackedOpenNode::make(probe.to_index, next, next));
1124 std::push_heap(scratch.weighted_frontier_.begin(),
1125 scratch.weighted_frontier_.end(),
1126 detail::packed_open_node_less);
1131 product.distance_.assign(node_count, infinite_distance);
1132 for (
const auto index : scratch.touched_) {
1133 const auto offset =
static_cast<std::size_t
>(index);
1134 product.distance_[offset] = scratch.distance_[offset];
1135 product.dependencies_.add_chunk(
1137 chunk_key<Shape>(tile_key<Shape>(detail::tile_coord<Shape>(index))));
1139 detail::capture_field_product_dependencies<World, Model>(
1140 world, scratch.touched_, scratch.chunk_seen_, product.dependencies_,
1143 cost_overflow ? PathStatus::CostOverflow : PathStatus::Found;
1144 product.expanded_nodes_ = expanded_nodes;
1145 product.reached_nodes_ = scratch.touched_.size();
1146 return {product.status_, product.expanded_nodes_, product.reached_nodes_};
1150template <
typename World,
typename Class>
1151[[nodiscard]]
auto build_weighted_distance_field_product(
1152 const World& world,
const GoalSet& goals, DistanceFieldProduct& product,
1153 DistanceFieldScratch& scratch) -> DistanceFieldResult {
1154 return build_weighted_distance_field_product<World, Class,
1155 AdjacentTransitions>(
1156 world, goals, product, scratch, AdjacentTransitions{});
1162template <
typename World,
typename Tag,
typename Prov
ider>
1166 using Shape =
typename World::shape_type;
1167 using Class = movement::movement_class_of<Tag>;
1168 using UnitClass = movement::detail::UnitMovementClass<Class>;
1170 const auto model = Model{provider};
1175 std::is_same_v<typename World::residency_type, AlwaysResident>,
1176 "distance_field_product_path requires an AlwaysResidentWorld; use "
1177 "distance_field_path for sparse worlds.");
1178 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1180 scratch.clear_path();
1181 if (!contains<Shape>(start)) {
1182 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1184 TESS_DIAG_EVENT(path_start_passability_check);
1185 if (!detail::is_passable<World, Tag>(world, start)) {
1186 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1188 if (product.status_ != PathStatus::Found || !product.is_valid(world) ||
1189 product.tile_count_ != detail::tile_count<World>() ||
1190 product.chunk_count_ != World::chunk_count ||
1191 product.local_tile_count_ != World::local_tile_count ||
1192 product.shape_size_ != ShapeTraits<Shape>::size ||
1193 product.chunk_extent_ != ShapeTraits<Shape>::chunk ||
1194 product.model_class_identity_ !=
1195 detail::tag_identity<typename Model::class_type>() ||
1196 product.model_lattice_identity_ !=
1197 static_cast<std::uint32_t
>(Model::lattice_identity) ||
1198 product.model_lattice_version_ != Model::lattice_version ||
1199 product.model_step_identity_ !=
1200 static_cast<std::uint32_t
>(Model::step_policy_identity) ||
1201 product.model_cost_scale_ != Model::cost_scale ||
1202 product.model_provider_identity_ != detail::tag_identity<Provider>() ||
1203 product.model_provider_instance_identity_ !=
1204 detail::transition_provider_instance_identity(provider) ||
1205 product.model_provider_revision_ != model.revision()) {
1206 return PathResult{PathStatus::NotComputed, 0, 0, 0, scratch.path_};
1209 const auto start_index = detail::tile_index<Shape>(start);
1210 auto current = start_index;
1211 auto current_distance = product.distance_[
static_cast<std::size_t
>(current)];
1212 if (current_distance == infinite_distance) {
1213 return PathResult{PathStatus::NoPath, 0, 0, product.reached_nodes_,
1217 scratch.path_.push_back(start);
1218 TESS_DIAG_EVENT(path_reconstruct_node);
1219 while (current_distance > 0) {
1220 const auto current_coord = detail::tile_coord<Shape>(current);
1221 auto next = current;
1222 auto next_distance = current_distance;
1223 if constexpr (Model::preserves_default_connectivity &&
1224 std::is_same_v<
typename Model::step_policy,
1226 detail::for_each_indexed_axis_neighbor<Shape>(
1227 current_coord, current, [&](
Coord3, std::uint64_t neighbor_index) {
1228 const auto neighbor_distance =
1229 product.distance_[
static_cast<std::size_t
>(neighbor_index)];
1230 if (neighbor_distance < next_distance) {
1231 next = neighbor_index;
1232 next_distance = neighbor_distance;
1235 if (next == current || next_distance + 1 != current_distance) {
1236 scratch.path_.clear();
1237 return PathResult{PathStatus::NotComputed, 0, 0, product.reached_nodes_,
1241 auto next_cost = std::uint32_t{0};
1242 model.for_each_forward(world, current_coord, current, [&](
auto probe) {
1243 if (probe.availability != TransitionAvailability::Legal ||
1244 probe.cost_overflow) {
1247 const auto neighbor_distance =
1248 product.distance_[
static_cast<std::size_t
>(probe.to_index)];
1249 if (neighbor_distance < next_distance &&
1250 detail::saturating_add(neighbor_distance, probe.cost) ==
1252 next = probe.to_index;
1253 next_distance = neighbor_distance;
1254 next_cost = probe.cost;
1258 if (next == current || detail::saturating_add(next_distance, next_cost) !=
1260 scratch.path_.clear();
1261 return PathResult{PathStatus::NotComputed, 0, 0, product.reached_nodes_,
1267 current_distance = product.distance_[
static_cast<std::size_t
>(current)];
1268 scratch.path_.push_back(detail::tile_coord<Shape>(current));
1269 TESS_DIAG_EVENT(path_reconstruct_node);
1273 product.distance_[
static_cast<std::size_t
>(start_index)],
1274 scratch.path_.size(),
1275 product.reached_nodes_,
1280template <
typename World,
typename Tag>
1289template <
typename World,
typename Class,
typename Prov
ider>
1293 static_assert(std::derived_from<Class, movement::movement_class_tag>,
1294 "weighted_distance_field_product_path requires Class to be a "
1296 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
1297 "weighted distance-field products are dense-only");
1298 using Shape =
typename World::shape_type;
1300 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1301 const auto model = Model{provider};
1303 scratch.clear_path();
1304 if (!contains<Shape>(start) ||
1305 !detail::is_passable<World, Class>(world, start)) {
1306 return {PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1308 const auto start_index = detail::tile_index<Shape>(start);
1311 if (detail::tile_entry_cost_index<World, Class>(world, start_index) == 0) {
1312 return {PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1314 if (product.status_ != PathStatus::Found || !product.is_valid(world) ||
1315 product.tile_count_ != detail::tile_count<World>() ||
1316 product.chunk_count_ != World::chunk_count ||
1317 product.local_tile_count_ != World::local_tile_count ||
1318 product.shape_size_ != ShapeTraits<Shape>::size ||
1319 product.chunk_extent_ != ShapeTraits<Shape>::chunk ||
1320 product.model_class_identity_ != detail::tag_identity<Class>() ||
1321 product.model_lattice_identity_ !=
1322 static_cast<std::uint32_t
>(Model::lattice_identity) ||
1323 product.model_lattice_version_ != Model::lattice_version ||
1324 product.model_step_identity_ !=
1325 static_cast<std::uint32_t
>(Model::step_policy_identity) ||
1326 product.model_cost_scale_ != Model::cost_scale ||
1327 product.model_provider_identity_ != detail::tag_identity<Provider>() ||
1328 product.model_provider_instance_identity_ !=
1329 detail::transition_provider_instance_identity(provider) ||
1330 product.model_provider_revision_ != model.revision()) {
1331 return {PathStatus::NotComputed, 0, 0, 0, scratch.path_};
1334 auto current = start_index;
1335 auto current_distance = product.distance_[start_index];
1336 if (current_distance == infinite_distance) {
1337 return {PathStatus::NoPath, 0, 0, product.reached_nodes_, scratch.path_};
1339 scratch.path_.push_back(start);
1340 while (current_distance > 0) {
1341 auto next = current;
1342 auto next_distance = current_distance;
1343 auto next_cost = std::uint32_t{0};
1344 model.for_each_forward(
1345 world, detail::tile_coord<Shape>(current), current, [&](
auto probe) {
1346 if (probe.availability != TransitionAvailability::Legal ||
1347 probe.cost_overflow) {
1350 const auto candidate = product.distance_[probe.to_index];
1351 if (candidate < next_distance &&
1352 detail::saturating_add(candidate, probe.cost) ==
1354 next = probe.to_index;
1355 next_distance = candidate;
1356 next_cost = probe.cost;
1359 if (next == current ||
1360 detail::saturating_add(next_distance, next_cost) != current_distance) {
1361 scratch.path_.clear();
1362 return {PathStatus::NotComputed, 0, 0, product.reached_nodes_,
1366 current_distance = product.distance_[current];
1367 scratch.path_.push_back(detail::tile_coord<Shape>(current));
1370 return {PathStatus::Found, product.distance_[start_index],
1371 scratch.path_.size(), product.reached_nodes_,
1372 scratch.path_, Model::cost_scale};
1376template <
typename World,
typename Class>
1377[[nodiscard]]
auto weighted_distance_field_product_path(
1380 return weighted_distance_field_product_path<
World, Class,
1388template <
typename World,
typename Tag,
typename Prov
ider>
1392 const Provider& provider)
1397 std::is_same_v<typename World::residency_type, AlwaysResident>,
1398 "nearest_target requires an AlwaysResidentWorld and a dense distance "
1401 world, start, product, scratch, provider);
1403 if (path.status == PathStatus::Found && !path.path.empty()) {
1404 target = path.path.back();
1407 path.status, path.cost, target, path.expanded_nodes,
1408 path.reached_nodes, path.path, path.cost_scale,
1412template <
typename World,
typename Tag>
1422template <
typename World,
typename Class,
typename Prov
ider>
1423[[nodiscard]]
auto weighted_nearest_target(
const World& world,
Coord3 start,
1426 const Provider& provider)
1428 const auto path = weighted_distance_field_product_path<World, Class>(
1429 world, start, product, scratch, provider);
1430 const auto target = path.status == PathStatus::Found && !path.path.empty()
1433 return {path.status, path.cost, target, path.expanded_nodes,
1434 path.reached_nodes, path.path, path.cost_scale};
1438template <
typename World,
typename Class>
1439[[nodiscard]]
auto weighted_nearest_target(
const World& world, Coord3 start,
1440 const DistanceFieldProduct& product,
1441 DistanceFieldScratch& scratch)
1442 -> NearestTargetResult {
1443 return weighted_nearest_target<World, Class, AdjacentTransitions>(
1444 world, start, product, scratch, AdjacentTransitions{});
Definition field_product_cache.h:60
auto distance_at(Coord3 coord) const noexcept -> std::uint32_t
Reads the stored distance-to-nearest-goal at coord, or the sentinel.
Definition field_product_cache.h:128
static constexpr std::uint32_t unreachable_distance
Sentinel returned by distance_at for unreached or invalid tiles.
Definition field_product_cache.h:118
friend auto distance_field_product_path(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> PathResult
Reconstructs a borrowed path from a valid multi-goal product.
Definition field_product_cache.h:1281
friend auto build_weighted_distance_field_product(const World &world, const GoalSet &goals, DistanceFieldProduct &product, DistanceFieldScratch &scratch, const Provider &provider) -> DistanceFieldResult
Builds a dense multi-goal weighted field into a reusable product.
Definition field_product_cache.h:1016
friend auto weighted_distance_field_product_path(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch, const Provider &provider) -> PathResult
Reconstructs an exact weighted path through a valid reusable product.
Definition field_product_cache.h:1290
friend auto nearest_target(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> NearestTargetResult
Finds the nearest reachable goal represented by a valid product.
Definition field_product_cache.h:1413
friend auto build_distance_field_product(const World &world, const GoalSet &goals, DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> DistanceFieldResult
Builds a dense multi-goal field into caller-owned reusable storage.
Definition field_product_cache.h:1005
auto lookup_weighted(const World &world, const GoalSet &goals, const Provider &provider) -> const DistanceFieldProduct *
Looks up a weighted product for the exact movement class and provider.
Definition field_product_cache.h:358
auto lookup(const World &world, const GoalSet &goals, const Provider &provider) -> const DistanceFieldProduct *
Looks up a product for the exact provider type and revision.
Definition field_product_cache.h:333
auto lookup_weighted(const World &world, const GoalSet &goals) -> const DistanceFieldProduct *
Looks up a weighted product using regular adjacent transitions.
Definition field_product_cache.h:385
auto store_reusing(DistanceFieldProduct &product, const Provider &provider) -> const DistanceFieldProduct *
Definition field_product_cache.h:440
auto store_reusing(DistanceFieldProduct &product) -> const DistanceFieldProduct *
Stores a product built for ordinary adjacent transitions.
Definition field_product_cache.h:454
auto store_weighted(DistanceFieldProduct &&product) -> bool
Stores a weighted product using regular adjacent transitions.
Definition field_product_cache.h:501
auto store_weighted(DistanceFieldProduct &&product, const Provider &provider) -> bool
Stores a weighted product under its movement class and provider.
Definition field_product_cache.h:462
auto store(DistanceFieldProduct &&product, const Provider &provider) -> bool
Stores a product under the exact provider type and revision.
Definition field_product_cache.h:425
auto store_weighted_reusing(DistanceFieldProduct &product, const Provider &provider) -> const DistanceFieldProduct *
Definition field_product_cache.h:481
Owns an ordered set of goals used to build a reusable distance product.
Definition field_product_cache.h:19
Definition transition_model.h:380
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Reports distance-field construction status and search work.
Definition path.h:70
Summarizes field-product cache residency and lookup outcomes.
Definition field_product_cache.h:238
Reports the closest reachable goal and a scratch-owned path to it.
Definition field_product_cache.h:42
Definition step_policy.h:26