From eac92d1b04a522152f462ee22501b2aa3aded61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20B=C3=A1lint=20Misius?= Date: Sat, 24 Jul 2021 18:47:41 +0200 Subject: [PATCH] Optimise undo history memory usage By storing only the differences between Snapshots where possible. NOTE: This may be reverted later if it causes too much trouble. --- src/gui/game/GameController.cpp | 4 + src/gui/game/GameModel.cpp | 198 ++++++++++++++++++++- src/gui/game/GameModel.h | 11 +- src/simulation/Snapshot.h | 2 + src/simulation/SnapshotDelta.cpp | 297 +++++++++++++++++++++++++++++++ src/simulation/SnapshotDelta.h | 76 ++++++++ src/simulation/meson.build | 1 + 7 files changed, 581 insertions(+), 8 deletions(-) create mode 100644 src/simulation/SnapshotDelta.cpp create mode 100644 src/simulation/SnapshotDelta.h diff --git a/src/gui/game/GameController.cpp b/src/gui/game/GameController.cpp index 74a44b82c..8dfc154f1 100644 --- a/src/gui/game/GameController.cpp +++ b/src/gui/game/GameController.cpp @@ -185,6 +185,8 @@ void GameController::HistoryRestore() void GameController::HistorySnapshot() { + // * Calling HistorySnapshot means the user decided to use the current state and + // forfeit the option to go back to whatever they Ctrl+Z'd their way back from. beforeRestore.reset(); gameModel->HistoryPush(gameModel->GetSimulation()->CreateSnapshot()); } @@ -196,6 +198,8 @@ void GameController::HistoryForward() return; } gameModel->HistoryForward(); + // * If gameModel has nothing more to give, we've Ctrl+Y'd our way back to the original + // state; restore this instead, then get rid of it. auto ¤t = gameModel->HistoryCurrent() ? *gameModel->HistoryCurrent() : *beforeRestore; gameModel->GetSimulation()->Restore(current); Client::Ref().OverwriteAuthorInfo(current.Authors); diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index ca981fa2a..1a96f4388 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -27,6 +27,7 @@ #include "simulation/Gravity.h" #include "simulation/Simulation.h" #include "simulation/Snapshot.h" +#include "simulation/SnapshotDelta.h" #include "simulation/ElementClasses.h" #include "simulation/ElementGraphics.h" #include "simulation/ToolClasses.h" @@ -34,6 +35,12 @@ #include "gui/game/DecorationTool.h" #include "gui/interface/Engine.h" +HistoryEntry::~HistoryEntry() +{ + // * Needed because Snapshot and SnapshotDelta are incomplete types in GameModel.h, + // so the default dtor for ~HistoryEntry cannot be generated. +} + GameModel::GameModel(): clipboard(NULL), placeSave(NULL), @@ -550,12 +557,146 @@ int GameModel::GetDecoSpace() return this->decoSpace; } +// * SnapshotDelta d is the difference between the two Snapshots A and B (i.e. d = B - A) +// if auto d = SnapshotDelta::FromSnapshots(A, B). In this case, a Snapshot that is +// identical to B can be constructed from d and A via d.Forward(A) (i.e. B = A + d) +// and a Snapshot that is identical to A can be constructed from d and B via +// d.Restore(B) (i.e. A = B - d). SnapshotDeltas often consume less memory than Snapshots, +// although pathological cases of pairs of Snapshots exist, the SnapshotDelta constructed +// from which actually consumes more than the two snapshots combined. +// * GameModel::history is an N-item deque of HistoryEntry structs, each of which owns either +// a SnapshotDelta, except for history[N-1], which always owns a Snapshot. A logical Snapshot +// accompanies each item in GameModel::history. This logical Snapshot may or may not be +// materialised (present in memory). If an item owns an actual Snapshot, the aforementioned +// logical Snapshot is this materialised Snapshot. If, however, an item owns a SnapshotDelta d, +// the accompanying logical Snapshot A is the Snapshot obtained via A = d.Restore(B), where B +// is the logical Snapshot accompanying the next (at an index that is one higher than the +// index of this item) item in history. Slightly more visually: +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// N - 1 | Snapshot A | Snapshot A | A | +// | | | / | +// N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | +// | | | / | +// N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | +// | | | / | +// N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | +// | | | / | +// ... | ... | ... | ... ... | +// +// * GameModel::historyPosition is an integer in the closed range 0 to N, which is decremented +// by GameModel::HistoryRestore and incremented by GameModel::HistoryForward, by 1 at a time. +// GameModel::historyCurrent "follows" historyPosition such that it always holds a Snapshot +// that is identical to the logical Snapshot of history[historyPosition], except when +// historyPosition = N, in which case it's empty. This following behaviour is achieved either +// by "stepping" historyCurrent by Forwarding and Restoring it via the SnapshotDelta in +// history[historyPosition], cloning the Snapshot in history[historyPosition] into it if +// historyPosition = N-1, or clearing if it historyPosition = N. +// * GameModel::historyCurrent is lost when a new Snapshot item is pushed into GameModel::history. +// This item appears wherever historyPosition currently points, and every other item above it +// is deleted. If historyPosition is below N, this gets rid of the Snapshot in history[N-1]. +// Thus, N is set to historyPosition, after which the new Snapshot is pushed and historyPosition +// is incremented to the new N. +// * Pushing a new Snapshot into the history is a bit involved: +// * If there are no history entries yet, the new Snapshot is simply placed into GameModel::history. +// From now on, we consider cases in which GameModel::history is originally not empty. +// +// === after pushing Snapshot A' into the history +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// 0 | Snapshot A | Snapshot A | A | +// +// * If there were discarded history entries (i.e. the user decided to continue from some state +// which they arrived to via at least one Ctrl+Z), history[N-2] is a SnapshotDelta that when +// Forwarded with the logical Snapshot of history[N-2] yields the logical Snapshot of history[N-1] +// from before the new item was pushed. This is not what we want, so we replace it with a +// SnapshotDelta that is the difference between the logical Snapshot of history[N-2] and the +// Snapshot freshly placed in history[N-1]. +// +// === after pushing Snapshot A' into the history +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// N - 1 | Snapshot A' | Snapshot A' | A' | b needs to be replaced with b', +// | | | / | B+b'=A'; otherwise we'd run +// N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | into problems when trying to +// | | | / | reconstruct B from A' and b +// N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | in HistoryRestore. +// | | | / | +// N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | +// | | | / | +// ... | ... | ... | ... ... | +// +// === after replacing b with b' +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// N - 1 | Snapshot A' | Snapshot A' | A' | +// | | | / | +// N - 2 | SnapshotDelta b'| Snapshot B | B+b'=A' b'-B | +// | | | / | +// N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | +// | | | / | +// N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | +// | | | / | +// ... | ... | ... | ... ... | +// +// * If there weren't any discarded history entries, history[N-2] is now also a Snapshot. Since +// the freshly pushed Snapshot in history[N-1] should be the only Snapshot in history, this is +// replaced with the SnapshotDelta that is the difference between history[N-2] and the Snapshot +// freshly placed in history[N-1]. +// +// === after pushing Snapshot A' into the history +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// N - 1 | Snapshot A' | Snapshot A' | A' | A needs to be converted to a, +// | | | | otherwise Snapshots would litter +// N - 1 | Snapshot A | Snapshot A | A | GameModel::history, which we +// | | | / | want to avoid because they +// N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | waste a ton of memory +// | | | / | +// N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | +// | | | / | +// N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | +// | | | / | +// ... | ... | ... | ... ... | +// +// === after replacing A with a +// +// i | history[i] | the logical Snapshot | relationships | +// | | accompanying history[i] | | +// -------|-----------------|-------------------------|---------------| +// | | | | +// N - 1 | Snapshot A' | Snapshot A' | A' | +// | | | / | +// N - 1 | SnapshotDelta a | Snapshot A | A+a=A' a-A | +// | | | / | +// N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | +// | | | / | +// N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | +// | | | / | +// N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | +// | | | / | +// ... | ... | ... | ... ... | +// +// * After all this, the front of the deque is truncated such that there are on more than +// undoHistoryLimit entries left. + const Snapshot *GameModel::HistoryCurrent() const { - if (historyPosition > history.size()) - { - return nullptr; - } return historyCurrent.get(); } @@ -566,8 +707,19 @@ bool GameModel::HistoryCanRestore() const void GameModel::HistoryRestore() { + if (!HistoryCanRestore()) + { + return; + } historyPosition -= 1U; - historyCurrent = std::make_unique(*history[historyPosition]); + if (history[historyPosition].snap) + { + historyCurrent = std::make_unique(*history[historyPosition].snap); + } + else + { + historyCurrent = history[historyPosition].delta->Restore(*historyCurrent); + } } bool GameModel::HistoryCanForward() const @@ -577,17 +729,49 @@ bool GameModel::HistoryCanForward() const void GameModel::HistoryForward() { + if (!HistoryCanForward()) + { + return; + } historyPosition += 1U; - historyCurrent = historyPosition < history.size() ? std::make_unique(*history[historyPosition]) : nullptr; + if (historyPosition == history.size()) + { + historyCurrent = nullptr; + } + else if (history[historyPosition].snap) + { + historyCurrent = std::make_unique(*history[historyPosition].snap); + } + else + { + historyCurrent = history[historyPosition - 1U].delta->Forward(*historyCurrent); + } } void GameModel::HistoryPush(std::unique_ptr last) { + Snapshot *rebaseOnto = nullptr; + if (historyPosition) + { + rebaseOnto = history.back().snap.get(); + if (historyPosition < history.size()) + { + historyCurrent = history[historyPosition - 1U].delta->Restore(*historyCurrent); + rebaseOnto = historyCurrent.get(); + } + } while (historyPosition < history.size()) { history.pop_back(); } - history.push_back(std::move(last)); + if (rebaseOnto) + { + auto &prev = history.back(); + prev.delta = SnapshotDelta::FromSnapshots(*rebaseOnto, *last); + prev.snap.reset(); + } + history.emplace_back(); + history.back().snap = std::move(last); historyPosition += 1U; historyCurrent.reset(); while (undoHistoryLimit < history.size()) diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index 5801bac4c..d9b0d5d5c 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -22,6 +22,7 @@ class SaveFile; class Simulation; class Renderer; class Snapshot; +struct SnapshotDelta; class GameSave; class ToolSelection @@ -33,6 +34,14 @@ public: }; }; +struct HistoryEntry +{ + std::unique_ptr snap; + std::unique_ptr delta; + + ~HistoryEntry(); +}; + class GameModel { private: @@ -65,7 +74,7 @@ private: Tool * regularToolset[4]; User currentUser; float toolStrength; - std::deque> history; + std::deque history; std::unique_ptr historyCurrent; unsigned int historyPosition; unsigned int undoHistoryLimit; diff --git a/src/simulation/Snapshot.h b/src/simulation/Snapshot.h index c6e0bc8cb..4d26288a9 100644 --- a/src/simulation/Snapshot.h +++ b/src/simulation/Snapshot.h @@ -3,6 +3,8 @@ #include #include "Particle.h" +#include "Sign.h" +#include "Stickman.h" #include "json/json.h" class Snapshot diff --git a/src/simulation/SnapshotDelta.cpp b/src/simulation/SnapshotDelta.cpp new file mode 100644 index 000000000..ec92dacc0 --- /dev/null +++ b/src/simulation/SnapshotDelta.cpp @@ -0,0 +1,297 @@ +#include "SnapshotDelta.h" + +#include "common/tpt-compat.h" +#include "common/tpt-minmax.h" + +#include + +// * A SnapshotDelta is a bidirectional difference type between Snapshots, defined such +// that SnapshotDelta d = SnapshotDelta::FromSnapshots(A, B) yields a SnapshotDelta which can be +// used to construct a Snapshot identical to A via d.Restore(B) and a Snapshot identical +// to B via d.Forward(A). Thus, d = B - A, A = B - d and B = A + d. +// * Fields in Snapshot can be classified into two groups: +// * Fields of static size, whose sizes are identical to the size of the corresponding field +// in all other Snapshots. Example of these fields include AmbientHeat (whose size depends +// on XRES, YRES and CELL, all compile-time constants) and WirelessData (whose size depends +// on CHANNELS, another compile-time constant). Note that these fields would be of "static +// size" even if their sizes weren't derived from compile-time constants, as they'd still +// be the same size throughout the life of a Simulation, and thus any Snapshot created from it. +// * Fields of dynamic size, whose sizes may be different between Snapshots. These are, fortunately, +// the minority: Particles, signs and Authors. +// * Each field in Snapshot has a mirror set of fields in SnapshotDelta. Fields of static size +// have mirror fields whose type is HunkVector, templated by the item type of the +// corresponding field; these fields are handled in a uniform manner. Fields of dynamic size are +// handled in a non-uniform, case-by-case manner. +// * A HunkVector is generated from two streams of identical size and is a collection +// of Hunks, a Hunk is an offset combined with a collection of Diffs, and a Diff is a pair of values, +// one originating from one stream and the other from the other. Thus, Hunks represent contiguous +// sequences of differences between the two streams, and a HunkVector is a compact way to represent +// all differences between the two streams it's generated from. In this case, these streams are +// the data in corresponding fields of static size in two Snapshots, and the HunkVector is the +// respective field in the SnapshotDelta that is the difference between the two Snapshots. +// * FillHunkVectorPtr is the d = B - A operation, which takes two Snapshot fields of static size and +// the corresponding SnapshotDelta field, and fills the latter with the HunkVector generated +// from the former streams. +// * ApplyHunkVector is the A = B - d operation, which takes a field of a SnapshotDelta and +// the corresponding field of a "newer" Snapshot, and fills the latter with the "old" values. +// * ApplyHunkVector is the B = A + d operation, which takes a field of a SnapshotDelta and +// the corresponding field of an "older" Snapshot, and fills the latter with the "new" values. +// * This difference type is intended for fields of static size. This covers all fields in Snapshot +// except for Particles, signs, and Authors. +// * A SingleDiff is, unsurprisingly enough, a single Diff, with an accompanying bool that signifies +// whether the Diff does in fact hold the "old" value of a field in the "old" Snapshot and the "new" +// value of the same field in the "new" Snapshot. If this bool is false, the data in the fields +// of both Snapshots are equivalent and the SingleDiff should be ignored. If it's true, the +// SingleDiff represents the difference between these fields. +// * FillSingleDiff is the d = B - A operation, while ApplySingleDiff and ApplySingleDiff +// are the A = B - d and B = A + d operations. These are self-explanatory. +// * This difference type is intended for fields of dynamic size whose data doesn't change often and +// doesn't consume too much memory. This covers the Snapshot fields signs and Authors. +// * This leaves Snapshot::Particles. This field mirrors Simulation::parts, which is actually also +// a field of static size, but since most of the time most of this array is empty, it doesn't make +// sense to store all of it in a Snapshot (unlike Air::hv, which can be fairly chaotic (i.e. may have +// a lot of interesting data in all of its cells) when ambient heat simulation is enabled, or +// Simulation::wireless, which is not big enough to need compression). This makes Snapshots smaller, +// but the life of a SnapshotDelta developer harder. The following, relatively simple approach is +// taken, as a sort of compromise between simplicity and memory usage: +// * The common part of the Particles arrays in the old and the new Snapshots is identified: this is +// the overlapping part, i.e. the first size cells of both arrays, where +// size = min(old.Particles.size(), new.Particles.size()), and a HunkVector is generated from it, +// as though it was a field of static size. For our purposes, it is indeed Static Enough:tm:, for +// it only needs to be the same size as the common part of the Particles arrays of the two Snapshots. +// * The rest of both Particles arrays is copied to the extra fields extraPartsOld and extraPartsNew. +// * One more trick is at work here: Particle structs are actually compared property-by-property rather +// than as a whole. This ends up being beneficial to memory usage, as many properties (e.g. type +// and ctype) don't often change over time, while others (e.g. x and y) do. Currently, all Particle +// properties are 4-byte integral values, which makes it feasible to just reinterpret_cast Particle +// structs as arrays of uint32_t values and generate HunkVectors from the resulting streams instead. +// This assumption is enforced by the following static_asserts. The same trick is used for playerst +// structs, even though Snapshot::stickmen is not big enough for us to benefit from this. The +// alternative would have been to implement operator ==(const playerst &, const playerst &), which +// would have been tedious. + +constexpr size_t ParticleUint32Count = sizeof(Particle) / sizeof(uint32_t); +static_assert(sizeof(Particle) % sizeof(uint32_t) == 0, "fix me"); + +constexpr size_t playerstUint32Count = sizeof(playerst) / sizeof(uint32_t); +static_assert(sizeof(playerst) % sizeof(uint32_t) == 0, "fix me"); + +// * Needed by FillHunkVector for handling Snapshot::stickmen. +bool operator ==(const playerst &lhs, const playerst &rhs) +{ + auto match = true; + for (auto i = 0U; i < 16U; ++i) + { + match = match && lhs.legs[i] == rhs.legs[i]; + } + for (auto i = 0U; i < 8U; ++i) + { + match = match && lhs.accs[i] == rhs.accs[i]; + } + return match && + lhs.comm == rhs.comm && + lhs.pcomm == rhs.pcomm && + lhs.elem == rhs.elem && + lhs.spwn == rhs.spwn && + lhs.frames == rhs.frames && + lhs.rocketBoots == rhs.rocketBoots && + lhs.fan == rhs.fan && + lhs.spawnID == rhs.spawnID; +} + +// * Needed by FillSingleDiff for handling Snapshot::signs. +bool operator ==(const std::vector &lhs, const std::vector &rhs) +{ + if (lhs.size() != rhs.size()) + { + return false; + } + for (auto i = 0U; i < lhs.size(); ++i) + { + if (!(lhs[i].x == rhs[i].x && + lhs[i].y == rhs[i].y && + lhs[i].ju == rhs[i].ju && + lhs[i].text == rhs[i].text)) + { + return false; + } + } + return true; +} + +template +void FillHunkVectorPtr(const Item *oldItems, const Item *newItems, SnapshotDelta::HunkVector &out, size_t size) +{ + auto i = 0U; + bool different = false; + auto offset = 0U; + auto markDifferent = [oldItems, newItems, &out, &i, &different, &offset](bool mark) { + if (mark && !different) + { + different = true; + offset = i; + } + else if (!mark && different) + { + different = false; + auto size = i - offset; + out.emplace_back(); + auto &hunk = out.back(); + hunk.offset = offset; + auto &diffs = hunk.diffs; + diffs.resize(size); + for (auto j = 0U; j < size; ++j) + { + diffs[j].oldItem = oldItems[offset + j]; + diffs[j].newItem = newItems[offset + j]; + } + } + }; + while (i < size) + { + markDifferent(!(oldItems[i] == newItems[i])); + i += 1U; + } + markDifferent(false); +} + +template +void FillHunkVector(const std::vector &oldItems, const std::vector &newItems, SnapshotDelta::HunkVector &out) +{ + FillHunkVectorPtr(&oldItems[0], &newItems[0], out, std::min(oldItems.size(), newItems.size())); +} + +template +void FillSingleDiff(const Item &oldItem, const Item &newItem, SnapshotDelta::SingleDiff &out) +{ + if (oldItem != newItem) + { + out.valid = true; + out.diff.oldItem = oldItem; + out.diff.newItem = newItem; + } +} + +template +void ApplyHunkVectorPtr(const SnapshotDelta::HunkVector &in, Item *items) +{ + for (auto &hunk : in) + { + auto offset = hunk.offset; + auto &diffs = hunk.diffs; + for (auto j = 0U; j < diffs.size(); ++j) + { + items[offset + j] = UseOld ? diffs[j].oldItem : diffs[j].newItem; + } + } +} + +template +void ApplyHunkVector(const SnapshotDelta::HunkVector &in, std::vector &items) +{ + ApplyHunkVectorPtr(in, &items[0]); +} + +template +void ApplySingleDiff(const SnapshotDelta::SingleDiff &in, Item &item) +{ + if (in.valid) + { + item = UseOld ? in.diff.oldItem : in.diff.newItem; + } +} + +std::unique_ptr SnapshotDelta::FromSnapshots(const Snapshot &oldSnap, const Snapshot &newSnap) +{ + auto ptr = std::make_unique(); + auto &delta = *ptr; + FillHunkVector(oldSnap.AirPressure , newSnap.AirPressure , delta.AirPressure ); + FillHunkVector(oldSnap.AirVelocityX , newSnap.AirVelocityX , delta.AirVelocityX ); + FillHunkVector(oldSnap.AirVelocityY , newSnap.AirVelocityY , delta.AirVelocityY ); + FillHunkVector(oldSnap.AmbientHeat , newSnap.AmbientHeat , delta.AmbientHeat ); + FillHunkVector(oldSnap.GravVelocityX , newSnap.GravVelocityX , delta.GravVelocityX ); + FillHunkVector(oldSnap.GravVelocityY , newSnap.GravVelocityY , delta.GravVelocityY ); + FillHunkVector(oldSnap.GravValue , newSnap.GravValue , delta.GravValue ); + FillHunkVector(oldSnap.GravMap , newSnap.GravMap , delta.GravMap ); + FillHunkVector(oldSnap.BlockMap , newSnap.BlockMap , delta.BlockMap ); + FillHunkVector(oldSnap.ElecMap , newSnap.ElecMap , delta.ElecMap ); + FillHunkVector(oldSnap.FanVelocityX , newSnap.FanVelocityX , delta.FanVelocityX ); + FillHunkVector(oldSnap.FanVelocityY , newSnap.FanVelocityY , delta.FanVelocityY ); + FillHunkVector(oldSnap.WirelessData , newSnap.WirelessData , delta.WirelessData ); + FillSingleDiff(oldSnap.signs , newSnap.signs , delta.signs ); + FillSingleDiff(oldSnap.Authors , newSnap.Authors , delta.Authors ); + FillHunkVectorPtr(reinterpret_cast(&oldSnap.PortalParticles[0]), reinterpret_cast(&newSnap.PortalParticles[0]), delta.PortalParticles, newSnap.PortalParticles.size() * ParticleUint32Count); + FillHunkVectorPtr(reinterpret_cast(&oldSnap.stickmen[0]) , reinterpret_cast(&newSnap.stickmen[0] ), delta.stickmen , newSnap.stickmen .size() * playerstUint32Count); + + // * Slightly more interesting; this will only diff the common parts, the rest is copied separately. + auto commonSize = std::min(oldSnap.Particles.size(), newSnap.Particles.size()); + FillHunkVectorPtr(reinterpret_cast(&oldSnap.Particles[0]), reinterpret_cast(&newSnap.Particles[0]), delta.commonParticles, commonSize * ParticleUint32Count); + delta.extraPartsOld.resize(oldSnap.Particles.size() - commonSize); + std::copy(oldSnap.Particles.begin() + commonSize, oldSnap.Particles.end(), delta.extraPartsOld.begin()); + delta.extraPartsNew.resize(newSnap.Particles.size() - commonSize); + std::copy(newSnap.Particles.begin() + commonSize, newSnap.Particles.end(), delta.extraPartsNew.begin()); + + return ptr; +} + +std::unique_ptr SnapshotDelta::Forward(const Snapshot &oldSnap) +{ + auto ptr = std::make_unique(oldSnap); + auto &newSnap = *ptr; + ApplyHunkVector(AirPressure , newSnap.AirPressure ); + ApplyHunkVector(AirVelocityX , newSnap.AirVelocityX ); + ApplyHunkVector(AirVelocityY , newSnap.AirVelocityY ); + ApplyHunkVector(AmbientHeat , newSnap.AmbientHeat ); + ApplyHunkVector(GravVelocityX , newSnap.GravVelocityX ); + ApplyHunkVector(GravVelocityY , newSnap.GravVelocityY ); + ApplyHunkVector(GravValue , newSnap.GravValue ); + ApplyHunkVector(GravMap , newSnap.GravMap ); + ApplyHunkVector(BlockMap , newSnap.BlockMap ); + ApplyHunkVector(ElecMap , newSnap.ElecMap ); + ApplyHunkVector(FanVelocityX , newSnap.FanVelocityX ); + ApplyHunkVector(FanVelocityY , newSnap.FanVelocityY ); + ApplyHunkVector(WirelessData , newSnap.WirelessData ); + ApplySingleDiff(signs , newSnap.signs ); + ApplySingleDiff(Authors , newSnap.Authors ); + ApplyHunkVectorPtr(PortalParticles, reinterpret_cast(&newSnap.PortalParticles[0])); + ApplyHunkVectorPtr(stickmen , reinterpret_cast(&newSnap.stickmen[0] )); + + // * Slightly more interesting; apply the common hunk vector, copy the extra portion separaterly. + ApplyHunkVectorPtr(commonParticles, reinterpret_cast(&newSnap.Particles[0])); + auto commonSize = oldSnap.Particles.size() - extraPartsOld.size(); + newSnap.Particles.resize(commonSize + extraPartsNew.size()); + std::copy(extraPartsNew.begin(), extraPartsNew.end(), newSnap.Particles.begin() + commonSize); + + return ptr; +} + +std::unique_ptr SnapshotDelta::Restore(const Snapshot &newSnap) +{ + auto ptr = std::make_unique(newSnap); + auto &oldSnap = *ptr; + ApplyHunkVector(AirPressure , oldSnap.AirPressure ); + ApplyHunkVector(AirVelocityX , oldSnap.AirVelocityX ); + ApplyHunkVector(AirVelocityY , oldSnap.AirVelocityY ); + ApplyHunkVector(AmbientHeat , oldSnap.AmbientHeat ); + ApplyHunkVector(GravVelocityX , oldSnap.GravVelocityX ); + ApplyHunkVector(GravVelocityY , oldSnap.GravVelocityY ); + ApplyHunkVector(GravValue , oldSnap.GravValue ); + ApplyHunkVector(GravMap , oldSnap.GravMap ); + ApplyHunkVector(BlockMap , oldSnap.BlockMap ); + ApplyHunkVector(ElecMap , oldSnap.ElecMap ); + ApplyHunkVector(FanVelocityX , oldSnap.FanVelocityX ); + ApplyHunkVector(FanVelocityY , oldSnap.FanVelocityY ); + ApplyHunkVector(WirelessData , oldSnap.WirelessData ); + ApplySingleDiff(signs , oldSnap.signs ); + ApplySingleDiff(Authors , oldSnap.Authors ); + ApplyHunkVectorPtr(PortalParticles, reinterpret_cast(&oldSnap.PortalParticles[0])); + ApplyHunkVectorPtr(stickmen , reinterpret_cast(&oldSnap.stickmen[0] )); + + // * Slightly more interesting; apply the common hunk vector, copy the extra portion separaterly. + ApplyHunkVectorPtr(commonParticles, reinterpret_cast(&oldSnap.Particles[0])); + auto commonSize = newSnap.Particles.size() - extraPartsNew.size(); + oldSnap.Particles.resize(commonSize + extraPartsOld.size()); + std::copy(extraPartsOld.begin(), extraPartsOld.end(), oldSnap.Particles.begin() + commonSize); + + return ptr; +} diff --git a/src/simulation/SnapshotDelta.h b/src/simulation/SnapshotDelta.h new file mode 100644 index 000000000..1ceb9397b --- /dev/null +++ b/src/simulation/SnapshotDelta.h @@ -0,0 +1,76 @@ +#pragma once + +#include "Snapshot.h" + +#include +#include + +struct SnapshotDelta +{ + template + struct Diff + { + Item oldItem, newItem; + }; + + template + struct Hunk + { + int offset; + std::vector> diffs; + }; + + template + struct SingleDiff + { + bool valid = false; + Diff diff; + }; + + template + struct HalfHunk + { + int offset; + std::vector items; + }; + + template + using HunkVector = std::vector>; + + template + struct HalfHunkVectorPair + { + std::vector> oldHunks, newHunks; + }; + + HunkVector AirPressure; + HunkVector AirVelocityX; + HunkVector AirVelocityY; + HunkVector AmbientHeat; + + HunkVector commonParticles; + std::vector extraPartsOld, extraPartsNew; + + HunkVector GravVelocityX; + HunkVector GravVelocityY; + HunkVector GravValue; + HunkVector GravMap; + + HunkVector BlockMap; + HunkVector ElecMap; + + HunkVector FanVelocityX; + HunkVector FanVelocityY; + + + HunkVector PortalParticles; + HunkVector WirelessData; + HunkVector stickmen; + SingleDiff> signs; + + SingleDiff Authors; + + static std::unique_ptr FromSnapshots(const Snapshot &oldSnap, const Snapshot &newSnap); + std::unique_ptr Forward(const Snapshot &oldSnap); + std::unique_ptr Restore(const Snapshot &newSnap); +}; diff --git a/src/simulation/meson.build b/src/simulation/meson.build index a81cc12ab..adb49630d 100644 --- a/src/simulation/meson.build +++ b/src/simulation/meson.build @@ -11,6 +11,7 @@ simulation_files = files( 'SimulationData.cpp', 'ToolClasses.cpp', 'Simulation.cpp', + 'SnapshotDelta.cpp', ) subdir('elements')