载入中...
搜索中...
未找到
PixelWorldReplay.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <charconv>
5#include <cstddef>
6#include <limits>
7#include <set>
8#include <type_traits>
9
11namespace {
12
13constexpr std::string_view kReplayType = "pixelworld.replay-log";
14constexpr std::uint64_t kReplaySchemaVersion = 2;
15constexpr std::size_t kMaximumReplayEntries = 1'000'000;
16constexpr std::size_t kMaximumReplayCheckpoints = 100'000;
17constexpr std::size_t kMaximumReplayChunkDigests = 1'000'000;
18
19eve::LogicalId replaySchema() {
20 const auto parsed = eve::LogicalId::parse("pixelworld:replay-log");
21 if (!parsed) std::terminate();
22 return *parsed;
23}
24
26 std::initializer_list<std::string_view> fields) {
27 const auto* result = value.getIf<eve::Value::Object>();
28 if (!result || result->size() != fields.size())
30 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "replay object has unknown or missing fields",
31 path, {}, "pixelworld.replay"));
32 for (const auto field : fields)
33 if (!result->contains(std::string(field)))
35 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "replay object has unknown or missing fields",
36 path, {}, "pixelworld.replay"));
38}
39
40eve::Result<std::uint64_t> decimal(const eve::Value& value, std::string path) {
41 const auto* text = value.getIf<std::string>();
42 if (!text)
44 eve::DiagnosticCode::ParseError, "expected decimal string", path, {}, "pixelworld.replay"));
45 std::uint64_t result = 0;
46 const auto [end, error] = std::from_chars(text->data(), text->data() + text->size(), result);
47 if (error != std::errc{} || end != text->data() + text->size())
49 eve::DiagnosticCode::ParseError, "invalid decimal string", path, {}, "pixelworld.replay"));
51}
52
53eve::Result<int> integer(const eve::Value& value, std::string path) {
54 const auto* number = value.getIf<std::int64_t>();
55 if (!number || *number < std::numeric_limits<int>::min() || *number > std::numeric_limits<int>::max())
57 eve::DiagnosticCode::ParseError, "expected in-range integer", path, {}, "pixelworld.replay"));
58 return eve::Result<int>::success(static_cast<int>(*number));
59}
60
61eve::Value commandValue(const eve::pixelworld::PixelEditCommand& command) {
63 {"centerX", eve::Value(command.centerX)},
64 {"centerY", eve::Value(command.centerY)},
65 {"kind", eve::Value(static_cast<int>(command.kind))},
66 {"material", eve::Value(static_cast<int>(command.material))},
67 {"radius", eve::Value(command.radius)},
68 {"sequence", eve::Value(std::to_string(command.sequence))},
69 {"strength", eve::Value(command.strength)},
70 {"temperatureDelta", eve::Value(int(command.temperatureDelta))},
71 });
72}
73
74eve::Result<eve::pixelworld::PixelEditCommand> parseCommand(const eve::Value& value, const std::string& path) {
75 auto record = object(value, path, {"centerX", "centerY", "kind", "material", "radius", "sequence",
76 "strength", "temperatureDelta"});
77 if (!record) return eve::Result<eve::pixelworld::PixelEditCommand>::failure(record.status());
79 auto centerX = integer(record.value()->at("centerX"), path + ".centerX");
80 auto centerY = integer(record.value()->at("centerY"), path + ".centerY");
81 auto kind = integer(record.value()->at("kind"), path + ".kind");
82 auto material = integer(record.value()->at("material"), path + ".material");
83 auto radius = integer(record.value()->at("radius"), path + ".radius");
84 auto sequence = decimal(record.value()->at("sequence"), path + ".sequence");
85 auto strength = integer(record.value()->at("strength"), path + ".strength");
86 auto temperature = integer(record.value()->at("temperatureDelta"), path + ".temperatureDelta");
87 if (!centerX || !centerY || !kind || !material || !radius || !sequence || !strength || !temperature)
89 eve::DiagnosticCode::ParseError, "invalid replay command", path, {}, "pixelworld.replay"));
90 if (kind.value() < 0 || kind.value() > static_cast<int>(eve::pixelworld::PixelEditKind::Explosion) ||
91 material.value() < 0 || material.value() > std::numeric_limits<std::uint16_t>::max() ||
92 temperature.value() < std::numeric_limits<std::int16_t>::min() ||
93 temperature.value() > std::numeric_limits<std::int16_t>::max())
95 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "replay command enum or scalar is out of range",
96 path, {}, "pixelworld.replay"));
97 command.sequence = sequence.value();
98 command.kind = static_cast<eve::pixelworld::PixelEditKind>(kind.value());
99 command.centerX = centerX.value();
100 command.centerY = centerY.value();
101 command.radius = radius.value();
102 command.material = static_cast<eve::pixelworld::MaterialId>(material.value());
103 command.strength = strength.value();
104 command.temperatureDelta = static_cast<std::int16_t>(temperature.value());
106}
107
108std::uint64_t hashBytes(std::span<const std::byte> bytes) noexcept {
109 std::uint64_t hash = 1469598103934665603ULL;
110 for (const std::byte value : bytes) {
111 hash ^= std::uint64_t(std::to_integer<std::uint8_t>(value));
112 hash *= 1099511628211ULL;
113 }
114 return hash;
115}
116
117template <class T>
118void hashValue(std::uint64_t& hash, T value) noexcept {
119 static_assert(std::is_integral_v<T>);
120 using Unsigned = std::make_unsigned_t<T>;
121 static_assert(sizeof(Unsigned) <= sizeof(std::uint64_t));
122 std::uint64_t encoded = static_cast<std::uint64_t>(static_cast<Unsigned>(value));
123 for (std::size_t index = 0; index < sizeof(T); ++index) {
124 hash ^= encoded & 0xffULL;
125 hash *= 1099511628211ULL;
126 encoded >>= 8;
127 }
128}
129
130std::uint64_t hashChunk(const eve::pixelworld::PixelChunkSnapshot& chunk) noexcept {
131 std::uint64_t hash = 1469598103934665603ULL;
132 hashValue(hash, std::int32_t(chunk.x));
133 hashValue(hash, std::int32_t(chunk.y));
134 for (const auto cell : chunk.cells) {
135 hashValue(hash, std::uint16_t(cell.material));
136 hashValue(hash, cell.temperature);
137 hashValue(hash, cell.lifetime);
138 hashValue(hash, cell.thermalRemainder);
139 }
140 return hash;
141}
142
144 auto snapshot = world.saveSnapshot();
145 if (!snapshot.ok()) return eve::Result<PixelReplayCheckpoint>::failure(snapshot.status());
146 PixelReplayCheckpoint checkpoint;
147 checkpoint.tick = eve::SimulationTick(world.tickValue());
148 checkpoint.revision = world.revision();
149 checkpoint.worldDigest = hashBytes(snapshot.value());
150 for (const auto& chunk : world.snapshotChangedChunks(0))
151 if (!chunk.removed)
152 checkpoint.chunks.push_back({chunk.x, chunk.y, hashChunk(chunk)});
153 return eve::Result<PixelReplayCheckpoint>::success(std::move(checkpoint));
154}
155
156eve::Value replayPayload(const std::vector<PixelReplayEntry>& entries,
157 const std::vector<PixelReplayCheckpoint>& checkpoints) {
158 eve::Value::Array encodedEntries;
159 encodedEntries.reserve(entries.size());
160 for (const auto& entry : entries)
161 encodedEntries.emplace_back(eve::Value::Object{
162 {"command", commandValue(entry.command)},
163 {"tick", eve::Value(std::to_string(entry.tick.value()))},
164 });
165
166 eve::Value::Array encodedCheckpoints;
167 encodedCheckpoints.reserve(checkpoints.size());
168 for (const auto& checkpoint : checkpoints) {
169 eve::Value::Array chunks;
170 chunks.reserve(checkpoint.chunks.size());
171 for (const auto& chunk : checkpoint.chunks)
172 chunks.emplace_back(eve::Value::Object{
173 {"digest", eve::Value(std::to_string(chunk.digest))},
174 {"x", eve::Value(chunk.x)},
175 {"y", eve::Value(chunk.y)},
176 });
177 encodedCheckpoints.emplace_back(eve::Value::Object{
178 {"chunks", eve::Value(std::move(chunks))},
179 {"revision", eve::Value(std::to_string(checkpoint.revision))},
180 {"tick", eve::Value(std::to_string(checkpoint.tick.value()))},
181 {"worldDigest", eve::Value(std::to_string(checkpoint.worldDigest))},
182 });
183 }
185 {"checkpoints", eve::Value(std::move(encodedCheckpoints))},
186 {"entries", eve::Value(std::move(encodedEntries))},
187 });
188}
189
190PixelReplayDivergence compare(const PixelReplayCheckpoint& expected,
191 const PixelReplayCheckpoint& actual) {
192 PixelReplayDivergence divergence;
193 divergence.tick = expected.tick;
194 divergence.expectedRevision = expected.revision;
195 divergence.actualRevision = actual.revision;
196 const std::size_t count = std::max(expected.chunks.size(), actual.chunks.size());
197 for (std::size_t index = 0; index < count; ++index) {
198 const bool expectedPresent = index < expected.chunks.size();
199 const bool actualPresent = index < actual.chunks.size();
200 if (expectedPresent && actualPresent && expected.chunks[index] == actual.chunks[index]) continue;
201 if (expectedPresent) divergence.expectedChunk = expected.chunks[index];
202 if (actualPresent) divergence.actualChunk = actual.chunks[index];
203 break;
204 }
205 return divergence;
206}
207
208} // namespace
209
210eve::Result<void> PixelReplayLog::append(PixelReplayEntry entry) {
211 if (entry.tick.value() == 0)
213 eve::DiagnosticCode::InvalidArgument, "replay command Tick must be positive",
214 "tick", {}, "pixelworld.replay"));
215 if (!entries_.empty() && entry.tick.value() < entries_.back().tick.value())
217 eve::DiagnosticCode::PreconditionViolation, "replay command Ticks must be nondecreasing",
218 "tick", {}, "pixelworld.replay"));
219 const std::uint64_t expectedSequence = entries_.empty() ? 1 : entries_.back().command.sequence + 1;
220 if (entry.command.sequence != expectedSequence)
222 eve::DiagnosticCode::PreconditionViolation, "replay edit sequence must be contiguous",
223 "command.sequence", {}, "pixelworld.replay"));
224 entries_.push_back(std::move(entry));
226}
227
228eve::Result<void> PixelReplayLog::captureCheckpoint(const eve::pixelworld::PixelWorld& world) {
229 if (!checkpoints_.empty() && world.tickValue() <= checkpoints_.back().tick.value())
231 eve::DiagnosticCode::PreconditionViolation, "checkpoint Tick must increase",
232 "tick", {}, "pixelworld.replay"));
233 auto checkpoint = checkpointFor(world);
234 if (!checkpoint.ok()) return eve::Result<void>::failure(checkpoint.status());
235 checkpoints_.push_back(std::move(checkpoint).value());
237}
238
239eve::Result<PixelReplayReceipt> PixelReplayLog::replay(
241 if (throughTick.value() == 0 || target.tickValue() != 0 || target.chunkCount() != 0 ||
242 target.lastEditSequence() != 0)
245 "replay requires a positive target Tick and an empty target world",
246 "target", {}, "pixelworld.replay"));
247 if ((!entries_.empty() && entries_.back().tick.value() > throughTick.value()) ||
248 (!checkpoints_.empty() && checkpoints_.back().tick.value() > throughTick.value()))
250 eve::DiagnosticCode::InvalidArgument, "throughTick does not cover the complete log",
251 "throughTick", {}, "pixelworld.replay"));
252
253 PixelReplayReceipt receipt;
254 std::size_t entryIndex = 0;
255 std::size_t checkpointIndex = 0;
256 for (std::uint64_t tick = 1; tick <= throughTick.value(); ++tick) {
257 while (entryIndex < entries_.size() && entries_[entryIndex].tick.value() == tick) {
258 auto applied = target.applyEdit(entries_[entryIndex].command);
259 if (!applied.ok()) return eve::Result<PixelReplayReceipt>::failure(applied.status());
260 ++entryIndex;
261 ++receipt.commandsApplied;
262 }
263 auto advanced = target.advance(eve::SimulationTick(tick));
264 if (!advanced.ok()) return eve::Result<PixelReplayReceipt>::failure(advanced.status());
266 if (checkpointIndex < checkpoints_.size() && checkpoints_[checkpointIndex].tick.value() == tick) {
267 auto actual = checkpointFor(target);
268 if (!actual.ok()) return eve::Result<PixelReplayReceipt>::failure(actual.status());
269 const auto& expected = checkpoints_[checkpointIndex];
270 if (actual.value().worldDigest != expected.worldDigest) {
271 receipt.match = PixelReplayMatch::Diverged;
272 receipt.firstDivergence = compare(expected, actual.value());
273 return eve::Result<PixelReplayReceipt>::success(std::move(receipt));
274 }
275 ++checkpointIndex;
276 }
277 }
278 return eve::Result<PixelReplayReceipt>::success(std::move(receipt));
279}
280
281eve::Result<eve::SnapshotEnvelope> PixelReplayLog::snapshot(
282 eve::PersistentId instanceId, const eve::SnapshotHashProvider& hashProvider) const {
283 const std::uint64_t entryTick = entries_.empty() ? 0 : entries_.back().tick.value();
284 const std::uint64_t checkpointTick = checkpoints_.empty() ? 0 : checkpoints_.back().tick.value();
286 std::string(kReplayType), replaySchema(), eve::SchemaVersion(kReplaySchemaVersion), instanceId,
287 eve::Revision(entries_.size() + checkpoints_.size()),
288 eve::SimulationTick(std::max(entryTick, checkpointTick)), replayPayload(entries_, checkpoints_),
289 hashProvider);
290}
291
292eve::Result<void> PixelReplayLog::restoreSnapshot(
293 const eve::SnapshotEnvelope& snapshot, const eve::SnapshotHashProvider& hashProvider) {
294 auto verified = eve::verifySnapshotEnvelope(snapshot, hashProvider);
295 if (!verified) return verified;
296 if (snapshot.type != kReplayType || snapshot.schema != replaySchema())
298 "replay snapshot type or schema does not match",
299 "schema", {}, "pixelworld.replay"));
300 if (snapshot.schemaVersion.value() < 1 || snapshot.schemaVersion.value() > kReplaySchemaVersion)
302 "unsupported replay snapshot schema version",
303 "schemaVersion", {}, "pixelworld.replay"));
304
305 auto root = object(snapshot.payload, "payload", {"checkpoints", "entries"});
306 if (!root) return eve::Result<void>::failure(root.status());
307 const auto* encodedEntries = root.value()->at("entries").getIf<eve::Value::Array>();
308 const auto* encodedCheckpoints = root.value()->at("checkpoints").getIf<eve::Value::Array>();
309 if (!encodedEntries || !encodedCheckpoints || encodedEntries->size() > kMaximumReplayEntries ||
310 encodedCheckpoints->size() > kMaximumReplayCheckpoints)
312 "replay arrays are invalid or exceed decode budget",
313 "payload", {}, "pixelworld.replay"));
314
315 PixelReplayLog candidate;
316 for (std::size_t index = 0; index < encodedEntries->size(); ++index) {
317 const std::string path = "payload.entries[" + std::to_string(index) + "]";
318 auto record = object((*encodedEntries)[index], path, {"command", "tick"});
319 if (!record) return eve::Result<void>::failure(record.status());
320 auto tick = decimal(record.value()->at("tick"), path + ".tick");
321 auto command = parseCommand(record.value()->at("command"), path + ".command");
322 if (!tick || !command)
324 eve::DiagnosticCode::ParseError, "invalid replay entry", path, {}, "pixelworld.replay"));
325 auto appended = candidate.append({eve::SimulationTick(tick.value()), std::move(command).value()});
326 if (!appended) return appended;
327 }
328
329 std::size_t totalChunks = 0;
330 std::uint64_t previousCheckpointTick = 0;
331 for (std::size_t index = 0; index < encodedCheckpoints->size(); ++index) {
332 const std::string path = "payload.checkpoints[" + std::to_string(index) + "]";
333 const bool versionOne = snapshot.schemaVersion.value() == 1;
334 auto record = versionOne
335 ? object((*encodedCheckpoints)[index], path, {"revision", "tick", "worldDigest"})
336 : object((*encodedCheckpoints)[index], path,
337 {"chunks", "revision", "tick", "worldDigest"});
338 if (!record) return eve::Result<void>::failure(record.status());
339 auto tick = decimal(record.value()->at("tick"), path + ".tick");
340 auto revision = decimal(record.value()->at("revision"), path + ".revision");
341 auto worldDigest = decimal(record.value()->at("worldDigest"), path + ".worldDigest");
342 if (!tick || !revision || !worldDigest || tick.value() == 0 || tick.value() <= previousCheckpointTick)
344 eve::DiagnosticCode::ParseError, "invalid replay checkpoint", path, {}, "pixelworld.replay"));
345 PixelReplayCheckpoint checkpoint;
346 checkpoint.tick = eve::SimulationTick(tick.value());
347 checkpoint.revision = revision.value();
348 checkpoint.worldDigest = worldDigest.value();
349
350 if (!versionOne) {
351 const auto* chunks = record.value()->at("chunks").getIf<eve::Value::Array>();
352 if (!chunks || chunks->size() > kMaximumReplayChunkDigests - totalChunks)
354 "replay Chunk digests exceed decode budget",
355 path + ".chunks", {}, "pixelworld.replay"));
356 totalChunks += chunks->size();
357 for (std::size_t chunkIndex = 0; chunkIndex < chunks->size(); ++chunkIndex) {
358 const std::string chunkPath = path + ".chunks[" + std::to_string(chunkIndex) + "]";
359 auto chunk = object((*chunks)[chunkIndex], chunkPath, {"digest", "x", "y"});
360 if (!chunk) return eve::Result<void>::failure(chunk.status());
361 auto x = integer(chunk.value()->at("x"), chunkPath + ".x");
362 auto y = integer(chunk.value()->at("y"), chunkPath + ".y");
363 auto digest = decimal(chunk.value()->at("digest"), chunkPath + ".digest");
364 if (!x || !y || !digest)
366 "invalid replay Chunk digest", chunkPath,
367 {}, "pixelworld.replay"));
368 PixelChunkDigest decoded{x.value(), y.value(), digest.value()};
369 if (!checkpoint.chunks.empty()) {
370 const auto& prior = checkpoint.chunks.back();
371 if (decoded.y < prior.y || (decoded.y == prior.y && decoded.x <= prior.x))
373 eve::DiagnosticCode::ParseError, "replay Chunk digests must be unique canonical order",
374 chunkPath, {}, "pixelworld.replay"));
375 }
376 checkpoint.chunks.push_back(decoded);
377 }
378 }
379 previousCheckpointTick = tick.value();
380 candidate.checkpoints_.push_back(std::move(checkpoint));
381 }
382
383 const std::uint64_t entryTick = candidate.entries_.empty() ? 0 : candidate.entries_.back().tick.value();
384 const std::uint64_t checkpointTick = candidate.checkpoints_.empty()
385 ? 0
386 : candidate.checkpoints_.back().tick.value();
387 if (snapshot.revision.value() != candidate.entries_.size() + candidate.checkpoints_.size() ||
388 snapshot.tick.value() != std::max(entryTick, checkpointTick))
390 eve::DiagnosticCode::Conflict, "replay payload count or Tick disagrees with envelope metadata", "payload",
391 {}, "pixelworld.replay"));
392
393 entries_ = std::move(candidate.entries_);
394 checkpoints_ = std::move(candidate.checkpoints_);
396}
397
398} // namespace eve::pixelworld_replay
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::unordered_map< std::string, QuestRuntime > entries
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
float temperature
std::string text
TokenKind kind
std::uint64_t bytes
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
std::string error
Definition Package.cpp:60
World3D * world
std::uint64_t revision
float radius
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
Material * material
double number
std::string digest
std::uint32_t count
Cell cell
SimulationTick tick
Json object
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Id128 public API.
Definition Identity.h:112
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Sparse, chunked, deterministic 2D falling-material world.
Definition PixelWorld.h:224
Owning command log and periodic checkpoint projection for PixelWorld.
eve::Result< void > append(PixelReplayEntry entry)
Append the next sequenced command; Tick order must be nondecreasing.
MaterialId
Compact stable material identifier stored in each authoritative cell.
PixelEditKind
Canonical deterministic edit operation admitted at a Tick boundary.
Definition PixelWorld.h:148
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
Result< SnapshotEnvelope > makeSnapshotEnvelope(std::string type, LogicalId schema, SchemaVersion schemaVersion, PersistentId instanceId, Revision revision, SimulationTick tick, Value payload, const SnapshotHashProvider &hashProvider)
Construct and seal a snapshot envelope.
Definition Snapshot.cpp:103
Result< void > verifySnapshotEnvelope(const SnapshotEnvelope &snapshot, const SnapshotHashProvider &hashProvider)
Verify an envelope's content hash without modifying it.
Definition Snapshot.cpp:63
std::function< Result< ContentId >(std::string_view canonicalInput)> SnapshotHashProvider
Injected content-digest implementation used by snapshots.
Definition Snapshot.h:36
Stable outer format shared by persistence and cross-process snapshots.
Definition Snapshot.h:46
SchemaVersion schemaVersion
Definition Snapshot.h:49
std::string type
Definition Snapshot.h:47
SimulationTick tick
Definition Snapshot.h:52
Owning, immutable projection of one authoritative simulation chunk.
Definition PixelWorld.h:77
std::vector< PixelCell > cells
Definition PixelWorld.h:82
Owning replayable edit command. Sequence must be exactly previous + 1.
Definition PixelWorld.h:155
Canonical content digest for one authoritative PixelWorld Chunk.
Periodic replay checkpoint containing world and per-Chunk diagnostics.
One deterministic edit admitted immediately before its simulation Tick.
eve::pixelworld::PixelEditCommand command
Receipt from deterministic replay through an exact target Tick.
std::optional< PixelReplayDivergence > firstDivergence