170class FieldProductCache {
172 explicit FieldProductCache(
173 std::size_t byte_budget =
174 std::numeric_limits<std::size_t>::max()) noexcept
175 : byte_budget_(byte_budget) {}
177 void set_byte_budget(std::size_t byte_budget) {
178 byte_budget_ = byte_budget;
182 void reserve_entries(std::size_t count) { entries_.reserve(count); }
184 void clear()
noexcept {
189 void reset_stats()
noexcept {
193 stale_rejections_ = 0;
198 entries_.size(), bytes_, hits_, misses_, evictions_, stale_rejections_,
206 template <
typename World,
typename Tag>
207 [[nodiscard]]
auto lookup(
const World& world,
const GoalSet& goals)
209 for (std::size_t i = 0; i < entries_.size(); ++i) {
210 auto& entry = entries_[i];
211 if (!key_matches<World, Tag>(entry.key, goals.goals())) {
214 if (!entry.product->is_valid(world)) {
220 entry.last_used = ++clock_;
221 return entry.product.get();
232 template <
typename World,
typename Tag>
234 if (product.status() != PathStatus::Found) {
238 auto key = make_key<World, Tag>(product.goals());
239 const auto bytes = entry_byte_size(key, product);
240 if (bytes > byte_budget_) {
245 for (std::size_t i = 0; i < entries_.size(); ++i) {
246 if (keys_equal(entries_[i].key, key)) {
247 bytes_ -= entries_[i].bytes;
248 *entries_[i].product = std::move(product);
249 entries_[i].last_used = ++clock_;
250 entries_[i].bytes = bytes;
257 auto& entry = entries_.emplace_back();
258 entry.key = std::move(key);
259 entry.product = std::make_unique<DistanceFieldProduct>(std::move(product));
260 entry.last_used = ++clock_;
269 std::uintptr_t passability_field = 0;
270 std::size_t tile_count = 0;
271 std::uint64_t chunk_count = 0;
272 std::uint64_t local_tile_count = 0;
275 std::vector<Coord3> goals;
283 std::unique_ptr<DistanceFieldProduct> product;
284 std::uint64_t last_used = 0;
285 std::size_t bytes = 0;
288 template <
typename World,
typename Tag>
289 [[nodiscard]]
static auto make_key(std::span<const Coord3> goals) -> Key {
291 detail::tag_identity<Tag>(),
292 detail::tile_count<World>(),
294 World::local_tile_count,
295 ShapeTraits<typename World::shape_type>::size,
296 ShapeTraits<typename World::shape_type>::chunk,
297 std::vector<Coord3>{goals.begin(), goals.end()},
301 [[nodiscard]]
static auto keys_equal(
const Key& lhs,
const Key& rhs)
noexcept
303 return lhs.passability_field == rhs.passability_field &&
304 lhs.tile_count == rhs.tile_count &&
305 lhs.chunk_count == rhs.chunk_count &&
306 lhs.local_tile_count == rhs.local_tile_count &&
307 lhs.shape_size == rhs.shape_size &&
308 lhs.chunk_extent == rhs.chunk_extent && lhs.goals == rhs.goals;
311 template <
typename World,
typename Tag>
312 [[nodiscard]]
static auto key_matches(
const Key& key,
313 std::span<const Coord3> goals)
noexcept
315 return key.passability_field == detail::tag_identity<Tag>() &&
316 key.tile_count == detail::tile_count<World>() &&
317 key.chunk_count == World::chunk_count &&
318 key.local_tile_count == World::local_tile_count &&
319 key.shape_size == ShapeTraits<typename World::shape_type>::size &&
320 key.chunk_extent == ShapeTraits<typename World::shape_type>::chunk &&
321 key.goals.size() == goals.size() &&
322 std::equal(key.goals.begin(), key.goals.end(), goals.begin());
325 [[nodiscard]]
static auto entry_byte_size(
329 key.goals.size() *
sizeof(
Coord3) + product.byte_size();
332 void erase_entry(std::size_t index)
noexcept {
333 bytes_ -= entries_[index].bytes;
334 entries_.erase(entries_.begin() +
static_cast<std::ptrdiff_t
>(index));
337 void evict_to_budget()
noexcept {
338 while (bytes_ > byte_budget_ && !entries_.empty()) {
339 auto oldest = std::size_t{0};
340 for (std::size_t i = 1; i < entries_.size(); ++i) {
341 if (entries_[i].last_used < entries_[oldest].last_used) {
350 std::vector<Entry> entries_;
351 std::size_t byte_budget_ = 0;
352 std::size_t bytes_ = 0;
353 std::size_t hits_ = 0;
354 std::size_t misses_ = 0;
355 std::size_t evictions_ = 0;
356 std::size_t stale_rejections_ = 0;
357 std::uint64_t clock_ = 0;
369 using Shape =
typename World::shape_type;
377 std::is_same_v<typename World::residency_type, AlwaysResident>,
378 "build_distance_field_product is dense-only; the sparse distance-field "
379 "slice lands later.");
380 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
382 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
383 scratch.clear_build();
385 product.goals_.assign(goals.goals().begin(), goals.goals().end());
386 product.tile_count_ = detail::tile_count<World>();
387 product.chunk_count_ = World::chunk_count;
388 product.local_tile_count_ = World::local_tile_count;
389 product.shape_size_ = ShapeTraits<Shape>::size;
390 product.chunk_extent_ = ShapeTraits<Shape>::chunk;
395 for (
const auto goal : goals.goals()) {
396 if (!contains<Shape>(goal)) {
399 TESS_DIAG_EVENT(path_goal_passability_check);
400 if (!detail::is_passable<World, Tag>(world, goal)) {
405 const auto node_count = detail::tile_count<World>();
406 if (scratch.distance_.size() != node_count) {
407 TESS_DIAG_EVENT(path_initialize);
408 scratch.generation_.assign(node_count, 0);
409 scratch.distance_.assign(node_count, infinite_distance);
412 for (
const auto goal : goals.goals()) {
413 const auto goal_index = detail::tile_index<Shape>(goal);
414 const auto goal_offset =
static_cast<std::size_t
>(goal_index);
415 if (scratch.is_current(goal_offset)) {
418 scratch.distance_[goal_offset] = 0;
419 scratch.touch_node(goal_index);
420 TESS_DIAG_EVENT(path_touch_node);
421 scratch.frontier_.push_back(goal_index);
422 TESS_DIAG_EVENT(path_heap_push);
425 std::size_t expanded_nodes = 0;
426 std::size_t head = 0;
427 while (head < scratch.frontier_.size()) {
428 const auto current = scratch.frontier_[head];
430 TESS_DIAG_EVENT(path_heap_pop);
433 const auto current_offset =
static_cast<std::size_t
>(current);
434 const auto current_distance =
435 scratch.distance_at(current_offset, infinite_distance);
436 const auto current_coord = detail::tile_coord<Shape>(current);
437 detail::for_each_indexed_axis_neighbor<Shape>(
438 current_coord, current, [&](
Coord3, std::uint64_t neighbor_index) {
439 TESS_DIAG_EVENT(path_neighbor_candidate);
440 const auto neighbor_offset =
static_cast<std::size_t
>(neighbor_index);
441 if (scratch.is_current(neighbor_offset)) {
442 TESS_DIAG_EVENT(path_neighbor_closed);
445 TESS_DIAG_EVENT(path_passability_check);
446 if (!detail::is_passable_index<World, Tag>(world, neighbor_index)) {
447 TESS_DIAG_EVENT(path_neighbor_blocked);
451 scratch.distance_[neighbor_offset] = current_distance + 1;
452 scratch.touch_node(neighbor_index);
453 TESS_DIAG_EVENT(path_touch_node);
454 scratch.frontier_.push_back(neighbor_index);
455 TESS_DIAG_EVENT(path_heap_push);
459 product.distance_.assign(node_count, infinite_distance);
460 for (
const auto index : scratch.touched_) {
461 const auto offset =
static_cast<std::size_t
>(index);
462 product.distance_[offset] = scratch.distance_[offset];
463 const auto key = tile_key<Shape>(detail::tile_coord<Shape>(index));
464 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
476 auto& seen = scratch.chunk_seen_;
477 seen.assign(
static_cast<std::size_t
>(World::chunk_count), 0);
478 const auto touched_chunks = product.dependencies_.size();
479 for (std::size_t i = 0; i < touched_chunks; ++i) {
480 seen[
static_cast<std::size_t
>(
481 product.dependencies_.chunks()[i].key.value)] = 1;
483 for (std::size_t i = 0; i < touched_chunks; ++i) {
485 chunk_coord<Shape>(product.dependencies_.chunks()[i].key);
487 const auto key = chunk_key<Shape>(neighbor);
488 auto& mark = seen[
static_cast<std::size_t
>(key.value)];
491 product.dependencies_.add_chunk_unique(world, key);
495 add(
ChunkCoord3{center.x - 1, center.y, center.z});
497 if (center.x + 1 < Traits::chunk_count_x) {
498 add(
ChunkCoord3{center.x + 1, center.y, center.z});
501 add(
ChunkCoord3{center.x, center.y - 1, center.z});
503 if (center.y + 1 < Traits::chunk_count_y) {
504 add(
ChunkCoord3{center.x, center.y + 1, center.z});
507 add(
ChunkCoord3{center.x, center.y, center.z - 1});
509 if (center.z + 1 < Traits::chunk_count_z) {
510 add(
ChunkCoord3{center.x, center.y, center.z + 1});
514 product.status_ = PathStatus::Found;
515 product.expanded_nodes_ = expanded_nodes;
516 product.reached_nodes_ = scratch.touched_.size();
519 product.reached_nodes_};
529 using Shape =
typename World::shape_type;
534 std::is_same_v<typename World::residency_type, AlwaysResident>,
535 "distance_field_product_path is dense-only; the sparse distance-field "
536 "product slice lands later.");
537 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
539 scratch.clear_path();
540 if (!contains<Shape>(start)) {
541 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
543 TESS_DIAG_EVENT(path_start_passability_check);
544 if (!detail::is_passable<World, Tag>(world, start)) {
545 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
547 if (product.status_ != PathStatus::Found || !product.is_valid(world) ||
548 product.tile_count_ != detail::tile_count<World>() ||
549 product.chunk_count_ != World::chunk_count ||
550 product.local_tile_count_ != World::local_tile_count ||
551 product.shape_size_ != ShapeTraits<Shape>::size ||
552 product.chunk_extent_ != ShapeTraits<Shape>::chunk) {
553 return PathResult{PathStatus::NoPath, 0, 0, 0, scratch.path_};
556 const auto start_index = detail::tile_index<Shape>(start);
557 auto current = start_index;
558 auto current_distance = product.distance_[
static_cast<std::size_t
>(current)];
559 if (current_distance == infinite_distance) {
560 return PathResult{PathStatus::NoPath, 0, 0, product.reached_nodes_,
564 scratch.path_.push_back(start);
565 TESS_DIAG_EVENT(path_reconstruct_node);
566 while (current_distance > 0) {
567 const auto current_coord = detail::tile_coord<Shape>(current);
569 auto next_distance = current_distance;
570 detail::for_each_indexed_axis_neighbor<Shape>(
571 current_coord, current, [&](
Coord3, std::uint64_t neighbor_index) {
572 const auto neighbor_distance =
573 product.distance_[
static_cast<std::size_t
>(neighbor_index)];
574 if (neighbor_distance < next_distance) {
575 next = neighbor_index;
576 next_distance = neighbor_distance;
580 if (next == current || next_distance + 1 != current_distance) {
581 scratch.path_.clear();
582 return PathResult{PathStatus::NoPath, 0, 0, product.reached_nodes_,
587 current_distance = product.distance_[
static_cast<std::size_t
>(current)];
588 scratch.path_.push_back(detail::tile_coord<Shape>(current));
589 TESS_DIAG_EVENT(path_reconstruct_node);
593 product.distance_[
static_cast<std::size_t
>(start_index)],
594 scratch.path_.size(), product.reached_nodes_,