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.
This commit is contained in:
Tamás Bálint Misius 2021-07-24 18:47:41 +02:00
parent ea07244119
commit eac92d1b04
No known key found for this signature in database
GPG Key ID: 5B472A12F6ECA9F2
7 changed files with 581 additions and 8 deletions

View File

@ -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 &current = gameModel->HistoryCurrent() ? *gameModel->HistoryCurrent() : *beforeRestore;
gameModel->GetSimulation()->Restore(current);
Client::Ref().OverwriteAuthorInfo(current.Authors);

View File

@ -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<Snapshot>(*history[historyPosition]);
if (history[historyPosition].snap)
{
historyCurrent = std::make_unique<Snapshot>(*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<Snapshot>(*history[historyPosition]) : nullptr;
if (historyPosition == history.size())
{
historyCurrent = nullptr;
}
else if (history[historyPosition].snap)
{
historyCurrent = std::make_unique<Snapshot>(*history[historyPosition].snap);
}
else
{
historyCurrent = history[historyPosition - 1U].delta->Forward(*historyCurrent);
}
}
void GameModel::HistoryPush(std::unique_ptr<Snapshot> 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())

View File

@ -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<Snapshot> snap;
std::unique_ptr<SnapshotDelta> delta;
~HistoryEntry();
};
class GameModel
{
private:
@ -65,7 +74,7 @@ private:
Tool * regularToolset[4];
User currentUser;
float toolStrength;
std::deque<std::unique_ptr<Snapshot>> history;
std::deque<HistoryEntry> history;
std::unique_ptr<Snapshot> historyCurrent;
unsigned int historyPosition;
unsigned int undoHistoryLimit;

View File

@ -3,6 +3,8 @@
#include <vector>
#include "Particle.h"
#include "Sign.h"
#include "Stickman.h"
#include "json/json.h"
class Snapshot

View File

@ -0,0 +1,297 @@
#include "SnapshotDelta.h"
#include "common/tpt-compat.h"
#include "common/tpt-minmax.h"
#include <utility>
// * 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<true> 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<false> 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<false> and ApplySingleDiff<true>
// 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<sign> &lhs, const std::vector<sign> &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<class Item>
void FillHunkVectorPtr(const Item *oldItems, const Item *newItems, SnapshotDelta::HunkVector<Item> &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<class Item>
void FillHunkVector(const std::vector<Item> &oldItems, const std::vector<Item> &newItems, SnapshotDelta::HunkVector<Item> &out)
{
FillHunkVectorPtr<Item>(&oldItems[0], &newItems[0], out, std::min(oldItems.size(), newItems.size()));
}
template<class Item>
void FillSingleDiff(const Item &oldItem, const Item &newItem, SnapshotDelta::SingleDiff<Item> &out)
{
if (oldItem != newItem)
{
out.valid = true;
out.diff.oldItem = oldItem;
out.diff.newItem = newItem;
}
}
template<bool UseOld, class Item>
void ApplyHunkVectorPtr(const SnapshotDelta::HunkVector<Item> &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<bool UseOld, class Item>
void ApplyHunkVector(const SnapshotDelta::HunkVector<Item> &in, std::vector<Item> &items)
{
ApplyHunkVectorPtr<UseOld, Item>(in, &items[0]);
}
template<bool UseOld, class Item>
void ApplySingleDiff(const SnapshotDelta::SingleDiff<Item> &in, Item &item)
{
if (in.valid)
{
item = UseOld ? in.diff.oldItem : in.diff.newItem;
}
}
std::unique_ptr<SnapshotDelta> SnapshotDelta::FromSnapshots(const Snapshot &oldSnap, const Snapshot &newSnap)
{
auto ptr = std::make_unique<SnapshotDelta>();
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<const uint32_t *>(&oldSnap.PortalParticles[0]), reinterpret_cast<const uint32_t *>(&newSnap.PortalParticles[0]), delta.PortalParticles, newSnap.PortalParticles.size() * ParticleUint32Count);
FillHunkVectorPtr(reinterpret_cast<const uint32_t *>(&oldSnap.stickmen[0]) , reinterpret_cast<const uint32_t *>(&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<const uint32_t *>(&oldSnap.Particles[0]), reinterpret_cast<const uint32_t *>(&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<Snapshot> SnapshotDelta::Forward(const Snapshot &oldSnap)
{
auto ptr = std::make_unique<Snapshot>(oldSnap);
auto &newSnap = *ptr;
ApplyHunkVector<false>(AirPressure , newSnap.AirPressure );
ApplyHunkVector<false>(AirVelocityX , newSnap.AirVelocityX );
ApplyHunkVector<false>(AirVelocityY , newSnap.AirVelocityY );
ApplyHunkVector<false>(AmbientHeat , newSnap.AmbientHeat );
ApplyHunkVector<false>(GravVelocityX , newSnap.GravVelocityX );
ApplyHunkVector<false>(GravVelocityY , newSnap.GravVelocityY );
ApplyHunkVector<false>(GravValue , newSnap.GravValue );
ApplyHunkVector<false>(GravMap , newSnap.GravMap );
ApplyHunkVector<false>(BlockMap , newSnap.BlockMap );
ApplyHunkVector<false>(ElecMap , newSnap.ElecMap );
ApplyHunkVector<false>(FanVelocityX , newSnap.FanVelocityX );
ApplyHunkVector<false>(FanVelocityY , newSnap.FanVelocityY );
ApplyHunkVector<false>(WirelessData , newSnap.WirelessData );
ApplySingleDiff<false>(signs , newSnap.signs );
ApplySingleDiff<false>(Authors , newSnap.Authors );
ApplyHunkVectorPtr<false>(PortalParticles, reinterpret_cast<uint32_t *>(&newSnap.PortalParticles[0]));
ApplyHunkVectorPtr<false>(stickmen , reinterpret_cast<uint32_t *>(&newSnap.stickmen[0] ));
// * Slightly more interesting; apply the common hunk vector, copy the extra portion separaterly.
ApplyHunkVectorPtr<false>(commonParticles, reinterpret_cast<uint32_t *>(&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<Snapshot> SnapshotDelta::Restore(const Snapshot &newSnap)
{
auto ptr = std::make_unique<Snapshot>(newSnap);
auto &oldSnap = *ptr;
ApplyHunkVector<true>(AirPressure , oldSnap.AirPressure );
ApplyHunkVector<true>(AirVelocityX , oldSnap.AirVelocityX );
ApplyHunkVector<true>(AirVelocityY , oldSnap.AirVelocityY );
ApplyHunkVector<true>(AmbientHeat , oldSnap.AmbientHeat );
ApplyHunkVector<true>(GravVelocityX , oldSnap.GravVelocityX );
ApplyHunkVector<true>(GravVelocityY , oldSnap.GravVelocityY );
ApplyHunkVector<true>(GravValue , oldSnap.GravValue );
ApplyHunkVector<true>(GravMap , oldSnap.GravMap );
ApplyHunkVector<true>(BlockMap , oldSnap.BlockMap );
ApplyHunkVector<true>(ElecMap , oldSnap.ElecMap );
ApplyHunkVector<true>(FanVelocityX , oldSnap.FanVelocityX );
ApplyHunkVector<true>(FanVelocityY , oldSnap.FanVelocityY );
ApplyHunkVector<true>(WirelessData , oldSnap.WirelessData );
ApplySingleDiff<true>(signs , oldSnap.signs );
ApplySingleDiff<true>(Authors , oldSnap.Authors );
ApplyHunkVectorPtr<true>(PortalParticles, reinterpret_cast<uint32_t *>(&oldSnap.PortalParticles[0]));
ApplyHunkVectorPtr<true>(stickmen , reinterpret_cast<uint32_t *>(&oldSnap.stickmen[0] ));
// * Slightly more interesting; apply the common hunk vector, copy the extra portion separaterly.
ApplyHunkVectorPtr<true>(commonParticles, reinterpret_cast<uint32_t *>(&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;
}

View File

@ -0,0 +1,76 @@
#pragma once
#include "Snapshot.h"
#include <memory>
#include <cstdint>
struct SnapshotDelta
{
template<class Item>
struct Diff
{
Item oldItem, newItem;
};
template<class Item>
struct Hunk
{
int offset;
std::vector<Diff<Item>> diffs;
};
template<class Item>
struct SingleDiff
{
bool valid = false;
Diff<Item> diff;
};
template<class Item>
struct HalfHunk
{
int offset;
std::vector<Item> items;
};
template<class Item>
using HunkVector = std::vector<Hunk<Item>>;
template<class Item>
struct HalfHunkVectorPair
{
std::vector<HalfHunk<Item>> oldHunks, newHunks;
};
HunkVector<float> AirPressure;
HunkVector<float> AirVelocityX;
HunkVector<float> AirVelocityY;
HunkVector<float> AmbientHeat;
HunkVector<uint32_t> commonParticles;
std::vector<Particle> extraPartsOld, extraPartsNew;
HunkVector<float> GravVelocityX;
HunkVector<float> GravVelocityY;
HunkVector<float> GravValue;
HunkVector<float> GravMap;
HunkVector<unsigned char> BlockMap;
HunkVector<unsigned char> ElecMap;
HunkVector<float> FanVelocityX;
HunkVector<float> FanVelocityY;
HunkVector<uint32_t> PortalParticles;
HunkVector<int> WirelessData;
HunkVector<uint32_t> stickmen;
SingleDiff<std::vector<sign>> signs;
SingleDiff<Json::Value> Authors;
static std::unique_ptr<SnapshotDelta> FromSnapshots(const Snapshot &oldSnap, const Snapshot &newSnap);
std::unique_ptr<Snapshot> Forward(const Snapshot &oldSnap);
std::unique_ptr<Snapshot> Restore(const Snapshot &newSnap);
};

View File

@ -11,6 +11,7 @@ simulation_files = files(
'SimulationData.cpp',
'ToolClasses.cpp',
'Simulation.cpp',
'SnapshotDelta.cpp',
)
subdir('elements')