tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
block.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/storage/world.h>
5
6#include <algorithm>
7#include <cassert>
8#include <cstddef>
9#include <cstdint>
10#include <cstdlib>
11#include <functional>
12#include <limits>
13#include <memory>
14#include <new>
15#include <optional>
16#include <span>
17#include <type_traits>
18#include <utility>
19#include <vector>
20
21namespace tess {
22
24enum class WritePolicy : std::uint8_t {
25 ReadOnly,
26 UniquePerTile,
27 UniquePerChunk,
28 Unsafe,
29};
30static_assert(sizeof(WritePolicy) == sizeof(std::uint8_t));
31
33[[nodiscard]] constexpr bool is_valid_write_policy(
34 WritePolicy policy) noexcept {
35 switch (policy) {
36 case WritePolicy::ReadOnly:
37 case WritePolicy::UniquePerTile:
38 case WritePolicy::UniquePerChunk:
39 case WritePolicy::Unsafe:
40 return true;
41 }
42 return false;
43}
44
46class BlockScratch {
47 public:
48 BlockScratch() = default;
49
50 BlockScratch(BlockScratch&& other) noexcept
51 : storage_(std::move(other.storage_)),
52 capacity_bytes_(std::exchange(other.capacity_bytes_, 0)),
53 used_bytes_(std::exchange(other.used_bytes_, 0)) {}
54
55 auto operator=(BlockScratch&& other) noexcept -> BlockScratch& {
56 storage_ = std::move(other.storage_);
57 capacity_bytes_ = std::exchange(other.capacity_bytes_, 0);
58 used_bytes_ = std::exchange(other.used_bytes_, 0);
59 return *this;
60 }
61
62 BlockScratch(const BlockScratch&) = delete;
63 auto operator=(const BlockScratch&) -> BlockScratch& = delete;
64
65 ~BlockScratch() = default;
66
67 // Growth allocates a fresh buffer: previously returned spans are
68 // invalidated and scratch contents are not preserved. Only the byte
69 // accounting (`used_bytes()`) carries over.
70 void reserve_bytes(std::size_t bytes) {
71 const auto word_count =
72 bytes / word_size + (bytes % word_size == 0 ? 0 : 1);
73 if (word_count > std::numeric_limits<std::size_t>::max() / word_size) {
74 throw std::bad_alloc{};
75 }
76 const auto byte_capacity = word_count * word_size;
77 if (byte_capacity > capacity_bytes_) {
78 // The std::byte array-new implicitly creates implicit-lifetime
79 // objects in its storage ([intro.object]/13), which makes the
80 // typed spans returned by allocate<T> well-defined.
81 storage_ = std::make_unique_for_overwrite<std::byte[]>(byte_capacity);
82 capacity_bytes_ = byte_capacity;
83 }
84 }
85
86 constexpr void reset() noexcept { used_bytes_ = 0; }
87
88 [[nodiscard]] constexpr auto capacity_bytes() const noexcept -> std::size_t {
89 return capacity_bytes_;
90 }
91
92 [[nodiscard]] constexpr auto used_bytes() const noexcept -> std::size_t {
93 return used_bytes_;
94 }
95
96 [[nodiscard]] constexpr auto remaining_bytes() const noexcept -> std::size_t {
97 return capacity_bytes() - used_bytes_;
98 }
99
100 template <typename T>
101 [[nodiscard]] auto allocate(std::size_t count) noexcept -> std::span<T> {
102 static_assert(!std::is_void_v<T>);
103 static_assert(alignof(T) <= alignof(std::max_align_t));
104 static_assert(std::is_trivially_default_constructible_v<T>);
105 static_assert(std::is_trivially_destructible_v<T>);
106
107 if (count == 0) {
108 return {};
109 }
110 if (count > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
111 return {};
112 }
113
114 const auto byte_count = count * sizeof(T);
115 const auto aligned_offset = align_offset(used_bytes_, alignof(T));
116 if (aligned_offset > capacity_bytes() ||
117 byte_count > capacity_bytes() - aligned_offset) {
118 return {};
119 }
120
121 // cppcheck misparses std::byte* as void* here (suppressed in
122 // TessProjectOptions.cmake); std::byte pointer arithmetic is
123 // well-defined.
124 auto* ptr =
125 std::launder(reinterpret_cast<T*>(storage_.get() + aligned_offset));
126 used_bytes_ = aligned_offset + byte_count;
127 return std::span<T>{ptr, count};
128 }
129
130 private:
131 static constexpr auto word_size = sizeof(std::max_align_t);
132
133 // `new std::byte[n]` only guarantees the default new alignment; the class
134 // promises alignof(std::max_align_t) for the buffer base.
135 static_assert(__STDCPP_DEFAULT_NEW_ALIGNMENT__ >= alignof(std::max_align_t));
136
137 [[nodiscard]] static constexpr auto align_offset(
138 std::size_t offset, std::size_t alignment) noexcept -> std::size_t {
139 const auto remainder = offset % alignment;
140 if (remainder == 0) {
141 return offset;
142 }
143 return offset + (alignment - remainder);
144 }
145
146 std::unique_ptr<std::byte[]> storage_;
147 std::size_t capacity_bytes_ = 0;
148 std::size_t used_bytes_ = 0;
149};
150
153 public:
154 constexpr void record_scratch_allocation_failure() noexcept {
155 ++scratch_allocation_failures_;
156 }
157
158 constexpr void reset() noexcept { scratch_allocation_failures_ = 0; }
159
160 [[nodiscard]] constexpr auto scratch_allocation_failures() const noexcept
161 -> std::size_t {
162 return scratch_allocation_failures_;
163 }
164
165 private:
166 std::size_t scratch_allocation_failures_ = 0;
167};
168
170class ChunkDomain {
171 public:
172 constexpr ChunkDomain() noexcept = default;
173
174 constexpr explicit ChunkDomain(std::span<const ChunkKey> keys) noexcept
175 : keys_(keys) {}
176
177 [[nodiscard]] constexpr auto keys() const noexcept
178 -> std::span<const ChunkKey> {
179 return keys_;
180 }
181
182 [[nodiscard]] constexpr auto begin() const noexcept { return keys_.begin(); }
183
184 [[nodiscard]] constexpr auto end() const noexcept { return keys_.end(); }
185
186 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
187 return keys_.size();
188 }
189
190 [[nodiscard]] constexpr bool empty() const noexcept { return keys_.empty(); }
191
192 private:
193 std::span<const ChunkKey> keys_;
194};
195
197class OwnedChunkDomain {
198 public:
199 OwnedChunkDomain() = default;
200
201 explicit OwnedChunkDomain(std::vector<ChunkKey> keys)
202 : keys_(std::move(keys)) {}
203
204 [[nodiscard]] constexpr auto view() const noexcept -> ChunkDomain {
205 return ChunkDomain{keys_};
206 }
207
208 [[nodiscard]] constexpr auto keys() const noexcept
209 -> std::span<const ChunkKey> {
210 return keys_;
211 }
212
213 [[nodiscard]] constexpr auto begin() const noexcept { return keys_.begin(); }
214
215 [[nodiscard]] constexpr auto end() const noexcept { return keys_.end(); }
216
217 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
218 return keys_.size();
219 }
220
221 [[nodiscard]] constexpr bool empty() const noexcept { return keys_.empty(); }
222
223 private:
224 std::vector<ChunkKey> keys_;
225};
226
228[[nodiscard]] constexpr auto chunk_domain(
229 std::span<const ChunkKey> keys) noexcept -> ChunkDomain {
230 return ChunkDomain{keys};
231}
232
234[[nodiscard]] constexpr auto chunk_domain(const OwnedChunkDomain& keys) noexcept
235 -> ChunkDomain {
236 return keys.view();
237}
238
240auto chunk_domain(OwnedChunkDomain&& keys) noexcept -> ChunkDomain = delete;
241
243[[nodiscard]] inline auto explicit_chunk_domain(std::span<const ChunkKey> keys)
245 std::vector<ChunkKey> domain{keys.begin(), keys.end()};
246 std::sort(domain.begin(), domain.end(),
247 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
248 return OwnedChunkDomain{std::move(domain)};
249}
250
251template <typename World>
253[[nodiscard]] auto dirty_chunk_domain(const World& world, std::uint32_t flags)
255 return OwnedChunkDomain{world.dirty_chunks(flags)};
256}
257
258template <typename World>
260[[nodiscard]] auto active_chunk_domain(const World& world, std::uint32_t flags)
262 return OwnedChunkDomain{world.active_chunks(flags)};
263}
264
265template <typename World>
267class ChunkView {
268 public:
269 using world_type = std::remove_reference_t<World>;
270 using mutable_world_type = std::remove_cv_t<world_type>;
271 using shape_type = mutable_world_type::shape_type;
272 using page_type =
273 std::conditional_t<std::is_const_v<world_type>,
274 const typename mutable_world_type::page_type,
275 typename mutable_world_type::page_type>;
276 using meta_type = std::conditional_t<std::is_const_v<world_type>,
277 const ChunkMeta, ChunkMeta>;
278
279 constexpr ChunkView(world_type& world, ChunkKey key) noexcept
280 : page_(&world.chunk(key)),
281 meta_(&world.meta(key)),
282 key_(key),
283 coord_(chunk_coord<shape_type>(key)),
284 bounds_(chunk_bounds(coord_)) {}
285
286 [[nodiscard]] constexpr auto page() const noexcept -> page_type& {
287 return *page_;
288 }
289
290 [[nodiscard]] constexpr auto meta() const noexcept -> meta_type& {
291 return *meta_;
292 }
293
294 [[nodiscard]] constexpr auto key() const noexcept -> ChunkKey { return key_; }
295
296 [[nodiscard]] constexpr auto coord() const noexcept -> ChunkCoord3 {
297 return coord_;
298 }
299
300 [[nodiscard]] constexpr auto bounds() const noexcept -> Box3 {
301 return bounds_;
302 }
303
304 [[nodiscard]] static constexpr auto local_bounds() noexcept -> Box3 {
305 return Box3{Coord3{0, 0, 0}, ShapeTraits<shape_type>::chunk};
306 }
307
308 [[nodiscard]] static constexpr bool contains_local(Coord3 coord) noexcept {
309 return tess::contains(local_bounds(), coord);
310 }
311
312 [[nodiscard]] static constexpr auto try_local_coord(Coord3 coord) noexcept
313 -> std::optional<LocalCoord3> {
314 if (!contains_local(coord)) {
315 return std::nullopt;
316 }
317
318 return LocalCoord3{
319 static_cast<std::uint64_t>(coord.x),
320 static_cast<std::uint64_t>(coord.y),
321 static_cast<std::uint64_t>(coord.z),
322 };
323 }
324
325 [[nodiscard]] static constexpr auto local_coord(LocalTileId id) noexcept
326 -> LocalCoord3 {
327 const auto chunk = ShapeTraits<shape_type>::chunk;
328 const auto local_xy = chunk.x * chunk.y;
329 const auto local_z = id.value / local_xy;
330 const auto remainder = id.value % local_xy;
331 const auto local_y = remainder / chunk.x;
332 const auto local_x = remainder % chunk.x;
333
334 return LocalCoord3{local_x, local_y, local_z};
335 }
336
337 [[nodiscard]] static constexpr auto local_tile_id(LocalCoord3 coord) noexcept
338 -> LocalTileId {
339 return tess::local_tile_id<shape_type>(coord);
340 }
341
342 // True when the tile lies on a chunk face along an axis whose chunk extent
343 // is greater than 1. Along a 1-tile-wide axis every tile touches both faces;
344 // such an axis is deliberately NOT counted as boundary -- even when neighbor
345 // chunks exist along it -- so a degenerate axis does not classify the whole
346 // chunk as boundary. Callers that need "has a neighbor chunk across this
347 // face" must consult the shape's chunk grid (as topology's boundary-exit
348 // derivation does); is_boundary/is_interior only describe the position
349 // within one chunk.
350 [[nodiscard]] static constexpr bool is_boundary(LocalCoord3 coord) noexcept {
351 const auto chunk = ShapeTraits<shape_type>::chunk;
352 return (chunk.x > 1 && (coord.x == 0 || coord.x + 1 == chunk.x)) ||
353 (chunk.y > 1 && (coord.y == 0 || coord.y + 1 == chunk.y)) ||
354 (chunk.z > 1 && (coord.z == 0 || coord.z + 1 == chunk.z));
355 }
356
357 [[nodiscard]] static constexpr bool is_interior(LocalCoord3 coord) noexcept {
358 return !is_boundary(coord);
359 }
360
361 [[nodiscard]] constexpr auto world_coord(
362 Coord3 local_candidate) const noexcept -> Coord3 {
363 const auto chunk = ShapeTraits<shape_type>::chunk;
364 return Coord3{
365 static_cast<std::int64_t>(coord_.x * chunk.x) + local_candidate.x,
366 static_cast<std::int64_t>(coord_.y * chunk.y) + local_candidate.y,
367 static_cast<std::int64_t>(coord_.z * chunk.z) + local_candidate.z,
368 };
369 }
370
371 [[nodiscard]] constexpr auto world_coord(LocalCoord3 coord) const noexcept
372 -> Coord3 {
373 return world_coord(Coord3{
374 static_cast<std::int64_t>(coord.x),
375 static_cast<std::int64_t>(coord.y),
376 static_cast<std::int64_t>(coord.z),
377 });
378 }
379
380 [[nodiscard]] constexpr auto world_coord(LocalTileId id) const noexcept
381 -> Coord3 {
382 return world_coord(local_coord(id));
383 }
384
385 template <typename Fn>
386 constexpr void for_each_tile(Fn&& fn) const {
387 for (std::uint64_t i = 0; i < ShapeTraits<shape_type>::local_tile_count;
388 ++i) {
389 const auto id = LocalTileId{i};
390 std::invoke(fn, id, local_coord(id));
391 }
392 }
393
394 template <typename Tag>
395 [[nodiscard]] constexpr auto field_span() const noexcept {
396 return page_->template field_span<Tag>();
397 }
398
399 private:
400 [[nodiscard]] static constexpr auto chunk_bounds(ChunkCoord3 coord) noexcept
401 -> Box3 {
402 const auto chunk = ShapeTraits<shape_type>::chunk;
403 return Box3{
404 Coord3{
405 static_cast<std::int64_t>(coord.x * chunk.x),
406 static_cast<std::int64_t>(coord.y * chunk.y),
407 static_cast<std::int64_t>(coord.z * chunk.z),
408 },
409 chunk,
410 };
411 }
412
413 page_type* page_;
414 meta_type* meta_;
415 ChunkKey key_;
416 ChunkCoord3 coord_;
417 Box3 bounds_;
418};
419
420template <typename World, WritePolicy Policy>
422class BlockCtx {
423 public:
424 static_assert(is_valid_write_policy(Policy));
425
426 using world_type = std::remove_reference_t<World>;
427 using view_world_type =
428 std::conditional_t<Policy == WritePolicy::ReadOnly,
429 const std::remove_const_t<world_type>, world_type>;
430
431 constexpr BlockCtx(world_type& world, ChunkDomain domain,
432 BlockScratch* scratch = nullptr,
433 BlockDiagnostics* diagnostics = nullptr) noexcept
434 : world_(&world),
435 domain_(domain),
436 scratch_(scratch),
437 diagnostics_(diagnostics) {}
438
439 [[nodiscard]] constexpr auto world() const noexcept -> view_world_type& {
440 return *world_;
441 }
442
443 [[nodiscard]] constexpr auto domain() const noexcept -> ChunkDomain {
444 return domain_;
445 }
446
447 [[nodiscard]] constexpr auto policy() const noexcept -> WritePolicy {
448 return Policy;
449 }
450
451 [[nodiscard]] constexpr auto size() const noexcept -> std::size_t {
452 return domain_.size();
453 }
454
455 [[nodiscard]] constexpr bool empty() const noexcept {
456 return domain_.empty();
457 }
458
459 [[nodiscard]] constexpr auto scratch() noexcept -> BlockScratch* {
460 return scratch_;
461 }
462
463 [[nodiscard]] constexpr auto scratch() const noexcept -> const BlockScratch* {
464 return scratch_;
465 }
466
467 constexpr void reset_scratch() const noexcept {
468 if (scratch_ != nullptr) {
469 scratch_->reset();
470 }
471 }
472
473 [[nodiscard]] constexpr auto diagnostics() noexcept -> BlockDiagnostics* {
474 return diagnostics_;
475 }
476
477 [[nodiscard]] constexpr auto diagnostics() const noexcept
478 -> const BlockDiagnostics* {
479 return diagnostics_;
480 }
481
482 constexpr void reset_diagnostics() const noexcept {
483 if (diagnostics_ != nullptr) {
484 diagnostics_->reset();
485 }
486 }
487
488 [[nodiscard]] constexpr auto chunk_view(ChunkKey key) const noexcept
490 return ChunkView<view_world_type>{*world_, key};
491 }
492
493 template <typename Fn>
494 constexpr void for_each_chunk(Fn&& fn) const {
495 for (const auto key : domain_) {
496 std::invoke(fn, chunk_view(key));
497 }
498 }
499
500 private:
501 world_type* world_;
502 ChunkDomain domain_;
503 BlockScratch* scratch_;
504 BlockDiagnostics* diagnostics_;
505};
506
507template <WritePolicy Policy, typename World>
509[[nodiscard]] constexpr auto block_ctx(World& world,
510 ChunkDomain domain) noexcept
512 return BlockCtx<World, Policy>{world, domain};
513}
514
515template <WritePolicy Policy, typename World>
517[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
518 BlockScratch& scratch) noexcept
520 return BlockCtx<World, Policy>{world, domain, &scratch};
521}
522
523template <WritePolicy Policy, typename World>
525[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
526 BlockDiagnostics& diagnostics) noexcept
528 return BlockCtx<World, Policy>{world, domain, nullptr, &diagnostics};
529}
530
531template <WritePolicy Policy, typename World>
533[[nodiscard]] constexpr auto block_ctx(World& world, ChunkDomain domain,
534 BlockScratch& scratch,
535 BlockDiagnostics& diagnostics) noexcept
537 return BlockCtx<World, Policy>{world, domain, &scratch, &diagnostics};
538}
539
540template <WritePolicy Policy, typename World, typename Fn>
542constexpr void for_each_chunk(World& world, ChunkDomain domain, Fn&& fn) {
543 block_ctx<Policy>(world, domain).for_each_chunk(std::forward<Fn>(fn));
544}
545
546namespace detail {
547
548template <WritePolicy Policy, typename World, typename Fn>
549constexpr void for_each_chunk_policy_view(World& world, ChunkDomain domain,
550 Fn&& fn) {
551 using world_type = std::remove_reference_t<World>;
552 using view_world_type =
553 std::conditional_t<Policy == WritePolicy::ReadOnly,
554 const std::remove_const_t<world_type>, world_type>;
555
556 if constexpr (std::is_invocable_v<Fn&, ChunkView<view_world_type>>) {
557 for (const auto key : domain) {
558 std::invoke(fn, ChunkView<view_world_type>{world, key});
559 }
560 } else {
561 assert(false && "callback cannot accept the selected block policy view");
562 std::abort();
563 }
564}
565
566} // namespace detail
567
568template <typename World, typename Fn>
570constexpr void for_each_chunk(World& world, ChunkDomain domain,
571 WritePolicy policy, Fn&& fn) {
572 assert(is_valid_write_policy(policy));
573 switch (policy) {
574 case WritePolicy::ReadOnly:
575 detail::for_each_chunk_policy_view<WritePolicy::ReadOnly>(
576 world, domain, std::forward<Fn>(fn));
577 return;
578 case WritePolicy::UniquePerTile:
579 detail::for_each_chunk_policy_view<WritePolicy::UniquePerTile>(
580 world, domain, std::forward<Fn>(fn));
581 return;
582 case WritePolicy::UniquePerChunk:
583 detail::for_each_chunk_policy_view<WritePolicy::UniquePerChunk>(
584 world, domain, std::forward<Fn>(fn));
585 return;
586 case WritePolicy::Unsafe:
587 detail::for_each_chunk_policy_view<WritePolicy::Unsafe>(
588 world, domain, std::forward<Fn>(fn));
589 return;
590 }
591 std::abort();
592}
593
594} // namespace tess
Definition block.h:422
Definition block.h:152
Definition block.h:46
Definition block.h:170
Definition block.h:267
Definition block.h:197
Definition world.h:22
Definition shape.h:75
Definition shape.h:39
Definition shape.h:67
Definition chunk_meta.h:26
Definition shape.h:30
Definition shape.h:49
Definition shape.h:59