tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
block.h
1#pragma once
2
3#include <tess/core/capacity.h>
4#include <tess/core/config.h>
5#include <tess/core/fail_fast.h>
6#include <tess/core/shape.h>
7#include <tess/storage/world.h>
8
9#include <algorithm>
10#include <cassert>
11#include <cstddef>
12#include <cstdint>
13#include <cstdlib>
14#include <functional>
15#include <limits>
16#include <memory>
17#include <new>
18#include <optional>
19#include <span>
20#include <type_traits>
21#include <utility>
22#include <vector>
23
24namespace tess {
25
27enum class WritePolicy : std::uint8_t {
28 ReadOnly,
29 UniquePerTile,
30 UniquePerChunk,
31 Unsafe,
32};
33static_assert(sizeof(WritePolicy) == sizeof(std::uint8_t));
34
36[[nodiscard]] constexpr bool is_valid_write_policy(
37 WritePolicy policy) noexcept {
38 switch (policy) {
39 case WritePolicy::ReadOnly:
40 case WritePolicy::UniquePerTile:
41 case WritePolicy::UniquePerChunk:
42 case WritePolicy::Unsafe:
43 return true;
44 }
45 return false;
46}
47
49class BlockScratch {
50 public:
51 BlockScratch() = default;
52
53 BlockScratch(BlockScratch&& other) noexcept
54 : storage_(std::move(other.storage_)),
55 capacity_bytes_(std::exchange(other.capacity_bytes_, 0)),
56 used_bytes_(std::exchange(other.used_bytes_, 0)) {}
57
58 auto operator=(BlockScratch&& other) noexcept -> BlockScratch& {
59 storage_ = std::move(other.storage_);
60 capacity_bytes_ = std::exchange(other.capacity_bytes_, 0);
61 used_bytes_ = std::exchange(other.used_bytes_, 0);
62 return *this;
63 }
64
65 BlockScratch(const BlockScratch&) = delete;
66 auto operator=(const BlockScratch&) -> BlockScratch& = delete;
67
68 ~BlockScratch() = default;
69
70 // Growth allocates a fresh buffer: previously returned spans are
71 // invalidated and scratch contents are not preserved. Only the byte
72 // accounting (`used_bytes()`) carries over.
73 [[nodiscard]] auto reserve_bytes_checked(std::size_t bytes) -> ReserveStatus {
74 const auto word_count =
75 bytes / word_size + (bytes % word_size == 0 ? 0 : 1);
76 if (word_count > std::numeric_limits<std::size_t>::max() / word_size) {
77 return ReserveStatus::CapacityExceeded;
78 }
79 const auto byte_capacity = word_count * word_size;
80 if (byte_capacity > capacity_bytes_) {
81 // The std::byte array-new implicitly creates implicit-lifetime
82 // objects in its storage ([intro.object]/13), which makes the
83 // typed spans returned by allocate<T> well-defined.
84 storage_ = std::make_unique_for_overwrite<std::byte[]>(byte_capacity);
85 capacity_bytes_ = byte_capacity;
86 }
87 return ReserveStatus::Reserved;
88 }
89
90 // Growth allocates a fresh buffer: previously returned spans are
91 // invalidated and scratch contents are not preserved. Only the byte
92 // accounting (`used_bytes()`) carries over.
93 void reserve_bytes(std::size_t bytes) {
94 if (reserve_bytes_checked(bytes) == ReserveStatus::Reserved) {
95 return;
96 }
97#if TESS_HAS_EXCEPTIONS
98 throw std::bad_alloc{};
99#else
100 detail::fail_fast("BlockScratch capacity exceeded");
101#endif
102 }
103
104 constexpr void reset() noexcept { used_bytes_ = 0; }
105
106 [[nodiscard]] constexpr auto capacity_bytes() const noexcept -> std::size_t {
107 return capacity_bytes_;
108 }
109
110 [[nodiscard]] constexpr auto used_bytes() const noexcept -> std::size_t {
111 return used_bytes_;
112 }
113
114 [[nodiscard]] constexpr auto remaining_bytes() const noexcept -> std::size_t {
115 return capacity_bytes() - used_bytes_;
116 }
117
118 template <typename T>
119 [[nodiscard]] auto allocate(std::size_t count) noexcept -> std::span<T> {
120 static_assert(!std::is_void_v<T>,
121 "BlockScratch::allocate<T> requires an object type");
122 static_assert(
123 alignof(T) <= alignof(std::max_align_t),
124 "BlockScratch::allocate<T> does not support over-aligned types");
125 static_assert(
126 std::is_trivially_default_constructible_v<T>,
127 "BlockScratch::allocate<T> requires T to be trivially default "
128 "constructible");
129 static_assert(std::is_trivially_destructible_v<T>,
130 "BlockScratch::allocate<T> requires T to be trivially "
131 "destructible");
132
133 if (count == 0) {
134 return {};
135 }
136 if (count > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
137 return {};
138 }
139
140 const auto byte_count = count * sizeof(T);
141 const auto aligned_offset = align_offset(used_bytes_, alignof(T));
142 if (aligned_offset > capacity_bytes() ||
143 byte_count > capacity_bytes() - aligned_offset) {
144 return {};
145 }
146
147 // cppcheck misparses std::byte* as void* here (suppressed in
148 // TessProjectOptions.cmake); std::byte pointer arithmetic is
149 // well-defined.
150 auto* ptr =
151 std::launder(reinterpret_cast<T*>(storage_.get() + aligned_offset));
152 used_bytes_ = aligned_offset + byte_count;
153 return std::span<T>{ptr, count};
154 }
155
156 private:
157 static constexpr auto word_size = sizeof(std::max_align_t);
158
159 // `new std::byte[n]` only guarantees the default new alignment; the class
160 // promises alignof(std::max_align_t) for the buffer base.
161 static_assert(__STDCPP_DEFAULT_NEW_ALIGNMENT__ >= alignof(std::max_align_t));
162
163 [[nodiscard]] static constexpr auto align_offset(
164 std::size_t offset, std::size_t alignment) noexcept -> std::size_t {
165 const auto remainder = offset % alignment;
166 if (remainder == 0) {
167 return offset;
168 }
169 return offset + (alignment - remainder);
170 }
171
172 std::unique_ptr<std::byte[]> storage_;
173 std::size_t capacity_bytes_ = 0;
174 std::size_t used_bytes_ = 0;
175};
176
179 public:
180 constexpr void record_scratch_allocation_failure() noexcept {
181 ++scratch_allocation_failures_;
182 }
183
184 constexpr void reset() noexcept { scratch_allocation_failures_ = 0; }
185
186 [[nodiscard]] constexpr auto scratch_allocation_failures() const noexcept
187 -> std::size_t {
188 return scratch_allocation_failures_;
189 }
190
191 private:
192 std::size_t scratch_allocation_failures_ = 0;
193};
194
196class ChunkDomain {
197 public:
198 constexpr ChunkDomain() noexcept = default;
199
200 constexpr explicit ChunkDomain(std::span<const ChunkKey> keys) noexcept
201 : keys_(keys) {}
202
203 [[nodiscard]] constexpr auto keys() const noexcept
204 -> std::span<const ChunkKey> {
205 return keys_;
206 }
207
208 [[nodiscard]] constexpr auto begin() const noexcept { return keys_.begin(); }
209
210 [[nodiscard]] constexpr auto end() const noexcept { return keys_.end(); }
211
212 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
213 return keys_.size();
214 }
215
216 [[nodiscard]] constexpr bool empty() const noexcept { return keys_.empty(); }
217
218 private:
219 std::span<const ChunkKey> keys_;
220};
221
223class OwnedChunkDomain {
224 public:
225 OwnedChunkDomain() = default;
226
227 explicit OwnedChunkDomain(std::vector<ChunkKey> keys)
228 : keys_(std::move(keys)) {}
229
230 // Every observer below hands out a span or iterator into keys_, so each
231 // is lvalue-only. The factories return by value, which made
232 // `explicit_chunk_domain(keys).view()` compile and yield a span into a
233 // vector destroyed at the end of the full expression. The deleted
234 // `chunk_domain(OwnedChunkDomain&&)` overload made that worse rather
235 // than better: a caller who hit its error would "fix" it by adding
236 // `.view()`, trading a compile error for undefined behaviour. size() and
237 // empty() return values and stay callable on a temporary.
238 [[nodiscard]] constexpr auto view() const& noexcept -> ChunkDomain {
239 return ChunkDomain{keys_};
240 }
241 auto view() const&& -> ChunkDomain = delete;
242
243 [[nodiscard]] constexpr auto keys() const& noexcept
244 -> std::span<const ChunkKey> {
245 return keys_;
246 }
247 auto keys() const&& -> std::span<const ChunkKey> = delete;
248
249 [[nodiscard]] constexpr auto begin() const& noexcept { return keys_.begin(); }
250 auto begin() const&& = delete;
251
252 [[nodiscard]] constexpr auto end() const& noexcept { return keys_.end(); }
253 auto end() const&& = delete;
254
255 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
256 return keys_.size();
257 }
258
259 [[nodiscard]] constexpr bool empty() const noexcept { return keys_.empty(); }
260
261 private:
262 std::vector<ChunkKey> keys_;
263};
264
266[[nodiscard]] constexpr auto chunk_domain(
267 std::span<const ChunkKey> keys) noexcept -> ChunkDomain {
268 return ChunkDomain{keys};
269}
270
272[[nodiscard]] constexpr auto chunk_domain(const OwnedChunkDomain& keys) noexcept
273 -> ChunkDomain {
274 return keys.view();
275}
276
278auto chunk_domain(OwnedChunkDomain&& keys) noexcept -> ChunkDomain = delete;
279
289[[nodiscard]] inline auto explicit_chunk_domain(std::span<const ChunkKey> keys)
291 std::vector<ChunkKey> domain{keys.begin(), keys.end()};
292 std::sort(domain.begin(), domain.end(),
293 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
294 return OwnedChunkDomain{std::move(domain)};
295}
296
297namespace detail {
298
299// A sparse world enumerates its matches in residency order, which is a
300// function of load and eviction history rather than of world content, so
301// the same content can yield different orders across runs. The domain
302// builders promise deterministic iteration and already allocate a vector,
303// so they absorb the sort. The underlying scans stay unordered: only
304// `collect_dirty_chunks`/`collect_active_chunks` can avoid allocating,
305// and only when the caller's output vector already has capacity.
306inline auto sorted_domain(std::vector<ChunkKey> keys) -> OwnedChunkDomain {
307 std::sort(keys.begin(), keys.end(),
308 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
309 return OwnedChunkDomain{std::move(keys)};
310}
311
312} // namespace detail
313
314template <typename World>
316[[nodiscard]] auto dirty_chunk_domain(const World& world, DirtyMask mask)
318 return detail::sorted_domain(world.dirty_chunks(mask));
319}
320
321template <typename World>
323[[nodiscard]] auto active_chunk_domain(const World& world, ActiveMask mask)
325 return detail::sorted_domain(world.active_chunks(mask));
326}
327
328template <typename World>
330class ChunkView {
331 public:
332 using world_type = std::remove_reference_t<World>;
333 using mutable_world_type = std::remove_cv_t<world_type>;
334 using shape_type = mutable_world_type::shape_type;
335 using page_type =
336 std::conditional_t<std::is_const_v<world_type>,
337 const typename mutable_world_type::page_type,
338 typename mutable_world_type::page_type>;
339 using meta_type = std::conditional_t<std::is_const_v<world_type>,
340 const ChunkMeta, ChunkMeta>;
341
342 constexpr ChunkView(world_type& world, ChunkKey key) noexcept
343 : page_(&world.chunk(key)),
344 meta_(&world.meta(key)),
345 key_(key),
346 coord_(chunk_coord<shape_type>(key)),
347 bounds_(chunk_bounds(coord_)) {}
348
349 [[nodiscard]] constexpr auto page() const noexcept -> page_type& {
350 return *page_;
351 }
352
353 [[nodiscard]] constexpr auto meta() const noexcept -> meta_type& {
354 return *meta_;
355 }
356
357 [[nodiscard]] constexpr auto key() const noexcept -> ChunkKey { return key_; }
358
359 [[nodiscard]] constexpr auto coord() const noexcept -> ChunkCoord3 {
360 return coord_;
361 }
362
363 [[nodiscard]] constexpr auto bounds() const noexcept -> Box3 {
364 return bounds_;
365 }
366
367 [[nodiscard]] static constexpr auto local_bounds() noexcept -> Box3 {
368 return Box3{Coord3{0, 0, 0}, ShapeTraits<shape_type>::chunk};
369 }
370
371 [[nodiscard]] static constexpr bool contains_local(Coord3 coord) noexcept {
372 return tess::contains(local_bounds(), coord);
373 }
374
375 [[nodiscard]] static constexpr auto try_local_coord(Coord3 coord) noexcept
376 -> std::optional<LocalCoord3> {
377 if (!contains_local(coord)) {
378 return std::nullopt;
379 }
380
381 return LocalCoord3{
382 static_cast<std::uint64_t>(coord.x),
383 static_cast<std::uint64_t>(coord.y),
384 static_cast<std::uint64_t>(coord.z),
385 };
386 }
387
388 [[nodiscard]] static constexpr auto local_coord(LocalTileId id) noexcept
389 -> LocalCoord3 {
390 const auto chunk = ShapeTraits<shape_type>::chunk;
391 const auto local_xy = chunk.x * chunk.y;
392 const auto local_z = id.value / local_xy;
393 const auto remainder = id.value % local_xy;
394 const auto local_y = remainder / chunk.x;
395 const auto local_x = remainder % chunk.x;
396
397 return LocalCoord3{local_x, local_y, local_z};
398 }
399
400 [[nodiscard]] static constexpr auto local_tile_id(LocalCoord3 coord) noexcept
401 -> LocalTileId {
402 return tess::local_tile_id<shape_type>(coord);
403 }
404
405 // True when the tile lies on a chunk face along an axis whose chunk extent
406 // is greater than 1. Along a 1-tile-wide axis every tile touches both faces;
407 // such an axis is deliberately NOT counted as boundary -- even when neighbor
408 // chunks exist along it -- so a degenerate axis does not classify the whole
409 // chunk as boundary. Callers that need "has a neighbor chunk across this
410 // face" must consult the shape's chunk grid (as topology's boundary-exit
411 // derivation does); is_boundary/is_interior only describe the position
412 // within one chunk.
413 [[nodiscard]] static constexpr bool is_boundary(LocalCoord3 coord) noexcept {
414 const auto chunk = ShapeTraits<shape_type>::chunk;
415 return (chunk.x > 1 && (coord.x == 0 || coord.x + 1 == chunk.x)) ||
416 (chunk.y > 1 && (coord.y == 0 || coord.y + 1 == chunk.y)) ||
417 (chunk.z > 1 && (coord.z == 0 || coord.z + 1 == chunk.z));
418 }
419
420 [[nodiscard]] static constexpr bool is_interior(LocalCoord3 coord) noexcept {
421 return !is_boundary(coord);
422 }
423
424 [[nodiscard]] constexpr auto world_coord(
425 Coord3 local_candidate) const noexcept -> Coord3 {
426 const auto chunk = ShapeTraits<shape_type>::chunk;
427 return Coord3{
428 static_cast<std::int64_t>(coord_.x * chunk.x) + local_candidate.x,
429 static_cast<std::int64_t>(coord_.y * chunk.y) + local_candidate.y,
430 static_cast<std::int64_t>(coord_.z * chunk.z) + local_candidate.z,
431 };
432 }
433
434 [[nodiscard]] constexpr auto world_coord(LocalCoord3 coord) const noexcept
435 -> Coord3 {
436 return world_coord(Coord3{
437 static_cast<std::int64_t>(coord.x),
438 static_cast<std::int64_t>(coord.y),
439 static_cast<std::int64_t>(coord.z),
440 });
441 }
442
443 [[nodiscard]] constexpr auto world_coord(LocalTileId id) const noexcept
444 -> Coord3 {
445 return world_coord(local_coord(id));
446 }
447
448 template <typename Fn>
449 constexpr void for_each_tile(Fn&& fn) const {
450 for (std::uint64_t i = 0; i < ShapeTraits<shape_type>::local_tile_count;
451 ++i) {
452 const auto id = LocalTileId{i};
453 std::invoke(fn, id, local_coord(id));
454 }
455 }
456
457 template <typename Tag>
458 [[nodiscard]] constexpr auto field_span() const noexcept {
459 return page_->template field_span<Tag>();
460 }
461
462 private:
463 [[nodiscard]] static constexpr auto chunk_bounds(ChunkCoord3 coord) noexcept
464 -> Box3 {
465 const auto chunk = ShapeTraits<shape_type>::chunk;
466 return Box3{
467 Coord3{
468 static_cast<std::int64_t>(coord.x * chunk.x),
469 static_cast<std::int64_t>(coord.y * chunk.y),
470 static_cast<std::int64_t>(coord.z * chunk.z),
471 },
472 chunk,
473 };
474 }
475
476 page_type* page_;
477 meta_type* meta_;
478 ChunkKey key_;
479 ChunkCoord3 coord_;
480 Box3 bounds_;
481};
482
483template <typename World, WritePolicy Policy>
485class BlockCtx {
486 public:
487 static_assert(is_valid_write_policy(Policy));
488
489 using world_type = std::remove_reference_t<World>;
490 using view_world_type =
491 std::conditional_t<Policy == WritePolicy::ReadOnly,
492 const std::remove_const_t<world_type>, world_type>;
493
494 constexpr BlockCtx(world_type& world, ChunkDomain domain,
495 BlockScratch* scratch = nullptr,
496 BlockDiagnostics* diagnostics = nullptr) noexcept
497 : world_(&world),
498 domain_(domain),
499 scratch_(scratch),
500 diagnostics_(diagnostics) {}
501
502 [[nodiscard]] constexpr auto world() const noexcept -> view_world_type& {
503 return *world_;
504 }
505
506 [[nodiscard]] constexpr auto domain() const noexcept -> ChunkDomain {
507 return domain_;
508 }
509
510 [[nodiscard]] constexpr auto policy() const noexcept -> WritePolicy {
511 return Policy;
512 }
513
514 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
515 return domain_.size();
516 }
517
518 [[nodiscard]] constexpr bool empty() const noexcept {
519 return domain_.empty();
520 }
521
522 [[nodiscard]] constexpr auto scratch() noexcept -> BlockScratch* {
523 return scratch_;
524 }
525
526 [[nodiscard]] constexpr auto scratch() const noexcept -> const BlockScratch* {
527 return scratch_;
528 }
529
530 constexpr void reset_scratch() const noexcept {
531 if (scratch_ != nullptr) {
532 scratch_->reset();
533 }
534 }
535
536 [[nodiscard]] constexpr auto diagnostics() noexcept -> BlockDiagnostics* {
537 return diagnostics_;
538 }
539
540 [[nodiscard]] constexpr auto diagnostics() const noexcept
541 -> const BlockDiagnostics* {
542 return diagnostics_;
543 }
544
545 constexpr void reset_diagnostics() const noexcept {
546 if (diagnostics_ != nullptr) {
547 diagnostics_->reset();
548 }
549 }
550
551 [[nodiscard]] constexpr auto chunk_view(ChunkKey key) const noexcept
553 return ChunkView<view_world_type>{*world_, key};
554 }
555
556 template <typename Fn>
557 constexpr void for_each_chunk(Fn&& fn) const {
558 for (const auto key : domain_) {
559 std::invoke(fn, chunk_view(key));
560 }
561 }
562
563 private:
564 world_type* world_;
565 ChunkDomain domain_;
566 BlockScratch* scratch_;
567 BlockDiagnostics* diagnostics_;
568};
569
570template <WritePolicy Policy, typename World>
572[[nodiscard]] constexpr auto block_ctx(World& world,
573 ChunkDomain domain) noexcept
575 return BlockCtx<World, Policy>{world, domain};
576}
577
578template <WritePolicy Policy, typename World>
580[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
581 BlockScratch& scratch) noexcept
583 return BlockCtx<World, Policy>{world, domain, &scratch};
584}
585
586template <WritePolicy Policy, typename World>
588[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
589 BlockDiagnostics& diagnostics) noexcept
591 return BlockCtx<World, Policy>{world, domain, nullptr, &diagnostics};
592}
593
594template <WritePolicy Policy, typename World>
596[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
597 BlockScratch& scratch,
598 BlockDiagnostics& diagnostics) noexcept
600 return BlockCtx<World, Policy>{world, domain, &scratch, &diagnostics};
601}
602
603template <WritePolicy Policy, typename World, typename Fn>
605constexpr void for_each_chunk(World& world, ChunkDomain domain, Fn&& fn) {
606 block_ctx<Policy>(world, domain).for_each_chunk(std::forward<Fn>(fn));
607}
608
609namespace detail {
610
611template <WritePolicy Policy, typename World, typename Fn>
612constexpr void for_each_chunk_policy_view(World& world, ChunkDomain domain,
613 Fn&& fn) {
614 using world_type = std::remove_reference_t<World>;
615 using view_world_type =
616 std::conditional_t<Policy == WritePolicy::ReadOnly,
617 const std::remove_const_t<world_type>, world_type>;
618
619 if constexpr (std::is_invocable_v<Fn&, ChunkView<view_world_type>>) {
620 for (const auto key : domain) {
621 std::invoke(fn, ChunkView<view_world_type>{world, key});
622 }
623 } else {
624 // Deliberately a runtime failure and not a static_assert, even though
625 // the condition is fully compile-time here. The runtime-dispatching
626 // for_each_chunk instantiates this template for all four policies
627 // whichever one the caller passes, so a static_assert would reject a
628 // callback that only accepts ReadOnly, even though that callback is valid
629 // whenever the runtime policy selects ReadOnly and this branch is not
630 // reached.
631 //
632 // It was `assert(false)` plus a bare std::abort(). That honoured
633 // NDEBUG but not TESS_ENABLE_ASSERTS, so a consumer following
634 // docs/integration-policy.md got no check at all, and the abort
635 // carried no message. fail_fast always checks and always says why.
636 fail_fast("callback cannot accept the selected block policy view");
637 }
638}
639
640} // namespace detail
641
642template <typename World, typename Fn>
644constexpr void for_each_chunk(World& world, ChunkDomain domain,
645 WritePolicy policy, Fn&& fn) {
646 assert(is_valid_write_policy(policy));
647 switch (policy) {
648 case WritePolicy::ReadOnly:
649 detail::for_each_chunk_policy_view<WritePolicy::ReadOnly>(
650 world, domain, std::forward<Fn>(fn));
651 return;
652 case WritePolicy::UniquePerTile:
653 detail::for_each_chunk_policy_view<WritePolicy::UniquePerTile>(
654 world, domain, std::forward<Fn>(fn));
655 return;
656 case WritePolicy::UniquePerChunk:
657 detail::for_each_chunk_policy_view<WritePolicy::UniquePerChunk>(
658 world, domain, std::forward<Fn>(fn));
659 return;
660 case WritePolicy::Unsafe:
661 detail::for_each_chunk_policy_view<WritePolicy::Unsafe>(
662 world, domain, std::forward<Fn>(fn));
663 return;
664 }
665 std::abort();
666}
667
668} // namespace tess
Definition block.h:485
Definition block.h:178
Definition block.h:49
Definition block.h:196
Definition block.h:330
Definition block.h:223
Definition world.h:22
Definition metadata_types.h:49
Definition shape.h:94
Definition shape.h:58
Definition shape.h:86
Definition chunk_meta.h:27
Definition shape.h:46
Definition metadata_types.h:12
Definition shape.h:68
Definition shape.h:78