tess 0.4.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/path/field_product_cache.h>
5#include <tess/path/path.h>
6#include <tess/path/portal_segment_cache.h>
7#include <tess/path/precheck.h>
8
9#include <algorithm>
10#include <cstddef>
11#include <cstdint>
12#include <limits>
13#include <span>
14#include <utility>
15#include <vector>
16
17namespace tess {
18
26struct PathTicket {
27 std::size_t value = 0;
28 std::uint64_t generation = 0;
29};
30
33 std::size_t clear_every_world_change = 0;
34 bool invalidate_unit_route_cache_on_world_change = true;
35 bool use_unit_field_product_cache = false;
36 std::size_t unit_field_product_min_goal_reuse = 2;
37 std::size_t unit_field_product_min_start_chunks = 2;
38 std::size_t unit_field_product_cache_byte_budget =
39 std::numeric_limits<std::size_t>::max();
40 std::size_t max_route_entries = RouteCacheScratch::default_max_entries;
41 std::size_t max_route_path_nodes = RouteCacheScratch::default_max_path_nodes;
42 std::size_t portal_segment_budget =
43 WeightedPortalSegmentCache::default_segment_budget;
44};
45
48 std::size_t submitted = 0;
49 std::size_t completed = 0;
50 std::size_t found = 0;
51 std::size_t invalid_start = 0;
52 std::size_t invalid_goal = 0;
53 std::size_t no_path = 0;
54 // Sparse worlds: the search could not rule out a route through a
55 // non-resident chunk (PathStatus::Indeterminate). Kept distinct from
56 // no_path so a stale/partial residency set is never counted as "no route".
57 std::size_t indeterminate = 0;
58 // Requests an optional topology precheck proved unreachable before A*, so no
59 // grid was expanded for them. A SUBSET of no_path (each ruled-out request is
60 // also counted there): the result is the same NoPath A* would have returned,
61 // this counter only measures how many were resolved without searching.
62 std::size_t precheck_ruled_out = 0;
63 std::size_t world_cache_invalidations = 0;
64 // Unit-cache clears forced by processing with a different movement class
65 // than the runtime was last bound to (see process_unit_cached). Correct but
66 // wasteful: keep one runtime per (world, class) to stay at zero.
67 std::size_t class_cache_invalidations = 0;
68 std::size_t cache_clears = 0;
69 std::size_t path_nodes = 0;
70 RouteCacheStats route_cache{};
71 FieldProductCacheStats field_product_cache{};
72 std::size_t field_product_candidate_groups = 0;
73 std::size_t field_product_used_groups = 0;
74 std::size_t field_product_skipped_groups = 0;
75 WeightedPathBatchStats weighted_batch{};
76 PortalSegmentCacheStats portal_segment_cache{};
77};
78
93 public:
95 void reserve_requests(std::size_t count) {
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);
108 }
109
111 void reserve_path_nodes(std::size_t count) {
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);
118 }
119
121 void reserve_search_nodes(std::size_t 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);
126 }
127
129 void reserve_unit_routes(std::size_t count) {
130 unit_route_cache_.reserve_routes(count);
131 }
132
134 void reserve_unit_field_products(std::size_t count) {
135 unit_field_product_cache_.reserve_entries(count);
136 }
137
145 unit_field_product_.reserve_dependencies(count);
146 }
147
149 void reserve_portal_segments(std::size_t count) {
150 portal_segment_cache_.reserve_segments(count);
151 }
152
157 void clear_requests() noexcept {
158 requests_.clear();
159 clear_results();
160 ++generation_;
161 }
162
164 void clear_caches() noexcept {
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;
170 ++cache_clears_;
171 }
172
178 [[nodiscard]] auto submit(PathRequest request) -> PathTicket {
179 const auto ticket = PathTicket{requests_.size(), generation_};
180 requests_.push_back(request);
181 return ticket;
182 }
183
187 [[nodiscard]] auto requests() const noexcept -> std::span<const PathRequest> {
188 return requests_;
189 }
190
194 [[nodiscard]] auto results() const noexcept -> std::span<const PathResult> {
195 return results_;
196 }
197
206 [[nodiscard]] auto result(PathTicket ticket) const noexcept -> PathResult {
207 TESS_ASSERT(ticket.generation == generation_);
208 TESS_ASSERT(ticket.value < results_.size());
209 if (ticket.generation != generation_ || ticket.value >= results_.size()) {
210 auto stale = PathResult{};
211 stale.status = PathStatus::NoPath;
212 return stale;
213 }
214 return results_[ticket.value];
215 }
216
218 [[nodiscard]] auto route_cache() noexcept -> RouteCacheScratch& {
219 return unit_route_cache_;
220 }
221
223 [[nodiscard]] auto route_cache() const noexcept -> const RouteCacheScratch& {
224 return unit_route_cache_;
225 }
226
228 [[nodiscard]] auto portal_segment_cache() noexcept
230 return portal_segment_cache_;
231 }
232
234 [[nodiscard]] auto portal_segment_cache() const noexcept
236 return portal_segment_cache_;
237 }
238
240 [[nodiscard]] auto stats() const noexcept -> PathRuntimeStats {
241 auto stats = stats_;
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_;
251 return stats;
252 }
253
264 template <typename World, typename ClassOrTag>
265 [[nodiscard]] auto process_unit_cached(
266 const World& world, PathRuntimeCachePolicy policy = {},
268 -> std::span<const PathResult> {
269 clear_results();
270 results_.resize(requests_.size());
271 offsets_.assign(requests_.size(), 0);
272 sizes_.assign(requests_.size(), 0);
273 processed_.assign(requests_.size(), 0);
274 stats_ = {};
275 // Bind AFTER prepare_process: a policy-triggered clear_caches() there
276 // zeroes the binding, and binding first would let this call refill the
277 // caches under an unbound (0) identity that a later class could then
278 // silently reuse.
279 prepare_process(world, policy);
280 bind_unit_class(
281 detail::tag_identity<movement::movement_class_of<ClassOrTag>>());
282 if (graph != nullptr) {
283 precheck_prepass<ClassOrTag>(world, *graph);
284 }
285 if constexpr (std::is_same_v<typename World::residency_type,
286 AlwaysResident>) {
287 // The unit field-product cache is dense-only (it sizes distance arrays by
288 // the global tile count). if constexpr, not a runtime if, so the
289 // dense-only process_repeated_goal_fields is never instantiated for a
290 // sparse world; the sparse unit path routes each request through
291 // cached_astar_path until a sparse field-product slice lands.
292 if (policy.use_unit_field_product_cache) {
293 process_repeated_goal_fields<World, ClassOrTag>(world, policy);
294 }
295 }
296
297 for (std::size_t i = 0; i < requests_.size(); ++i) {
298 if (processed_[i] != 0) {
299 continue;
300 }
301 const auto result = cached_astar_path<World, ClassOrTag>(
302 world, requests_[i], unit_scratch_, unit_route_cache_);
303 copy_result(i, result);
304 record_status(result.status);
305 }
306 refresh_result_spans();
307 return results_;
308 }
309
315 template <typename World, typename Class, std::uint32_t MaxCost>
316 [[nodiscard]] auto process_weighted_batch(
317 const World& world, PathRuntimeCachePolicy policy = {},
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);
326 }
327
334 template <typename World, typename PassableTag, typename CostTag,
335 std::uint32_t MaxCost>
336 [[nodiscard]] auto process_weighted_batch(
337 const World& world, PathRuntimeCachePolicy policy = {},
339 -> std::span<const PathResult> {
340 return process_weighted_batch_impl<
341 World, movement::LegacyWeighted<PassableTag, CostTag>, MaxCost,
342 PassableTag>(world, policy, graph);
343 }
344
345 private:
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> {
352 clear_results();
353 results_.resize(requests_.size());
354 offsets_.assign(requests_.size(), 0);
355 sizes_.assign(requests_.size(), 0);
356 processed_.assign(requests_.size(), 0);
357 stats_ = {};
358 prepare_process(world, policy);
359
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);
366 }
367 refresh_result_spans();
368 return results_;
369 }
370
371 // Precheck partitions the batch: requests proven unreachable resolve to
372 // NoPath now, and only the survivors run through weighted A*. The batch is
373 // positional, so survivor results scatter back to their original slots.
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]);
381 }
382 }
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);
389 }
390 refresh_result_spans();
391 return results_;
392 }
393
394 // The unit route cache keys entries on (start, goal) plus a world-version
395 // fingerprint and nothing on the movement class, so a runtime reused
396 // across classes would serve one class's route for another. Each unit
397 // process call binds the runtime to its (normalized) class; a rebind
398 // clears the unit caches -- correct even on misuse -- and counts in
399 // stats().class_cache_invalidations. One runtime per (world, class) is
400 // therefore the PERF contract, not a correctness precondition. The
401 // field-product cache already folds the class identity into its keys, and
402 // the weighted batch keeps no cross-call cache. The portal segment cache
403 // binds through for_class(), so a class change safely clears its entries.
404 // Keeping one runtime per (world, class) remains the performance contract:
405 // it avoids both kinds of conservative whole-cache rebind invalidation.
406 void bind_unit_class(std::uintptr_t identity) noexcept {
407 if (bound_unit_class_ == identity) {
408 return;
409 }
410 if (bound_unit_class_ != 0) {
411 unit_route_cache_.clear();
412 unit_field_product_cache_.clear();
413 ++class_cache_invalidations_;
414 }
415 bound_unit_class_ = identity;
416 }
417
418 void clear_results() noexcept {
419 results_.clear();
420 offsets_.clear();
421 sizes_.clear();
422 processed_.clear();
423 paths_.clear();
424 stats_ = {};
425 }
426
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) {
433 return;
434 }
435 if (!unit_route_cache_.invalidate_if_world_changed(world)) {
436 return;
437 }
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) {
442 clear_caches();
443 }
444 }
445
446 // Groups repeated-goal requests in one O(n) pass: a reusable
447 // open-addressed flat map (power-of-two capacity, linear probing) assigns
448 // each distinct goal a group id in first-occurrence order, member indices
449 // are bucketed with a counting sort, and per-group distinct start chunks
450 // come from a sort+unique over reusable scratch. Group processing order
451 // and all stats semantics match the previous per-request rescan.
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;
456
457 if (policy.unit_field_product_min_goal_reuse < 2) {
458 policy.unit_field_product_min_goal_reuse = 2;
459 }
460 if (policy.unit_field_product_min_start_chunks == 0) {
461 policy.unit_field_product_min_start_chunks = 1;
462 }
463 unit_field_product_cache_.set_byte_budget(
464 policy.unit_field_product_cache_byte_budget);
465
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
471 ? std::size_t{16}
472 : goal_group_slots_.size();
473 while (slot_capacity < (requests_.size() + 1u) * 2u) {
474 slot_capacity *= 2u;
475 }
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) {
480 continue;
481 }
482 // The grouping fast path converts starts to unchecked tile keys below.
483 // Resolve only out-of-shape starts here, as part of the existing O(n)
484 // grouping pass. Passability and goal validation remain in the field
485 // builder or ordinary A* fallback, avoiding duplicate world reads.
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);
491 processed_[i] = 1;
492 continue;
493 }
494 const auto goal = requests_[i].goal;
495 auto slot =
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) {
501 group = candidate;
502 break;
503 }
504 slot = (slot + 1u) & slot_mask;
505 }
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);
511 }
512 request_group_[i] = group;
513 ++group_counts_[group];
514 }
515
516 // Bucket member indices per group; members stay in submission order.
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];
521 }
522 }
523 for (std::size_t g = 1; g < group_offsets_.size(); ++g) {
524 group_offsets_[g] += group_offsets_[g - 1u];
525 }
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);
532 }
533 }
534
535 for (std::uint32_t g = 0; g < group_goals_.size(); ++g) {
536 if (group_counts_[g] < policy.unit_field_product_min_goal_reuse) {
537 continue;
538 }
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);
546 }
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());
551
552 ++stats_.field_product_candidate_groups;
553 if (start_chunk_count < policy.unit_field_product_min_start_chunks) {
554 ++stats_.field_product_skipped_groups;
555 continue;
556 }
557
558 unit_field_goals_.clear();
559 unit_field_goals_.add(group_goals_[g]);
560 auto* product =
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) {
567 // The cache takes the product by move; the next rebuild through
568 // build_distance_field_product() clears and reassigns
569 // unit_field_product_, so the moved-from state is never observed.
570 (void)unit_field_product_cache_.template store<World, PassableTag>(
571 std::move(unit_field_product_));
572 product =
573 unit_field_product_cache_.template lookup<World, PassableTag>(
574 world, unit_field_goals_);
575 }
576 }
577
578 if (product == nullptr) {
579 ++stats_.field_product_skipped_groups;
580 continue;
581 }
582
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_);
588 copy_result(j, result);
589 record_status(result.status);
590 processed_[j] = 1;
591 }
592 }
593 }
594
595 // Resolves every not-yet-processed request the region graph proves
596 // unreachable to a NoPath result without running A*, marking it processed so
597 // downstream field-product grouping and the search loop skip it. The graph's
598 // freshness, class-agreement, and no-false-negative guarantees live in
599 // precheck_path: a graph stamped for another movement class reads as
600 // GraphStale there, so this pass rules nothing out and A* stays
601 // authoritative.
602 template <typename ClassOrTag, typename World>
603 void precheck_prepass(
604 const World& world,
605 const RegionGraphT<typename World::residency_type>& graph) {
606 for (std::size_t i = 0; i < requests_.size(); ++i) {
607 if (processed_[i] != 0) {
608 continue;
609 }
610 const auto status =
611 precheck_path<ClassOrTag>(graph, world, requests_[i].start,
612 requests_[i].goal, precheck_scratch_);
613 if (!precheck_rules_out_path(status)) {
614 continue;
615 }
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;
621 processed_[i] = 1;
622 }
623 }
624
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{
630 result.status, result.cost, result.expanded_nodes,
631 result.reached_nodes, {},
632 };
633 }
634
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 = {};
639 } else {
640 results_[i].path =
641 std::span<const Coord3>{paths_.data() + offsets_[i], sizes_[i]};
642 }
643 }
644 }
645
646 void record_status(PathStatus status) noexcept {
647 switch (status) {
648 case PathStatus::Found:
649 ++stats_.found;
650 return;
651 case PathStatus::InvalidStart:
652 ++stats_.invalid_start;
653 return;
654 case PathStatus::InvalidGoal:
655 ++stats_.invalid_goal;
656 return;
657 case PathStatus::NoPath:
658 ++stats_.no_path;
659 return;
660 case PathStatus::Indeterminate:
661 ++stats_.indeterminate;
662 return;
663 }
664 }
665
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_;
672 // Optional pre-A* topology precheck: reused BFS scratch plus the survivor
673 // partition (surviving requests and their original slot indices) that lets
674 // the monolithic weighted batch skip proven-unreachable requests.
675 RegionGraphScratch precheck_scratch_;
676 std::vector<PathRequest> precheck_survivors_;
677 std::vector<std::size_t> survivor_original_;
678 // Reusable repeated-goal grouping scratch: flat-hash goal map slots,
679 // per-group goals/counts/member buckets, and start-chunk dedup storage.
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;
699 // Movement-class identity the unit caches are bound to (0 = unbound); see
700 // bind_unit_class.
701 std::uintptr_t bound_unit_class_ = 0;
702 std::size_t class_cache_invalidations_ = 0;
703 std::uint64_t generation_ = 0;
704};
705
706} // namespace tess
Definition path_runtime.h:92
void clear_requests() noexcept
Definition path_runtime.h:157
void reserve_unit_field_product_dependencies(std::size_t count)
Definition path_runtime.h:144
auto results() const noexcept -> std::span< const PathResult >
Definition path_runtime.h:194
auto portal_segment_cache() const noexcept -> const WeightedPortalSegmentCache &
Definition path_runtime.h:234
void reserve_unit_field_products(std::size_t count)
Definition path_runtime.h:134
auto submit(PathRequest request) -> PathTicket
Definition path_runtime.h:178
auto route_cache() const noexcept -> const RouteCacheScratch &
Definition path_runtime.h:223
void reserve_requests(std::size_t count)
Definition path_runtime.h:95
void clear_caches() noexcept
Definition path_runtime.h:164
auto portal_segment_cache() noexcept -> WeightedPortalSegmentCache &
Definition path_runtime.h:228
void reserve_portal_segments(std::size_t count)
Definition path_runtime.h:149
auto stats() const noexcept -> PathRuntimeStats
Definition path_runtime.h:240
auto requests() const noexcept -> std::span< const PathRequest >
Definition path_runtime.h:187
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:265
void reserve_unit_routes(std::size_t count)
Definition path_runtime.h:129
auto route_cache() noexcept -> RouteCacheScratch &
Definition path_runtime.h:218
auto result(PathTicket ticket) const noexcept -> PathResult
Definition path_runtime.h:206
void reserve_search_nodes(std::size_t count)
Definition path_runtime.h:121
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:336
void reserve_path_nodes(std::size_t count)
Definition path_runtime.h:111
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:316
Region graph storage specialized by dense or sparse residency policy.
Definition topology.h:309
Bounded scratch cache for exact and same-goal suffix unit routes.
Definition route_cache.h:42
Bounded, movement-class-safe cache of verified weighted path segments.
Definition portal_segment_cache.h:48
Definition world.h:22
Summarizes field-product cache residency and lookup outcomes.
Definition field_product_cache.h:153
Specifies inclusive start and goal coordinates for a path query.
Definition path.h:54
Definition path.h:63
Definition path_runtime.h:32
Definition path_runtime.h:47
Definition path_runtime.h:26
Snapshot of weighted portal-segment cache occupancy and lifecycle counts.
Definition portal_segment_cache.h:25
Snapshot of unit-route cache occupancy, hits, misses, and invalidations.
Definition route_cache.h:14
Definition path.h:80