tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
shape.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/lattice.h>
5#include <tess/core/uint128.h>
6
7#include <cstdint>
8#include <limits>
9#include <type_traits>
10
11namespace tess {
12
14struct Extent3 {
15 std::uint64_t x = 0;
16 std::uint64_t y = 0;
17 std::uint64_t z = 1;
18
19 friend constexpr bool operator==(Extent3 lhs, Extent3 rhs) noexcept = default;
20};
21
23struct Coord3;
24
26struct Coord2 {
27 std::int64_t x = 0;
28 std::int64_t y = 0;
29
31 [[nodiscard]] constexpr operator Coord3() const noexcept;
32
33 friend constexpr bool operator==(Coord2 lhs, Coord2 rhs) noexcept = default;
34};
35
37struct HexCoord {
38 std::int64_t q = 0;
39 std::int64_t r = 0;
40
41 friend constexpr bool operator==(HexCoord lhs,
42 HexCoord rhs) noexcept = default;
43};
44
46struct Coord3 {
47 std::int64_t x = 0;
48 std::int64_t y = 0;
49 std::int64_t z = 0;
50
51 friend constexpr bool operator==(Coord3 lhs, Coord3 rhs) noexcept = default;
52};
53
55constexpr Coord2::operator Coord3() const noexcept { return Coord3{x, y, 0}; }
56
59 std::uint64_t x = 0;
60 std::uint64_t y = 0;
61 std::uint64_t z = 0;
62
63 friend constexpr bool operator==(ChunkCoord3 lhs,
64 ChunkCoord3 rhs) noexcept = default;
65};
66
69 std::uint64_t x = 0;
70 std::uint64_t y = 0;
71 std::uint64_t z = 0;
72
73 friend constexpr bool operator==(LocalCoord3 lhs,
74 LocalCoord3 rhs) noexcept = default;
75};
76
79 std::uint64_t value = 0;
80
81 friend constexpr bool operator==(LocalTileId lhs,
82 LocalTileId rhs) noexcept = default;
83};
84
86struct ChunkKey {
87 std::uint64_t value = 0;
88
89 friend constexpr bool operator==(ChunkKey lhs,
90 ChunkKey rhs) noexcept = default;
91};
92
94struct Box3 {
95 Coord3 origin{};
96 Extent3 extent{};
97
98 friend constexpr bool operator==(Box3 lhs, Box3 rhs) noexcept = default;
99};
100
101template <typename Shape>
103struct TileKey;
104
105template <typename Shape>
108 ChunkKey chunk_key{};
109 LocalTileId local_tile_id{};
110
111 friend constexpr bool operator==(ResolvedTile lhs,
112 ResolvedTile rhs) noexcept = default;
113};
114
116[[nodiscard]] constexpr Coord3 to_coord3(Coord2 coord) noexcept {
117 return coord;
118}
119
121[[nodiscard]] constexpr Coord3 to_coord3(HexCoord coord) noexcept {
122 return Coord3{coord.q, coord.r, 0};
123}
124
126[[nodiscard]] constexpr HexCoord to_hex_coord(Coord3 coord) noexcept {
127 TESS_ASSERT(coord.z == 0);
128 return HexCoord{coord.x, coord.y};
129}
130
131namespace detail {
132
133[[nodiscard]] constexpr bool is_power_of_two(std::uint64_t value) noexcept {
134 return value != 0 && (value & (value - 1)) == 0;
135}
136
137[[nodiscard]] constexpr bool is_valid_extent(Extent3 extent) noexcept {
138 return extent.x > 0 && extent.y > 0 && extent.z > 0;
139}
140
141[[nodiscard]] constexpr bool is_divisible_by(Extent3 size,
142 Extent3 chunk) noexcept {
143 return size.x % chunk.x == 0 && size.y % chunk.y == 0 &&
144 size.z % chunk.z == 0;
145}
146
147// Wrap-proof "x * y * z fits uint64" for nonzero axes: UInt128 arithmetic
148// wraps mod 2^128, so a triple product of uint64 axes can wrap and satisfy a
149// fits-uint64 comparison it should fail. Division sidesteps that; purely
150// compile-time (used only in ShapeTraits static_asserts).
151[[nodiscard]] constexpr bool product_fits_uint64(std::uint64_t x,
152 std::uint64_t y,
153 std::uint64_t z) noexcept {
154 constexpr auto max = std::numeric_limits<std::uint64_t>::max();
155 if (x > max / y) {
156 return false;
157 }
158 return x * y <= max / z;
159}
160
161[[nodiscard]] constexpr UInt128 product(Extent3 extent) noexcept {
162 return static_cast<UInt128>(extent.x) * static_cast<UInt128>(extent.y) *
163 static_cast<UInt128>(extent.z);
164}
165
166[[nodiscard]] constexpr UInt128 chunk_count(Extent3 size,
167 Extent3 chunk) noexcept {
168 return static_cast<UInt128>(size.x / chunk.x) *
169 static_cast<UInt128>(size.y / chunk.y) *
170 static_cast<UInt128>(size.z / chunk.z);
171}
172
173[[nodiscard]] constexpr std::uint32_t bit_width(UInt128 value) noexcept {
174 std::uint32_t bits = 0;
175 while (value != 0) {
176 ++bits;
177 value >>= 1;
178 }
179 return bits;
180}
181
182[[nodiscard]] constexpr std::uint32_t bits_for_count(UInt128 count) noexcept {
183 return count <= 1 ? 0 : bit_width(count - 1);
184}
185
186template <std::uint32_t Bits>
187using KeyStorage = std::conditional_t<Bits <= 64, std::uint64_t, UInt128>;
188
189[[nodiscard]] constexpr std::uint64_t magnitude(std::int64_t value) noexcept {
190 return static_cast<std::uint64_t>(-(value + 1)) + 1;
191}
192
193[[nodiscard]] constexpr std::uint64_t axis_delta(std::int64_t origin,
194 std::int64_t coord) noexcept {
195 if (origin >= 0) {
196 return static_cast<std::uint64_t>(coord - origin);
197 }
198
199 const auto origin_magnitude = magnitude(origin);
200 if (coord < 0) {
201 return origin_magnitude - magnitude(coord);
202 }
203
204 return origin_magnitude + static_cast<std::uint64_t>(coord);
205}
206
207[[nodiscard]] constexpr bool axis_contains(std::int64_t origin,
208 std::uint64_t extent,
209 std::int64_t coord) noexcept {
210 if (coord < origin) {
211 return false;
212 }
213
214 return axis_delta(origin, coord) < extent;
215}
216
217// Magnitude of the difference between two signed coordinates without the
218// signed-overflow UB of `lhs - rhs` at the int64 extremes.
219[[nodiscard]] constexpr std::uint64_t abs_delta(std::int64_t lhs,
220 std::int64_t rhs) noexcept {
221 return lhs < rhs
222 ? static_cast<std::uint64_t>(rhs) - static_cast<std::uint64_t>(lhs)
223 : static_cast<std::uint64_t>(lhs) -
224 static_cast<std::uint64_t>(rhs);
225}
226
227[[nodiscard]] constexpr std::uint64_t saturating_add(
228 std::uint64_t lhs, std::uint64_t rhs) noexcept {
229 constexpr auto max = std::numeric_limits<std::uint64_t>::max();
230 return lhs > max - rhs ? max : lhs + rhs;
231}
232
233[[nodiscard]] constexpr UInt128 add(UInt128 lhs, UInt128 rhs) noexcept {
234 const auto low = lhs.lo + rhs.lo;
235 const auto carry = low < lhs.lo ? std::uint64_t{1} : std::uint64_t{0};
236 return UInt128::from_parts(lhs.hi + rhs.hi + carry, low);
237}
238
239struct SignedMagnitude {
240 bool negative = false;
241 std::uint64_t magnitude = 0;
242};
243
244[[nodiscard]] constexpr SignedMagnitude signed_delta(
245 std::int64_t lhs, std::int64_t rhs) noexcept {
246 return SignedMagnitude{lhs < rhs, abs_delta(lhs, rhs)};
247}
248
249[[nodiscard]] constexpr UInt128 signed_sum_magnitude(
250 SignedMagnitude lhs, SignedMagnitude rhs) noexcept {
251 if (lhs.negative == rhs.negative) {
252 return add(UInt128{lhs.magnitude}, UInt128{rhs.magnitude});
253 }
254 return lhs.magnitude < rhs.magnitude ? UInt128{rhs.magnitude - lhs.magnitude}
255 : UInt128{lhs.magnitude - rhs.magnitude};
256}
257
258} // namespace detail
259
261[[nodiscard]] constexpr bool contains(Box3 box, Coord3 coord) noexcept {
262 return detail::axis_contains(box.origin.x, box.extent.x, coord.x) &&
263 detail::axis_contains(box.origin.y, box.extent.y, coord.y) &&
264 detail::axis_contains(box.origin.z, box.extent.z, coord.z);
265}
266
270[[nodiscard]] constexpr auto manhattan_distance(Coord3 lhs, Coord3 rhs) noexcept
271 -> std::uint64_t {
272 return detail::saturating_add(
273 detail::abs_delta(lhs.x, rhs.x),
274 detail::saturating_add(detail::abs_delta(lhs.y, rhs.y),
275 detail::abs_delta(lhs.z, rhs.z)));
276}
277
279[[nodiscard]] constexpr auto hex_distance(HexCoord lhs, HexCoord rhs) noexcept
280 -> std::uint64_t {
281 const auto dq = detail::signed_delta(lhs.q, rhs.q);
282 const auto dr = detail::signed_delta(lhs.r, rhs.r);
283 auto twice_distance =
284 detail::add(UInt128{dq.magnitude}, UInt128{dr.magnitude});
285 twice_distance =
286 detail::add(twice_distance, detail::signed_sum_magnitude(dq, dr));
287 const auto distance = twice_distance >> 1U;
288 if (distance.hi != 0) {
289 return std::numeric_limits<std::uint64_t>::max();
290 }
291 return distance.lo;
292}
293
294template <Extent3 Size, Extent3 Chunk, typename Lattice = lattice::Orthogonal>
296struct Shape {
297 static constexpr Extent3 size = Size;
298 static constexpr Extent3 chunk = Chunk;
299 using lattice_type = Lattice;
300
301 static_assert(detail::is_valid_extent(Size),
302 "Shape size dimensions must be greater than zero.");
303 static_assert(detail::is_valid_extent(Chunk),
304 "Shape chunk dimensions must be greater than zero.");
305 static_assert(detail::is_power_of_two(Chunk.x) &&
306 detail::is_power_of_two(Chunk.y) &&
307 detail::is_power_of_two(Chunk.z),
308 "Shape chunk dimensions must be powers of two.");
309 static_assert(detail::is_divisible_by(Size, Chunk),
310 "Shape size dimensions must be multiples of chunk "
311 "dimensions.");
312 static_assert(lattice::LatticeType<Lattice>,
313 "Shape lattice must satisfy lattice::LatticeType.");
314 static_assert(Lattice::template valid_shape<Size, Chunk>,
315 "Shape dimensions are invalid for the selected lattice.");
316};
317
318template <typename Shape>
321 static constexpr Extent3 size = Shape::size;
322 static constexpr Extent3 chunk = Shape::chunk;
323 using lattice_type = typename Shape::lattice_type;
324 static constexpr auto lattice_identity = lattice_type::identity;
325 static constexpr auto lattice_version = lattice_type::version;
326
327 static constexpr std::uint64_t chunk_count_x = size.x / chunk.x;
328 static constexpr std::uint64_t chunk_count_y = size.y / chunk.y;
329 static constexpr std::uint64_t chunk_count_z = size.z / chunk.z;
330
331 static constexpr auto precise_chunk_count = detail::chunk_count(size, chunk);
332 static constexpr auto precise_local_tile_count = detail::product(chunk);
333
334 // The precise_* UInt128 products wrap mod 2^128 for extreme axes, which
335 // would defeat the fits-uint64 asserts below; establish wrap-proof
336 // (division-based) bounds first. Also require each size axis to fit int64
337 // so coord() cannot wrap a Coord3 component. Compile-time only.
338 static_assert(detail::product_fits_uint64(chunk_count_x, chunk_count_y,
339 chunk_count_z),
340 "Shape chunk count must fit std::uint64_t without wrapping.");
341 static_assert(detail::product_fits_uint64(chunk.x, chunk.y, chunk.z),
342 "Shape local tile count must fit std::uint64_t without "
343 "wrapping.");
344 static_assert(
345 size.x <= static_cast<std::uint64_t>(
346 std::numeric_limits<std::int64_t>::max()) &&
347 size.y <= static_cast<std::uint64_t>(
348 std::numeric_limits<std::int64_t>::max()) &&
349 size.z <= static_cast<std::uint64_t>(
350 std::numeric_limits<std::int64_t>::max()),
351 "Shape size axes must fit std::int64_t so coordinates cannot wrap.");
352
353 static_assert(
354 precise_chunk_count <=
355 static_cast<UInt128>(std::numeric_limits<std::uint64_t>::max()),
356 "Shape chunk count must fit std::uint64_t.");
357 static_assert(
358 precise_local_tile_count <=
359 static_cast<UInt128>(std::numeric_limits<std::uint64_t>::max()),
360 "Shape local tile count must fit LocalTileId.");
361
362 static constexpr std::uint64_t chunk_count =
363 static_cast<std::uint64_t>(precise_chunk_count);
364 static constexpr std::uint64_t local_tile_count =
365 static_cast<std::uint64_t>(precise_local_tile_count);
366
367 static constexpr std::uint32_t local_bits =
368 detail::bits_for_count(precise_local_tile_count);
369 static constexpr std::uint32_t chunk_bits =
370 detail::bits_for_count(precise_chunk_count);
371 static constexpr std::uint32_t tile_key_bits = local_bits + chunk_bits;
372
373 static_assert(chunk_bits <= 64, "ChunkKey must fit std::uint64_t.");
374 static_assert(tile_key_bits <= 128, "TileKey must fit u64 or u128.");
375
376 using TileKeyStorage = detail::KeyStorage<tile_key_bits>;
377
378 static constexpr bool single_chunk = chunk_count == 1;
379 static constexpr bool degenerate_x = size.x == 1;
380 static constexpr bool degenerate_y = size.y == 1;
381 static constexpr bool degenerate_z = size.z == 1;
382};
383
384template <typename Shape>
386struct TileKey {
387 ShapeTraits<Shape>::TileKeyStorage value{};
388
389 friend constexpr bool operator==(TileKey lhs, TileKey rhs) noexcept = default;
390};
391
392template <typename Shape>
394[[nodiscard]] constexpr bool contains(Coord3 coord) noexcept {
395 return contains(Box3{Coord3{0, 0, 0}, ShapeTraits<Shape>::size}, coord);
396}
397
398template <typename Shape>
400[[nodiscard]] constexpr ChunkCoord3 chunk_coord(Coord3 coord) noexcept {
401 const auto chunk = ShapeTraits<Shape>::chunk;
402 return ChunkCoord3{
403 static_cast<std::uint64_t>(coord.x) / chunk.x,
404 static_cast<std::uint64_t>(coord.y) / chunk.y,
405 static_cast<std::uint64_t>(coord.z) / chunk.z,
406 };
407}
408
409template <typename Shape>
411[[nodiscard]] constexpr LocalCoord3 local_coord(Coord3 coord) noexcept {
412 const auto chunk = ShapeTraits<Shape>::chunk;
413 return LocalCoord3{
414 static_cast<std::uint64_t>(coord.x) % chunk.x,
415 static_cast<std::uint64_t>(coord.y) % chunk.y,
416 static_cast<std::uint64_t>(coord.z) % chunk.z,
417 };
418}
419
420template <typename Shape>
422[[nodiscard]] constexpr LocalTileId local_tile_id(LocalCoord3 coord) noexcept {
423 const auto chunk = ShapeTraits<Shape>::chunk;
424 return LocalTileId{coord.x + coord.y * chunk.x + coord.z * chunk.x * chunk.y};
425}
426
427template <typename Shape>
429[[nodiscard]] constexpr Coord3 coord(ChunkCoord3 chunk_coord,
430 LocalTileId local_tile_id) noexcept {
431 const auto chunk = ShapeTraits<Shape>::chunk;
432 const auto local_xy = chunk.x * chunk.y;
433 const auto local_z = local_tile_id.value / local_xy;
434 const auto remainder = local_tile_id.value % local_xy;
435 const auto local_y = remainder / chunk.x;
436 const auto local_x = remainder % chunk.x;
437
438 return Coord3{
439 static_cast<std::int64_t>(chunk_coord.x * chunk.x + local_x),
440 static_cast<std::int64_t>(chunk_coord.y * chunk.y + local_y),
441 static_cast<std::int64_t>(chunk_coord.z * chunk.z + local_z),
442 };
443}
444
445template <typename Shape>
447[[nodiscard]] constexpr ChunkKey chunk_key(ChunkCoord3 coord) noexcept {
448 using Traits = ShapeTraits<Shape>;
449 return ChunkKey{coord.x + coord.y * Traits::chunk_count_x +
450 coord.z * Traits::chunk_count_x * Traits::chunk_count_y};
451}
452
453template <typename Shape>
455[[nodiscard]] constexpr ChunkCoord3 chunk_coord(ChunkKey key) noexcept {
456 using Traits = ShapeTraits<Shape>;
457 const auto chunk_xy = Traits::chunk_count_x * Traits::chunk_count_y;
458 const auto z = key.value / chunk_xy;
459 const auto remainder = key.value % chunk_xy;
460 const auto y = remainder / Traits::chunk_count_x;
461 const auto x = remainder % Traits::chunk_count_x;
462
463 return ChunkCoord3{x, y, z};
464}
465
466template <typename Shape>
468[[nodiscard]] constexpr TileKey<Shape> tile_key(Coord3 coord) noexcept {
469 using Traits = ShapeTraits<Shape>;
470 TESS_ASSERT(contains<Shape>(coord));
471 const auto local = local_tile_id<Shape>(local_coord<Shape>(coord));
472 using Storage = Traits::TileKeyStorage;
473
474 if constexpr (Traits::chunk_bits == 0) {
475 // Single-chunk shapes: the key is the local id. Shifting the chunk key
476 // into place would shift by local_bits, which may equal the storage
477 // width and would be undefined behavior.
478 return TileKey<Shape>{static_cast<Storage>(local.value)};
479 } else {
480 const auto chunk = chunk_key<Shape>(chunk_coord<Shape>(coord));
481 return TileKey<Shape>{
482 (static_cast<Storage>(chunk.value) << Traits::local_bits) |
483 static_cast<Storage>(local.value),
484 };
485 }
486}
487
488template <typename Shape>
490[[nodiscard]] constexpr ChunkKey chunk_key(TileKey<Shape> key) noexcept {
491 using Traits = ShapeTraits<Shape>;
492 if constexpr (Traits::chunk_bits == 0) {
493 // Single-chunk shapes store only the local id; shifting right by
494 // local_bits may equal the storage width and would be undefined
495 // behavior.
496 static_cast<void>(key);
497 return ChunkKey{0};
498 } else {
499 return ChunkKey{
500 static_cast<std::uint64_t>(key.value >> Traits::local_bits),
501 };
502 }
503}
504
505template <typename Shape>
507[[nodiscard]] constexpr LocalTileId local_tile_id(TileKey<Shape> key) noexcept {
508 using Traits = ShapeTraits<Shape>;
509 using Storage = Traits::TileKeyStorage;
510 Storage mask = 0;
511 if constexpr (Traits::local_bits == 64) {
512 mask = static_cast<Storage>(std::numeric_limits<std::uint64_t>::max());
513 } else {
514 mask = (static_cast<Storage>(1) << Traits::local_bits) - 1;
515 }
516
517 return LocalTileId{static_cast<std::uint64_t>(key.value & mask)};
518}
519
520template <typename Shape>
522[[nodiscard]] constexpr Coord3 coord(TileKey<Shape> key) noexcept {
523 return coord<Shape>(chunk_coord<Shape>(chunk_key<Shape>(key)),
524 local_tile_id<Shape>(key));
525}
526
527} // namespace tess
Definition lattice.h:34
Definition shape.h:94
Definition shape.h:58
Definition shape.h:86
Definition shape.h:26
Definition shape.h:46
Definition shape.h:14
Definition shape.h:37
Definition shape.h:68
Definition shape.h:78
Definition shape.h:107
Definition shape.h:320
Definition shape.h:296
Definition shape.h:386
Packed 128-bit key storage with a deliberately partial operator set.
Definition uint128.h:34