tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
path_runtime.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/fail_fast.h>
5#include <tess/path/field_product_cache.h>
6#include <tess/path/path.h>
7#include <tess/path/portal_route.h>
8#include <tess/path/portal_segment_cache.h>
9#include <tess/path/precheck.h>
10
11#include <algorithm>
12#include <cstddef>
13#include <cstdint>
14#include <limits>
15#include <optional>
16#include <span>
17#include <utility>
18#include <vector>
19
20namespace tess {
21
29struct PathTicket {
30 std::size_t value = 0;
31 std::uint64_t generation = 0;
32};
33
34namespace detail {
35
36// Portal-first eligibility: the class must use the ordinary adjacent-step
37// policy because chunk portals do not model custom regular-step rules.
38template <typename Class>
39struct portal_replan_tags {
40 static constexpr bool eligible =
41 std::derived_from<Class, movement::movement_class_tag> &&
42 std::same_as<movement::step_policy_of<Class>, movement::DefaultSteps>;
43};
44
45} // namespace detail
46
47// How single-goal weighted replans are served.
48//
49// ExactAStar: the batch's singleton fallback runs raw weighted A*; results are
50// optimal.
51//
52// PortalFirst: eligible singletons (dense orthogonal-lattice worlds,
53// default adjacent transitions, and explicit movement classes — Manhattan
54// is an admissible lower bound on that lattice, and the premium
55// cap's guarantee rests on it) first try a chunk-portal route
56// stitched through the runtime's segment cache. Accepted routes are legal
57// and verified but may exceed the optimal cost, bounded by the premium
58// cap below; every other outcome — no candidate, a failed segment, a cap
59// rejection, or an ineligible request — falls back to exact A* with
60// byte-identical results. A rejection costs the portal work PLUS the
61// exact search, so the cap is a route-quality contract, not a latency
62// bound; the stats record every outcome.
64enum class WeightedReplanStrategy : std::uint8_t {
65 ExactAStar,
66 PortalFirst,
67};
68
71 MissingChunkPolicy missing_chunk_policy =
72 MissingChunkPolicy::ReportIndeterminate;
73 // In ScopedFeasible staleness mode world changes do not advance this
74 // deep-clear counter: its rationale — bounding staleness accumulated
75 // behind the 64-bit fingerprint — is superseded by exact per-chunk
76 // validation. Note the periodic clear it would have fired sweeps ALL
77 // runtime caches (clear_caches), so scoped mode also retains
78 // field-product and portal entries longer; both self-validate their
79 // dependencies and stay budget-bounded.
80 std::size_t clear_every_world_change = 0;
81 bool invalidate_unit_route_cache_on_world_change = true;
82 // Opt-in scoped staleness for the unit route cache (see
83 // UnitRouteStaleness): surviving routes are legal with truthful cost and
84 // were optimal when stored; an edit elsewhere can leave them suboptimal
85 // until retired. Default preserves exact whole-world invalidation.
86 UnitRouteStaleness unit_route_staleness = UnitRouteStaleness::WholeWorldExact;
87 // Opt-in portal-first serving for single-goal weighted replans (see
88 // WeightedReplanStrategy). The premium cap alpha = num/den accepts a
89 // portal route only when its cost is at most alpha times the request's
90 // Manhattan distance — an admissible lower bound on the optimal cost for
91 // the eligible model class, so acceptance guarantees cost <= alpha x
92 // optimal. num == 0 disables the cap (accept every verified route).
93 WeightedReplanStrategy weighted_replan_strategy =
94 WeightedReplanStrategy::ExactAStar;
95 std::uint32_t portal_premium_limit_num = 4;
96 std::uint32_t portal_premium_limit_den = 3;
97 bool use_unit_field_product_cache = false;
98 std::size_t unit_field_product_min_goal_reuse = 2;
99 std::size_t unit_field_product_min_start_chunks = 2;
100 // Unit and weighted products share one runtime cache. The latest processing
101 // pass applies its corresponding budget to the combined retained footprint,
102 // so lowering this value may evict weighted products too.
103 std::size_t unit_field_product_cache_byte_budget =
104 std::numeric_limits<std::size_t>::max();
105 bool use_weighted_field_product_cache = false;
106 std::size_t weighted_field_product_min_goal_reuse = 2;
107 std::size_t weighted_field_product_min_start_chunks = 2;
108 // See unit_field_product_cache_byte_budget: this pass likewise budgets the
109 // shared cache and may evict retained unit products.
110 std::size_t weighted_field_product_cache_byte_budget =
111 std::numeric_limits<std::size_t>::max();
112 std::size_t max_route_entries = UnitRouteCache::default_max_entries;
113 std::size_t max_route_path_nodes = UnitRouteCache::default_max_path_nodes;
114 // Scoped staleness only: total budget for stored (chunk, content version)
115 // dependency pairs, with the same oversized-skip / cap-invalidate
116 // lifecycle as the path-node cap.
117 std::size_t max_route_dependency_pairs =
118 UnitRouteCache::default_max_path_nodes / 8u;
119 std::size_t portal_segment_budget =
120 WeightedPortalSegmentCache::default_segment_budget;
121};
122
123// Portal-first single-goal replan accounting (see
124// PathRuntimeCachePolicy::weighted_replan_strategy). The identity
125// attempts == accepted + no_candidates + verification_failures +
126// premium_rejections holds; exact_fallbacks counts every attempt that was
127// ultimately served by exact A* (all non-accepted attempts).
130 std::size_t attempts = 0;
131 std::size_t accepted = 0;
132 std::size_t no_candidates = 0;
133 std::size_t verification_failures = 0;
134 std::size_t premium_rejections = 0;
135 std::size_t exact_fallbacks = 0;
136 // Requests processed while PortalFirst was requested but the batch's
137 // instantiation is ineligible (sparse world, custom provider, or
138 // non-default-step class): they take the exact path without an
139 // attempt, and this counter is what distinguishes a misconfigured policy
140 // from a disabled one. Not part of the attempts identity above.
141 std::size_t ineligible_fallbacks = 0;
142};
143
146 std::size_t submitted = 0;
147 std::size_t completed = 0;
148 std::size_t found = 0;
149 std::size_t invalid_start = 0;
150 std::size_t invalid_goal = 0;
151 std::size_t no_path = 0;
152 std::size_t not_computed = 0;
153 std::size_t no_candidate = 0;
154 // Sparse worlds: the search could not rule out a route through a
155 // non-resident chunk (PathStatus::Indeterminate). Kept distinct from
156 // no_path so a stale/partial residency set is never counted as "no route".
157 std::size_t indeterminate = 0;
158 std::size_t cost_overflow = 0;
159 // Requests an optional topology precheck proved unreachable before A*, so no
160 // grid was expanded for them. A SUBSET of no_path (each ruled-out request is
161 // also counted there): the result is the same NoPath A* would have returned,
162 // this counter only measures how many were resolved without searching.
163 std::size_t precheck_ruled_out = 0;
164 std::size_t world_cache_invalidations = 0;
165 // Unit-cache clears forced by processing with a different movement class
166 // than the runtime was last bound to (see process_unit_cached). Correct but
167 // wasteful: keep one runtime per (world, class) to stay at zero.
168 std::size_t class_cache_invalidations = 0;
169 std::size_t cache_clears = 0;
170 std::size_t path_nodes = 0;
171 UnitRouteCacheStats route_cache{};
172 FieldProductCacheStats field_product_cache{};
173 std::size_t field_product_candidate_groups = 0;
174 std::size_t field_product_used_groups = 0;
175 std::size_t field_product_skipped_groups = 0;
176 WeightedPathBatchStats weighted_batch{};
177 PortalSegmentCacheStats portal_segment_cache{};
178 WeightedPortalReplanStats portal_replan{};
179};
180
198 public:
200 void reserve_requests(std::size_t count) {
201 requests_.reserve(count);
202 results_.reserve(count);
203 offsets_.reserve(count);
204 sizes_.reserve(count);
205 processed_.reserve(count);
206 request_group_.reserve(count);
207 group_members_.reserve(count);
208 group_start_chunks_.reserve(count);
209 precheck_survivors_.reserve(count);
210 survivor_original_.reserve(count);
211 weighted_batch_.reserve_requests(count);
212 unit_field_goals_.reserve(1);
213 }
214
216 void reserve_path_nodes(std::size_t count) {
217 paths_.reserve(count);
218 unit_route_cache_.reserve_path_nodes(count);
219 unit_field_scratch_.reserve_nodes(count);
220 unit_field_product_.reserve_nodes(count);
221 weighted_batch_.reserve_path_nodes(count);
222 portal_segment_cache_.reserve_path_nodes(count);
223 portal_replan_product_.reserve_path_nodes(count);
224 }
225
227 void reserve_search_nodes(std::size_t count) {
228 unit_scratch_.reserve_nodes(count);
229 unit_field_scratch_.reserve_nodes(count);
230 unit_field_product_.reserve_nodes(count);
231 weighted_batch_.reserve_search_nodes(count);
232 }
233
235 void reserve_unit_routes(std::size_t count) {
236 unit_route_cache_.reserve_routes(count);
237 }
238
240 void reserve_unit_field_products(std::size_t count) {
241 unit_field_product_cache_.reserve_entries(count);
242 }
243
251 unit_field_product_.reserve_dependencies(count);
252 }
253
259 void reserve_weighted_field_products(std::size_t count) {
260 unit_field_product_cache_.reserve_entries(count);
261 }
262
265 unit_field_product_.reserve_dependencies(count);
266 }
267
269 void reserve_portal_segments(std::size_t count) {
270 portal_segment_cache_.reserve_segments(count);
271 portal_replan_product_.reserve_waypoints(count);
272 }
273
278 void clear_requests() noexcept {
279 requests_.clear();
280 clear_results();
281 ++generation_;
282 }
283
285 void clear_caches() noexcept {
286 unit_route_cache_.clear();
287 unit_field_product_cache_.clear();
288 portal_segment_cache_.clear();
289 world_changes_since_clear_ = 0;
290 bound_unit_class_ = 0;
291 ++cache_clears_;
292 }
293
299 [[nodiscard]] auto submit(PathRequest request) -> PathTicket {
300 const auto ticket = PathTicket{requests_.size(), generation_};
301 requests_.push_back(request);
302 return ticket;
303 }
304
308 [[nodiscard]] auto requests() const noexcept -> std::span<const PathRequest> {
309 return requests_;
310 }
311
315 [[nodiscard]] auto results() const noexcept -> std::span<const PathResult> {
316 return results_published_ ? std::span<const PathResult>{results_}
317 : std::span<const PathResult>{};
318 }
319
329 [[nodiscard]] auto try_result(PathTicket ticket) const noexcept
330 -> std::optional<PathResult> {
331 if (ticket.generation != generation_ || !results_published_ ||
332 ticket.value >= results_.size()) {
333 return std::nullopt;
334 }
335 return results_[ticket.value];
336 }
337
346 [[nodiscard]] auto result(PathTicket ticket) const noexcept -> PathResult {
347 if (ticket.generation != generation_) {
348 detail::fail_fast(
349 "PathRequestRuntime::result received a stale PathTicket; use "
350 "try_result() for uncertain lookup");
351 }
352 if (!results_published_) {
353 detail::fail_fast(
354 "PathRequestRuntime::result has no published result batch; process "
355 "requests first or use try_result()");
356 }
357 if (ticket.value >= results_.size()) {
358 detail::fail_fast(
359 "PathRequestRuntime::result received an out-of-range PathTicket; "
360 "use try_result() for uncertain lookup");
361 }
362 return results_[ticket.value];
363 }
364
366 [[nodiscard]] auto route_cache() noexcept -> UnitRouteCache& {
367 return unit_route_cache_;
368 }
369
371 [[nodiscard]] auto route_cache() const noexcept -> const UnitRouteCache& {
372 return unit_route_cache_;
373 }
374
376 [[nodiscard]] auto portal_segment_cache() noexcept
378 return portal_segment_cache_;
379 }
380
382 [[nodiscard]] auto portal_segment_cache() const noexcept
384 return portal_segment_cache_;
385 }
386
388 [[nodiscard]] auto stats() const noexcept -> PathRuntimeStats {
389 auto stats = stats_;
390 stats.submitted = requests_.size();
391 if (results_published_) {
392 stats.completed = results_.size();
393 stats.path_nodes = paths_.size();
394 } else {
395 stats.completed = 0;
396 stats.found = 0;
397 stats.invalid_start = 0;
398 stats.invalid_goal = 0;
399 stats.no_path = 0;
400 stats.not_computed = 0;
401 stats.no_candidate = 0;
402 stats.indeterminate = 0;
403 stats.cost_overflow = 0;
404 stats.precheck_ruled_out = 0;
405 stats.path_nodes = 0;
406 stats.field_product_candidate_groups = 0;
407 stats.field_product_used_groups = 0;
408 stats.field_product_skipped_groups = 0;
409 stats.portal_replan = {};
410 }
411 stats.route_cache = unit_route_cache_.stats();
412 stats.field_product_cache = unit_field_product_cache_.stats();
413 stats.weighted_batch =
414 results_published_ ? weighted_batch_.stats() : WeightedPathBatchStats{};
415 stats.portal_segment_cache = portal_segment_cache_.stats();
416 stats.cache_clears = cache_clears_;
417 stats.class_cache_invalidations = class_cache_invalidations_;
418 return stats;
419 }
420
431 template <typename World, typename ClassOrTag>
432 [[nodiscard]] auto process_unit_cached(
433 const World& world, PathRuntimeCachePolicy policy = {},
435 -> std::span<const PathResult> {
436 clear_results();
437 results_.resize(requests_.size());
438 offsets_.assign(requests_.size(), 0);
439 sizes_.assign(requests_.size(), 0);
440 processed_.assign(requests_.size(), 0);
441 stats_ = {};
442 // Bind AFTER prepare_process: a policy-triggered clear_caches() there
443 // zeroes the binding, and binding first would let this call refill the
444 // caches under an unbound (0) identity that a later class could then
445 // silently reuse.
446 prepare_process(world, policy);
447 bind_unit_class(
448 detail::tag_identity<movement::movement_class_of<ClassOrTag>>());
449 if (graph != nullptr) {
450 precheck_prepass<ClassOrTag>(world, *graph, policy.missing_chunk_policy,
451 AdjacentTransitions{});
452 }
453 if constexpr (std::is_same_v<typename World::residency_type,
454 AlwaysResident>) {
455 // The unit field-product cache is dense-only (it sizes distance arrays by
456 // the global tile count). if constexpr, not a runtime if, so the
457 // dense-only process_repeated_goal_fields is never instantiated for a
458 // sparse world; sparse unit path processing routes each request through
459 // cached_astar_path instead.
460 if (policy.use_unit_field_product_cache) {
461 process_repeated_goal_fields<World, ClassOrTag>(world, policy);
462 }
463 }
464
465 for (std::size_t i = 0; i < requests_.size(); ++i) {
466 if (processed_[i] != 0) {
467 continue;
468 }
469 const auto result = cached_astar_path<World, ClassOrTag>(
470 world, requests_[i], unit_scratch_, unit_route_cache_,
471 policy.missing_chunk_policy);
472 copy_result(i, result);
473 record_status(result.status);
474 }
475 refresh_result_spans();
476 results_published_ = true;
477 return results_;
478 }
479
481 template <typename World, typename ClassOrTag, typename Provider>
482 [[nodiscard]] auto process_unit_cached(
483 const World& world, PathRuntimeCachePolicy policy,
485 const Provider& provider) -> std::span<const PathResult> {
486 clear_results();
487 results_.resize(requests_.size());
488 offsets_.assign(requests_.size(), 0);
489 sizes_.assign(requests_.size(), 0);
490 processed_.assign(requests_.size(), 0);
491 stats_ = {};
492 prepare_process(world, policy);
493 bind_unit_class(
494 detail::tag_identity<movement::movement_class_of<ClassOrTag>>());
495 if (graph != nullptr && graph->matches_provider(provider)) {
496 precheck_prepass<ClassOrTag>(world, *graph, policy.missing_chunk_policy,
497 provider);
498 }
499
500 // Provider-aware products are public, but the runtime deliberately uses
501 // its exact route cache here until provider revision is threaded through
502 // the repeated-goal selection policy and its counters.
503 for (std::size_t i = 0; i < requests_.size(); ++i) {
504 if (processed_[i] != 0) {
505 continue;
506 }
507 const auto result = cached_astar_path<World, ClassOrTag, Provider>(
508 world, requests_[i], unit_scratch_, unit_route_cache_, provider,
509 policy.missing_chunk_policy);
510 copy_result(i, result);
511 record_status(result.status);
512 }
513 refresh_result_spans();
514 results_published_ = true;
515 return results_;
516 }
517
523 template <typename World, typename Class, std::uint32_t MaxCost>
524 [[nodiscard]] auto process_weighted_batch(
525 const World& world, PathRuntimeCachePolicy policy = {},
527 -> std::span<const PathResult> {
528 static_assert(std::derived_from<Class, movement::movement_class_tag>,
529 "process_weighted_batch<World, Class, MaxCost> requires a "
530 "MovementClass; pass a movement class such as "
531 "PositiveCostFieldMovement.");
532 return process_weighted_batch_impl<World, Class, MaxCost, Class>(
533 world, policy, graph, AdjacentTransitions{});
534 }
535
537 template <typename World, typename Class, std::uint32_t MaxCost,
538 typename Provider>
539 [[nodiscard]] auto process_weighted_batch(
540 const World& world, PathRuntimeCachePolicy policy,
542 const Provider& provider) -> std::span<const PathResult> {
543 static_assert(std::derived_from<Class, movement::movement_class_tag>,
544 "process_weighted_batch<World, Class, MaxCost> requires a "
545 "MovementClass; pass a movement class such as "
546 "PositiveCostFieldMovement.");
547 return process_weighted_batch_impl<World, Class, MaxCost, Class>(
548 world, policy, graph, provider);
549 }
550
551 private:
552 template <typename World, typename BatchClass, std::uint32_t MaxCost,
553 typename PrecheckClassOrTag, typename Provider>
554 [[nodiscard]] auto process_weighted_batch_impl(
555 const World& world, PathRuntimeCachePolicy policy,
557 const Provider& provider) -> std::span<const PathResult> {
558 clear_results();
559 results_.resize(requests_.size());
560 offsets_.assign(requests_.size(), 0);
561 sizes_.assign(requests_.size(), 0);
562 processed_.assign(requests_.size(), 0);
563 stats_ = {};
564 prepare_process(world, policy);
565
566 if (graph != nullptr && !graph->matches_provider(provider)) {
567 graph = nullptr;
568 }
569 if (graph != nullptr) {
570 precheck_prepass<PrecheckClassOrTag>(
571 world, *graph, policy.missing_chunk_policy, provider);
572 }
573 if constexpr (std::is_same_v<typename World::residency_type,
574 AlwaysResident>) {
575 if (policy.use_weighted_field_product_cache) {
576 process_repeated_goal_weighted_fields<World, BatchClass>(world, policy,
577 provider);
578 }
579 }
580
581 // Portal-first pass over eligible single-goal survivors: an accepted
582 // portal route is copied into result storage immediately (the returned
583 // path borrows the shared product workspace) and marks the request
584 // processed; every other outcome leaves the request untouched so it
585 // flows into the exact batch below — fallback parity by construction.
586 if constexpr (std::is_same_v<typename World::residency_type,
587 AlwaysResident> &&
588 std::is_same_v<typename ShapeTraits<
589 typename World::shape_type>::lattice_type,
590 lattice::Orthogonal> &&
591 std::is_same_v<Provider, AdjacentTransitions> &&
592 detail::portal_replan_tags<BatchClass>::eligible) {
593 if (policy.weighted_replan_strategy ==
594 WeightedReplanStrategy::PortalFirst) {
595 process_portal_first_singletons<World, BatchClass>(world, policy);
596 }
597 } else {
598 // PortalFirst requested on an ineligible instantiation: everything
599 // takes the exact path, and the counter is what separates a
600 // misconfigured policy from a disabled one.
601 if (policy.weighted_replan_strategy ==
602 WeightedReplanStrategy::PortalFirst) {
603 for (std::size_t i = 0; i < requests_.size(); ++i) {
604 if (processed_[i] == 0) {
605 ++stats_.portal_replan.ineligible_fallbacks;
606 }
607 }
608 }
609 }
610
611 // Precheck and product reuse partition the batch. Only survivors run
612 // through the existing weighted batch, then scatter to original slots.
613 precheck_survivors_.clear();
614 survivor_original_.clear();
615 for (std::size_t i = 0; i < requests_.size(); ++i) {
616 if (processed_[i] == 0) {
617 survivor_original_.push_back(i);
618 precheck_survivors_.push_back(requests_[i]);
619 }
620 }
621 const auto batch =
622 weighted_path_batch<World, BatchClass, MaxCost, Provider>(
623 world, precheck_survivors_, weighted_batch_,
624 policy.missing_chunk_policy, provider);
625 for (std::size_t s = 0; s < batch.size(); ++s) {
626 const auto i = survivor_original_[s];
627 copy_result(i, batch[s]);
628 record_status(batch[s].status);
629 }
630 refresh_result_spans();
631 results_published_ = true;
632 return results_;
633 }
634
635 template <typename World, typename Class, typename Provider>
636 void process_repeated_goal_weighted_fields(const World& world,
637 PathRuntimeCachePolicy policy,
638 const Provider& provider) {
639 using Shape = typename World::shape_type;
640 if (policy.weighted_field_product_min_goal_reuse < 2) {
641 policy.weighted_field_product_min_goal_reuse = 2;
642 }
643 if (policy.weighted_field_product_min_start_chunks == 0) {
644 policy.weighted_field_product_min_start_chunks = 1;
645 }
646 unit_field_product_cache_.set_byte_budget(
647 policy.weighted_field_product_cache_byte_budget);
648 const auto product_distance_fits =
649 unit_field_product_cache_.can_fit_distance_storage(
650 detail::NodeIndexSpace<World>{world}.capacity_hint());
651
652 constexpr auto no_group = std::numeric_limits<std::uint32_t>::max();
653 group_goals_.clear();
654 group_counts_.clear();
655 request_group_.assign(requests_.size(), no_group);
656 auto slot_capacity = goal_group_slots_.size() < 16u
657 ? std::size_t{16}
658 : goal_group_slots_.size();
659 while (slot_capacity < (requests_.size() + 1u) * 2u) {
660 slot_capacity *= 2u;
661 }
662 goal_group_slots_.assign(slot_capacity, 0u);
663 const auto slot_mask = slot_capacity - 1u;
664 for (std::size_t i = 0; i < requests_.size(); ++i) {
665 if (processed_[i] != 0) {
666 continue;
667 }
668 if (!contains<Shape>(requests_[i].start)) {
669 auto invalid = PathResult{};
670 invalid.status = PathStatus::InvalidStart;
671 copy_result(i, invalid);
672 record_status(invalid.status);
673 processed_[i] = 1;
674 continue;
675 }
676 const auto goal = requests_[i].goal;
677 auto slot =
678 static_cast<std::size_t>(detail::coord_hash(goal)) & slot_mask;
679 auto group = no_group;
680 while (goal_group_slots_[slot] != 0u) {
681 const auto candidate = goal_group_slots_[slot] - 1u;
682 if (group_goals_[candidate] == goal) {
683 group = candidate;
684 break;
685 }
686 slot = (slot + 1u) & slot_mask;
687 }
688 if (group == no_group) {
689 group = static_cast<std::uint32_t>(group_goals_.size());
690 goal_group_slots_[slot] = group + 1u;
691 group_goals_.push_back(goal);
692 group_counts_.push_back(0u);
693 }
694 request_group_[i] = group;
695 ++group_counts_[group];
696 }
697
698 group_offsets_.assign(group_goals_.size() + 1u, 0u);
699 for (std::size_t i = 0; i < requests_.size(); ++i) {
700 if (request_group_[i] != no_group) {
701 ++group_offsets_[request_group_[i] + 1u];
702 }
703 }
704 for (std::size_t group = 1; group < group_offsets_.size(); ++group) {
705 group_offsets_[group] += group_offsets_[group - 1u];
706 }
707 group_cursors_.assign(group_offsets_.begin(), group_offsets_.end());
708 group_members_.assign(group_offsets_.back(), 0u);
709 for (std::size_t i = 0; i < requests_.size(); ++i) {
710 if (request_group_[i] != no_group) {
711 group_members_[group_cursors_[request_group_[i]]++] =
712 static_cast<std::uint32_t>(i);
713 }
714 }
715
716 for (std::uint32_t group = 0; group < group_goals_.size(); ++group) {
717 if (group_counts_[group] < policy.weighted_field_product_min_goal_reuse) {
718 continue;
719 }
720 const auto members_begin = group_offsets_[group];
721 const auto members_end = group_offsets_[group + 1u];
722 group_start_chunks_.clear();
723 for (auto member = members_begin; member < members_end; ++member) {
724 const auto& request = requests_[group_members_[member]];
725 group_start_chunks_.push_back(
726 chunk_key<Shape>(tile_key<Shape>(request.start)).value);
727 }
728 std::sort(group_start_chunks_.begin(), group_start_chunks_.end());
729 const auto start_chunk_count = static_cast<std::size_t>(
730 std::unique(group_start_chunks_.begin(), group_start_chunks_.end()) -
731 group_start_chunks_.begin());
732 ++stats_.field_product_candidate_groups;
733 if (start_chunk_count < policy.weighted_field_product_min_start_chunks) {
734 ++stats_.field_product_skipped_groups;
735 continue;
736 }
737 if (!product_distance_fits) {
738 // The normal weighted batch still builds one shared truncated field.
739 // Do not first build a full cache product that cannot possibly fit:
740 // an over-budget store would discard it and clear unrelated entries.
741 ++stats_.field_product_skipped_groups;
742 continue;
743 }
744
745 unit_field_goals_.clear();
746 unit_field_goals_.add(group_goals_[group]);
747 auto* product = unit_field_product_cache_
748 .template lookup_weighted<World, Class, Provider>(
749 world, unit_field_goals_, provider);
750 if (product == nullptr) {
751 const auto field =
752 build_weighted_distance_field_product<World, Class, Provider>(
753 world, unit_field_goals_, unit_field_product_,
754 unit_field_scratch_, provider);
755 if (field.status == PathStatus::Found) {
756 // See the unit path below: the store returns what it stored, so
757 // the second lookup (a full rescan that also counted a spurious
758 // hit) is gone, and the product keeps reusable storage.
759 product =
760 unit_field_product_cache_
761 .template store_weighted_reusing<World, Class, Provider>(
762 unit_field_product_, provider);
763 }
764 }
765 if (product == nullptr) {
766 ++stats_.field_product_skipped_groups;
767 continue;
768 }
769
770 ++stats_.field_product_used_groups;
771 for (auto member = members_begin; member < members_end; ++member) {
772 const auto index = static_cast<std::size_t>(group_members_[member]);
773 const auto result =
774 weighted_distance_field_product_path<World, Class, Provider>(
775 world, requests_[index].start, *product, unit_field_scratch_,
776 provider);
777 copy_result(index, result);
778 record_status(result.status);
779 processed_[index] = 1;
780 }
781 }
782 }
783
784 // The unit route cache keys entries on (start, goal) — staleness carried
785 // by the world fingerprint or, in scoped mode, per-chunk dependency
786 // records — and nothing on the movement class, so a runtime reused
787 // across classes would serve one class's route for another. Each unit
788 // process call binds the runtime to its (normalized) class; a rebind
789 // clears the unit caches -- correct even on misuse -- and counts in
790 // stats().class_cache_invalidations. One runtime per (world, class) is
791 // therefore the PERF contract, not a correctness precondition. The
792 // field-product cache already folds the class identity into its keys, and
793 // the weighted batch keeps no cross-call cache. The portal segment cache
794 // binds through for_class(), so a class change safely clears its entries.
795 // Keeping one runtime per (world, class) remains the performance contract:
796 // it avoids both kinds of conservative whole-cache rebind invalidation.
797 void bind_unit_class(std::uintptr_t identity) noexcept {
798 if (bound_unit_class_ == identity) {
799 return;
800 }
801 if (bound_unit_class_ != 0) {
802 unit_route_cache_.clear();
803 unit_field_product_cache_.clear();
804 ++class_cache_invalidations_;
805 }
806 bound_unit_class_ = identity;
807 }
808
809 void clear_results() noexcept {
810 results_published_ = false;
811 results_.clear();
812 offsets_.clear();
813 sizes_.clear();
814 processed_.clear();
815 paths_.clear();
816 stats_ = {};
817 }
818
819 template <typename World>
820 void prepare_process(const World& world, PathRuntimeCachePolicy policy) {
821 unit_route_cache_.set_caps(UnitRouteCacheLimits{
822 policy.max_route_entries, policy.max_route_path_nodes});
823 unit_route_cache_.set_dependency_cap(policy.max_route_dependency_pairs);
824 // A staleness-mode flip clears the unit caches unconditionally (entries
825 // stored under one mode's semantics are never served under the
826 // other's), independent of the world-change flag below.
827 unit_route_cache_.set_staleness(policy.unit_route_staleness);
828 portal_segment_cache_.set_segment_budget(policy.portal_segment_budget);
829 if (!policy.invalidate_unit_route_cache_on_world_change) {
830 return;
831 }
832 if (!unit_route_cache_.refresh_if_world_changed(world)) {
833 return;
834 }
835 ++stats_.world_cache_invalidations;
836 // The deep-clear counter pauses only when scoped validation is
837 // actually in effect: sparse worlds fall back to the exact fingerprint
838 // lifecycle regardless of the requested policy, so they keep the
839 // periodic collision backstop and cache sweep.
840 if constexpr (std::is_same_v<typename World::residency_type,
841 AlwaysResident>) {
842 if (policy.unit_route_staleness == UnitRouteStaleness::ScopedFeasible) {
843 // Scoped mode: the change armed per-entry validation instead of
844 // dropping entries; the deep-clear counter stays put (see the
845 // policy field's comment).
846 return;
847 }
848 }
849 ++world_changes_since_clear_;
850 if (policy.clear_every_world_change != 0 &&
851 world_changes_since_clear_ >= policy.clear_every_world_change) {
852 clear_caches();
853 }
854 }
855
856 // Groups repeated-goal requests in one O(n) pass: a reusable
857 // open-addressed flat map (power-of-two capacity, linear probing) assigns
858 // each distinct goal a group id in first-occurrence order, member indices
859 // are bucketed with a counting sort, and per-group distinct start chunks
860 // come from a sort+unique over reusable scratch. Group processing order
861 // and all stats semantics match the previous per-request rescan.
862 template <typename World, typename PassableTag>
863 void process_repeated_goal_fields(const World& world,
864 PathRuntimeCachePolicy policy) {
865 using Shape = typename World::shape_type;
866
867 if (policy.unit_field_product_min_goal_reuse < 2) {
868 policy.unit_field_product_min_goal_reuse = 2;
869 }
870 if (policy.unit_field_product_min_start_chunks == 0) {
871 policy.unit_field_product_min_start_chunks = 1;
872 }
873 unit_field_product_cache_.set_byte_budget(
874 policy.unit_field_product_cache_byte_budget);
875 const auto product_distance_fits =
876 unit_field_product_cache_.can_fit_distance_storage(
877 detail::NodeIndexSpace<World>{world}.capacity_hint());
878
879 constexpr auto no_group = std::numeric_limits<std::uint32_t>::max();
880 group_goals_.clear();
881 group_counts_.clear();
882 request_group_.assign(requests_.size(), no_group);
883 auto slot_capacity = goal_group_slots_.size() < 16u
884 ? std::size_t{16}
885 : goal_group_slots_.size();
886 while (slot_capacity < (requests_.size() + 1u) * 2u) {
887 slot_capacity *= 2u;
888 }
889 goal_group_slots_.assign(slot_capacity, 0u);
890 const auto slot_mask = slot_capacity - 1u;
891 for (std::size_t i = 0; i < requests_.size(); ++i) {
892 if (processed_[i] != 0) {
893 continue;
894 }
895 // The grouping fast path converts starts to unchecked tile keys below.
896 // Resolve only out-of-shape starts here, as part of the existing O(n)
897 // grouping pass. Passability and goal validation remain in the field
898 // builder or ordinary A* fallback, avoiding duplicate world reads.
899 if (!contains<Shape>(requests_[i].start)) {
900 auto invalid = PathResult{};
901 invalid.status = PathStatus::InvalidStart;
902 copy_result(i, invalid);
903 record_status(invalid.status);
904 processed_[i] = 1;
905 continue;
906 }
907 const auto goal = requests_[i].goal;
908 auto slot =
909 static_cast<std::size_t>(detail::coord_hash(goal)) & slot_mask;
910 auto group = no_group;
911 while (goal_group_slots_[slot] != 0u) {
912 const auto candidate = goal_group_slots_[slot] - 1u;
913 if (group_goals_[candidate] == goal) {
914 group = candidate;
915 break;
916 }
917 slot = (slot + 1u) & slot_mask;
918 }
919 if (group == no_group) {
920 group = static_cast<std::uint32_t>(group_goals_.size());
921 goal_group_slots_[slot] = group + 1u;
922 group_goals_.push_back(goal);
923 group_counts_.push_back(0u);
924 }
925 request_group_[i] = group;
926 ++group_counts_[group];
927 }
928
929 // Bucket member indices per group; members stay in submission order.
930 group_offsets_.assign(group_goals_.size() + 1u, 0u);
931 for (std::size_t i = 0; i < requests_.size(); ++i) {
932 if (request_group_[i] != no_group) {
933 ++group_offsets_[request_group_[i] + 1u];
934 }
935 }
936 for (std::size_t g = 1; g < group_offsets_.size(); ++g) {
937 group_offsets_[g] += group_offsets_[g - 1u];
938 }
939 group_cursors_.assign(group_offsets_.begin(), group_offsets_.end());
940 group_members_.assign(group_offsets_.back(), 0u);
941 for (std::size_t i = 0; i < requests_.size(); ++i) {
942 if (request_group_[i] != no_group) {
943 group_members_[group_cursors_[request_group_[i]]++] =
944 static_cast<std::uint32_t>(i);
945 }
946 }
947
948 for (std::uint32_t g = 0; g < group_goals_.size(); ++g) {
949 if (group_counts_[g] < policy.unit_field_product_min_goal_reuse) {
950 continue;
951 }
952 const auto members_begin = group_offsets_[g];
953 const auto members_end = group_offsets_[g + 1u];
954 group_start_chunks_.clear();
955 for (auto m = members_begin; m < members_end; ++m) {
956 const auto& request = requests_[group_members_[m]];
957 group_start_chunks_.push_back(
958 chunk_key<Shape>(tile_key<Shape>(request.start)).value);
959 }
960 std::sort(group_start_chunks_.begin(), group_start_chunks_.end());
961 const auto start_chunk_count = static_cast<std::size_t>(
962 std::unique(group_start_chunks_.begin(), group_start_chunks_.end()) -
963 group_start_chunks_.begin());
964
965 ++stats_.field_product_candidate_groups;
966 if (start_chunk_count < policy.unit_field_product_min_start_chunks) {
967 ++stats_.field_product_skipped_groups;
968 continue;
969 }
970 if (!product_distance_fits) {
971 // Ordinary A* remains the exact fallback. Avoid building a world-sized
972 // product that store() must reject (and whose rejection clears useful
973 // cache entries that already fit the configured budget).
974 ++stats_.field_product_skipped_groups;
975 continue;
976 }
977
978 unit_field_goals_.clear();
979 unit_field_goals_.add(group_goals_[g]);
980 auto* product =
981 unit_field_product_cache_.template lookup<World, PassableTag>(
982 world, unit_field_goals_);
983 if (product == nullptr) {
984 const auto field = build_distance_field_product<World, PassableTag>(
985 world, unit_field_goals_, unit_field_product_, unit_field_scratch_);
986 if (field.status == PathStatus::Found) {
987 // The store returns what it stored, so no second lookup is
988 // needed. That lookup rescanned every entry AND recorded a hit,
989 // inflating the published cache-hit rate by one on every build.
990 // It also leaves `unit_field_product_` holding storage the cache
991 // displaced, so the next rebuild reuses that capacity instead of
992 // reallocating a world-sized distance array.
993 product = unit_field_product_cache_
994 .template store_reusing<World, PassableTag>(
995 unit_field_product_);
996 }
997 }
998
999 if (product == nullptr) {
1000 ++stats_.field_product_skipped_groups;
1001 continue;
1002 }
1003
1004 ++stats_.field_product_used_groups;
1005 for (auto m = members_begin; m < members_end; ++m) {
1006 const auto j = static_cast<std::size_t>(group_members_[m]);
1007 const auto result = distance_field_product_path<World, PassableTag>(
1008 world, requests_[j].start, *product, unit_field_scratch_);
1009 copy_result(j, result);
1010 record_status(result.status);
1011 processed_[j] = 1;
1012 }
1013 }
1014 }
1015
1016 // Resolves every not-yet-processed request the region graph proves
1017 // unreachable to a NoPath result without running A*, marking it processed so
1018 // downstream field-product grouping and the search loop skip it. The graph's
1019 // freshness, class-agreement, and no-false-negative guarantees live in
1020 // precheck_path: a graph stamped for another movement class reads as
1021 // GraphStale there, so this pass rules nothing out and A* stays
1022 // authoritative.
1023 template <typename ClassOrTag, typename World, typename Provider>
1024 void precheck_prepass(
1025 const World& world,
1026 const RegionGraphT<typename World::residency_type>& graph,
1027 MissingChunkPolicy missing_chunk_policy, const Provider& provider) {
1028 for (std::size_t i = 0; i < requests_.size(); ++i) {
1029 if (processed_[i] != 0) {
1030 continue;
1031 }
1032 const auto status = precheck_path<ClassOrTag>(
1033 graph, world, requests_[i], precheck_scratch_, missing_chunk_policy,
1034 provider);
1035 if (!precheck_rules_out_path(status)) {
1036 continue;
1037 }
1038 auto ruled_out = PathResult{};
1039 ruled_out.status = PathStatus::NoPath;
1040 copy_result(i, ruled_out);
1041 record_status(ruled_out.status);
1042 ++stats_.precheck_ruled_out;
1043 processed_[i] = 1;
1044 }
1045 }
1046
1047 // Serves eligible single-goal weighted requests through the chunk-portal
1048 // product path (see WeightedReplanStrategy::PortalFirst). Runs only under
1049 // the compile-time eligibility gate at the call site. Requests whose
1050 // endpoints are out of shape are skipped without counting an attempt —
1051 // the batch classifies them — and a goal shared by two unprocessed
1052 // requests is never a singleton.
1053 template <typename World, typename BatchClass>
1054 void process_portal_first_singletons(const World& world,
1055 const PathRuntimeCachePolicy& policy) {
1056 using Shape = typename World::shape_type;
1057 // O(n) goal-occurrence counts over unprocessed requests, reusing the
1058 // grouping scratch (any earlier fields pass has finished with it).
1059 constexpr auto no_group = std::numeric_limits<std::uint32_t>::max();
1060 group_goals_.clear();
1061 group_counts_.clear();
1062 auto slot_capacity = goal_group_slots_.size() < 16u
1063 ? std::size_t{16}
1064 : goal_group_slots_.size();
1065 while (slot_capacity < (requests_.size() + 1u) * 2u) {
1066 slot_capacity *= 2u;
1067 }
1068 goal_group_slots_.assign(slot_capacity, 0u);
1069 const auto slot_mask = slot_capacity - 1u;
1070 const auto group_of = [&](Coord3 goal, bool insert) {
1071 auto slot =
1072 static_cast<std::size_t>(detail::coord_hash(goal)) & slot_mask;
1073 while (goal_group_slots_[slot] != 0u) {
1074 const auto candidate = goal_group_slots_[slot] - 1u;
1075 if (group_goals_[candidate] == goal) {
1076 return candidate;
1077 }
1078 slot = (slot + 1u) & slot_mask;
1079 }
1080 if (!insert) {
1081 return no_group;
1082 }
1083 const auto group = static_cast<std::uint32_t>(group_goals_.size());
1084 goal_group_slots_[slot] = group + 1u;
1085 group_goals_.push_back(goal);
1086 group_counts_.push_back(0u);
1087 return group;
1088 };
1089 for (std::size_t i = 0; i < requests_.size(); ++i) {
1090 if (processed_[i] != 0) {
1091 continue;
1092 }
1093 ++group_counts_[group_of(requests_[i].goal, true)];
1094 }
1095
1096 auto& replan_stats = stats_.portal_replan;
1097 for (std::size_t i = 0; i < requests_.size(); ++i) {
1098 if (processed_[i] != 0) {
1099 continue;
1100 }
1101 const auto request = requests_[i];
1102 if (!contains<Shape>(request.start) || !contains<Shape>(request.goal)) {
1103 continue;
1104 }
1105 if (group_counts_[group_of(request.goal, false)] != 1u) {
1106 continue;
1107 }
1108 ++replan_stats.attempts;
1109 const auto result =
1110 build_weighted_chunk_portal_route_product_cached<World, BatchClass>(
1111 world, request, unit_scratch_, portal_segment_cache_,
1112 portal_replan_product_);
1113 if (result.status != PathStatus::Found) {
1114 // Same-chunk requests always select a (waypoint-free) candidate,
1115 // so an empty waypoint list marks a selection failure only when
1116 // the endpoints sit in different chunks; a same-chunk failure is
1117 // a failed direct segment, i.e. a verification failure.
1118 const auto same_chunk = chunk_coord<Shape>(request.start) ==
1119 chunk_coord<Shape>(request.goal);
1120 if (!same_chunk && portal_replan_product_.waypoints().empty()) {
1121 ++replan_stats.no_candidates;
1122 } else {
1123 ++replan_stats.verification_failures;
1124 }
1125 ++replan_stats.exact_fallbacks;
1126 continue;
1127 }
1128 // Premium cap: Manhattan is an admissible lower bound for the
1129 // eligible scale-1 model class, so cost * den <= manhattan * num
1130 // implies cost <= (num/den) x optimal. num == 0 means no cap; a
1131 // zero denominator normalizes to 1 so a misconfigured policy can
1132 // never silently accept every route through cost * 0 <= bound.
1133 const auto premium_den = policy.portal_premium_limit_den == 0
1134 ? std::uint32_t{1}
1135 : policy.portal_premium_limit_den;
1136 const auto lower_bound = static_cast<std::uint64_t>(
1137 detail::manhattan(request.start, request.goal));
1138 const auto accept =
1139 policy.portal_premium_limit_num == 0 ||
1140 static_cast<std::uint64_t>(result.cost) * premium_den <=
1141 lower_bound * policy.portal_premium_limit_num;
1142 if (!accept) {
1143 ++replan_stats.premium_rejections;
1144 ++replan_stats.exact_fallbacks;
1145 continue;
1146 }
1147 // The result's path borrows the shared product; copy_result copies
1148 // it into runtime storage before the next singleton rebuilds. The
1149 // eligible model class is scale-1, matching the result's default.
1150 copy_result(i, result);
1151 record_status(result.status);
1152 processed_[i] = 1;
1153 ++replan_stats.accepted;
1154 }
1155 }
1156
1157 void copy_result(std::size_t index, PathResult result) {
1158 offsets_[index] = paths_.size();
1159 sizes_[index] = result.path.size();
1160 paths_.insert(paths_.end(), result.path.begin(), result.path.end());
1161 results_[index] = PathResult{
1162 result.status, result.cost, result.expanded_nodes,
1163 result.reached_nodes, {}, result.cost_scale,
1164 };
1165 }
1166
1167 void refresh_result_spans() noexcept {
1168 for (std::size_t i = 0; i < results_.size(); ++i) {
1169 if (sizes_[i] == 0) {
1170 results_[i].path = {};
1171 } else {
1172 results_[i].path =
1173 std::span<const Coord3>{paths_.data() + offsets_[i], sizes_[i]};
1174 }
1175 }
1176 }
1177
1178 void record_status(PathStatus status) noexcept {
1179 switch (status) {
1180 case PathStatus::NotComputed:
1181 ++stats_.not_computed;
1182 return;
1183 case PathStatus::Found:
1184 ++stats_.found;
1185 return;
1186 case PathStatus::InvalidStart:
1187 ++stats_.invalid_start;
1188 return;
1189 case PathStatus::InvalidGoal:
1190 ++stats_.invalid_goal;
1191 return;
1192 case PathStatus::NoPath:
1193 ++stats_.no_path;
1194 return;
1195 case PathStatus::Indeterminate:
1196 ++stats_.indeterminate;
1197 return;
1198 case PathStatus::CostOverflow:
1199 ++stats_.cost_overflow;
1200 return;
1201 case PathStatus::NoCandidate:
1202 ++stats_.no_candidate;
1203 return;
1204 }
1205 }
1206
1207 std::vector<PathRequest> requests_;
1208 std::vector<PathResult> results_;
1209 std::vector<std::size_t> offsets_;
1210 std::vector<std::size_t> sizes_;
1211 std::vector<std::uint8_t> processed_;
1212 std::vector<Coord3> paths_;
1213 bool results_published_ = false;
1214 // Optional pre-A* topology precheck: reused BFS scratch plus the survivor
1215 // partition (surviving requests and their original slot indices) that lets
1216 // the monolithic weighted batch skip proven-unreachable requests.
1217 RegionGraphScratch precheck_scratch_;
1218 std::vector<PathRequest> precheck_survivors_;
1219 std::vector<std::size_t> survivor_original_;
1220 // Reusable repeated-goal grouping scratch: flat-hash goal map slots,
1221 // per-group goals/counts/member buckets, and start-chunk dedup storage.
1222 std::vector<std::uint32_t> goal_group_slots_;
1223 std::vector<Coord3> group_goals_;
1224 std::vector<std::uint32_t> group_counts_;
1225 std::vector<std::uint32_t> group_offsets_;
1226 std::vector<std::uint32_t> group_cursors_;
1227 std::vector<std::uint32_t> group_members_;
1228 std::vector<std::uint32_t> request_group_;
1229 std::vector<std::uint64_t> group_start_chunks_;
1230 PathScratch unit_scratch_;
1231 UnitRouteCache unit_route_cache_;
1232 DistanceFieldScratch unit_field_scratch_;
1233 GoalSet unit_field_goals_;
1234 DistanceFieldProduct unit_field_product_;
1235 // The historical unit_ name predates weighted product reuse. Both processing
1236 // paths intentionally share this cache, its capacity, and its active budget.
1237 FieldProductCache unit_field_product_cache_;
1238 WeightedPathBatchScratch weighted_batch_;
1239 WeightedPortalSegmentCache portal_segment_cache_;
1240 // Portal-first replan workspace, reused across singletons in a batch;
1241 // every accepted route is copied out via copy_result before the next
1242 // request rebuilds it (the returned path borrows this product).
1243 WeightedPortalRouteProduct portal_replan_product_;
1244 PathRuntimeStats stats_;
1245 std::size_t world_changes_since_clear_ = 0;
1246 std::size_t cache_clears_ = 0;
1247 // Movement-class identity the unit caches are bound to (0 = unbound); see
1248 // bind_unit_class.
1249 std::uintptr_t bound_unit_class_ = 0;
1250 std::size_t class_cache_invalidations_ = 0;
1251 std::uint64_t generation_ = 0;
1252};
1253
1254} // namespace tess
Definition path_runtime.h:197
void clear_requests() noexcept
Definition path_runtime.h:278
void reserve_unit_field_product_dependencies(std::size_t count)
Definition path_runtime.h:250
auto results() const noexcept -> std::span< const PathResult >
Definition path_runtime.h:315
auto portal_segment_cache() const noexcept -> const WeightedPortalSegmentCache &
Definition path_runtime.h:382
void reserve_unit_field_products(std::size_t count)
Definition path_runtime.h:240
auto submit(PathRequest request) -> PathTicket
Definition path_runtime.h:299
void reserve_requests(std::size_t count)
Definition path_runtime.h:200
void clear_caches() noexcept
Definition path_runtime.h:285
auto process_unit_cached(const World &world, PathRuntimeCachePolicy policy, const RegionGraphT< typename World::residency_type > *graph, const Provider &provider) -> std::span< const PathResult >
Definition path_runtime.h:482
auto portal_segment_cache() noexcept -> WeightedPortalSegmentCache &
Definition path_runtime.h:376
void reserve_portal_segments(std::size_t count)
Definition path_runtime.h:269
auto try_result(PathTicket ticket) const noexcept -> std::optional< PathResult >
Definition path_runtime.h:329
void reserve_weighted_field_products(std::size_t count)
Definition path_runtime.h:259
auto stats() const noexcept -> PathRuntimeStats
Definition path_runtime.h:388
auto route_cache() noexcept -> UnitRouteCache &
Definition path_runtime.h:366
auto requests() const noexcept -> std::span< const PathRequest >
Definition path_runtime.h:308
auto route_cache() const noexcept -> const UnitRouteCache &
Definition path_runtime.h:371
auto process_unit_cached(const World &world, PathRuntimeCachePolicy policy={}, const RegionGraphT< typename World::residency_type > *graph=nullptr) -> std::span< const PathResult >
Definition path_runtime.h:432
void reserve_unit_routes(std::size_t count)
Definition path_runtime.h:235
auto process_weighted_batch(const World &world, PathRuntimeCachePolicy policy, const RegionGraphT< typename World::residency_type > *graph, const Provider &provider) -> std::span< const PathResult >
Definition path_runtime.h:539
auto result(PathTicket ticket) const noexcept -> PathResult
Definition path_runtime.h:346
void reserve_search_nodes(std::size_t count)
Definition path_runtime.h:227
void reserve_weighted_field_product_dependencies(std::size_t count)
Definition path_runtime.h:264
void reserve_path_nodes(std::size_t count)
Definition path_runtime.h:216
auto process_weighted_batch(const World &world, PathRuntimeCachePolicy policy={}, const RegionGraphT< typename World::residency_type > *graph=nullptr) -> std::span< const PathResult >
Definition path_runtime.h:524
Region graph storage specialized by dense or sparse residency policy.
Definition topology.h:382
Definition route_cache.h:94
Definition portal_segment_cache.h:61
Definition world.h:22
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Summarizes field-product cache residency and lookup outcomes.
Definition field_product_cache.h:238
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition path.h:60
Definition path_runtime.h:70
Definition path_runtime.h:145
Definition path_runtime.h:29
Snapshot of weighted portal-segment cache occupancy and lifecycle counts.
Definition portal_segment_cache.h:29
Snapshot of unit-route cache occupancy, hits, misses, and invalidations.
Definition route_cache.h:38
Definition path.h:78
Counters for the portal-first weighted replan pass.
Definition path_runtime.h:129
Definition step_policy.h:26