tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
archive.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/storage/residency.h>
5#include <tess/storage/sparse_world.h>
6#include <tess/storage/world.h>
7#include <tess/version.h>
8
9#include <algorithm>
10#include <array>
11#include <bit>
12#include <cstddef>
13#include <cstdint>
14#include <limits>
15#include <span>
16#include <tuple>
17#include <type_traits>
18#include <vector>
19
20namespace tess {
21
24template <typename Tag, std::uint64_t Id, std::uint32_t Version = 1>
26 using tag_type = Tag;
27 static constexpr std::uint64_t id = Id;
28 static constexpr std::uint32_t version = Version;
29};
30
31namespace detail {
32
33template <typename... Fields>
34consteval bool unique_persisted_field_ids() {
35 constexpr std::array ids{Fields::id...};
36 for (std::size_t i = 0; i < ids.size(); ++i) {
37 for (std::size_t j = i + 1; j < ids.size(); ++j) {
38 if (ids[i] == ids[j]) {
39 return false;
40 }
41 }
42 }
43 return true;
44}
45
46template <typename... Fields>
47consteval bool unique_persisted_field_tags() {
48 return is_valid_field_schema_v<Field<typename Fields::tag_type, Fields>...>;
49}
50
51} // namespace detail
52
54template <std::uint64_t Id, std::uint32_t Version, typename... Fields>
56 static_assert(detail::unique_persisted_field_ids<Fields...>(),
57 "PersistenceSchema field IDs must be unique.");
58 static_assert(detail::unique_persisted_field_tags<Fields...>(),
59 "PersistenceSchema field tags must be unique.");
60
61 static constexpr std::uint64_t id = Id;
62 static constexpr std::uint32_t version = Version;
63 static constexpr std::size_t field_count = sizeof...(Fields);
64 using fields = std::tuple<Fields...>;
65};
66
68enum class WorldArchiveResidency : std::uint8_t {
71};
72
74enum class WorldArchiveStatus : std::uint8_t {
75 Ok,
76 InvalidMagic,
77 UnsupportedFormat,
78 Truncated,
79 Corrupt,
80 ShapeMismatch,
81 LatticeMismatch,
82 KeyLayoutMismatch,
83 ResidencyMismatch,
84 SchemaMismatch,
85 MigrationRequired,
86 FieldMismatch,
87 ResidencyCapacityExceeded,
88 InvalidChunk,
89};
90
93 std::uint32_t format_version = 0;
94 Extent3 size{};
95 Extent3 chunk{};
96 lattice::Identity lattice_identity = lattice::Identity::Orthogonal;
97 std::uint32_t lattice_version = 0;
98 std::uint32_t key_layout_version = 0;
99 std::uint64_t schema_id = 0;
100 std::uint32_t schema_version = 0;
101 std::uint32_t library_major = 0;
102 std::uint32_t library_minor = 0;
103 std::uint32_t library_patch = 0;
104 WorldArchiveResidency residency = WorldArchiveResidency::AlwaysResident;
105 std::uint32_t field_count = 0;
106 std::uint64_t chunk_count = 0;
107};
108
111 WorldArchiveStatus status = WorldArchiveStatus::Ok;
112 WorldArchiveInfo info{};
113 std::size_t bytes_processed = 0;
114};
115
134 WorldArchiveInfo info{};
135 std::size_t bytes_written = 0;
136};
137
138namespace detail {
139
140inline constexpr std::array<std::byte, 8> world_archive_magic{
141 std::byte{'T'}, std::byte{'E'}, std::byte{'S'}, std::byte{'S'},
142 std::byte{'W'}, std::byte{'L'}, std::byte{'D'}, std::byte{0},
143};
144inline constexpr std::uint32_t world_archive_format_version = 2;
145inline constexpr std::uint32_t world_archive_key_layout_version = 1;
146
147inline constexpr std::size_t world_archive_header_size = 121;
148inline constexpr std::size_t world_archive_checksum_offset = 20;
149inline constexpr std::size_t world_archive_checksum_size =
150 sizeof(std::uint32_t);
151inline constexpr std::size_t world_archive_field_desc_size = 17;
152inline constexpr std::size_t world_archive_chunk_prefix_size = 16;
153inline constexpr std::uint32_t world_archive_max_fields = 1024;
154
155// The header stores the lattice version in a fixed 32-bit field, while
156// `LatticeType` only requires convertibility to uint32. Casting on the
157// write alone is not enough: a version above the 32-bit range would still
158// save `Ok` and then fail to load, because the truncated stored value can
159// never equal the full-width trait the load compares it against. A lattice
160// whose version cannot be represented simply cannot be persisted in
161// format v2, so both paths reject it at compile time instead.
162template <typename Shape>
163constexpr void assert_lattice_version_is_representable() noexcept {
164 static_assert(
165 static_cast<std::uint64_t>(ShapeTraits<Shape>::lattice_version) <=
166 std::numeric_limits<std::uint32_t>::max(),
167 "lattice_version exceeds the 32-bit archive header field; a world "
168 "using this lattice cannot be persisted in archive format v2");
169}
170
171enum class ArchiveScalarKind : std::uint8_t {
172 Unsigned = 1,
173 Signed = 2,
174 Floating = 3,
175 Boolean = 4,
176};
177
178template <typename T, bool IsEnum = std::is_enum_v<T>>
179struct ArchiveScalarBase {
180 using type = T;
181};
182
183template <typename T>
184struct ArchiveScalarBase<T, true> {
185 using type = std::underlying_type_t<T>;
186};
187
188template <typename T>
189using ArchiveScalarBaseT = typename ArchiveScalarBase<T>::type;
190
191// C++20 has no standard fixed-underlying-enum trait. Scoped enums are always
192// fixed and, unlike unscoped enums, do not convert implicitly to their
193// underlying type; conservatively use that distinction to reject every
194// unscoped enum before hostile bytes can reach an out-of-range conversion.
195template <typename T, bool IsEnum = std::is_enum_v<T>>
196struct ArchiveEnumSupported : std::true_type {};
197
198template <typename T>
199struct ArchiveEnumSupported<T, true>
200 : std::bool_constant<!std::is_convertible_v<T, std::underlying_type_t<T>>> {
201};
202
203template <typename T>
204inline constexpr bool archive_scalar_supported_v =
205 ArchiveEnumSupported<T>::value &&
206 (std::is_integral_v<ArchiveScalarBaseT<T>> ||
207 std::is_floating_point_v<ArchiveScalarBaseT<T>>) &&
208 (sizeof(ArchiveScalarBaseT<T>) == 1 || sizeof(ArchiveScalarBaseT<T>) == 2 ||
209 sizeof(ArchiveScalarBaseT<T>) == 4 || sizeof(ArchiveScalarBaseT<T>) == 8);
210
211template <typename T>
212consteval auto archive_scalar_kind() -> ArchiveScalarKind {
213 using Base = ArchiveScalarBaseT<T>;
214 static_assert(archive_scalar_supported_v<T>,
215 "Persisted field values must be bool, integral, scoped enum, "
216 "float, or double scalar types of 1, 2, 4, or 8 bytes.");
217 if constexpr (std::is_same_v<Base, bool>) {
218 return ArchiveScalarKind::Boolean;
219 } else if constexpr (std::is_floating_point_v<Base>) {
220 return ArchiveScalarKind::Floating;
221 } else if constexpr (std::is_signed_v<Base>) {
222 return ArchiveScalarKind::Signed;
223 } else {
224 return ArchiveScalarKind::Unsigned;
225 }
226}
227
228template <typename UInt>
229void append_unsigned_le(std::vector<std::byte>& out, UInt value) {
230 static_assert(std::is_unsigned_v<UInt>);
231 for (std::size_t i = 0; i < sizeof(UInt); ++i) {
232 out.push_back(
233 static_cast<std::byte>((value >> (i * 8U)) & static_cast<UInt>(0xff)));
234 }
235}
236
237template <typename T>
238void append_scalar(std::vector<std::byte>& out, T value) {
239 using Base = ArchiveScalarBaseT<T>;
240 static_assert(archive_scalar_supported_v<T>);
241 const auto base = static_cast<Base>(value);
242 if constexpr (std::is_same_v<Base, bool>) {
243 out.push_back(base ? std::byte{1} : std::byte{0});
244 } else if constexpr (std::is_floating_point_v<Base>) {
245 using UInt =
246 std::conditional_t<sizeof(Base) == 4, std::uint32_t, std::uint64_t>;
247 append_unsigned_le(out, std::bit_cast<UInt>(base));
248 } else {
249 using UInt = std::make_unsigned_t<Base>;
250 append_unsigned_le(out, std::bit_cast<UInt>(base));
251 }
252}
253
254class ArchiveCursor {
255 public:
256 explicit ArchiveCursor(std::span<const std::byte> bytes) : bytes_(bytes) {}
257
258 template <typename UInt>
259 bool read_unsigned_le(UInt& value) {
260 static_assert(std::is_unsigned_v<UInt>);
261 if (remaining() < sizeof(UInt)) {
262 return false;
263 }
264 value = 0;
265 for (std::size_t i = 0; i < sizeof(UInt); ++i) {
266 value = static_cast<UInt>(
267 value |
268 (static_cast<UInt>(std::to_integer<unsigned int>(bytes_[at_ + i]))
269 << (i * 8U)));
270 }
271 at_ += sizeof(UInt);
272 return true;
273 }
274
275 bool read_byte(std::uint8_t& value) {
276 if (remaining() == 0) {
277 return false;
278 }
279 value =
280 static_cast<std::uint8_t>(std::to_integer<unsigned int>(bytes_[at_++]));
281 return true;
282 }
283
284 bool skip(std::size_t count) {
285 if (remaining() < count) {
286 return false;
287 }
288 at_ += count;
289 return true;
290 }
291
292 [[nodiscard]] std::size_t position() const noexcept { return at_; }
293 [[nodiscard]] std::size_t remaining() const noexcept {
294 return bytes_.size() - at_;
295 }
296
297 private:
298 std::span<const std::byte> bytes_;
299 std::size_t at_ = 0;
300};
301
302template <typename T>
303bool read_scalar(ArchiveCursor& cursor, T& value) {
304 using Base = ArchiveScalarBaseT<T>;
305 static_assert(archive_scalar_supported_v<T>);
306 Base base{};
307 if constexpr (std::is_same_v<Base, bool>) {
308 std::uint8_t byte = 0;
309 if (!cursor.read_byte(byte) || byte > 1) {
310 return false;
311 }
312 base = byte != 0;
313 } else if constexpr (std::is_floating_point_v<Base>) {
314 using UInt =
315 std::conditional_t<sizeof(Base) == 4, std::uint32_t, std::uint64_t>;
316 UInt bits = 0;
317 if (!cursor.read_unsigned_le(bits)) {
318 return false;
319 }
320 base = std::bit_cast<Base>(bits);
321 } else {
322 using UInt = std::make_unsigned_t<Base>;
323 UInt bits = 0;
324 if (!cursor.read_unsigned_le(bits)) {
325 return false;
326 }
327 base = std::bit_cast<Base>(bits);
328 }
329 value = static_cast<T>(base);
330 return true;
331}
332
333inline void update_crc32(std::uint32_t& crc, const std::byte* bytes,
334 std::size_t size) noexcept {
335 for (std::size_t i = 0; i < size; ++i) {
336 crc ^= std::to_integer<std::uint8_t>(bytes[i]);
337 for (int bit = 0; bit < 8; ++bit) {
338 const auto mask =
339 static_cast<std::uint32_t>(-static_cast<std::int32_t>(crc & 1U));
340 crc = (crc >> 1U) ^ (0xedb88320U & mask);
341 }
342 }
343}
344
345inline auto archive_crc32(std::span<const std::byte> bytes) noexcept
346 -> std::uint32_t {
347 constexpr auto kSuffixOffset =
348 world_archive_checksum_offset + world_archive_checksum_size;
349 if (bytes.size() < kSuffixOffset) {
350 return 0;
351 }
352 auto crc = std::uint32_t{0xffffffffU};
353 // The checksum covers the complete canonical archive except its own slot.
354 // Splitting the input avoids an allocation and avoids defining integrity in
355 // terms of a temporary zero value that is not part of the wire format.
356 update_crc32(crc, bytes.data(), world_archive_checksum_offset);
357 update_crc32(crc, bytes.data() + kSuffixOffset, bytes.size() - kSuffixOffset);
358 return ~crc;
359}
360
361struct ArchiveFieldDesc {
362 std::uint64_t id = 0;
363 std::uint32_t version = 0;
364 ArchiveScalarKind kind = ArchiveScalarKind::Unsigned;
365 std::uint32_t width = 0;
366};
367
368struct ParsedArchive {
369 WorldArchiveResult result{};
370 std::span<const std::byte> body;
371 std::vector<ArchiveFieldDesc> fields;
372 std::size_t chunks_offset = 0;
373 std::size_t chunk_record_size = 0;
374};
375
376inline bool checked_add(std::size_t lhs, std::size_t rhs,
377 std::size_t& result) noexcept {
378 if (lhs > std::numeric_limits<std::size_t>::max() - rhs) {
379 return false;
380 }
381 result = lhs + rhs;
382 return true;
383}
384
385inline bool checked_multiply(std::size_t lhs, std::size_t rhs,
386 std::size_t& result) noexcept {
387 if (lhs != 0 && rhs > std::numeric_limits<std::size_t>::max() / lhs) {
388 return false;
389 }
390 result = lhs * rhs;
391 return true;
392}
393
394inline auto parse_world_archive(std::span<const std::byte> bytes)
395 -> ParsedArchive {
396 ParsedArchive parsed;
397 auto& info = parsed.result.info;
398 auto fail = [&](WorldArchiveStatus status) {
399 parsed.result.status = status;
400 return parsed;
401 };
402 if (bytes.size() < world_archive_magic.size()) {
403 return fail(WorldArchiveStatus::Truncated);
404 }
405 if (!std::equal(world_archive_magic.begin(), world_archive_magic.end(),
406 bytes.begin())) {
407 return fail(WorldArchiveStatus::InvalidMagic);
408 }
409
410 ArchiveCursor cursor(bytes.subspan(world_archive_magic.size()));
411 if (!cursor.read_unsigned_le(info.format_version)) {
412 return fail(WorldArchiveStatus::Truncated);
413 }
414 // The version is the only format-independent value after the magic. A
415 // future format may extend or reinterpret the v2 envelope, so classifying
416 // it must not depend on satisfying v2's length or checksum equations.
417 if (info.format_version != world_archive_format_version) {
418 return fail(WorldArchiveStatus::UnsupportedFormat);
419 }
420 if (bytes.size() < world_archive_header_size) {
421 return fail(WorldArchiveStatus::Truncated);
422 }
423
424 auto body_size = std::uint64_t{};
425 auto checksum = std::uint32_t{};
426 auto lattice_id = std::uint32_t{};
427 auto residency = std::uint8_t{};
428 if (!cursor.read_unsigned_le(body_size) ||
429 !cursor.read_unsigned_le(checksum) ||
430 !cursor.read_unsigned_le(info.size.x) ||
431 !cursor.read_unsigned_le(info.size.y) ||
432 !cursor.read_unsigned_le(info.size.z) ||
433 !cursor.read_unsigned_le(info.chunk.x) ||
434 !cursor.read_unsigned_le(info.chunk.y) ||
435 !cursor.read_unsigned_le(info.chunk.z) ||
436 !cursor.read_unsigned_le(lattice_id) ||
437 !cursor.read_unsigned_le(info.lattice_version) ||
438 !cursor.read_unsigned_le(info.key_layout_version) ||
439 !cursor.read_unsigned_le(info.schema_id) ||
440 !cursor.read_unsigned_le(info.schema_version) ||
441 !cursor.read_unsigned_le(info.library_major) ||
442 !cursor.read_unsigned_le(info.library_minor) ||
443 !cursor.read_unsigned_le(info.library_patch) ||
444 !cursor.read_byte(residency) ||
445 !cursor.read_unsigned_le(info.field_count) ||
446 !cursor.read_unsigned_le(info.chunk_count)) {
447 return fail(WorldArchiveStatus::Truncated);
448 }
449 info.lattice_identity = static_cast<lattice::Identity>(lattice_id);
450 info.residency = static_cast<WorldArchiveResidency>(residency);
451
452 if (body_size > std::numeric_limits<std::size_t>::max()) {
453 return fail(WorldArchiveStatus::Corrupt);
454 }
455 auto expected_size = std::size_t{};
456 if (!checked_add(world_archive_header_size,
457 static_cast<std::size_t>(body_size), expected_size)) {
458 return fail(WorldArchiveStatus::Corrupt);
459 }
460 if (bytes.size() < expected_size) {
461 return fail(WorldArchiveStatus::Truncated);
462 }
463 if (bytes.size() != expected_size) {
464 return fail(WorldArchiveStatus::Corrupt);
465 }
466 parsed.body = bytes.subspan(world_archive_header_size);
467 if (archive_crc32(bytes) != checksum) {
468 return fail(WorldArchiveStatus::Corrupt);
469 }
470 if (info.field_count > world_archive_max_fields || info.size.x == 0 ||
471 info.size.y == 0 || info.size.z == 0 || info.chunk.x == 0 ||
472 info.chunk.y == 0 || info.chunk.z == 0 ||
473 info.size.x % info.chunk.x != 0 || info.size.y % info.chunk.y != 0 ||
474 info.size.z % info.chunk.z != 0 ||
475 (info.residency != WorldArchiveResidency::AlwaysResident &&
476 info.residency != WorldArchiveResidency::SparseResident)) {
477 return fail(WorldArchiveStatus::Corrupt);
478 }
479
480 ArchiveCursor body_cursor(parsed.body);
481 parsed.fields.reserve(info.field_count);
482 auto bytes_per_tile = std::size_t{};
483 for (std::uint32_t i = 0; i < info.field_count; ++i) {
484 ArchiveFieldDesc field;
485 auto kind = std::uint8_t{};
486 if (!body_cursor.read_unsigned_le(field.id) ||
487 !body_cursor.read_unsigned_le(field.version) ||
488 !body_cursor.read_byte(kind) ||
489 !body_cursor.read_unsigned_le(field.width)) {
490 // The outer envelope size and checksum already passed. Exhausting that
491 // complete body inside its declared descriptor table is structural
492 // corruption, not transport truncation.
493 return fail(WorldArchiveStatus::Corrupt);
494 }
495 field.kind = static_cast<ArchiveScalarKind>(kind);
496 const auto valid_width = field.width == 1 || field.width == 2 ||
497 field.width == 4 || field.width == 8;
498 const auto valid_kind = field.kind == ArchiveScalarKind::Unsigned ||
499 field.kind == ArchiveScalarKind::Signed ||
500 field.kind == ArchiveScalarKind::Floating ||
501 field.kind == ArchiveScalarKind::Boolean;
502 if (!valid_width || !valid_kind ||
503 (field.kind == ArchiveScalarKind::Boolean && field.width != 1) ||
504 (field.kind == ArchiveScalarKind::Floating && field.width != 4 &&
505 field.width != 8) ||
506 !checked_add(bytes_per_tile, field.width, bytes_per_tile)) {
507 return fail(WorldArchiveStatus::Corrupt);
508 }
509 // IDs are the persistent join key used by typed loading. Blessing
510 // duplicates during inspection would report Ok for an archive no schema can
511 // load.
512 if (std::any_of(parsed.fields.begin(), parsed.fields.end(),
513 [&](const ArchiveFieldDesc& existing) {
514 return existing.id == field.id;
515 })) {
516 return fail(WorldArchiveStatus::Corrupt);
517 }
518 parsed.fields.push_back(field);
519 }
520 parsed.chunks_offset = body_cursor.position();
521
522 std::size_t local_xy = 0;
523 std::size_t local_tiles = 0;
524 if (info.chunk.x > std::numeric_limits<std::size_t>::max() ||
525 info.chunk.y > std::numeric_limits<std::size_t>::max() ||
526 info.chunk.z > std::numeric_limits<std::size_t>::max() ||
527 !checked_multiply(static_cast<std::size_t>(info.chunk.x),
528 static_cast<std::size_t>(info.chunk.y), local_xy) ||
529 !checked_multiply(local_xy, static_cast<std::size_t>(info.chunk.z),
530 local_tiles)) {
531 return fail(WorldArchiveStatus::Corrupt);
532 }
533 std::size_t field_bytes = 0;
534 if (!checked_multiply(local_tiles, bytes_per_tile, field_bytes) ||
535 !checked_add(world_archive_chunk_prefix_size, field_bytes,
536 parsed.chunk_record_size)) {
537 return fail(WorldArchiveStatus::Corrupt);
538 }
539 std::size_t all_chunk_bytes = 0;
540 std::size_t expected_body_size = 0;
541 if (info.chunk_count > std::numeric_limits<std::size_t>::max() ||
542 !checked_multiply(static_cast<std::size_t>(info.chunk_count),
543 parsed.chunk_record_size, all_chunk_bytes) ||
544 !checked_add(parsed.chunks_offset, all_chunk_bytes, expected_body_size) ||
545 expected_body_size != parsed.body.size()) {
546 return fail(WorldArchiveStatus::Corrupt);
547 }
548
549 const auto chunks_x = info.size.x / info.chunk.x;
550 const auto chunks_y = info.size.y / info.chunk.y;
551 const auto chunks_z = info.size.z / info.chunk.z;
552 if (chunks_x > std::numeric_limits<std::uint64_t>::max() / chunks_y ||
553 chunks_x * chunks_y >
554 std::numeric_limits<std::uint64_t>::max() / chunks_z) {
555 return fail(WorldArchiveStatus::Corrupt);
556 }
557 const auto logical_chunks = chunks_x * chunks_y * chunks_z;
558 // Sparse archives may encode any canonical subset. A dense archive is only
559 // meaningful when it contains every logical chunk; rejecting a shorter but
560 // otherwise self-consistent body here also keeps schema-free inspection
561 // from blessing bytes that every dense typed load must reject.
562 if (info.residency == WorldArchiveResidency::AlwaysResident &&
563 info.chunk_count != logical_chunks) {
564 return fail(WorldArchiveStatus::InvalidChunk);
565 }
566 auto previous_key = std::uint64_t{};
567 for (std::uint64_t i = 0; i < info.chunk_count; ++i) {
568 auto key = std::uint64_t{};
569 auto active = std::uint32_t{};
570 auto entities = std::uint32_t{};
571 if (!body_cursor.read_unsigned_le(key) ||
572 !body_cursor.read_unsigned_le(active) ||
573 !body_cursor.read_unsigned_le(entities) || key >= logical_chunks ||
574 (i != 0 && key <= previous_key) || !body_cursor.skip(field_bytes)) {
575 return fail(WorldArchiveStatus::InvalidChunk);
576 }
577 previous_key = key;
578 }
579 if (body_cursor.remaining() != 0) {
580 return fail(WorldArchiveStatus::Corrupt);
581 }
582 parsed.result.status = WorldArchiveStatus::Ok;
583 parsed.result.bytes_processed = bytes.size();
584 return parsed;
585}
586
587template <typename World>
588consteval auto world_archive_residency() -> WorldArchiveResidency {
589 if constexpr (std::is_same_v<typename World::residency_type,
590 AlwaysResident>) {
591 return WorldArchiveResidency::AlwaysResident;
592 } else {
593 static_assert(
594 std::is_same_v<typename World::residency_type, SparseResident>);
595 return WorldArchiveResidency::SparseResident;
596 }
597}
598
599template <typename Archive, typename World>
600consteval bool archive_fields_supported() {
601 auto supported = true;
602 std::apply(
603 [&]<typename... Fields>(Fields...) {
604 supported = ((World::schema_type::template contains<
605 typename Fields::tag_type> &&
606 archive_scalar_supported_v<
607 typename World::schema_type::template value_type<
608 typename Fields::tag_type>>) &&
609 ...);
610 },
611 typename Archive::fields{});
612 return supported;
613}
614
615template <typename Archive, typename World>
616auto expected_field_descs() {
617 std::array<ArchiveFieldDesc, Archive::field_count> fields;
618 auto index = std::size_t{};
619 std::apply(
620 [&]<typename... Fields>(Fields...) {
621 ((fields[index++] =
622 ArchiveFieldDesc{
623 Fields::id, Fields::version,
624 archive_scalar_kind<
625 typename World::schema_type::template value_type<
626 typename Fields::tag_type>>(),
627 sizeof(typename World::schema_type::template value_type<
628 typename Fields::tag_type>)}),
629 ...);
630 },
631 typename Archive::fields{});
632 return fields;
633}
634
635template <typename Archive, typename World>
636void append_chunk_fields(const World& world, ChunkKey key,
637 std::vector<std::byte>& body) {
638 std::apply(
639 [&]<typename... Fields>(Fields...) {
640 (
641 [&] {
642 const auto values =
643 world.template field_span<typename Fields::tag_type>(key);
644 for (const auto value : values) {
645 append_scalar(body, value);
646 }
647 }(),
648 ...);
649 },
650 typename Archive::fields{});
651}
652
653template <typename Archive, typename World>
654bool read_chunk_fields(World& world, ChunkKey key, ArchiveCursor& cursor) {
655 return std::apply(
656 [&]<typename... Fields>(Fields...) {
657 const auto read_field = [&]<typename Field>(Field) {
658 auto values =
659 world.template field_span<typename Field::tag_type>(key);
660 for (auto& value : values) {
661 if (!read_scalar(cursor, value)) {
662 return false;
663 }
664 }
665 return true;
666 };
667 // The && fold is intentional. Public loads preflight every scalar, but
668 // this decoder remains fail-closed on direct/internal use: after the
669 // first bad scalar it must neither consume later bytes nor partly
670 // overwrite later fields.
671 return (read_field(Fields{}) && ...);
672 },
673 typename Archive::fields{});
674}
675
676template <typename Field, typename WorldType>
677bool validate_chunk_field(ArchiveCursor& cursor) {
678 using Value = typename WorldType::schema_type::template value_type<
679 typename Field::tag_type>;
680 for (std::uint64_t i = 0; i < WorldType::local_tile_count; ++i) {
681 // Preflight needs only the encoding check. Decode an enum through its
682 // underlying scalar so valid scoped enums need not declare a zero
683 // enumerator merely to provide temporary storage.
684 auto scalar = ArchiveScalarBaseT<Value>{};
685 if (!read_scalar(cursor, scalar)) {
686 return false;
687 }
688 }
689 return true;
690}
691
692template <typename Archive, typename WorldType, std::size_t... Indices>
693bool validate_chunk_fields_impl(ArchiveCursor& cursor,
694 std::index_sequence<Indices...>) {
695 return (
696 validate_chunk_field<
697 std::tuple_element_t<Indices, typename Archive::fields>, WorldType>(
698 cursor) &&
699 ...);
700}
701
702template <typename Archive, typename WorldType>
703bool validate_chunk_fields(ArchiveCursor& cursor) {
704 return validate_chunk_fields_impl<Archive, WorldType>(
705 cursor, std::make_index_sequence<Archive::field_count>{});
706}
707
708template <typename Archive, typename World>
709consteval std::size_t archive_field_bytes_per_chunk() {
710 auto bytes = std::size_t{};
711 std::apply(
712 [&]<typename... Fields>(Fields...) {
713 ((bytes += sizeof(typename World::schema_type::template value_type<
714 typename Fields::tag_type>) *
715 static_cast<std::size_t>(World::local_tile_count)),
716 ...);
717 },
718 typename Archive::fields{});
719 return bytes;
720}
721
722template <typename World>
723auto archive_chunk_keys(const World& world) -> std::vector<ChunkKey> {
724 std::vector<ChunkKey> keys;
725 if constexpr (std::is_same_v<typename World::residency_type,
726 AlwaysResident>) {
727 keys.reserve(static_cast<std::size_t>(World::chunk_count));
728 for (std::uint64_t key = 0; key < World::chunk_count; ++key) {
729 keys.push_back(ChunkKey{key});
730 }
731 } else {
732 const auto resident = world.resident_chunk_keys();
733 keys.assign(resident.begin(), resident.end());
734 std::sort(keys.begin(), keys.end(),
735 [](ChunkKey lhs, ChunkKey rhs) { return lhs.value < rhs.value; });
736 }
737 return keys;
738}
739
740template <typename World>
741void prepare_world_for_load(World& world,
742 std::span<const ChunkKey> archive_keys) {
743 if constexpr (std::is_same_v<typename World::residency_type,
744 SparseResident>) {
745 // Re-materialize even keys present in both sets. Besides making the
746 // archive's resident set exact, this advances sparse residency generations
747 // so generation-stamped derived state cannot survive a world replacement.
748 const auto current_span = world.resident_chunk_keys();
749 const std::vector current(current_span.begin(), current_span.end());
750 for (const auto key : current) {
751 static_cast<void>(world.evict(key));
752 }
753 for (const auto key : archive_keys) {
754 static_cast<void>(world.ensure_resident(key));
755 }
756 }
757}
758
759template <typename World>
760void restore_chunk_metadata(World& world, ChunkKey key, ActiveMask active_mask,
761 std::uint32_t entity_count,
762 DirtyMask invalidation_mask) {
763 world.clear_dirty(key, DirtyMask{std::numeric_limits<std::uint32_t>::max()});
764 world.clear_active(key,
765 ActiveMask{std::numeric_limits<std::uint32_t>::max()});
766 auto& meta = world.meta(key);
767 const auto old_content_version = meta.content_version;
768 const auto old_topology_version = meta.topology_version;
769 meta = ChunkMeta{};
770 meta.content_version = old_content_version;
771 meta.topology_version = old_topology_version;
772 meta.entity_count = entity_count;
773 world.mark_active(key, active_mask);
774 const auto bounds =
775 Box3{coord<typename World::shape_type>(
776 chunk_coord<typename World::shape_type>(key), LocalTileId{}),
777 World::shape_type::chunk};
778 if (invalidation_mask) {
779 world.mark_topology_dirty(key, invalidation_mask, bounds);
780 } else {
781 ++meta.content_version;
782 world.mark_topology_rebuilt(key);
783 }
784}
785
786} // namespace detail
787
789[[nodiscard]] inline auto inspect_world_archive(
790 std::span<const std::byte> bytes) -> WorldArchiveResult {
791 return detail::parse_world_archive(bytes).result;
792}
793
800template <typename Archive, typename World>
801[[nodiscard]] auto save_world_archive(const World& world,
802 std::vector<std::byte>& out)
803 -> WorldArchiveSaveResult {
804 static_assert(detail::archive_fields_supported<Archive, World>(),
805 "Archive fields must exist in the world and use supported "
806 "scalar value types.");
807 static_assert(Archive::field_count <= detail::world_archive_max_fields);
808
809 const auto keys = detail::archive_chunk_keys(world);
810 auto body = std::vector<std::byte>{};
811 body.reserve(Archive::field_count * detail::world_archive_field_desc_size +
812 keys.size() *
813 (detail::world_archive_chunk_prefix_size +
814 detail::archive_field_bytes_per_chunk<Archive, World>()));
815 const auto fields = detail::expected_field_descs<Archive, World>();
816 for (const auto field : fields) {
817 detail::append_unsigned_le(body, field.id);
818 detail::append_unsigned_le(body, field.version);
819 body.push_back(static_cast<std::byte>(field.kind));
820 detail::append_unsigned_le(body, field.width);
821 }
822 for (const auto key : keys) {
823 detail::append_unsigned_le(body, key.value);
824 detail::append_unsigned_le(body, world.active_mask(key).value);
825 detail::append_unsigned_le(body, world.meta(key).entity_count);
826 detail::append_chunk_fields<Archive>(world, key, body);
827 }
828
829 out.clear();
830 out.reserve(detail::world_archive_header_size + body.size());
831 out.insert(out.end(), detail::world_archive_magic.begin(),
832 detail::world_archive_magic.end());
833 detail::append_unsigned_le(out, detail::world_archive_format_version);
834 detail::append_unsigned_le(out, static_cast<std::uint64_t>(body.size()));
835 detail::append_unsigned_le(out, std::uint32_t{0});
836 detail::append_unsigned_le(out, World::shape_type::size.x);
837 detail::append_unsigned_le(out, World::shape_type::size.y);
838 detail::append_unsigned_le(out, World::shape_type::size.z);
839 detail::append_unsigned_le(out, World::shape_type::chunk.x);
840 detail::append_unsigned_le(out, World::shape_type::chunk.y);
841 detail::append_unsigned_le(out, World::shape_type::chunk.z);
842 detail::append_unsigned_le(
843 out, static_cast<std::uint32_t>(
844 ShapeTraits<typename World::shape_type>::lattice_identity));
845 // Explicitly widened like the identity above. `append_unsigned_le` emits
846 // sizeof(UInt) bytes and `lattice_version` is `static constexpr auto`,
847 // while `LatticeType` only requires convertibility to uint32. Without the
848 // cast a wide version wrote a 125-byte header against the fixed-width
849 // reader; without the static assert beside it, a version ABOVE the
850 // 32-bit range would still save Ok and never load, since the truncated
851 // stored value can never equal the trait the load compares against.
852 detail::assert_lattice_version_is_representable<typename World::shape_type>();
853 detail::append_unsigned_le(
854 out, static_cast<std::uint32_t>(
855 ShapeTraits<typename World::shape_type>::lattice_version));
856 detail::append_unsigned_le(out, detail::world_archive_key_layout_version);
857 detail::append_unsigned_le(out, Archive::id);
858 detail::append_unsigned_le(out, Archive::version);
859 detail::append_unsigned_le(out,
860 static_cast<std::uint32_t>(library_version.major));
861 detail::append_unsigned_le(out,
862 static_cast<std::uint32_t>(library_version.minor));
863 detail::append_unsigned_le(out,
864 static_cast<std::uint32_t>(library_version.patch));
865 out.push_back(
866 static_cast<std::byte>(detail::world_archive_residency<World>()));
867 detail::append_unsigned_le(out,
868 static_cast<std::uint32_t>(Archive::field_count));
869 detail::append_unsigned_le(out, static_cast<std::uint64_t>(keys.size()));
870 out.insert(out.end(), body.begin(), body.end());
871 const auto checksum = detail::archive_crc32(out);
872 for (std::size_t i = 0; i < detail::world_archive_checksum_size; ++i) {
873 out[detail::world_archive_checksum_offset + i] =
874 static_cast<std::byte>((checksum >> (i * 8U)) & std::uint32_t{0xff});
875 }
876 WorldArchiveSaveResult result;
877 result.info.format_version = detail::world_archive_format_version;
878 result.info.size = World::shape_type::size;
879 result.info.chunk = World::shape_type::chunk;
880 result.info.lattice_identity =
881 ShapeTraits<typename World::shape_type>::lattice_identity;
882 result.info.lattice_version = static_cast<std::uint32_t>(
883 ShapeTraits<typename World::shape_type>::lattice_version);
884 result.info.key_layout_version = detail::world_archive_key_layout_version;
885 result.info.schema_id = Archive::id;
886 result.info.schema_version = Archive::version;
887 result.info.library_major = static_cast<std::uint32_t>(library_version.major);
888 result.info.library_minor = static_cast<std::uint32_t>(library_version.minor);
889 result.info.library_patch = static_cast<std::uint32_t>(library_version.patch);
890 result.info.residency = detail::world_archive_residency<World>();
891 result.info.field_count = static_cast<std::uint32_t>(Archive::field_count);
892 result.info.chunk_count = static_cast<std::uint64_t>(keys.size());
893 result.bytes_written = out.size();
894 return result;
895}
896
906template <typename Archive, typename World>
907[[nodiscard]] auto load_world_archive(
908 World& world, std::span<const std::byte> bytes,
909 DirtyMask invalidation_mask = DirtyMask{
910 std::numeric_limits<std::uint32_t>::max()}) -> WorldArchiveResult {
911 static_assert(detail::archive_fields_supported<Archive, World>(),
912 "Archive fields must exist in the world and use supported "
913 "scalar value types.");
914 static_assert(Archive::field_count <= detail::world_archive_max_fields);
915 auto parsed = detail::parse_world_archive(bytes);
916 if (parsed.result.status != WorldArchiveStatus::Ok) {
917 return parsed.result;
918 }
919 auto fail = [&](WorldArchiveStatus status) {
920 parsed.result.status = status;
921 parsed.result.bytes_processed = 0;
922 return parsed.result;
923 };
924 const auto& info = parsed.result.info;
925 if (info.size != World::shape_type::size ||
926 info.chunk != World::shape_type::chunk) {
927 return fail(WorldArchiveStatus::ShapeMismatch);
928 }
929 if (info.lattice_identity !=
930 ShapeTraits<typename World::shape_type>::lattice_identity ||
931 info.lattice_version !=
932 static_cast<std::uint32_t>(
933 ShapeTraits<typename World::shape_type>::lattice_version)) {
934 return fail(WorldArchiveStatus::LatticeMismatch);
935 }
936 if (info.key_layout_version != detail::world_archive_key_layout_version) {
937 return fail(WorldArchiveStatus::KeyLayoutMismatch);
938 }
939 if (info.residency != detail::world_archive_residency<World>()) {
940 return fail(WorldArchiveStatus::ResidencyMismatch);
941 }
942 if (info.schema_id != Archive::id) {
943 return fail(WorldArchiveStatus::SchemaMismatch);
944 }
945 if (info.schema_version != Archive::version) {
946 return fail(WorldArchiveStatus::MigrationRequired);
947 }
948 const auto expected = detail::expected_field_descs<Archive, World>();
949 if (parsed.fields.size() != expected.size() ||
950 !std::equal(parsed.fields.begin(), parsed.fields.end(), expected.begin(),
951 [](const auto& lhs, const auto& rhs) {
952 return lhs.id == rhs.id && lhs.version == rhs.version &&
953 lhs.kind == rhs.kind && lhs.width == rhs.width;
954 })) {
955 return fail(WorldArchiveStatus::FieldMismatch);
956 }
957 if constexpr (std::is_same_v<typename World::residency_type,
958 SparseResident>) {
959 if (info.chunk_count > world.capacity()) {
960 return fail(WorldArchiveStatus::ResidencyCapacityExceeded);
961 }
962 } else if (info.chunk_count != World::chunk_count) {
963 return fail(WorldArchiveStatus::InvalidChunk);
964 }
965
966 std::vector<ChunkKey> keys;
967 keys.reserve(static_cast<std::size_t>(info.chunk_count));
968 detail::ArchiveCursor key_cursor(parsed.body.subspan(parsed.chunks_offset));
969 for (std::uint64_t i = 0; i < info.chunk_count; ++i) {
970 auto key = std::uint64_t{};
971 static_cast<void>(key_cursor.read_unsigned_le(key));
972 keys.push_back(ChunkKey{key});
973 static_cast<void>(key_cursor.skip(sizeof(std::uint32_t) * 2));
974 // Decode every scalar before preparing sparse residency or writing any
975 // field. This second pass is deliberate: scalar-level damage (including
976 // an invalid bool following a valid enum) must retain the load operation's
977 // strong no-mutation-on-preflight-failure guarantee.
978 if (!detail::validate_chunk_fields<Archive, World>(key_cursor)) {
979 return fail(WorldArchiveStatus::Corrupt);
980 }
981 }
982 detail::prepare_world_for_load(world, keys);
983
984 detail::ArchiveCursor cursor(parsed.body.subspan(parsed.chunks_offset));
985 for (const auto key : keys) {
986 auto encoded_key = std::uint64_t{};
987 auto active_mask = std::uint32_t{};
988 auto entity_count = std::uint32_t{};
989 static_cast<void>(cursor.read_unsigned_le(encoded_key));
990 static_cast<void>(cursor.read_unsigned_le(active_mask));
991 static_cast<void>(cursor.read_unsigned_le(entity_count));
992 if (!detail::read_chunk_fields<Archive>(world, key, cursor)) {
993 return fail(WorldArchiveStatus::Corrupt);
994 }
995 detail::restore_chunk_metadata(world, key, ActiveMask{active_mask},
996 entity_count, invalidation_mask);
997 }
998 parsed.result.bytes_processed = bytes.size();
999 return parsed.result;
1000}
1001
1002} // namespace tess
Definition world.h:22
Definition world.h:18
Definition shape.h:86
Definition shape.h:14
Definition archive.h:25
Defines the persistent application schema carried by one world archive.
Definition archive.h:55
Definition residency.h:18
Parsed compatibility metadata from the fixed world-archive envelope.
Definition archive.h:92
Result shared by archive inspection and loading, both of which can fail.
Definition archive.h:110
Definition archive.h:133