63 static constexpr std::size_t default_segment_budget = 256;
65 void set_segment_budget(std::size_t budget)
noexcept {
67 if (entries_.size() > budget_) {
68 evict_oldest(entries_.size() - budget_);
72 [[nodiscard]]
auto segment_budget()
const noexcept -> std::size_t {
76 [[nodiscard]]
auto reserve_segments_checked(std::size_t count)
78 if (count > detail::effective_capacity_limit(entries_.max_size())) {
79 return ReserveStatus::CapacityExceeded;
81 entries_.reserve(count);
82 return ReserveStatus::Reserved;
85 void reserve_segments(std::size_t count) {
86 if (reserve_segments_checked(count) != ReserveStatus::Reserved) {
87 capacity_failure(
"portal segment cache entry capacity exceeded");
91 [[nodiscard]]
auto reserve_path_nodes_checked(std::size_t count)
93 if (count > detail::effective_capacity_limit(paths_.max_size())) {
94 return ReserveStatus::CapacityExceeded;
96 paths_.reserve(count);
97 return ReserveStatus::Reserved;
100 void reserve_path_nodes(std::size_t count) {
101 if (reserve_path_nodes_checked(count) != ReserveStatus::Reserved) {
102 capacity_failure(
"portal segment cache path capacity exceeded");
106 void clear()
noexcept {
111 void reset_stats()
noexcept {
114 stale_rejections_ = 0;
120 entries_.size(), paths_.size(), sweeps_,
121 evictions_, stale_rejections_, class_rebinds_,
129 template <
typename Class>
132 template <
typename World>
134 std::vector<Coord3>& out_path)
136 cache_->bind_class(identity_);
137 return cache_->lookup_append(world, request, out_path);
140 template <
typename World>
142 if (store_checked(world, request, result) !=
143 PortalSegmentStoreStatus::Completed) {
144 capacity_failure(
"portal segment cache capacity exceeded");
148 template <
typename World>
151 -> PortalSegmentStoreStatus {
152 return cache_->store_checked_for_class(identity_, world, request, result);
156 friend class WeightedPortalSegmentCache;
157 ClassView(WeightedPortalSegmentCache& cache,
158 std::uintptr_t identity) noexcept
159 : cache_(&cache), identity_(identity) {}
161 WeightedPortalSegmentCache* cache_;
162 std::uintptr_t identity_;
165 template <
typename ClassOrTag>
166 [[nodiscard]]
auto for_class() noexcept
167 ->
ClassView<movement::movement_class_of<ClassOrTag>> {
168 using Class = movement::movement_class_of<ClassOrTag>;
169 const auto identity = detail::tag_identity<Class>();
170 bind_class(identity);
177 template <
typename World>
178 void sweep_stale(
const World& world) {
179 if (compact_checked([&](
const Entry& entry) {
180 return entry.dependencies.is_valid(world);
181 }) != PortalSegmentStoreStatus::Completed) {
182 capacity_failure(
"portal segment cache compaction capacity exceeded");
187 [[nodiscard]]
auto size() const noexcept -> std::
size_t {
188 return entries_.size();
196 template <
typename World>
197 [[nodiscard]]
auto lookup_append(
const World& world, PathRequest request,
198 std::vector<Coord3>& out_path)
200 const auto* entry = find(world, request);
201 if (entry ==
nullptr) {
204 const auto cached = path(*entry);
205 const auto stitch = !out_path.empty() && !cached.empty() &&
206 out_path.back() == cached.front();
207 out_path.insert(out_path.end(),
208 cached.begin() + (stitch ? std::ptrdiff_t{1} : 0),
210 return SegmentHit{
true, entry->status, entry->cost};
213 template <
typename World>
214 void store(
const World& world, PathRequest request, PathResult result) {
215 if (store_checked(world, request, result) !=
216 PortalSegmentStoreStatus::Completed) {
217 capacity_failure(
"portal segment cache capacity exceeded");
221 template <
typename World>
222 [[nodiscard]]
auto store_checked(
const World& world, PathRequest request,
224 -> PortalSegmentStoreStatus {
225 using Shape = World::shape_type;
227 if (budget_ == 0 || result.status != PathStatus::Found) {
228 return PortalSegmentStoreStatus::Completed;
230 auto pending_stale_rejections = std::size_t{0};
231 if (find(world, request, &pending_stale_rejections) !=
nullptr) {
232 stale_rejections_ += pending_stale_rejections;
233 return PortalSegmentStoreStatus::Completed;
238 if (store_capacity_precheck(result.path.size()) !=
239 PortalSegmentStoreStatus::Completed) {
240 return PortalSegmentStoreStatus::CapacityExceeded;
246 auto entry = Entry{};
247 entry.request = request;
248 entry.status = result.status;
249 entry.cost = result.cost;
250 entry.path_size = result.path.size();
251 for (
const auto coord : result.path) {
252 entry.dependencies.add_chunk(world,
253 chunk_key<Shape>(tile_key<Shape>(coord)));
256 if (entries_.size() >= budget_) {
260 const auto compacted = compact_checked(
261 [&](
const Entry& current) {
262 return current.dependencies.is_valid(world);
264 1, result.path.size());
265 if (compacted != PortalSegmentStoreStatus::Completed) {
269 if (entries_.size() >= budget_) {
270 evict_oldest(entries_.size() - budget_ + 1);
273 if (reserve_append_capacity_checked(1, result.path.size()) !=
274 PortalSegmentStoreStatus::Completed) {
275 return PortalSegmentStoreStatus::CapacityExceeded;
279 entry.path_offset = paths_.size();
280 for (
const auto coord : result.path) {
281 paths_.push_back(coord);
283 entries_.push_back(std::move(entry));
284 stale_rejections_ += pending_stale_rejections;
285 return PortalSegmentStoreStatus::Completed;
288 template <
typename World>
289 [[nodiscard]]
auto store_checked_for_class(std::uintptr_t identity,
293 -> PortalSegmentStoreStatus {
294 const auto entry_limit =
295 detail::effective_capacity_limit(entries_.max_size());
296 const auto path_limit = detail::effective_capacity_limit(paths_.max_size());
297 if (budget_ != 0 && result.status == PathStatus::Found &&
298 bound_class_ != identity &&
299 (entry_limit == 0 || result.path.size() > path_limit)) {
300 return PortalSegmentStoreStatus::CapacityExceeded;
302 bind_class(identity);
303 return store_checked(world, request, result);
307 PathRequest request{};
308 PathStatus status = PathStatus::NotComputed;
309 std::uint32_t cost = 0;
310 std::size_t path_offset = 0;
311 std::size_t path_size = 0;
312 ContentVersionDependencies dependencies{};
314 static_assert(std::is_nothrow_move_constructible_v<Entry>);
315 static_assert(std::is_nothrow_move_assignable_v<Entry>);
316 static_assert(std::is_nothrow_copy_constructible_v<Coord3>);
318 template <
typename World>
319 [[nodiscard]]
auto find(
const World& world, PathRequest request,
320 std::size_t* pending_stale_rejections =
321 nullptr) noexcept -> const Entry* {
322 for (
const auto& entry : entries_) {
323 if (entry.request.start != request.start ||
324 entry.request.goal != request.goal) {
327 if (!entry.dependencies.is_valid(world)) {
328 if (pending_stale_rejections !=
nullptr) {
329 ++*pending_stale_rejections;
340 [[nodiscard]]
auto path(
const Entry& entry)
const noexcept
341 -> std::span<const Coord3> {
342 return std::span<const Coord3>{paths_.data() + entry.path_offset,
348 void evict_oldest(std::size_t count)
noexcept {
349 const auto evicted = count < entries_.size() ? count : entries_.size();
353 const auto first_path = evicted == entries_.size()
355 : entries_[evicted].path_offset;
356 std::move(paths_.begin() +
static_cast<std::ptrdiff_t
>(first_path),
357 paths_.end(), paths_.begin());
358 paths_.erase(paths_.end() -
static_cast<std::ptrdiff_t
>(first_path),
360 entries_.erase(entries_.begin(),
361 entries_.begin() +
static_cast<std::ptrdiff_t
>(evicted));
362 for (
auto& entry : entries_) {
363 entry.path_offset -= first_path;
365 evictions_ += evicted;
368 void clear_storage() noexcept {
373 void bind_class(std::uintptr_t identity)
noexcept {
374 if (bound_class_ == identity) {
377 if (bound_class_ != 0) {
381 bound_class_ = identity;
384 [[nodiscard]]
auto reserve_append_capacity_checked(
385 std::size_t additional_entries, std::size_t additional_path_nodes)
386 -> PortalSegmentStoreStatus {
387 const auto entry_limit =
388 detail::effective_capacity_limit(entries_.max_size());
389 const auto path_limit = detail::effective_capacity_limit(paths_.max_size());
390 if (entries_.size() > entry_limit || paths_.size() > path_limit ||
391 additional_entries > entry_limit - entries_.size() ||
392 additional_path_nodes > path_limit - paths_.size()) {
393 return PortalSegmentStoreStatus::CapacityExceeded;
395 entries_.reserve(entries_.size() + additional_entries);
396 paths_.reserve(paths_.size() + additional_path_nodes);
397 return PortalSegmentStoreStatus::Completed;
408 [[nodiscard]]
auto store_capacity_precheck(std::size_t path_nodes)
const
409 -> PortalSegmentStoreStatus {
410 if (entries_.size() < budget_) {
411 const auto entry_limit =
412 detail::effective_capacity_limit(entries_.max_size());
413 const auto path_limit =
414 detail::effective_capacity_limit(paths_.max_size());
415 if (entries_.size() >= entry_limit || paths_.size() > path_limit ||
416 path_nodes > path_limit - paths_.size()) {
417 return PortalSegmentStoreStatus::CapacityExceeded;
419 return PortalSegmentStoreStatus::Completed;
421 const auto compact_entry_limit =
422 detail::effective_capacity_limit(compact_entries_.max_size());
423 const auto compact_path_limit =
424 detail::effective_capacity_limit(compact_paths_.max_size());
428 if (compact_entry_limit == 0 || path_nodes > compact_path_limit) {
429 return PortalSegmentStoreStatus::CapacityExceeded;
431 return PortalSegmentStoreStatus::Completed;
434 template <
typename Keep>
435 [[nodiscard]]
auto compact_checked(Keep keep,
436 std::size_t additional_entries = 0,
437 std::size_t additional_path_nodes = 0)
438 -> PortalSegmentStoreStatus {
439 compact_entries_.clear();
440 compact_paths_.clear();
441 compact_indices_.clear();
443 const auto entry_limit =
444 detail::effective_capacity_limit(compact_entries_.max_size());
445 const auto index_limit =
446 detail::effective_capacity_limit(compact_indices_.max_size());
447 const auto path_limit =
448 detail::effective_capacity_limit(compact_paths_.max_size());
449 auto kept_path_nodes = std::size_t{0};
450 for (std::size_t index = 0; index < entries_.size(); ++index) {
451 const auto& entry = entries_[index];
455 if (compact_indices_.size() >= index_limit ||
456 kept_path_nodes > path_limit ||
457 entry.path_size > path_limit - kept_path_nodes) {
458 return PortalSegmentStoreStatus::CapacityExceeded;
460 kept_path_nodes += entry.path_size;
461 compact_indices_.push_back(index);
464 if (compact_indices_.size() > entry_limit || kept_path_nodes > path_limit ||
465 additional_entries > entry_limit - compact_indices_.size() ||
466 additional_path_nodes > path_limit - kept_path_nodes) {
467 return PortalSegmentStoreStatus::CapacityExceeded;
469 compact_entries_.reserve(compact_indices_.size() + additional_entries);
470 compact_paths_.reserve(kept_path_nodes + additional_path_nodes);
475 for (
const auto index : compact_indices_) {
476 auto& entry = entries_[index];
477 const auto offset = compact_paths_.size();
478 for (std::size_t path_index = 0; path_index < entry.path_size;
480 compact_paths_.push_back(paths_[entry.path_offset + path_index]);
482 entry.path_offset = offset;
483 compact_entries_.push_back(std::move(entry));
485 entries_.swap(compact_entries_);
486 paths_.swap(compact_paths_);
487 return PortalSegmentStoreStatus::Completed;
490 [[noreturn]]
static void capacity_failure(
const char* message) {
491#if TESS_HAS_EXCEPTIONS
492 throw std::length_error{message};
494 detail::fail_fast(message);
498 std::vector<Entry> entries_;
499 std::vector<Coord3> paths_;
500 std::vector<Entry> compact_entries_;
501 std::vector<Coord3> compact_paths_;
502 std::vector<std::size_t> compact_indices_;
503 std::size_t budget_ = default_segment_budget;
504 std::size_t sweeps_ = 0;
505 std::size_t evictions_ = 0;
506 std::size_t stale_rejections_ = 0;
507 std::size_t class_rebinds_ = 0;
508 std::uintptr_t bound_class_ = 0;
517 using Shape = World::shape_type;
522 std::is_same_v<typename World::residency_type, AlwaysResident>,
523 "build_weighted_portal_route_product requires an AlwaysResidentWorld; "
524 "use weighted_astar_path for sparse worlds.");
526 std::vector<Coord3> stash;
527 const auto source = product.stash_if_owned(waypoints, stash);
530 product.request_ = request;
531 product.waypoints_.assign(source.begin(), source.end());
532 auto class_cache = cache.template for_class<Class>();
534 auto from = request.start;
535 auto total_cost = std::uint64_t{0};
536 auto total_expanded = std::size_t{0};
537 auto total_reached = std::size_t{0};
538 auto append_path = [&](std::span<const Coord3> path) {
539 for (std::size_t i = product.path_.empty() ? 0u : 1u; i < path.size();
541 product.path_.push_back(path[i]);
544 auto append_segment = [&](
PathRequest segment_request) {
546 class_cache.lookup_append(world, segment_request, product.path_);
548 total_cost += hit.cost;
549 if (total_cost >= std::numeric_limits<std::uint32_t>::max()) {
550 product.path_.clear();
551 product.status_ = PathStatus::CostOverflow;
552 product.expanded_nodes_ = total_expanded;
553 product.reached_nodes_ = total_reached;
554 detail::capture_failure_dependencies<Shape>(
555 world, request, product.status_, product.dependencies_);
562 weighted_astar_path<World, Class>(world, segment_request, scratch);
563 class_cache.store(world, segment_request, result);
564 total_expanded += result.expanded_nodes;
565 total_reached += result.reached_nodes;
566 if (result.status != PathStatus::Found) {
567 product.path_.clear();
568 product.status_ = result.status;
569 product.expanded_nodes_ = total_expanded;
570 product.reached_nodes_ = total_reached;
573 detail::capture_failure_dependencies<Shape>(
574 world, segment_request, result.status, product.dependencies_);
577 total_cost += result.cost;
578 if (total_cost >= std::numeric_limits<std::uint32_t>::max()) {
579 product.path_.clear();
580 product.status_ = PathStatus::CostOverflow;
581 product.expanded_nodes_ = total_expanded;
582 product.reached_nodes_ = total_reached;
583 detail::capture_failure_dependencies<Shape>(
584 world, request, product.status_, product.dependencies_);
587 append_path(result.path.span());
591 for (
const auto waypoint : source) {
592 if (!append_segment(
PathRequest{from, waypoint})) {
593 return PathResult{product.status_, 0, total_expanded, total_reached,
598 if (!append_segment(
PathRequest{from, request.goal})) {
599 return PathResult{product.status_, 0, total_expanded, total_reached,
603 product.status_ = PathStatus::Found;
604 product.cost_ =
static_cast<std::uint32_t
>(total_cost);
605 product.expanded_nodes_ = total_expanded;
606 product.reached_nodes_ = total_reached;
607 for (
const auto coord : product.path_) {
608 const auto key = tile_key<Shape>(coord);
609 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
611 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
612 product.reached_nodes_, product.path_};