tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
node_index_space.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/storage/residency.h>
5#include <tess/storage/world.h>
6
7#include <cstddef>
8#include <cstdint>
9#include <limits>
10
11namespace tess::detail {
12
13// Maps a global tile index (0..tile_count) to a compact node-array offset and
14// reports the node-array capacity a search must allocate. For the dense
15// AlwaysResident world the map is the identity and the capacity is the whole
16// tile count, so A* node storage and indexing are byte-identical to indexing
17// the arrays by the raw tile index. The sparse specialization (added in a
18// later slice) remaps through the resident chunk directory so node arrays are
19// bounded by the residency budget rather than the global tile count, letting
20// A* run over worlds far too large to index densely.
21template <typename World, typename Residency = typename World::residency_type>
22struct NodeIndexSpace {
23 static_assert(sizeof(World) == 0,
24 "NodeIndexSpace has no specialization for this residency "
25 "policy; the SparseResident mapping lands in a later slice.");
26};
27
28template <typename World>
29struct NodeIndexSpace<World, AlwaysResident> {
30 // Every chunk of a dense world is resident, so the map is the identity and
31 // A* can index its node arrays by the raw tile index. is_dense lets the
32 // shared search code drop residency guards entirely under `if constexpr`,
33 // keeping the dense path byte-identical.
34 static constexpr bool is_dense = true;
35
36 explicit NodeIndexSpace(const World& /*world*/) noexcept {}
37
38 [[nodiscard]] constexpr bool is_resident_index(
39 std::uint64_t /*index*/) const noexcept {
40 return true;
41 }
42
43 [[nodiscard]] constexpr std::size_t offset(
44 std::uint64_t index) const noexcept {
45 return static_cast<std::size_t>(index);
46 }
47
48 // Dense identity: never npos (mirrors the sparse combined probe).
49 static constexpr std::size_t npos_offset =
50 std::numeric_limits<std::size_t>::max();
51
52 [[nodiscard]] constexpr std::size_t resident_offset(
53 std::uint64_t index) const noexcept {
54 return static_cast<std::size_t>(index);
55 }
56
57 [[nodiscard]] constexpr std::size_t capacity_hint() const noexcept {
58 static_assert(World::chunk_count <=
59 std::numeric_limits<std::size_t>::max() /
60 World::local_tile_count);
61 return static_cast<std::size_t>(World::chunk_count *
62 World::local_tile_count);
63 }
64};
65
66// Sparse specialization: node-array offsets are bounded by the residency
67// budget rather than the global tile count. A tile's offset is its resident
68// chunk slot times the per-chunk tile count plus the tile's chunk-local id, so
69// the node arrays a search allocates are sized to `capacity * local_tile_count`
70// no matter how enormous the shape is. offset must only be called for a
71// resident tile (guard with is_resident_index first); the slot mapping is
72// stable for the duration of a single search because the world is const.
73template <typename World>
74struct NodeIndexSpace<World, SparseResident> {
75 using shape_type = typename World::shape_type;
76 static constexpr bool is_dense = false;
77 static constexpr std::uint64_t local_tile_count = World::local_tile_count;
78
79 explicit NodeIndexSpace(const World& world) noexcept : world_(&world) {}
80
81 [[nodiscard]] bool is_resident_index(std::uint64_t index) const noexcept {
82 return world_->resident_slot(chunk_key_of(index)) != World::npos_slot;
83 }
84
85 [[nodiscard]] std::size_t offset(std::uint64_t index) const noexcept {
86 const auto slot = world_->resident_slot(chunk_key_of(index));
87 return slot * static_cast<std::size_t>(local_tile_count) + local_of(index);
88 }
89
90 static constexpr std::size_t npos_offset =
91 std::numeric_limits<std::size_t>::max();
92
93 // Residency test and offset in ONE directory probe (npos_offset when the
94 // chunk is not resident) -- the split is_resident_index-then-offset pair
95 // paid two probes per neighbor in the hottest search loops
96 // (audit 2026-07-11, sparse neighbor probing low).
97 [[nodiscard]] std::size_t resident_offset(
98 std::uint64_t index) const noexcept {
99 const auto slot = world_->resident_slot(chunk_key_of(index));
100 if (slot == World::npos_slot) {
101 return npos_offset;
102 }
103 return slot * static_cast<std::size_t>(local_tile_count) + local_of(index);
104 }
105
106 [[nodiscard]] std::size_t capacity_hint() const noexcept {
107 return world_->capacity() * static_cast<std::size_t>(local_tile_count);
108 }
109
110 private:
111 using Storage = typename ShapeTraits<shape_type>::TileKeyStorage;
112
113 [[nodiscard]] static ChunkKey chunk_key_of(std::uint64_t index) noexcept {
114 return chunk_key<shape_type>(
115 TileKey<shape_type>{static_cast<Storage>(index)});
116 }
117
118 [[nodiscard]] static std::size_t local_of(std::uint64_t index) noexcept {
119 return static_cast<std::size_t>(
120 local_tile_id<shape_type>(
121 TileKey<shape_type>{static_cast<Storage>(index)})
122 .value);
123 }
124
125 const World* world_;
126};
127
128} // namespace tess::detail