1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
#pragma once
/**
* @file binary_write_visitor.h
* @brief The one generic writer — §11.2.
*
* Everything about the *container* is the hand-written writer's, unchanged: the
* same header, the same reserved index slots 0 and 1, the same deferred-write
* queue with breadth-first level transfer, the same 16-byte `0xAA` alignment.
* What is generic is only the per-record body, which now comes from `reflect()`
* instead of from a `visit()` overload per struct.
*
* The queue discipline is the subtle part and is reproduced exactly: a nested
* reference appends to `currentLevel_`, and a level is transferred to the *front*
* of `deferred_` in reverse, which is what keeps same-level chunks contiguous.
* Get that wrong and every offset in the file moves while every value stays
* right — precisely the failure the byte-level parity test exists to catch,
* because no structural round trip would notice it.
*/
#include <whiteout/models/wem/document.h>
#include <whiteout/models/wem/reflect.h>
#include "../../common/binary_writer.h"
#include "chunk_tags.h"
#include <deque>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>
namespace whiteout {
namespace models {
namespace wem {
class BinaryWriteVisitor {
public:
explicit BinaryWriteVisitor(common::BinaryWriter& writer) : writer_(writer) {}
static constexpr bool kReading = false;
/// Writes @p root as the file's root chunk, stamping @p formatVersion into
/// the header. The root's own chunk version is its `ChunkTagTraits`.
///
/// @p unknown is re-emitted verbatim (§11.4), each chunk back into the
/// index-table slot it came from.
template <class Root>
void write(const Root& root, u32 formatVersion, std::span<const UnknownChunk> unknown = {});
/// Preserved chunks nothing this build wrote points at. Not an error and not
/// a loss -- they are still in the file -- but a chunk reachable only from
/// another preserved chunk is worth saying out loud, because that is the
/// shape data goes missing in one version later.
const std::vector<u32>& unreferencedUnknownSlots() const {
return unreferenced_;
}
// ---- the visitor surface (§11.2) ----
template <class T>
void field(const char*, const T& value) {
constexpr FieldKind kind = ClassifyField<T>();
if constexpr (kind == FieldKind::Enum) {
writer_.write(static_cast<std::underlying_type_t<T>>(value));
} else if constexpr (kind == FieldKind::Bool) {
writer_.write<u8>(value ? 1 : 0);
} else if constexpr (kind == FieldKind::String) {
writeString(value);
} else if constexpr (kind == FieldKind::Vector) {
writeVector(value);
} else if constexpr (kind == FieldKind::Reflected) {
// `reflect()` is non-const because the reader writes through it. The
// writer only reads, so this is a signature detail, not a mutation.
const_cast<T&>(value).reflect(*this);
} else {
writer_.write(value);
}
}
template <class T>
void optional(const char* name, const std::optional<T>& value) {
writer_.write<u8>(value.has_value() ? 1 : 0);
if (value.has_value()) {
field(name, *value);
}
}
/// One record written as a chunk of its own, referenced inline.
///
/// This is what makes a material kind or a feature payload *skippable*
/// (§11.3): the record carries a fixed 12-byte `Reference` whether or not the
/// reader understands what is on the other end, so a reader that has never
/// heard of `PBRDeferred` still parses the material header and the features
/// around it. Written inline, an unknown body would have no length and the
/// whole record would be unreadable.
template <class T>
void chunk(const char* name, const T& value) {
u32 ignored = 0;
chunkInto(name, value, ignored);
}
/// One alternative of a variant, written as a chunk -- and the one case
/// where the *reader* may decline. If @p value does not hold @p Alt, this
/// build could not read the block on the way in: the chunk itself is
/// preserved at the container level, and what has to be put back here is
/// the reference to it, pointing at the slot it still occupies.
template <class Alt, class Variant>
void chunkAlternative(const char* name, Variant& value, u32& slot) {
if (std::holds_alternative<Alt>(value)) {
chunkInto(name, std::get<Alt>(value), slot);
return;
}
Reference ref{};
ref.entries = 1;
ref.index = slot;
writer_.write(ref);
noteReference(slot);
}
template <class T>
void chunkInto(const char*, const T& value, u32& slot) {
const auto refPos = writer_.getPosition();
writer_.write(Reference{});
currentLevel_.push_back([this, refPos, &value, &slot]() {
const auto entryIndex = static_cast<u32>(index_.size());
const auto offset = writer_.getPosition();
index_.emplace_back(
IndexEntry{ChunkTagTraits<T>::value, offset, 1, ChunkTagTraits<T>::max_version});
slot = entryIndex;
writer_.setPosition(refPos);
Reference ref{};
ref.entries = 1;
ref.index = entryIndex;
writer_.write(ref);
noteReference(entryIndex);
writer_.setPosition(offset);
const_cast<T&>(value).reflect(*this);
transferLevel();
writer_.AlignTo(16, 0xAA);
});
}
/// An inline `u32` count followed by one `Reference` per element — the shape
/// `Mesh::uvSets` has, and the one case a plain `field()` cannot express
/// because a vector of vectors has no chunk tag of its own.
template <class T>
void inlineList(const char* name, const std::vector<T>& value) {
writer_.write<u32>(static_cast<u32>(value.size()));
for (const T& element : value) {
field(name, element);
}
}
/// Emits a container's element count inline, for a run whose elements are
/// then visited in place rather than referenced as a chunk. `std::pair`
/// members are the reason it exists: a pair has no `reflect()` and no chunk
/// tag, so the slot tables spell their two halves out themselves.
template <class C>
void count(const char*, const C& container) {
writer_.write<u32>(static_cast<u32>(container.size()));
}
/// The writer always emits; the chunk version it stamps is what tells a
/// reader whether to expect the field.
BinaryWriteVisitor& since(u32) {
return *this;
}
private:
template <class T>
void writeVector(const std::vector<T>& container);
void writeString(const std::string& text);
void transferLevel();
/// Records every index slot an emitted `Reference` names, so the orphan
/// report above can be computed without a second pass over the file.
void noteReference(u32 slot) {
referenced_.push_back(slot);
}
std::deque<std::function<void()>> currentLevel_;
std::deque<std::function<void()>> deferred_;
std::vector<IndexEntry> index_;
std::vector<u32> referenced_;
std::vector<u32> unreferenced_;
common::BinaryWriter& writer_;
};
template <class Root>
void BinaryWriteVisitor::write(const Root& root, u32 formatVersion,
std::span<const UnknownChunk> unknown) {
index_.clear();
deferred_.clear();
currentLevel_.clear();
referenced_.clear();
unreferenced_.clear();
WEMHeader header{};
header.magic = kWoemMagic;
header.version = formatVersion;
writer_.write(header);
index_.emplace_back(IndexEntry{kWoemMagic, 0, 1, 0});
const u32 rootIndex = static_cast<u32>(index_.size());
index_.emplace_back(IndexEntry{chunkTag<Root>, 0, 1, ChunkTagTraits<Root>::max_version});
header.documentRef.entries = 1;
header.documentRef.index = rootIndex;
noteReference(rootIndex);
// Hold every preserved slot open *before* anything else allocates one. A
// `Reference` inside a preserved chunk names a slot number, and those bytes
// cannot be rewritten -- this build has no idea where the references inside
// them are -- so the numbering has to be the one they were written against.
// Slots in the reserved range that no preserved chunk claims stay as holes;
// an index entry nothing points at costs 16 bytes and confuses nobody.
for (const UnknownChunk& chunk : unknown) {
while (index_.size() <= chunk.index) {
index_.emplace_back(IndexEntry{kHoleTag, 0, 0, 0});
}
}
deferred_.push_back([this, &root, rootIndex]() {
index_[rootIndex].offset = writer_.getPosition();
const_cast<Root&>(root).reflect(*this);
writer_.AlignTo(16, 0xAA);
transferLevel();
});
while (!deferred_.empty()) {
auto emit = std::move(deferred_.front());
deferred_.pop_front();
emit();
}
for (const UnknownChunk& chunk : unknown) {
const u32 offset = writer_.getPosition();
index_[chunk.index] = IndexEntry{chunk.tag, offset, chunk.count, chunk.version};
writer_.writeBytes(reinterpret_cast<const char*>(chunk.data.data()),
static_cast<u32>(chunk.data.size()));
// The captured bytes already carry their own alignment fill, so aligning
// again here would insert a second run of it and move every chunk after.
}
for (const UnknownChunk& chunk : unknown) {
bool seen = false;
for (const u32 slot : referenced_) {
if (slot == chunk.index) {
seen = true;
break;
}
}
if (!seen) {
unreferenced_.push_back(chunk.index);
}
}
const u32 indexOffset = writer_.getPosition();
writer_.write(index_);
header.indexOffset = indexOffset;
header.indexCount = static_cast<u32>(index_.size());
writer_.setPosition(0);
writer_.write(header);
writer_.setPosition(indexOffset + static_cast<u32>(index_.size()) * sizeof(IndexEntry));
}
template <class T>
void BinaryWriteVisitor::writeVector(const std::vector<T>& container) {
Reference const nullRef{};
if (container.empty()) {
writer_.write(nullRef);
return;
}
const auto refPos = writer_.getPosition();
writer_.write(nullRef); // placeholder, backpatched below
currentLevel_.push_back([this, refPos, &container]() {
const auto entryIndex = static_cast<u32>(index_.size());
const auto offset = writer_.getPosition();
index_.emplace_back(IndexEntry{ChunkTagTraits<T>::value, offset,
static_cast<u32>(container.size()),
ChunkTagTraits<T>::max_version});
writer_.setPosition(refPos);
Reference ref{};
ref.entries = static_cast<u32>(container.size());
ref.index = entryIndex;
writer_.write(ref);
noteReference(entryIndex);
writer_.setPosition(offset);
if constexpr (ChunkTagTraits<T>::is_trivial) {
writer_.write(container);
} else {
for (const T& element : container) {
const_cast<T&>(element).reflect(*this);
transferLevel();
}
}
writer_.AlignTo(16, 0xAA);
});
}
} // namespace wem
} // namespace models
} // namespace whiteout