17#include <unordered_map>
23std::atomic<std::uint64_t> nextPixelWorldId{1};
25int floorDiv(
int value)
noexcept {
29int floorMod(
int value)
noexcept {
37 friend bool operator==(
const ChunkCoord&,
const ChunkCoord&) =
default;
38 friend bool operator<(
const ChunkCoord&
a,
const ChunkCoord&
b) {
39 return a.y !=
b.y ?
a.y <
b.y :
a.x <
b.x;
43struct ChunkCoordHash {
44 std::size_t operator()(ChunkCoord coord)
const noexcept {
45 const auto x = std::uint64_t(std::uint32_t(coord.x));
46 const auto y = std::uint64_t(std::uint32_t(coord.y));
47 return std::size_t((
x << 32U) ^
y);
52 std::array<PixelCell, kPixelChunkSize * kPixelChunkSize>
cells{};
53 std::array<std::uint64_t, kPixelChunkSize * kPixelChunkSize>
updated{};
67 static std::size_t
index(
int x,
int y)
noexcept {
78void append(std::vector<std::byte>& out, T
value) {
79 const auto*
first =
reinterpret_cast<const std::byte*
>(&
value);
80 out.insert(out.end(),
first,
first +
sizeof(T));
99 std::unordered_map<ChunkCoord, Chunk, ChunkCoordHash>
chunks;
100 std::unordered_map<ChunkCoord, std::uint64_t, ChunkCoordHash>
removedChunks;
112 std::uint64_t
worldId = nextPixelWorldId.fetch_add(1, std::memory_order_relaxed);
159 std::uint8_t(targetFluid && targetDef.density < sourceDef.density);
163 for (std::size_t ruleIndex = 0; ruleIndex <
reactions.size(); ++ruleIndex) {
165 const std::size_t
first = std::size_t(rule.first);
166 const std::size_t
second = std::size_t(rule.second);
168 ruleIndex > std::size_t(std::numeric_limits<std::uint16_t>::max()))
171 std::uint16_t(ruleIndex));
174 std::uint16_t(ruleIndex));
179 const std::size_t sourceIndex = std::size_t(
source);
180 const std::size_t targetIndex = std::size_t(
target);
187 const std::size_t sourceIndex = std::size_t(
source);
188 const std::size_t targetIndex = std::size_t(
target);
207 if (
cell.thermalRemainder != 0) ++chunk.thermalRemainderCells;
209 if (
material < chunk.materialCounts.size())
212 ++chunk.materialOverflow;
213 chunk.minimumTemperature = std::min(chunk.minimumTemperature,
cell.temperature);
214 chunk.maximumTemperature = std::max(chunk.maximumTemperature,
cell.temperature);
221 if (
cell.thermalRemainder != 0) --chunk.thermalRemainderCells;
223 if (
material < chunk.materialCounts.size())
226 --chunk.materialOverflow;
227 if (
cell.temperature == chunk.minimumTemperature ||
cell.temperature == chunk.maximumTemperature)
228 chunk.temperatureBoundsDirty =
true;
232 chunk.materialCounts.fill(0);
235 chunk.thermalRemainderCells = 0;
236 chunk.materialOverflow = 0;
237 chunk.minimumTemperature = 32767;
238 chunk.maximumTemperature = -32768;
239 chunk.temperatureBoundsDirty =
false;
244 if (!chunk.temperatureBoundsDirty)
return;
245 chunk.minimumTemperature = 32767;
246 chunk.maximumTemperature = -32768;
249 chunk.minimumTemperature = std::min(chunk.minimumTemperature,
cell.temperature);
250 chunk.maximumTemperature = std::max(chunk.maximumTemperature,
cell.temperature);
252 chunk.temperatureBoundsDirty =
false;
257 return chunk ? chunk->cells[Chunk::index(floorMod(
x), floorMod(
y))] :
PixelCell{};
266 std::int64_t energy = std::int64_t(
cell.temperature) *
capacity +
cell.thermalRemainder;
268 std::int64_t remainder = energy %
capacity;
273 if (
temperature < std::numeric_limits<std::int16_t>::min()) {
274 temperature = std::numeric_limits<std::int16_t>::min();
276 }
else if (
temperature > std::numeric_limits<std::int16_t>::max()) {
277 temperature = std::numeric_limits<std::int16_t>::max();
281 cell.thermalRemainder = std::uint16_t(remainder);
287 return chunk ? chunk->updated[Chunk::index(floorMod(
x), floorMod(
y))] : 0;
291 const int cx = floorDiv(
x),
cy = floorDiv(
y);
292 for (
int oy = -1;
oy <= 1; ++
oy)
293 for (
int ox = -1;
ox <= 1; ++
ox) {
303 const auto index = Chunk::index(floorMod(
x), floorMod(
y));
305 if (oldCell !=
cell) {
310 if (markUpdated) chunk.updated[
index] = updatedTick;
313 chunk.touched =
true;
317 std::uint64_t updatedTick,
bool markUpdated =
true) {
319 if (oldCell !=
cell) {
324 if (markUpdated) chunk.updated[
index] = updatedTick;
327 chunk.touched =
true;
331 std::uint64_t updatedTick,
bool markUpdated =
true) {
335 bool moveOrSwap(
int x,
int y,
int tx,
int ty, std::uint64_t currentTick) {
341 std::uint64_t currentTick) {
352 std::uint64_t currentTick) {
353 const auto sourceIndex = Chunk::index(
sourceX, sourceY);
354 const auto targetIndex = Chunk::index(
targetX, targetY);
357 chunk.cells[targetIndex] =
source;
358 chunk.cells[sourceIndex] =
target;
359 chunk.updated[targetIndex] = currentTick;
360 chunk.updated[sourceIndex] = currentTick;
363 chunk.touched =
true;
374 bool pseudoBit(
int x,
int y, std::uint64_t currentTick)
const noexcept {
375 std::uint64_t
value =
seed ^ (std::uint64_t(std::uint32_t(
x)) << 32U) ^
376 std::uint64_t(std::uint32_t(
y)) ^ (currentTick * 0x9E3779B97F4A7C15ULL);
378 value *= 0xBF58476D1CE4E5B9ULL;
380 return (
value & 1U) != 0;
389 : impl_(
std::make_unique<
Impl>(
std::move(catalog))) {
400 if (
this == &other)
return *
this;
402 impl_ = std::move(other.impl_);
413 return impl_->catalog.definition(
material).displayRgba;
424 "material Catalog reload requires a paused world",
"paused");
425 if (expectedFingerprint != impl_->catalog.fingerprint())
427 "material Catalog expected fingerprint is stale",
"expectedFingerprint");
428 const auto current = impl_->catalog.definitions();
430 if (
current.size() != replacement.size())
432 "live Catalog reload cannot add or remove material ids",
"materials");
436 "live Catalog reload must preserve every material id and name",
437 "materials[" + std::to_string(
index) +
"]");
448 impl_->catalog = std::move(catalog);
449 impl_->rebuildMaterialRuntimeTables();
452 impl_->lastEditSequence = 0;
453 for (
auto& [coord, chunk] : impl_->chunks) {
455 impl_->rebuildActivity(chunk);
456 impl_->refreshTemperatureBounds(chunk);
457 chunk.revision = impl_->revision;
459 chunk.touched =
true;
471 impl_->put(
x,
y,
cell, impl_->tick);
472 impl_->wakeAround(
x,
y);
481 auto resolved = impl_->catalog.resolve(
material);
485 cell.temperature = impl_->catalog.definition(
cell.material).defaultTemperature;
486 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(impl_->catalog.definition(
cell.material).defaultLifetime,
487 std::numeric_limits<std::uint8_t>::max()));
494 .expect(
"PixelWorld::paintCircle requires a registered material");
500 std::size_t changed = 0;
501 auto resolved = impl_->catalog.resolve(
material);
506 const int dx =
x - centerX,
dy =
y - centerY;
510 cell.temperature = impl_->catalog.definition(
id).defaultTemperature;
511 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(impl_->catalog.definition(
id).defaultLifetime,
512 std::numeric_limits<std::uint8_t>::max()));
520 const auto reject = [](std::string
message, std::string
path) {
525 if (command.
sequence != impl_->lastEditSequence + 1)
526 return reject(
"edit sequence must be exactly previous sequence + 1",
"sequence");
528 return reject(
"edit radius must be in [0, 4096]",
"radius");
529 if (command.
centerX < -100'000'000 || command.
centerX > 100'000'000 ||
531 return reject(
"edit center is outside the supported coordinate range",
"center");
533 std::size_t(command.
material) >= impl_->catalog.definitions().size())
534 return reject(
"paint command references an unknown material id",
"material");
536 return reject(
"explosion strength must be in [0, 1000000]",
"strength");
545 std::vector<Candidate> candidates;
546 const std::int64_t radiusSquared = std::int64_t(command.
radius) * command.
radius;
549 const std::int64_t
dx = std::int64_t(
x) - command.
centerX;
550 const std::int64_t
dy = std::int64_t(
y) - command.
centerY;
551 const std::int64_t distanceSquared =
dx *
dx +
dy *
dy;
552 if (distanceSquared > radiusSquared)
continue;
560 const auto& definition = impl_->catalog.definition(command.
material);
561 newCell.
temperature = definition.defaultTemperature;
562 newCell.
lifetime = std::uint8_t(std::min<std::uint16_t>(
563 definition.defaultLifetime, std::numeric_limits<std::uint8_t>::max()));
566 newCell.
temperature = std::int16_t(std::clamp<int>(
571 const std::int64_t denominator = std::max<std::int64_t>(1, radiusSquared);
572 const int radialStrength = int(std::int64_t(command.
strength) *
573 (radiusSquared - distanceSquared + 1) / denominator);
574 if (radialStrength >= impl_->catalog.definition(oldCell.
material).blastResistance) {
579 (radiusSquared - distanceSquared + 1) / denominator);
580 newCell.
temperature = std::int16_t(std::clamp<int>(
581 int(oldCell.
temperature) + radialHeat, -32768, 32767));
585 if (newCell == oldCell)
continue;
586 candidates.push_back({
x,
y, newCell,
removed, heated});
592 for (
const Candidate& candidate : candidates) {
593 impl_->put(candidate.x, candidate.y, candidate.cell, impl_->tick);
594 impl_->wakeAround(candidate.x, candidate.y);
599 impl_->lastEditSequence = command.
sequence;
606 std::int16_t temperatureDelta) {
608 command.
sequence = impl_->lastEditSequence + 1;
615 return std::move(
applyEdit(command)).expect(
"PixelWorld::explode generated an invalid edit command");
619 PixelRegion region,
int supportY, std::uint32_t minimumCells) {
624 const std::int64_t
width = std::int64_t(region.
maxX) - region.
minX + 1;
625 const std::int64_t
height = std::int64_t(region.
maxY) - region.
minY + 1;
626 if (region.
minX < -100'000'000 || region.
maxX > 100'000'000 ||
627 region.
minY < -100'000'000 || region.
maxY > 100'000'000)
629 "fragment scan region is outside the supported coordinate range",
"region");
630 if (
width <= 0 || height <= 0 || width * height > 16'777'216)
632 "fragment scan region must be non-empty and contain at most 16777216 cells",
"region");
633 if (minimumCells == 0)
636 using Coord = std::pair<int, int>;
637 std::set<Coord> visited;
638 std::vector<PixelFragment> fragments;
639 constexpr std::array<Coord, 4> neighbors{{{0, -1}, {-1, 0}, {1, 0}, {0, 1}}};
640 const auto isSolid = [
this](
int x,
int y) {
646 if (!isSolid(
x,
y) || visited.contains({
x,
y}))
continue;
648 std::vector<Coord> component;
649 visited.emplace(
x,
y);
650 bool supported =
false;
651 int minX =
x, maxX =
x, minY =
y, maxY =
y;
655 component.emplace_back(
cx,
cy);
656 minX = std::min(minX,
cx);
657 maxX = std::max(maxX,
cx);
658 minY = std::min(minY,
cy);
659 maxY = std::max(maxY,
cy);
660 if (
cy >= supportY) supported =
true;
661 for (
const auto& [
dx,
dy] : neighbors) {
666 if (isSolid(
nx,
ny)) supported =
true;
669 if (isSolid(
nx,
ny) && visited.emplace(
nx,
ny).second)
673 if (supported || component.size() < minimumCells)
continue;
677 fragment.
id = impl_->nextFragmentId++;
680 fragment.
width = maxX - minX + 1;
681 fragment.
height = maxY - minY + 1;
683 fragment.
cells.resize(std::size_t(fragment.
width) * std::size_t(fragment.
height));
684 for (
const auto& [
cx,
cy] : component)
685 fragment.
cells[std::size_t(
cy - minY) * std::size_t(fragment.
width) + std::size_t(
cx - minX)] =
687 fragments.push_back(std::move(fragment));
690 if (!fragments.empty()) {
692 for (
int y = 0;
y < fragment.height; ++
y)
693 for (
int x = 0;
x < fragment.width; ++
x) {
694 const PixelCell&
cell = fragment.cells[std::size_t(
y) * std::size_t(fragment.width) +
x];
696 impl_->put(fragment.originX +
x, fragment.originY +
y, {}, impl_->tick);
697 impl_->wakeAround(fragment.originX +
x, fragment.originY +
y);
712 "fragment belongs to another world or an invalidated world epoch",
"source");
713 if (fragment.
id == 0 || fragment.
width <= 0 || fragment.
height <= 0 ||
714 std::uint64_t(fragment.
width) * std::uint64_t(fragment.
height) != fragment.
cells.size())
716 std::uint32_t cellsPlaced = 0;
717 for (
int y = 0;
y < fragment.
height; ++
y)
718 for (
int x = 0;
x < fragment.
width; ++
x) {
721 if (std::size_t(
cell.material) >= impl_->catalog.definitions().size())
723 const std::int64_t
wx = std::int64_t(originX) +
x,
wy = std::int64_t(originY) +
y;
724 if (
wx < std::numeric_limits<int>::min() ||
wx > std::numeric_limits<int>::max() ||
725 wy < std::numeric_limits<int>::min() ||
wy > std::numeric_limits<int>::max())
738 for (
int y = 0;
y < fragment.
height; ++
y)
739 for (
int x = 0;
x < fragment.
width; ++
x) {
742 impl_->put(originX +
x, originY +
y,
cell, impl_->tick);
743 impl_->wakeAround(originX +
x, originY +
y);
745 if (cellsPlaced != 0) ++impl_->revision;
753 impl_->chunks.clear();
754 impl_->removedChunks.clear();
757 impl_->lastEditSequence = 0;
763 return advanceImpl(
tick,
nullptr);
769 return advanceImpl(
tick, &scheduler);
774 if (
tick.value() <= impl_->tick)
777 "PixelWorld ticks must increase monotonically",
"tick", {},
"pixelworld"));
779 const auto stepStarted = std::chrono::steady_clock::now();
780 impl_->tick =
tick.value();
783 std::vector<ChunkCoord>
order;
784 order.reserve(impl_->chunks.size());
785 for (
auto& [coord, chunk] : impl_->chunks) {
786 if (chunk.active)
order.push_back(coord);
787 chunk.touched =
false;
791 struct ChunkPhaseSummary {
795 std::uint64_t materialMask = 0;
798 bool materialMaskOverflow =
false;
800 const auto summarize = [](
const Chunk& chunk) {
801 ChunkPhaseSummary summary;
802 summary.nonAir = chunk.nonAir;
803 summary.mobile = chunk.mobile;
804 summary.thermalRemainderCells = chunk.thermalRemainderCells;
805 summary.minimumTemperature = chunk.minimumTemperature;
806 summary.maximumTemperature = chunk.maximumTemperature;
807 summary.materialMaskOverflow = chunk.materialOverflow != 0;
809 if (chunk.materialCounts[
material] != 0)
810 summary.materialMask |= std::uint64_t(1) <<
material;
813 std::unordered_map<ChunkCoord, ChunkPhaseSummary, ChunkCoordHash> phaseSummaries;
814 const auto rebuildSummaries = [&] {
815 phaseSummaries.clear();
816 phaseSummaries.reserve(impl_->chunks.size());
817 for (
auto& [coord, chunk] : impl_->chunks) {
818 impl_->refreshTemperatureBounds(chunk);
819 phaseSummaries.emplace(coord, summarize(chunk));
824 struct MovementRows {
825 std::array<std::uint64_t, kPixelChunkSize>
mobile{};
826 std::array<std::uint64_t, kPixelChunkSize> rightFirst{};
828 std::vector<MovementRows> movementCandidates(
order.size());
829 std::vector<std::size_t> movementWork;
830 movementWork.reserve(
order.size());
832 const auto summary = phaseSummaries.find(
order[
index]);
833 if (summary != phaseSummaries.end() && summary->second.mobile != 0)
834 movementWork.push_back(
index);
836 const auto generateMovementCandidates = [&](std::size_t workIndex) {
837 const std::size_t orderIndex = movementWork[workIndex];
838 const ChunkCoord coord =
order[orderIndex];
839 auto& candidates = movementCandidates[orderIndex];
840 candidates.mobile.fill(0);
841 candidates.rightFirst.fill(0);
842 const auto found = impl_->chunks.find(coord);
843 if (
found == impl_->chunks.end())
return;
844 const Chunk& chunk =
found->second;
847 const auto cellIndex = Chunk::index(lx, ly);
848 if (chunk.updated[cellIndex] ==
tick.value())
continue;
849 const PixelCell
cell = chunk.cells[cellIndex];
850 const auto state = impl_->materialState(
cell.material);
855 const std::uint64_t bit = std::uint64_t(1) << lx;
856 candidates.mobile[std::size_t(ly)] |= bit;
857 if (
direction > 0) candidates.rightFirst[std::size_t(ly)] |= bit;
861 if (scheduler && movementWork.size() > 1) {
862 scheduler->
parallelFor(movementWork.size(), generateMovementCandidates);
863 stats.parallelTasks += std::uint32_t(movementWork.size());
866 generateMovementCandidates(
index);
868 for (std::size_t orderIndex = 0; orderIndex <
order.size(); ++orderIndex) {
869 ++
stats.chunksVisited;
870 const ChunkCoord sourceCoord =
order[orderIndex];
871 auto sourceFound = impl_->chunks.find(sourceCoord);
872 if (sourceFound == impl_->chunks.end())
continue;
873 Chunk& sourceChunk = sourceFound->second;
874 const MovementRows& candidates = movementCandidates[orderIndex];
875 for (
int sourceLocalY =
kPixelChunkSize - 1; sourceLocalY >= 0; --sourceLocalY) {
876 std::uint64_t remaining = candidates.mobile[std::size_t(sourceLocalY)];
877 const bool leftFirst = ((
tick.value() + std::uint64_t(sourceLocalY)) & 1U) == 0;
878 while (remaining != 0) {
879 const int sourceLocalX = leftFirst
880 ? int(std::countr_zero(remaining))
881 : 63 - int(
std::countl_zero(remaining));
882 const std::uint64_t candidateBit = std::uint64_t(1) << sourceLocalX;
883 remaining &= ~candidateBit;
885 (candidates.rightFirst[std::size_t(sourceLocalY)] & candidateBit) != 0 ? 1 : -1;
886 const auto sourceIndex = Chunk::index(sourceLocalX, sourceLocalY);
887 if (sourceChunk.updated[sourceIndex] ==
tick.value())
continue;
888 const PixelCell
cell = sourceChunk.cells[sourceIndex];
889 const auto state = impl_->materialState(
cell.material);
891 ++
stats.cellsVisited;
892 const auto tryMove = [&](
int targetLocalX,
int targetLocalY) {
895 return impl_->moveOrSwapWithinChunk(
896 sourceCoord, sourceChunk, sourceLocalX, sourceLocalY,
cell,
897 targetLocalX, targetLocalY,
tick.value());
900 return impl_->moveOrSwapKnownSource(
905 bool moved = tryMove(sourceLocalX, sourceLocalY + 1);
907 moved = tryMove(sourceLocalX +
direction, sourceLocalY + 1);
909 moved = tryMove(sourceLocalX -
direction, sourceLocalY + 1);
911 moved = tryMove(sourceLocalX +
direction, sourceLocalY);
913 moved = tryMove(sourceLocalX -
direction, sourceLocalY);
916 stats.cellsChanged += 2;
922 if (
stats.cellsMoved != 0) rebuildSummaries();
927 struct ThermalChunkContributions {
928 std::array<std::int64_t, kPixelChunkSize * kPixelChunkSize>
local{};
929 std::array<std::int64_t, kPixelChunkSize> rightHalo{};
930 std::array<std::int64_t, kPixelChunkSize> bottomHalo{};
932 std::vector<ThermalChunkContributions> thermalCandidates(
order.size());
933 std::vector<std::uint32_t> transferCounts(
order.size());
934 std::vector<std::uint64_t> transferEnergy(
order.size());
935 const auto hasPossibleThermalGradient = [&](ChunkCoord coord) {
936 const auto ownSummary = phaseSummaries.find(coord);
937 if (ownSummary == phaseSummaries.end() || ownSummary->second.nonAir == 0)
return false;
938 const auto& own = ownSummary->second;
939 if (own.thermalRemainderCells != 0)
return true;
940 if (own.minimumTemperature != own.maximumTemperature)
return true;
941 for (
const auto& [
dx,
dy] :
942 std::array<
std::pair<int, int>, 4>{{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}}) {
943 const auto neighbor = phaseSummaries.find({coord.x +
dx, coord.y +
dy});
944 if (neighbor == phaseSummaries.end() || neighbor->second.nonAir == 0)
continue;
945 if (neighbor->second.minimumTemperature != own.minimumTemperature ||
946 neighbor->second.maximumTemperature != own.maximumTemperature)
951 std::vector<std::size_t> thermalWork;
952 thermalWork.reserve(
order.size());
954 if (hasPossibleThermalGradient(
order[
index])) thermalWork.push_back(
index);
955 const auto& readOnlyChunks = impl_->chunks;
956 const auto calculateThermalChunk = [&](std::size_t workIndex) {
957 const std::size_t orderIndex = thermalWork[workIndex];
958 const ChunkCoord coord =
order[orderIndex];
959 const auto ownFound = readOnlyChunks.find(coord);
960 if (ownFound == readOnlyChunks.end())
return;
961 const Chunk& ownChunk = ownFound->second;
962 const auto rightFound = readOnlyChunks.find({coord.x + 1, coord.y});
963 const auto bottomFound = readOnlyChunks.find({coord.x, coord.y + 1});
964 const Chunk* rightChunk = rightFound == readOnlyChunks.end() ? nullptr : &rightFound->second;
965 const Chunk* bottomChunk = bottomFound == readOnlyChunks.end() ? nullptr : &bottomFound->second;
966 auto& candidates = thermalCandidates[orderIndex];
967 transferCounts[orderIndex] = 0;
968 transferEnergy[orderIndex] = 0;
971 const PixelCell
source = ownChunk.cells[Chunk::index(lx, ly)];
973 for (
const auto& [
ox,
oy] :
std::array<
std::pair<int, int>, 2>{{{1, 0}, {0, 1}}}) {
976 if (rightChunk ==
nullptr)
continue;
977 target = rightChunk->cells[Chunk::index(0, ly)];
979 if (bottomChunk ==
nullptr)
continue;
980 target = bottomChunk->cells[Chunk::index(lx, 0)];
982 target = ownChunk.cells[Chunk::index(lx +
ox, ly +
oy)];
985 const int conductivity = std::min<int>(
986 impl_->thermalConductivity(
source.material),
987 impl_->thermalConductivity(
target.material));
988 if (conductivity == 0)
continue;
989 const std::int64_t sourceCapacity = impl_->heatCapacity(
source.material);
990 const std::int64_t targetCapacity = impl_->heatCapacity(
target.material);
991 const std::int64_t sourceEnergy =
992 std::int64_t(
source.temperature) * sourceCapacity +
source.thermalRemainder;
993 const std::int64_t targetEnergy =
994 std::int64_t(
target.temperature) * targetCapacity +
target.thermalRemainder;
995 const std::int64_t equilibriumTransfer =
996 (targetEnergy * sourceCapacity - sourceEnergy * targetCapacity) /
997 (sourceCapacity + targetCapacity);
998 if (equilibriumTransfer == 0)
continue;
999 std::int64_t transfer = equilibriumTransfer * conductivity / 255;
1000 if (transfer == 0) transfer = equilibriumTransfer < 0 ? -1 : 1;
1001 const std::size_t sourceIndex = Chunk::index(lx, ly);
1002 candidates.local[sourceIndex] += transfer;
1004 candidates.rightHalo[std::size_t(ly)] -= transfer;
1006 candidates.bottomHalo[std::size_t(lx)] -= transfer;
1008 candidates.local[Chunk::index(lx +
ox, ly +
oy)] -= transfer;
1009 ++transferCounts[orderIndex];
1010 transferEnergy[orderIndex] += std::uint64_t(transfer < 0 ? -transfer : transfer);
1014 if (scheduler && thermalWork.size() > 1) {
1015 scheduler->
parallelFor(thermalWork.size(), calculateThermalChunk);
1016 stats.parallelTasks += std::uint32_t(thermalWork.size());
1020 using ThermalDeltaChunk = std::array<std::int64_t, kPixelChunkSize * kPixelChunkSize>;
1021 std::unordered_map<ChunkCoord, ThermalDeltaChunk, ChunkCoordHash> thermalEnergyDeltas;
1022 thermalEnergyDeltas.reserve(
order.size());
1023 for (std::size_t
index = 0;
index < thermalCandidates.size(); ++
index) {
1024 stats.temperatureTransfers += transferCounts[
index];
1025 stats.thermalEnergyTransferred += transferEnergy[
index];
1027 const ThermalChunkContributions& contributions = thermalCandidates[
index];
1028 auto& localDeltas = thermalEnergyDeltas[coord];
1029 for (std::size_t cellIndex = 0; cellIndex < contributions.local.size(); ++cellIndex)
1030 localDeltas[cellIndex] += contributions.local[cellIndex];
1031 auto& rightDeltas = thermalEnergyDeltas[{coord.x + 1, coord.y}];
1032 auto& bottomDeltas = thermalEnergyDeltas[{coord.x, coord.y + 1}];
1034 rightDeltas[Chunk::index(0,
int(
offset))] += contributions.rightHalo[
offset];
1035 bottomDeltas[Chunk::index(
int(
offset), 0)] += contributions.bottomHalo[
offset];
1038 std::vector<ChunkCoord> thermalChunkOrder;
1039 thermalChunkOrder.reserve(thermalEnergyDeltas.size());
1040 for (
const auto& [coord, deltas] : thermalEnergyDeltas) {
1042 thermalChunkOrder.push_back(coord);
1044 std::sort(thermalChunkOrder.begin(), thermalChunkOrder.end());
1045 struct ThermalCommitStats {
1046 std::uint32_t cellsChanged = 0;
1047 std::uint64_t energyClamped = 0;
1049 std::vector<Chunk*> thermalCommitChunks;
1050 std::vector<const ThermalDeltaChunk*> thermalCommitDeltas;
1051 thermalCommitChunks.reserve(thermalChunkOrder.size());
1052 thermalCommitDeltas.reserve(thermalChunkOrder.size());
1053 for (
const ChunkCoord coord : thermalChunkOrder) {
1054 auto chunkFound = impl_->chunks.find(coord);
1055 if (chunkFound == impl_->chunks.end())
continue;
1056 thermalCommitChunks.push_back(&chunkFound->second);
1057 thermalCommitDeltas.push_back(&thermalEnergyDeltas.at(coord));
1059 std::vector<ThermalCommitStats> thermalCommitStats(thermalCommitChunks.size());
1060 const auto commitThermalChunk = [&](std::size_t
index) {
1061 Chunk& chunk = *thermalCommitChunks[
index];
1062 const ThermalDeltaChunk& deltas = *thermalCommitDeltas[
index];
1063 ThermalCommitStats& commitStats = thermalCommitStats[
index];
1066 const std::size_t cellIndex = Chunk::index(localX, localY);
1067 const std::int64_t energyDelta = deltas[cellIndex];
1068 if (energyDelta == 0)
continue;
1069 PixelCell
cell = chunk.cells[cellIndex];
1071 impl_->catalog.definition(
cell.material).heatCapacity;
1072 const std::int64_t beforeEnergy =
1074 const std::int64_t requestedEnergy = beforeEnergy + energyDelta;
1076 std::int64_t remainder = requestedEnergy %
capacity;
1077 if (remainder < 0) {
1081 if (
temperature < std::numeric_limits<std::int16_t>::min()) {
1082 temperature = std::numeric_limits<std::int16_t>::min();
1084 }
else if (
temperature > std::numeric_limits<std::int16_t>::max()) {
1085 temperature = std::numeric_limits<std::int16_t>::max();
1089 cell.thermalRemainder = std::uint16_t(remainder);
1090 const std::int64_t appliedEnergy =
1092 if (appliedEnergy != requestedEnergy)
1093 commitStats.energyClamped += std::uint64_t(
1094 appliedEnergy > requestedEnergy ? appliedEnergy - requestedEnergy
1095 : requestedEnergy - appliedEnergy);
1096 if (appliedEnergy == beforeEnergy)
continue;
1097 impl_->putNormalizedWithinChunk(chunk, cellIndex,
cell,
tick.value(),
false);
1098 ++commitStats.cellsChanged;
1101 if (scheduler && thermalCommitChunks.size() > 1) {
1102 scheduler->
parallelFor(thermalCommitChunks.size(), commitThermalChunk);
1103 stats.parallelTasks += std::uint32_t(thermalCommitChunks.size());
1105 for (std::size_t
index = 0;
index < thermalCommitChunks.size(); ++
index)
1106 commitThermalChunk(
index);
1108 for (
const ThermalCommitStats& commitStats : thermalCommitStats) {
1109 stats.cellsChanged += commitStats.cellsChanged;
1110 stats.thermalEnergyClamped += commitStats.energyClamped;
1114 for (
const ChunkCoord coord :
order) {
1115 if (impl_->chunks.find(coord) == impl_->chunks.end())
continue;
1116 const auto summary = phaseSummaries.find(coord);
1117 if (summary != phaseSummaries.end() && !summary->second.materialMaskOverflow) {
1118 bool mayTransition =
false;
1119 for (
const auto& rule : impl_->catalog.phaseRules()) {
1120 const auto material = std::size_t(rule.source);
1122 (summary->second.materialMask & (std::uint64_t(1) <<
material)) == 0)
1124 const bool temperatureMayMatch =
1126 ? summary->second.maximumTemperature >= rule.threshold
1127 : summary->second.minimumTemperature <= rule.threshold;
1128 if (temperatureMayMatch) {
1129 mayTransition =
true;
1133 if (!mayTransition)
continue;
1139 PixelCell
cell = impl_->get(
x,
y);
1142 for (
const auto& rule : impl_->catalog.phaseRules()) {
1143 if (rule.source !=
cell.material)
continue;
1145 ?
cell.temperature >= rule.threshold
1146 :
cell.temperature <= rule.threshold;
1147 if (!matches)
continue;
1148 cell.material = rule.result;
1149 cell.temperature = std::int16_t(std::clamp<int>(
1150 int(
cell.temperature) + rule.temperatureDelta, -32768, 32767));
1151 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(
1152 impl_->catalog.definition(
cell.material).defaultLifetime,
1153 std::numeric_limits<std::uint8_t>::max()));
1155 ++
stats.phaseChanges;
1156 ++
stats.cellsChanged;
1162 if (
stats.phaseChanges != 0) rebuildSummaries();
1164 std::uint64_t worldMaterialMask = 0;
1165 bool worldMaterialMaskOverflow =
false;
1166 for (
const auto& [coord, summary] : phaseSummaries) {
1168 worldMaterialMask |= summary.materialMask;
1169 worldMaterialMaskOverflow = worldMaterialMaskOverflow || summary.materialMaskOverflow;
1171 bool worldCanReact = worldMaterialMaskOverflow;
1173 for (
const auto& rule : impl_->catalog.
reactions()) {
1174 const auto first = std::size_t(rule.first),
second = std::size_t(rule.second);
1176 ((worldMaterialMask & (std::uint64_t(1) <<
first)) != 0 &&
1177 (worldMaterialMask & (std::uint64_t(1) <<
second)) != 0)) {
1178 worldCanReact =
true;
1183 for (
auto it =
order.rbegin(); it !=
order.rend(); ++it) {
1184 const ChunkCoord coord = *it;
1185 auto sourceFound = impl_->chunks.find(coord);
1186 if (sourceFound == impl_->chunks.end())
continue;
1187 Chunk* sourceChunk = &sourceFound->second;
1188 const auto summary = phaseSummaries.find(coord);
1189 if (summary != phaseSummaries.end() && !summary->second.materialMaskOverflow) {
1190 bool requiresScan = worldCanReact;
1191 if (!requiresScan) {
1193 if ((summary->second.materialMask & (std::uint64_t(1) <<
material)) == 0)
continue;
1196 requiresScan =
true;
1201 if (!requiresScan)
continue;
1207 const std::size_t sourceIndex = Chunk::index(lx, ly);
1208 if (sourceChunk->updated[sourceIndex] ==
tick.value())
continue;
1209 PixelCell
cell = sourceChunk->cells[sourceIndex];
1211 ++
stats.cellsVisited;
1212 bool reacted =
false;
1213 if (impl_->catalog.canReact(
cell.material))
1214 for (
const auto& [
ox,
oy] :
1215 std::array<
std::pair<int, int>, 4>{{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}}) {
1216 const int neighborLocalX = lx +
ox;
1217 const int neighborLocalY = ly +
oy;
1218 const bool sameChunk = neighborLocalX >= 0 &&
1220 neighborLocalY >= 0 &&
1222 const std::size_t neighborIndex =
1223 sameChunk ? Chunk::index(neighborLocalX, neighborLocalY) : 0;
1224 PixelCell neighbor = sameChunk ? sourceChunk->cells[neighborIndex]
1225 : impl_->get(
x +
ox,
y +
oy);
1226 for (
const std::uint16_t ruleIndex :
1228 const auto& rule = impl_->catalog.reactions()[ruleIndex];
1229 const bool direct = rule.first ==
cell.material && rule.second == neighbor.material;
1230 const bool reverse = rule.second ==
cell.material && rule.first == neighbor.material;
1231 if ((!direct && !reverse) ||
1232 std::max(
cell.temperature, neighbor.temperature) < rule.minimumTemperature)
1234 cell.material = direct ? rule.firstResult : rule.secondResult;
1235 neighbor.material = direct ? rule.secondResult : rule.firstResult;
1236 cell.temperature = std::int16_t(std::clamp<int>(
int(
cell.temperature) + rule.heatDelta,
1238 neighbor.temperature = std::int16_t(std::clamp<int>(
1239 int(neighbor.temperature) + rule.heatDelta, -32768, 32767));
1240 if (
cell.lifetime == 0)
1241 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(
1242 impl_->catalog.definition(
cell.material).defaultLifetime,
1243 std::numeric_limits<std::uint8_t>::max()));
1244 if (neighbor.lifetime == 0)
1245 neighbor.lifetime = std::uint8_t(std::min<std::uint16_t>(
1246 impl_->catalog.definition(neighbor.material).defaultLifetime,
1247 std::numeric_limits<std::uint8_t>::max()));
1249 impl_->putWithinChunk(*sourceChunk, neighborIndex, neighbor,
1252 impl_->put(
x +
ox,
y +
oy, neighbor,
tick.value());
1255 sourceFound = impl_->chunks.find(coord);
1256 if (sourceFound == impl_->chunks.end())
break;
1257 sourceChunk = &sourceFound->second;
1260 ++
stats.cellsChanged;
1264 if (reacted) ++
stats.reactions;
1265 const auto state = impl_->catalog.definition(
cell.material).state;
1267 impl_->putWithinChunk(*sourceChunk, sourceIndex,
cell,
tick.value());
1269 if (
cell.lifetime > 0) {
1271 ++
stats.cellsChanged;
1277 bool moved = impl_->moveOrSwap(
x,
y,
x,
y - 1,
tick.value());
1278 if (!moved) moved = impl_->moveOrSwap(
x,
y,
x +
direction,
y - 1,
tick.value());
1281 stats.cellsChanged += 2;
1283 sourceFound = impl_->chunks.find(coord);
1284 if (sourceFound == impl_->chunks.end())
break;
1285 sourceChunk = &sourceFound->second;
1290 std::vector<ChunkCoord> reclaimed;
1291 for (
auto& [coord, chunk] : impl_->chunks) {
1292 if (chunk.touched) {
1293 chunk.idleTicks = 0;
1294 chunk.active =
true;
1299 if (!chunk.active && chunk.nonAir == 0)
1300 reclaimed.push_back(coord);
1302 for (
const ChunkCoord coord : reclaimed) {
1303 impl_->chunks.erase(coord);
1304 impl_->removedChunks[coord] = impl_->revision + 1;
1305 ++
stats.chunksReclaimed;
1307 if (
stats.cellsChanged != 0 ||
stats.chunksReclaimed != 0) ++impl_->revision;
1308 const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
1309 std::chrono::steady_clock::now() - stepStarted)
1317 return std::move(result).expect(
"PixelWorld::step generated an invalid tick");
1329 return int(std::count_if(impl_->chunks.begin(), impl_->chunks.end(), [](
const auto& entry) {
1330 return entry.second.active;
1336 std::vector<ChunkCoord>
order;
1337 order.reserve(impl_->chunks.size());
1338 for (
const auto& [coord, chunk] : impl_->chunks)
1339 if (chunk.revision > sinceRevision)
order.push_back(coord);
1340 for (
const auto& [coord,
revision] : impl_->removedChunks)
1344 std::vector<PixelChunkSnapshot> snapshots;
1345 snapshots.reserve(
order.size());
1346 for (
const ChunkCoord coord :
order) {
1348 snapshot.
x = coord.x;
1349 snapshot.
y = coord.y;
1350 const auto chunk = impl_->chunks.find(coord);
1351 if (chunk != impl_->chunks.end()) {
1352 snapshot.
revision = chunk->second.revision;
1353 snapshot.
cells.assign(chunk->second.cells.begin(), chunk->second.cells.end());
1355 snapshot.
revision = impl_->removedChunks.at(coord);
1358 snapshots.push_back(std::move(snapshot));
1368 "pixelworld.chunk-region"));
1369 const std::uint64_t
width = std::uint64_t(std::int64_t(region.
maxX) - region.
minX) + 1;
1370 const std::uint64_t
height = std::uint64_t(std::int64_t(region.
maxY) - region.
minY) + 1;
1374 "pixelworld.chunk-region"));
1376 std::vector<PixelChunkSnapshot> result;
1377 for (
int y = region.
minY;; ++
y) {
1378 for (
int x = region.
minX;; ++
x) {
1379 const ChunkCoord coord{
x,
y};
1380 const auto present = impl_->chunks.find(coord);
1381 if (present != impl_->chunks.end() && present->second.revision > sinceRevision) {
1385 snapshot.
revision = present->second.revision;
1386 snapshot.
cells.assign(present->second.cells.begin(), present->second.cells.end());
1387 result.push_back(std::move(snapshot));
1389 const auto removed = impl_->removedChunks.find(coord);
1390 if (
removed != impl_->removedChunks.end() &&
removed->second > sinceRevision)
1391 result.push_back({
x,
y,
removed->second,
true, {}});
1393 if (
x == region.
maxX)
break;
1395 if (
y == region.maxY)
break;
1405 "pixelworld.chunk-diagnostics"));
1406 const std::uint64_t
width = std::uint64_t(std::int64_t(region.
maxX) - region.
minX) + 1;
1407 const std::uint64_t
height = std::uint64_t(std::int64_t(region.
maxY) - region.
minY) + 1;
1411 "pixelworld.chunk-diagnostics"));
1412 std::vector<PixelChunkDiagnostic> result;
1413 for (
int y = region.
minY;; ++
y) {
1414 for (
int x = region.
minX;; ++
x) {
1415 const auto found = impl_->chunks.find({
x,
y});
1416 if (
found != impl_->chunks.end()) {
1417 const Chunk& chunk =
found->second;
1419 20, 20, chunk.idleTicks, chunk.active};
1420 if (chunk.nonAir != 0) {
1421 diagnostic.minimumTemperature = 32767;
1422 diagnostic.maximumTemperature = -32768;
1424 if (
cell.material == MaterialId::Air)
continue;
1425 diagnostic.minimumTemperature = std::min(diagnostic.minimumTemperature,
1427 diagnostic.maximumTemperature = std::max(diagnostic.maximumTemperature,
1431 result.push_back(diagnostic);
1433 if (
x == region.
maxX)
break;
1435 if (
y == region.
maxY)
break;
1446 if (impl_->revision != expectedRevision)
1450 "catalogFingerprint");
1458 if (batch.
chunks.size() > 1'000'000U)
1461 std::optional<ChunkCoord>
previous;
1464 const ChunkCoord coord{snapshot.x, snapshot.y};
1467 "Chunk batch must be unique canonical y/x order",
"chunks");
1469 if (snapshot.revision == 0 || snapshot.revision > batch.
sourceRevision)
1471 "Chunk revision must be positive and no newer than source",
"chunks.revision");
1472 if (snapshot.removed) {
1473 if (!snapshot.cells.empty())
1475 "removed Chunk must not contain cells",
"chunks.cells");
1480 "present Chunk must contain exactly 64x64 cells",
"chunks.cells");
1482 if (std::size_t(
cell.material) >= impl_->catalog.definitions().size())
1484 "Chunk references an unknown material",
"chunks.cells.material");
1485 const auto capacity = impl_->catalog.definition(
cell.material).heatCapacity;
1486 if ((
cell.material == MaterialId::Air &&
cell.thermalRemainder != 0) ||
1489 "Chunk contains a non-canonical thermal remainder",
1490 "chunks.cells.thermalRemainder");
1496 candidate.
worldId = impl_->worldId;
1497 candidate.
epoch = impl_->epoch;
1499 candidate.
paused = impl_->paused;
1503 for (
const auto& snapshot : batch.
chunks) {
1504 const ChunkCoord coord{snapshot.x, snapshot.y};
1505 if (snapshot.removed) {
1506 candidate.
chunks.erase(coord);
1512 std::copy(snapshot.cells.begin(), snapshot.cells.end(), chunk.cells.begin());
1513 chunk.revision = snapshot.revision;
1514 chunk.active =
true;
1515 chunk.touched =
true;
1516 chunk.idleTicks = 0;
1518 candidate.
chunks.insert_or_assign(coord, std::move(chunk));
1528 *impl_ = std::move(candidate);
1533 std::vector<ChunkCoord>
order;
1534 order.reserve(impl_->chunks.size());
1535 for (
const auto& [coord, chunk] : impl_->chunks) {
1537 order.push_back(coord);
1540 std::vector<std::byte> out;
1541 out.insert(out.end(), {std::byte{0x45}, std::byte{0x56}, std::byte{0x50}, std::byte{0x57}});
1542 append<std::uint16_t>(out, 4);
1543 append(out, impl_->catalog.fingerprint());
1544 append(out, impl_->seed);
1545 append(out, impl_->revision);
1546 append(out, impl_->tick);
1547 append(out, impl_->lastEditSequence);
1548 append<std::uint32_t>(out, std::uint32_t(
order.size()));
1549 for (
const ChunkCoord coord :
order) {
1550 append<std::int32_t>(out, coord.x);
1551 append<std::int32_t>(out, coord.y);
1552 const Chunk& chunk = impl_->chunks.at(coord);
1554 append<std::uint16_t>(out, std::uint16_t(
cell.material));
1555 append(out,
cell.temperature);
1556 append(out,
cell.lifetime);
1557 append(out,
cell.thermalRemainder);
1565 if (
bytes.size() < 4 ||
bytes[0] != std::byte{0x45} ||
bytes[1] != std::byte{0x56} ||
1566 bytes[2] != std::byte{0x50} ||
bytes[3] != std::byte{0x57})
1569 std::uint16_t version = 0;
1570 Impl candidate(impl_->catalog);
1571 std::uint32_t
count = 0;
1573 (version != 1 && version != 2 && version != 3 && version != 4))
1576 std::uint64_t fingerprint = 0;
1577 if (!read(
bytes,
cursor, fingerprint) || fingerprint != impl_->catalog.fingerprint())
1579 }
else if (impl_->catalog.fingerprint() != MaterialCatalog::builtIn().fingerprint()) {
1589 for (std::uint32_t i = 0; i <
count; ++i) {
1603 if ((
cell.material == MaterialId::Air &&
cell.thermalRemainder != 0) ||
1607 const auto [ignored, inserted] = candidate.
chunks.emplace(coord, std::move(chunk));
1612 for (
auto& [coord, chunk] : candidate.
chunks) {
1615 chunk.revision = candidate.
revision;
1617 candidate.
worldId = impl_->worldId;
1618 candidate.
epoch = impl_->epoch + 1;
1620 candidate.
paused = impl_->paused;
1621 *impl_ = std::move(candidate);
std::vector< QuestEvent > pending
graphics::Canvas * previous
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::int16_t maximumTemperature
std::array< std::uint16_t, 64 > materialCounts
std::uint32_t materialOverflow
bool temperatureBoundsDirty
std::int16_t minimumTemperature
std::uint32_t thermalRemainderCells
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
RoadLaneDirection direction
Structured operation status used by the common Result foundation.
Battle::Reactions reactions
const UnitySourceAsset & source
std::vector< char > inside
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.
void expect(std::string_view message) const
Require success for a void operation.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
Structured status and zero or more diagnostics for an operation.
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Owning immutable material and reaction table used by one PixelWorld.
const MaterialDefinition & definition(MaterialId id) const noexcept
Resolve a validated id; invalid ids safely project to air.
static MaterialCatalog builtIn()
Construct the engine's canonical built-in catalog.
std::span< const MaterialReactionRule > reactions() const noexcept
Borrow immutable canonical-priority reaction rules.
std::span< const MaterialDefinition > definitions() const noexcept
Borrow immutable definitions until this catalog is destroyed.
std::uint64_t fingerprint() const noexcept
Stable deterministic fingerprint included in world snapshots.
Synchronous executor for owning PixelWorld candidate buffers.
virtual void parallelFor(std::size_t workItems, const std::function< void(std::size_t)> &body)=0
Execute every index in [0, workItems) exactly once and synchronously join.
Sparse, chunked, deterministic 2D falling-material world.
int activeChunkCount() const noexcept
Active chunk count.
std::uint64_t lastEditSequence() const noexcept
Last edit sequence.
eve::Result< PixelEditReceipt > applyEdit(const PixelEditCommand &command)
Validate and atomically apply one strictly sequenced edit command.
std::uint64_t materialCatalogFingerprint() const noexcept
Fingerprint of the immutable catalog that gives cell ids their meaning.
void setMaterial(int x, int y, std::string_view material)
Set one cell from a stable built-in material name.
eve::Result< PixelCatalogReloadReceipt > reloadMaterialCatalog(MaterialCatalog catalog, std::uint64_t expectedFingerprint)
Transactionally hot-reload a compatible Catalog while paused.
eve::Result< void > setMaterialChecked(int x, int y, std::string_view material)
Canonical checked name-based setter; unknown names leave the world unchanged.
PixelEditReceipt explode(int centerX, int centerY, int radius, int strength, std::int16_t temperatureDelta)
Script/demo explosion facade that emits the next command sequence.
std::size_t paintCircle(int centerX, int centerY, int radius, std::string_view material)
Paint a filled material circle and return the number of changed cells.
eve::Result< std::size_t > paintCircleChecked(int centerX, int centerY, int radius, std::string_view material)
Canonical checked circle edit; unknown names leave the world unchanged.
~PixelWorld()
Pixel world.
eve::Result< std::vector< PixelChunkSnapshot > > snapshotChunksInRegion(PixelChunkRegion region, std::uint64_t sinceRevision=0) const
Copy present Chunks and retained tombstones in one finite Chunk-coordinate region.
std::uint64_t seed() const noexcept
Seed.
std::uint64_t tickValue() const noexcept
Tick value.
void setCell(int x, int y, PixelCell cell)
Set one cell and wake its chunk and neighbors. Air never allocates a missing chunk.
PixelWorld & operator=(PixelWorld &&) noexcept
Operator =.
eve::Result< PixelFragmentRasterReceipt > rasterizeFragment(const PixelFragment &fragment, int originX, int originY)
Transactionally rasterize a detached fragment at a new world-space origin.
StepStats step()
Script/demo facade that advances the next integral tick.
bool isPaused() const noexcept
Whether ordinary simulation entry points are currently paused.
std::uint32_t materialDisplayRgba(MaterialId material) const noexcept
Query the Catalog-owned preview/render color encoded as 0xRRGGBBAA.
PixelWorldLink worldLink() const noexcept
Current runtime link used for stale fragment detection.
int chunkCount() const noexcept
Chunk count.
bool isSolidMaterial(MaterialId material) const noexcept
Query whether a validated material id participates in structural solid collision.
PixelWorld(std::uint64_t seed=1)
Construct an empty world with an explicit deterministic seed.
eve::Result< StepStats > advance(eve::SimulationTick tick)
Advance exactly one scheduler-owned tick.
int getMaterial(int x, int y) const noexcept
Convenience query returning the numeric material id.
eve::Result< std::vector< PixelFragment > > extractUnsupportedFragments(PixelRegion region, int supportY, std::uint32_t minimumCells=1)
Atomically detach unsupported solid components inside a finite region.
PixelCell getCell(int x, int y) const noexcept
Return the cell at world coordinates; absent chunks read as air.
std::uint64_t revision() const noexcept
Revision.
eve::Result< StepStats > advanceScheduled(eve::SimulationTick tick, PixelWorkScheduler &scheduler)
Advance one deterministic tick using a synchronous candidate scheduler.
void clear() noexcept
Remove all chunks and reset the simulation tick.
void setPaused(bool paused) noexcept
Pause or resume ordinary simulation entry points without changing state.
std::vector< PixelChunkSnapshot > snapshotChangedChunks(std::uint64_t sinceRevision) const
Copy chunks changed after sinceRevision in canonical y/x order.
PixelWorldControlService & pixelWorldControlService()
Process-lifetime PixelWorld tooling registry.
MaterialId
Compact stable material identifier stored in each authoritative cell.
constexpr std::uint8_t kPixelSleepHysteresisTicks
MaterialState
Broad movement class for one pixel material.
constexpr int kPixelChunkSize
T read(const RuntimeTensor &v, size_t i)
Reads read.
DiagnosticCode
Stable machine-readable diagnostic codes.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
std::uint16_t heatCapacity
Receipt for one transactional compatible material Catalog hot reload.
std::uint64_t fingerprintBefore
bool replayHistoryInvalidated
std::uint64_t revisionAfter
std::uint64_t revisionBefore
std::uint64_t fingerprintAfter
std::uint32_t chunksRebuilt
Compact authoritative state for one simulated world pixel.
std::uint16_t thermalRemainder
Canonical sub-degree thermal energy in [0, material heatCapacity).
Receipt for one all-or-nothing authoritative Chunk batch application.
std::uint64_t revisionBefore
std::uint64_t revisionAfter
std::uint32_t chunksRemoved
std::uint32_t chunksReplaced
Owning authoritative Chunk correction with source world metadata.
std::uint64_t sourceRevision
bool fullResync
Replace the replica projection instead of applying an incremental correction.
std::vector< PixelChunkSnapshot > chunks
std::uint64_t catalogFingerprint
eve::SimulationTick sourceTick
std::uint64_t sourceLastEditSequence
Read-only per-Chunk simulation diagnostics for tooling and overlays.
Inclusive finite rectangle expressed in Chunk coordinates.
Owning, immutable projection of one authoritative simulation chunk.
std::vector< PixelCell > cells
Owning replayable edit command. Sequence must be exactly previous + 1.
std::int16_t temperatureDelta
Deterministic receipt for one atomically accepted edit command.
std::uint64_t revisionAfter
std::uint32_t cellsChanged
std::uint32_t cellsHeated
std::uint64_t revisionBefore
std::uint32_t cellsRemoved
Receipt for one all-or-nothing fragment rasterization.
std::uint32_t cellsPlaced
std::uint64_t revisionAfter
std::uint64_t revisionBefore
Owning material bitmap detached atomically from a PixelWorld.
std::vector< PixelCell > cells
std::uint32_t solidCellCount
Finite inclusive world-space rectangle used by structural queries.
Runtime identity of the world epoch from which a fragment was detached.
MaterialState materialState(MaterialId material) const noexcept
Chunk & ensureChunk(int x, int y)
bool pseudoBit(int x, int y, std::uint64_t currentTick) const noexcept
std::vector< std::uint16_t > heatCapacities
void putWithinChunk(Chunk &chunk, std::size_t index, PixelCell cell, std::uint64_t updatedTick, bool markUpdated=true)
bool isMobile(MaterialId material) const noexcept
void wakeAround(int x, int y)
std::uint8_t thermalConductivity(MaterialId material) const noexcept
void refreshTemperatureBounds(Chunk &chunk) const noexcept
Chunk * findChunk(int x, int y) noexcept
std::vector< std::vector< std::uint16_t > > reactionRulesByPair
const Chunk * findChunk(int x, int y) const noexcept
void put(int x, int y, PixelCell cell, std::uint64_t updatedTick, bool markUpdated=true)
std::vector< std::uint8_t > canDisplaceTable
std::uint16_t heatCapacity(MaterialId material) const noexcept
std::vector< std::uint8_t > thermalConductivities
void rebuildMaterialRuntimeTables()
std::uint64_t nextFragmentId
std::span< const std::uint16_t > reactionRules(MaterialId source, MaterialId target) const noexcept
void addActivity(Chunk &chunk, const PixelCell &cell) const noexcept
std::unordered_map< ChunkCoord, std::uint64_t, ChunkCoordHash > removedChunks
std::size_t materialRuntimeCount
Impl(MaterialCatalog ownedCatalog=MaterialCatalog::builtIn())
std::uint64_t updatedAt(int x, int y) const noexcept
std::unordered_map< ChunkCoord, Chunk, ChunkCoordHash > chunks
std::vector< MaterialState > materialStates
void putNormalizedWithinChunk(Chunk &chunk, std::size_t index, const PixelCell &cell, std::uint64_t updatedTick, bool markUpdated=true)
bool moveOrSwap(int x, int y, int tx, int ty, std::uint64_t currentTick)
PixelCell normalizeCell(PixelCell cell) const noexcept
bool canDisplace(MaterialId source, MaterialId target) const noexcept
PixelCell get(int x, int y) const noexcept
void rebuildActivity(Chunk &chunk) const noexcept
bool moveOrSwapWithinChunk(ChunkCoord coord, Chunk &chunk, int sourceX, int sourceY, PixelCell source, int targetX, int targetY, std::uint64_t currentTick)
bool moveOrSwapKnownSource(int x, int y, PixelCell source, int tx, int ty, std::uint64_t currentTick)
std::uint64_t lastEditSequence
void removeActivity(Chunk &chunk, const PixelCell &cell) const noexcept
Counters produced by one deterministic simulation step.