50 static constexpr std::size_t default_segment_budget = 256;
52 void set_segment_budget(std::size_t budget)
noexcept {
54 if (entries_.size() > budget_) {
55 evict_oldest(entries_.size() - budget_);
59 [[nodiscard]]
auto segment_budget()
const noexcept -> std::size_t {
63 void reserve_segments(std::size_t count) { entries_.reserve(count); }
65 void reserve_path_nodes(std::size_t count) { paths_.reserve(count); }
67 void clear()
noexcept {
72 void reset_stats()
noexcept {
75 stale_rejections_ = 0;
81 entries_.size(), paths_.size(), sweeps_,
82 evictions_, stale_rejections_, class_rebinds_,
90 template <
typename Class>
93 template <
typename World>
95 std::vector<Coord3>& out_path)
97 cache_->bind_class(identity_);
98 return cache_->lookup_append(world, request, out_path);
101 template <
typename World>
103 cache_->bind_class(identity_);
104 cache_->store(world, request, result);
108 friend class WeightedPortalSegmentCache;
109 ClassView(WeightedPortalSegmentCache& cache,
110 std::uintptr_t identity) noexcept
111 : cache_(&cache), identity_(identity) {}
113 WeightedPortalSegmentCache* cache_;
114 std::uintptr_t identity_;
117 template <
typename ClassOrTag>
118 [[nodiscard]]
auto for_class() noexcept
119 ->
ClassView<movement::movement_class_of<ClassOrTag>> {
120 using Class = movement::movement_class_of<ClassOrTag>;
121 const auto identity = detail::tag_identity<Class>();
122 bind_class(identity);
129 template <
typename World>
130 void sweep_stale(
const World& world) {
132 [&](
const Entry& entry) {
return entry.dependencies.is_valid(world); });
136 [[nodiscard]]
auto size() const noexcept -> std::
size_t {
137 return entries_.size();
145 template <
typename World>
146 [[nodiscard]]
auto lookup_append(
const World& world, PathRequest request,
147 std::vector<Coord3>& out_path)
149 const auto* entry = find(world, request);
150 if (entry ==
nullptr) {
153 const auto cached = path(*entry);
154 const auto stitch = !out_path.empty() && !cached.empty() &&
155 out_path.back() == cached.front();
156 out_path.insert(out_path.end(),
157 cached.begin() + (stitch ? std::ptrdiff_t{1} : 0),
159 return SegmentHit{
true, entry->status, entry->cost};
162 template <
typename World>
163 void store(
const World& world, PathRequest request, PathResult result) {
164 using Shape = World::shape_type;
166 if (budget_ == 0 || result.status != PathStatus::Found) {
169 auto pending_stale_rejections = std::size_t{0};
170 if (find(world, request, &pending_stale_rejections) !=
nullptr) {
171 stale_rejections_ += pending_stale_rejections;
178 auto entry = Entry{};
179 entry.request = request;
180 entry.status = result.status;
181 entry.cost = result.cost;
182 entry.path_size = result.path.size();
183 for (
const auto coord : result.path) {
184 entry.dependencies.add_chunk(world,
185 chunk_key<Shape>(tile_key<Shape>(coord)));
188 if (entries_.size() >= budget_) {
193 [&](
const Entry& current) {
194 return current.dependencies.is_valid(world);
196 1, result.path.size());
198 if (entries_.size() >= budget_) {
199 evict_oldest(entries_.size() - budget_ + 1);
202 reserve_append_capacity(1, result.path.size());
205 entry.path_offset = paths_.size();
206 for (
const auto coord : result.path) {
207 paths_.push_back(coord);
209 entries_.push_back(std::move(entry));
210 stale_rejections_ += pending_stale_rejections;
214 PathRequest request{};
215 PathStatus status = PathStatus::NoPath;
216 std::uint32_t cost = 0;
217 std::size_t path_offset = 0;
218 std::size_t path_size = 0;
219 ChunkVersionDependencies dependencies{};
221 static_assert(std::is_nothrow_move_constructible_v<Entry>);
222 static_assert(std::is_nothrow_move_assignable_v<Entry>);
223 static_assert(std::is_nothrow_copy_constructible_v<Coord3>);
225 template <
typename World>
226 [[nodiscard]]
auto find(
const World& world, PathRequest request,
227 std::size_t* pending_stale_rejections =
228 nullptr) noexcept -> const Entry* {
229 for (
const auto& entry : entries_) {
230 if (entry.request.start != request.start ||
231 entry.request.goal != request.goal) {
234 if (!entry.dependencies.is_valid(world)) {
235 if (pending_stale_rejections !=
nullptr) {
236 ++*pending_stale_rejections;
247 [[nodiscard]]
auto path(
const Entry& entry)
const noexcept
248 -> std::span<const Coord3> {
249 return std::span<const Coord3>{paths_.data() + entry.path_offset,
255 void evict_oldest(std::size_t count)
noexcept {
256 const auto evicted = count < entries_.size() ? count : entries_.size();
260 const auto first_path = evicted == entries_.size()
262 : entries_[evicted].path_offset;
263 std::move(paths_.begin() +
static_cast<std::ptrdiff_t
>(first_path),
264 paths_.end(), paths_.begin());
265 paths_.erase(paths_.end() -
static_cast<std::ptrdiff_t
>(first_path),
267 entries_.erase(entries_.begin(),
268 entries_.begin() +
static_cast<std::ptrdiff_t
>(evicted));
269 for (
auto& entry : entries_) {
270 entry.path_offset -= first_path;
272 evictions_ += evicted;
275 void clear_storage() noexcept {
280 void bind_class(std::uintptr_t identity)
noexcept {
281 if (bound_class_ == identity) {
284 if (bound_class_ != 0) {
288 bound_class_ = identity;
291 void reserve_append_capacity(std::size_t additional_entries,
292 std::size_t additional_path_nodes) {
293 if (additional_entries > entries_.max_size() - entries_.size() ||
294 additional_path_nodes > paths_.max_size() - paths_.size()) {
295 throw std::length_error{
"portal segment cache capacity overflow"};
297 entries_.reserve(entries_.size() + additional_entries);
298 paths_.reserve(paths_.size() + additional_path_nodes);
301 template <
typename Keep>
302 void compact(Keep keep, std::size_t additional_entries = 0,
303 std::size_t additional_path_nodes = 0) {
304 compact_entries_.clear();
305 compact_paths_.clear();
306 compact_indices_.clear();
308 auto kept_path_nodes = std::size_t{0};
309 for (std::size_t index = 0; index < entries_.size(); ++index) {
310 const auto& entry = entries_[index];
314 if (entry.path_size > compact_paths_.max_size() - kept_path_nodes) {
315 throw std::length_error{
"portal segment path count exceeds max_size"};
317 kept_path_nodes += entry.path_size;
318 compact_indices_.push_back(index);
321 if (additional_entries >
322 compact_entries_.max_size() - compact_indices_.size() ||
323 additional_path_nodes > compact_paths_.max_size() - kept_path_nodes) {
324 throw std::length_error{
"portal segment cache capacity overflow"};
326 compact_entries_.reserve(compact_indices_.size() + additional_entries);
327 compact_paths_.reserve(kept_path_nodes + additional_path_nodes);
332 for (
const auto index : compact_indices_) {
333 auto& entry = entries_[index];
334 const auto offset = compact_paths_.size();
335 for (std::size_t path_index = 0; path_index < entry.path_size;
337 compact_paths_.push_back(paths_[entry.path_offset + path_index]);
339 entry.path_offset = offset;
340 compact_entries_.push_back(std::move(entry));
342 entries_.swap(compact_entries_);
343 paths_.swap(compact_paths_);
346 std::vector<Entry> entries_;
347 std::vector<Coord3> paths_;
348 std::vector<Entry> compact_entries_;
349 std::vector<Coord3> compact_paths_;
350 std::vector<std::size_t> compact_indices_;
351 std::size_t budget_ = default_segment_budget;
352 std::size_t sweeps_ = 0;
353 std::size_t evictions_ = 0;
354 std::size_t stale_rejections_ = 0;
355 std::size_t class_rebinds_ = 0;
356 std::uintptr_t bound_class_ = 0;
363 std::span<const Coord3> waypoints,
368 using Shape = World::shape_type;
374 std::is_same_v<typename World::residency_type, AlwaysResident>,
375 "build_weighted_portal_route_product (segment cache) is dense-only; it "
376 "needs sparse topology and route-cache support from a later slice.");
378 std::vector<Coord3> stash;
379 const auto source = product.stash_if_owned(waypoints, stash);
382 product.request_ = request;
383 product.waypoints_.assign(source.begin(), source.end());
386 .template for_class<movement::LegacyWeighted<PassableTag, CostTag>>();
388 auto from = request.start;
389 auto total_cost = std::uint32_t{0};
390 auto total_expanded = std::size_t{0};
391 auto total_reached = std::size_t{0};
392 auto append_path = [&](std::span<const Coord3> path) {
393 for (std::size_t i = product.path_.empty() ? 0u : 1u; i < path.size();
395 product.path_.push_back(path[i]);
398 auto append_segment = [&](
PathRequest segment_request) {
400 class_cache.lookup_append(world, segment_request, product.path_);
402 total_cost = detail::saturating_add(total_cost, hit.cost);
406 const auto result = weighted_astar_path<World, PassableTag, CostTag>(
407 world, segment_request, scratch);
408 class_cache.store(world, segment_request, result);
409 total_expanded += result.expanded_nodes;
410 total_reached += result.reached_nodes;
411 if (result.status != PathStatus::Found) {
412 product.path_.clear();
413 product.status_ = result.status;
414 product.expanded_nodes_ = total_expanded;
415 product.reached_nodes_ = total_reached;
418 detail::capture_failure_dependencies<Shape>(
419 world, segment_request, result.status, product.dependencies_);
422 total_cost = detail::saturating_add(total_cost, result.cost);
423 append_path(result.path.span());
427 for (
const auto waypoint : source) {
428 if (!append_segment(
PathRequest{from, waypoint})) {
429 return PathResult{product.status_, 0, total_expanded, total_reached,
434 if (!append_segment(
PathRequest{from, request.goal})) {
435 return PathResult{product.status_, 0, total_expanded, total_reached,
439 product.status_ = PathStatus::Found;
440 product.cost_ = total_cost;
441 product.expanded_nodes_ = total_expanded;
442 product.reached_nodes_ = total_reached;
443 for (
const auto coord : product.path_) {
444 const auto key = tile_key<Shape>(coord);
445 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
447 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
448 product.reached_nodes_, product.path_};