96 requests_.reserve(count);
97 results_.reserve(count);
98 offsets_.reserve(count);
99 sizes_.reserve(count);
100 processed_.reserve(count);
101 request_group_.reserve(count);
102 group_members_.reserve(count);
103 group_start_chunks_.reserve(count);
104 precheck_survivors_.reserve(count);
105 survivor_original_.reserve(count);
106 weighted_batch_.reserve_requests(count);
107 unit_field_goals_.reserve(1);
112 paths_.reserve(count);
113 unit_route_cache_.reserve_path_nodes(count);
114 unit_field_scratch_.reserve_nodes(count);
115 unit_field_product_.reserve_nodes(count);
116 weighted_batch_.reserve_path_nodes(count);
117 portal_segment_cache_.reserve_path_nodes(count);
122 unit_scratch_.reserve_nodes(count);
123 unit_field_scratch_.reserve_nodes(count);
124 unit_field_product_.reserve_nodes(count);
125 weighted_batch_.reserve_search_nodes(count);
130 unit_route_cache_.reserve_routes(count);
135 unit_field_product_cache_.reserve_entries(count);
145 unit_field_product_.reserve_dependencies(count);
150 portal_segment_cache_.reserve_segments(count);
165 unit_route_cache_.clear();
166 unit_field_product_cache_.clear();
167 portal_segment_cache_.clear();
168 world_changes_since_clear_ = 0;
169 bound_unit_class_ = 0;
179 const auto ticket =
PathTicket{requests_.size(), generation_};
180 requests_.push_back(request);
207 TESS_ASSERT(ticket.generation == generation_);
208 TESS_ASSERT(ticket.value < results_.size());
209 if (ticket.generation != generation_ || ticket.value >= results_.size()) {
211 stale.status = PathStatus::NoPath;
214 return results_[ticket.value];
219 return unit_route_cache_;
224 return unit_route_cache_;
230 return portal_segment_cache_;
236 return portal_segment_cache_;
242 stats.submitted = requests_.size();
243 stats.completed = results_.size();
244 stats.path_nodes = paths_.size();
245 stats.route_cache = unit_route_cache_.stats();
246 stats.field_product_cache = unit_field_product_cache_.stats();
247 stats.weighted_batch = weighted_batch_.stats();
248 stats.portal_segment_cache = portal_segment_cache_.stats();
249 stats.cache_clears = cache_clears_;
250 stats.class_cache_invalidations = class_cache_invalidations_;
264 template <
typename World,
typename ClassOrTag>
268 -> std::span<const PathResult> {
270 results_.resize(requests_.size());
271 offsets_.assign(requests_.size(), 0);
272 sizes_.assign(requests_.size(), 0);
273 processed_.assign(requests_.size(), 0);
279 prepare_process(world, policy);
281 detail::tag_identity<movement::movement_class_of<ClassOrTag>>());
282 if (graph !=
nullptr) {
283 precheck_prepass<ClassOrTag>(world, *graph);
285 if constexpr (std::is_same_v<
typename World::residency_type,
292 if (policy.use_unit_field_product_cache) {
293 process_repeated_goal_fields<World, ClassOrTag>(world, policy);
297 for (std::size_t i = 0; i < requests_.size(); ++i) {
298 if (processed_[i] != 0) {
301 const auto result = cached_astar_path<World, ClassOrTag>(
302 world, requests_[i], unit_scratch_, unit_route_cache_);
304 record_status(
result.status);
306 refresh_result_spans();
315 template <
typename World,
typename Class, std::u
int32_t MaxCost>
319 -> std::span<const PathResult> {
320 static_assert(std::derived_from<Class, movement::movement_class_tag>,
321 "process_weighted_batch<World, Class, MaxCost> requires a "
322 "MovementClass; legacy tag pairs go through the "
323 "<World, PassableTag, CostTag, MaxCost> overload.");
324 return process_weighted_batch_impl<World, Class, MaxCost, Class>(
325 world, policy, graph);
334 template <
typename World,
typename PassableTag,
typename CostTag,
335 std::uint32_t MaxCost>
339 -> std::span<const PathResult> {
340 return process_weighted_batch_impl<
341 World, movement::LegacyWeighted<PassableTag, CostTag>, MaxCost,
342 PassableTag>(world, policy, graph);
346 template <
typename World,
typename BatchClass, std::uint32_t MaxCost,
347 typename PrecheckClassOrTag>
348 [[nodiscard]]
auto process_weighted_batch_impl(
349 const World& world, PathRuntimeCachePolicy policy,
350 const RegionGraphT<typename World::residency_type>* graph)
351 -> std::span<const PathResult> {
353 results_.resize(requests_.size());
354 offsets_.assign(requests_.size(), 0);
355 sizes_.assign(requests_.size(), 0);
356 processed_.assign(requests_.size(), 0);
358 prepare_process(world, policy);
360 if (graph ==
nullptr) {
361 const auto batch = weighted_path_batch<World, BatchClass, MaxCost>(
362 world, requests_, weighted_batch_);
363 for (std::size_t i = 0; i < batch.size(); ++i) {
364 copy_result(i, batch[i]);
365 record_status(batch[i].status);
367 refresh_result_spans();
374 precheck_prepass<PrecheckClassOrTag>(world, *graph);
375 precheck_survivors_.clear();
376 survivor_original_.clear();
377 for (std::size_t i = 0; i < requests_.size(); ++i) {
378 if (processed_[i] == 0) {
379 survivor_original_.push_back(i);
380 precheck_survivors_.push_back(requests_[i]);
383 const auto batch = weighted_path_batch<World, BatchClass, MaxCost>(
384 world, precheck_survivors_, weighted_batch_);
385 for (std::size_t s = 0; s < batch.size(); ++s) {
386 const auto i = survivor_original_[s];
387 copy_result(i, batch[s]);
388 record_status(batch[s].status);
390 refresh_result_spans();
406 void bind_unit_class(std::uintptr_t identity)
noexcept {
407 if (bound_unit_class_ == identity) {
410 if (bound_unit_class_ != 0) {
411 unit_route_cache_.clear();
412 unit_field_product_cache_.clear();
413 ++class_cache_invalidations_;
415 bound_unit_class_ = identity;
418 void clear_results() noexcept {
427 template <
typename World>
428 void prepare_process(
const World& world, PathRuntimeCachePolicy policy) {
429 unit_route_cache_.set_caps(policy.max_route_entries,
430 policy.max_route_path_nodes);
431 portal_segment_cache_.set_segment_budget(policy.portal_segment_budget);
432 if (!policy.invalidate_unit_route_cache_on_world_change) {
435 if (!unit_route_cache_.invalidate_if_world_changed(world)) {
438 ++stats_.world_cache_invalidations;
439 ++world_changes_since_clear_;
440 if (policy.clear_every_world_change != 0 &&
441 world_changes_since_clear_ >= policy.clear_every_world_change) {
452 template <
typename World,
typename PassableTag>
453 void process_repeated_goal_fields(
const World& world,
454 PathRuntimeCachePolicy policy) {
455 using Shape =
typename World::shape_type;
457 if (policy.unit_field_product_min_goal_reuse < 2) {
458 policy.unit_field_product_min_goal_reuse = 2;
460 if (policy.unit_field_product_min_start_chunks == 0) {
461 policy.unit_field_product_min_start_chunks = 1;
463 unit_field_product_cache_.set_byte_budget(
464 policy.unit_field_product_cache_byte_budget);
466 constexpr auto no_group = std::numeric_limits<std::uint32_t>::max();
467 group_goals_.clear();
468 group_counts_.clear();
469 request_group_.assign(requests_.size(), no_group);
470 auto slot_capacity = goal_group_slots_.size() < 16u
472 : goal_group_slots_.size();
473 while (slot_capacity < (requests_.size() + 1u) * 2u) {
476 goal_group_slots_.assign(slot_capacity, 0u);
477 const auto slot_mask = slot_capacity - 1u;
478 for (std::size_t i = 0; i < requests_.size(); ++i) {
479 if (processed_[i] != 0) {
486 if (!contains<Shape>(requests_[i].start)) {
487 auto invalid = PathResult{};
488 invalid.status = PathStatus::InvalidStart;
489 copy_result(i, invalid);
490 record_status(invalid.status);
494 const auto goal = requests_[i].goal;
496 static_cast<std::size_t
>(detail::coord_hash(goal)) & slot_mask;
497 auto group = no_group;
498 while (goal_group_slots_[slot] != 0u) {
499 const auto candidate = goal_group_slots_[slot] - 1u;
500 if (group_goals_[candidate] == goal) {
504 slot = (slot + 1u) & slot_mask;
506 if (group == no_group) {
507 group =
static_cast<std::uint32_t
>(group_goals_.size());
508 goal_group_slots_[slot] = group + 1u;
509 group_goals_.push_back(goal);
510 group_counts_.push_back(0u);
512 request_group_[i] = group;
513 ++group_counts_[group];
517 group_offsets_.assign(group_goals_.size() + 1u, 0u);
518 for (std::size_t i = 0; i < requests_.size(); ++i) {
519 if (request_group_[i] != no_group) {
520 ++group_offsets_[request_group_[i] + 1u];
523 for (std::size_t g = 1; g < group_offsets_.size(); ++g) {
524 group_offsets_[g] += group_offsets_[g - 1u];
526 group_cursors_.assign(group_offsets_.begin(), group_offsets_.end());
527 group_members_.assign(group_offsets_.back(), 0u);
528 for (std::size_t i = 0; i < requests_.size(); ++i) {
529 if (request_group_[i] != no_group) {
530 group_members_[group_cursors_[request_group_[i]]++] =
531 static_cast<std::uint32_t
>(i);
535 for (std::uint32_t g = 0; g < group_goals_.size(); ++g) {
536 if (group_counts_[g] < policy.unit_field_product_min_goal_reuse) {
539 const auto members_begin = group_offsets_[g];
540 const auto members_end = group_offsets_[g + 1u];
541 group_start_chunks_.clear();
542 for (
auto m = members_begin; m < members_end; ++m) {
543 const auto& request = requests_[group_members_[m]];
544 group_start_chunks_.push_back(
545 chunk_key<Shape>(tile_key<Shape>(request.start)).value);
547 std::sort(group_start_chunks_.begin(), group_start_chunks_.end());
548 const auto start_chunk_count =
static_cast<std::size_t
>(
549 std::unique(group_start_chunks_.begin(), group_start_chunks_.end()) -
550 group_start_chunks_.begin());
552 ++stats_.field_product_candidate_groups;
553 if (start_chunk_count < policy.unit_field_product_min_start_chunks) {
554 ++stats_.field_product_skipped_groups;
558 unit_field_goals_.clear();
559 unit_field_goals_.add(group_goals_[g]);
561 unit_field_product_cache_.template lookup<World, PassableTag>(
562 world, unit_field_goals_);
563 if (product ==
nullptr) {
564 const auto field = build_distance_field_product<World, PassableTag>(
565 world, unit_field_goals_, unit_field_scratch_, unit_field_product_);
566 if (field.status == PathStatus::Found) {
570 (void)unit_field_product_cache_.template store<World, PassableTag>(
571 std::move(unit_field_product_));
573 unit_field_product_cache_.template lookup<World, PassableTag>(
574 world, unit_field_goals_);
578 if (product ==
nullptr) {
579 ++stats_.field_product_skipped_groups;
583 ++stats_.field_product_used_groups;
584 for (
auto m = members_begin; m < members_end; ++m) {
585 const auto j =
static_cast<std::size_t
>(group_members_[m]);
586 const auto result = distance_field_product_path<World, PassableTag>(
587 world, requests_[j].start, *product, unit_field_scratch_);
589 record_status(
result.status);
602 template <
typename ClassOrTag,
typename World>
603 void precheck_prepass(
605 const RegionGraphT<typename World::residency_type>& graph) {
606 for (std::size_t i = 0; i < requests_.size(); ++i) {
607 if (processed_[i] != 0) {
611 precheck_path<ClassOrTag>(graph, world, requests_[i].start,
612 requests_[i].goal, precheck_scratch_);
613 if (!precheck_rules_out_path(status)) {
616 auto ruled_out = PathResult{};
617 ruled_out.status = PathStatus::NoPath;
618 copy_result(i, ruled_out);
619 record_status(ruled_out.status);
620 ++stats_.precheck_ruled_out;
625 void copy_result(std::size_t index, PathResult
result) {
626 offsets_[index] = paths_.size();
627 sizes_[index] =
result.path.size();
628 paths_.insert(paths_.end(),
result.path.begin(),
result.path.end());
629 results_[index] = PathResult{
635 void refresh_result_spans() noexcept {
636 for (std::size_t i = 0; i < results_.size(); ++i) {
637 if (sizes_[i] == 0) {
638 results_[i].path = {};
641 std::span<const Coord3>{paths_.data() + offsets_[i], sizes_[i]};
646 void record_status(PathStatus status)
noexcept {
648 case PathStatus::Found:
651 case PathStatus::InvalidStart:
652 ++stats_.invalid_start;
654 case PathStatus::InvalidGoal:
655 ++stats_.invalid_goal;
657 case PathStatus::NoPath:
660 case PathStatus::Indeterminate:
661 ++stats_.indeterminate;
666 std::vector<PathRequest> requests_;
667 std::vector<PathResult> results_;
668 std::vector<std::size_t> offsets_;
669 std::vector<std::size_t> sizes_;
670 std::vector<std::uint8_t> processed_;
671 std::vector<Coord3> paths_;
675 RegionGraphScratch precheck_scratch_;
676 std::vector<PathRequest> precheck_survivors_;
677 std::vector<std::size_t> survivor_original_;
680 std::vector<std::uint32_t> goal_group_slots_;
681 std::vector<Coord3> group_goals_;
682 std::vector<std::uint32_t> group_counts_;
683 std::vector<std::uint32_t> group_offsets_;
684 std::vector<std::uint32_t> group_cursors_;
685 std::vector<std::uint32_t> group_members_;
686 std::vector<std::uint32_t> request_group_;
687 std::vector<std::uint64_t> group_start_chunks_;
688 PathScratch unit_scratch_;
689 RouteCacheScratch unit_route_cache_;
690 DistanceFieldScratch unit_field_scratch_;
691 GoalSet unit_field_goals_;
692 DistanceFieldProduct unit_field_product_;
693 FieldProductCache unit_field_product_cache_;
694 WeightedPathBatchScratch weighted_batch_;
695 WeightedPortalSegmentCache portal_segment_cache_;
696 PathRuntimeStats stats_;
697 std::size_t world_changes_since_clear_ = 0;
698 std::size_t cache_clears_ = 0;
701 std::uintptr_t bound_unit_class_ = 0;
702 std::size_t class_cache_invalidations_ = 0;
703 std::uint64_t generation_ = 0;