#include "UndoRedo.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #define CEREAL_FUTURE_EXPERIMENTAL #include #include #include #include #include #ifndef NDEBUG // #define SLIC3R_UNDOREDO_DEBUG #endif /* NDEBUG */ #if 0 // Stop at a fraction of the normal Undo / Redo stack size. #define UNDO_REDO_DEBUG_LOW_MEM_FACTOR 10000 #else #define UNDO_REDO_DEBUG_LOW_MEM_FACTOR 1 #endif namespace Slic3r { namespace UndoRedo { SnapshotData::SnapshotData() : printer_technology(ptUnknown), flags(0), layer_range_idx(-1) { } static std::string topmost_snapshot_name = "@@@ Topmost @@@"; bool Snapshot::is_topmost() const { return this->name == topmost_snapshot_name; } // Time interval, start is closed, end is open. struct Interval { public: Interval(size_t begin, size_t end) : m_begin(begin), m_end(end) {} size_t begin() const { return m_begin; } size_t end() const { return m_end; } bool is_valid() const { return m_begin >= 0 && m_begin < m_end; } // This interval comes strictly before the rhs interval. bool strictly_before(const Interval &rhs) const { return this->is_valid() && rhs.is_valid() && m_end <= rhs.m_begin; } // This interval comes strictly after the rhs interval. bool strictly_after(const Interval &rhs) const { return this->is_valid() && rhs.is_valid() && rhs.m_end <= m_begin; } bool operator<(const Interval &rhs) const { return (m_begin < rhs.m_begin) || (m_begin == rhs.m_begin && m_end < rhs.m_end); } bool operator==(const Interval &rhs) const { return m_begin == rhs.m_begin && m_end == rhs.m_end; } void trim_begin(size_t new_begin) { m_begin = std::max(m_begin, new_begin); } void trim_end(size_t new_end) { m_end = std::min(m_end, new_end); } void extend_end(size_t new_end) { assert(new_end >= m_end); m_end = new_end; } size_t memsize() const { return sizeof(this); } private: size_t m_begin; size_t m_end; }; // History of a single object tracked by the Undo / Redo stack. The object may be mutable or immutable. class ObjectHistoryBase { public: virtual ~ObjectHistoryBase() {} // Is the object captured by this history mutable or immutable? virtual bool is_mutable() const = 0; virtual bool is_immutable() const = 0; // The object is optional, it may be released if the Undo / Redo stack memory grows over the limits. virtual bool is_optional() const { return false; } // If it is an immutable object, return its pointer. There is a map assigning a temporary ObjectID to the immutable object pointer. virtual const void* immutable_object_ptr() const { return nullptr; } // If the history is empty, the ObjectHistory object could be released. virtual bool empty() = 0; // Release all data before the given timestamp. For the ImmutableObjectHistory, the shared pointer is NOT released. // Return the amount of memory released. virtual size_t release_before_timestamp(size_t timestamp) = 0; // Release all data after the given timestamp. For the ImmutableObjectHistory, the shared pointer is NOT released. // Return the amount of memory released. virtual size_t release_after_timestamp(size_t timestamp) = 0; // Release all optional data of this history. virtual size_t release_optional() = 0; // Restore optional data possibly released by release_optional. virtual void restore_optional() = 0; // Estimated size in memory, to be used to drop least recently used snapshots. virtual size_t memsize() const = 0; #ifdef SLIC3R_UNDOREDO_DEBUG // Human readable debug information. virtual std::string format() = 0; #endif /* SLIC3R_UNDOREDO_DEBUG */ #ifndef NDEBUG virtual bool valid() = 0; #endif /* NDEBUG */ }; template class ObjectHistory : public ObjectHistoryBase { public: ~ObjectHistory() override {} // If the history is empty, the ObjectHistory object could be released. bool empty() override { return m_history.empty(); } // Release all data before the given timestamp. For the ImmutableObjectHistory, the shared pointer is NOT released. size_t release_before_timestamp(size_t timestamp) override { size_t mem_released = 0; if (! m_history.empty()) { assert(this->valid()); // it points to an interval which either starts with timestamp, or follows the timestamp. auto it = std::lower_bound(m_history.begin(), m_history.end(), T(timestamp, timestamp)); // Find the first iterator with begin() < timestamp. if (it == m_history.end()) -- it; while (it != m_history.begin() && it->begin() >= timestamp) -- it; if (it->begin() < timestamp && it->end() > timestamp) { it->trim_begin(timestamp); if (it != m_history.begin()) -- it; } if (it->end() <= timestamp) { auto it_end = ++ it; for (it = m_history.begin(); it != it_end; ++ it) mem_released += it->memsize(); m_history.erase(m_history.begin(), it_end); } assert(this->valid()); } return mem_released; } // Release all data after the given timestamp. The shared pointer is NOT released. size_t release_after_timestamp(size_t timestamp) override { size_t mem_released = 0; if (! m_history.empty()) { assert(this->valid()); // it points to an interval which either starts with timestamp, or follows the timestamp. auto it = std::lower_bound(m_history.begin(), m_history.end(), T(timestamp, timestamp)); if (it != m_history.begin()) { auto it_prev = it; -- it_prev; assert(it_prev->begin() < timestamp); // Trim the last interval with timestamp. it_prev->trim_end(timestamp); } for (auto it2 = it; it2 != m_history.end(); ++ it2) mem_released += it2->memsize(); m_history.erase(it, m_history.end()); assert(this->valid()); } return mem_released; } protected: std::vector m_history; }; // Big objects (mainly the triangle meshes) are tracked by Slicer using the shared pointers // and they are immutable. // The Undo / Redo stack therefore may keep a shared pointer to these immutable objects // and as long as the ref counter of these objects is higher than 1 (1 reference is held // by the Undo / Redo stack), there is no cost associated to holding the object // at the Undo / Redo stack. Once the reference counter drops to 1 (only the Undo / Redo // stack holds the reference), the shared pointer may get serialized (and possibly compressed) // and the shared pointer may be released. // The history of a single immutable object may not be continuous, as an immutable object may // be removed from the scene while being kept at the Copy / Paste stack. template class ImmutableObjectHistory : public ObjectHistory { public: ImmutableObjectHistory(std::shared_ptr shared_object, bool optional) : m_shared_object(shared_object), m_optional(optional) {} ~ImmutableObjectHistory() override {} bool is_mutable() const override { return false; } bool is_immutable() const override { return true; } bool is_optional() const override { return m_optional; } // If it is an immutable object, return its pointer. There is a map assigning a temporary ObjectID to the immutable object pointer. const void* immutable_object_ptr() const { return (const void*)m_shared_object.get(); } // Estimated size in memory, to be used to drop least recently used snapshots. size_t memsize() const override { size_t memsize = sizeof(*this); if (this->is_serialized()) memsize += m_serialized.size(); else if (m_shared_object.use_count() == 1) // Only count the shared object's memsize into the total Undo / Redo stack memsize if it is referenced from the Undo / Redo stack only. memsize += m_shared_object->memsize(); memsize += m_history.size() * sizeof(Interval); return memsize; } void save(size_t active_snapshot_time, size_t current_time) { assert(m_history.empty() || m_history.back().end() <= active_snapshot_time || // The snapshot of an immutable object may have already been taken from another mutable object. (m_history.back().begin() <= active_snapshot_time && m_history.back().end() == current_time + 1)); if (m_history.empty() || m_history.back().end() < active_snapshot_time) m_history.emplace_back(active_snapshot_time, current_time + 1); else m_history.back().extend_end(current_time + 1); } bool has_snapshot(size_t timestamp) { if (m_history.empty()) return false; auto it = std::lower_bound(m_history.begin(), m_history.end(), Interval(timestamp, timestamp)); if (it == m_history.end() || it->begin() > timestamp) { if (it == m_history.begin()) return false; -- it; } return timestamp >= it->begin() && timestamp < it->end(); } // Release all optional data of this history. size_t release_optional() override { size_t mem_released = 0; if (m_optional) { bool released = false; if (this->is_serialized()) { mem_released += m_serialized.size(); m_serialized.clear(); released = true; } else if (m_shared_object.use_count() == 1) { mem_released += m_shared_object->memsize(); m_shared_object.reset(); released = true; } if (released) { mem_released += m_history.size() * sizeof(Interval); m_history.clear(); } } else if (m_shared_object.use_count() == 1) { // The object is in memory, but it is not shared with the scene. Let the object decide whether there is any optional data to release. const_cast(m_shared_object.get())->release_optional(); } return mem_released; } // Restore optional data possibly released by this->release_optional(). void restore_optional() override { if (m_shared_object.use_count() == 1) const_cast(m_shared_object.get())->restore_optional(); } bool is_serialized() const { return m_shared_object.get() == nullptr; } const std::string& serialized_data() const { return m_serialized; } std::shared_ptr& shared_ptr(StackImpl &stack); #ifdef SLIC3R_UNDOREDO_DEBUG std::string format() override { std::string out = typeid(T).name(); out += this->is_serialized() ? std::string(" len:") + std::to_string(m_serialized.size()) : std::string(" shared_ptr:") + ptr_to_string(m_shared_object.get()); for (const Interval &interval : m_history) out += std::string(", <") + std::to_string(interval.begin()) + "," + std::to_string(interval.end()) + ")"; return out; } #endif /* SLIC3R_UNDOREDO_DEBUG */ #ifndef NDEBUG bool valid() override; #endif /* NDEBUG */ private: // Either the source object is held by a shared pointer and the m_serialized field is empty, // or the shared pointer is null and the object is being serialized into m_serialized. std::shared_ptr m_shared_object; // If this object is optional, then it may be deleted from the Undo / Redo stack and recalculated from other data (for example mesh convex hull). bool m_optional; std::string m_serialized; }; struct MutableHistoryInterval { private: struct Data { // Reference counter of this data chunk. We may have used shared_ptr, but the shared_ptr is thread safe // with the associated cost of CPU cache invalidation on refcount change. size_t refcnt; size_t size; char data[1]; bool matches(const std::string& rhs) { return this->size == rhs.size() && memcmp(this->data, rhs.data(), this->size) == 0; } }; Interval m_interval; Data *m_data; public: MutableHistoryInterval(const Interval &interval, const std::string &input_data) : m_interval(interval), m_data(nullptr) { m_data = (Data*)new char[offsetof(Data, data) + input_data.size()]; m_data->refcnt = 1; m_data->size = input_data.size(); memcpy(m_data->data, input_data.data(), input_data.size()); } MutableHistoryInterval(const Interval &interval, MutableHistoryInterval &other) : m_interval(interval), m_data(other.m_data) { ++ m_data->refcnt; } // as a key for std::lower_bound MutableHistoryInterval(const size_t begin, const size_t end) : m_interval(begin, end), m_data(nullptr) {} MutableHistoryInterval(MutableHistoryInterval&& rhs) : m_interval(rhs.m_interval), m_data(rhs.m_data) { rhs.m_data = nullptr; } MutableHistoryInterval& operator=(MutableHistoryInterval&& rhs) { m_interval = rhs.m_interval; m_data = rhs.m_data; rhs.m_data = nullptr; return *this; } ~MutableHistoryInterval() { if (m_data != nullptr && -- m_data->refcnt == 0) delete[] (char*)m_data; } const Interval& interval() const { return m_interval; } size_t begin() const { return m_interval.begin(); } size_t end() const { return m_interval.end(); } void trim_begin(size_t timestamp) { m_interval.trim_begin(timestamp); } void trim_end (size_t timestamp) { m_interval.trim_end(timestamp); } void extend_end(size_t timestamp) { m_interval.extend_end(timestamp); } bool operator<(const MutableHistoryInterval& rhs) const { return m_interval < rhs.m_interval; } bool operator==(const MutableHistoryInterval& rhs) const { return m_interval == rhs.m_interval; } const char* data() const { return m_data->data; } size_t size() const { return m_data->size; } size_t refcnt() const { return m_data->refcnt; } bool matches(const std::string& data) { return m_data->matches(data); } size_t memsize() const { return m_data->refcnt == 1 ? // Count just the size of the snapshot data. m_data->size : // Count the size of the snapshot data divided by the number of references, rounded up. (m_data->size + m_data->refcnt - 1) / m_data->refcnt; } private: MutableHistoryInterval(const MutableHistoryInterval &rhs); MutableHistoryInterval& operator=(const MutableHistoryInterval &rhs); }; #ifdef SLIC3R_UNDOREDO_DEBUG static inline std::string ptr_to_string(const void* ptr) { char buf[64]; sprintf(buf, "%p", ptr); return buf; } #endif // Smaller objects (Model, ModelObject, ModelInstance, ModelVolume, DynamicPrintConfig) // are mutable and there is not tracking of the changes, therefore a snapshot needs to be // taken every time and compared to the previous data at the Undo / Redo stack. // The serialized data is stored if it is different from the last value on the stack, otherwise // the serialized data is discarded. // The history of a single mutable object may not be continuous, as an mutable object may // be removed from the scene while being kept at the Copy / Paste stack, therefore an object snapshot // with the same serialized object data may be shared by multiple history intervals. template class MutableObjectHistory : public ObjectHistory { public: ~MutableObjectHistory() override {} bool is_mutable() const override { return true; } bool is_immutable() const override { return false; } // Estimated size in memory, to be used to drop least recently used snapshots. size_t memsize() const override { size_t memsize = sizeof(*this); memsize += m_history.size() * sizeof(MutableHistoryInterval); for (const MutableHistoryInterval &interval : m_history) memsize += interval.memsize(); return memsize; } void save(size_t active_snapshot_time, size_t current_time, const std::string &data) { assert(m_history.empty() || m_history.back().end() <= active_snapshot_time); if (m_history.empty() || m_history.back().end() < active_snapshot_time) { if (! m_history.empty() && m_history.back().matches(data)) // Share the previous data by reference counting. m_history.emplace_back(Interval(current_time, current_time + 1), m_history.back()); else // Allocate new data. m_history.emplace_back(Interval(current_time, current_time + 1), data); } else { assert(! m_history.empty()); assert(m_history.back().end() == active_snapshot_time); if (m_history.back().matches(data)) // Just extend the last interval using the old data. m_history.back().extend_end(current_time + 1); else // Allocate new data time continuous with the previous data. m_history.emplace_back(Interval(active_snapshot_time, current_time + 1), data); } } std::string load(size_t timestamp) const { assert(! m_history.empty()); auto it = std::lower_bound(m_history.begin(), m_history.end(), MutableHistoryInterval(timestamp, timestamp)); if (it == m_history.end() || it->begin() > timestamp) { assert(it != m_history.begin()); -- it; } assert(timestamp >= it->begin() && timestamp < it->end()); return std::string(it->data(), it->data() + it->size()); } // Currently all mutable snapshots are mandatory. size_t release_optional() override { return 0; } // Currently there is no way to release optional data from the mutable objects. void restore_optional() override {} #ifdef SLIC3R_UNDOREDO_DEBUG std::string format() override { std::string out = typeid(T).name(); for (const MutableHistoryInterval &interval : m_history) out += std::string(", ptr:") + ptr_to_string(interval.data()) + " len:" + std::to_string(interval.size()) + " <" + std::to_string(interval.begin()) + "," + std::to_string(interval.end()) + ")"; return out; } #endif /* SLIC3R_UNDOREDO_DEBUG */ #ifndef NDEBUG bool valid() override; #endif /* NDEBUG */ }; #ifndef NDEBUG template bool ImmutableObjectHistory::valid() { // The immutable object content is captured either by a shared object, or by its serialization, but not both. assert(! m_shared_object == ! m_serialized.empty()); // Verify that the history intervals are sorted and do not overlap. if (! m_history.empty()) for (size_t i = 1; i < m_history.size(); ++ i) assert(m_history[i - 1].strictly_before(m_history[i])); return true; } #endif /* NDEBUG */ #ifndef NDEBUG template bool MutableObjectHistory::valid() { // Verify that the history intervals are sorted and do not overlap, and that the data reference counters are correct. if (! m_history.empty()) { std::map refcntrs; assert(m_history.front().data() != nullptr); ++ refcntrs[m_history.front().data()]; for (size_t i = 1; i < m_history.size(); ++ i) { assert(m_history[i - 1].interval().strictly_before(m_history[i].interval())); ++ refcntrs[m_history[i].data()]; } for (const auto &hi : m_history) { assert(hi.data() != nullptr); assert(refcntrs[hi.data()] == hi.refcnt()); } } return true; } #endif /* NDEBUG */ class StackImpl { public: // Stack needs to be initialized. An empty stack is not valid, there must be a "New Project" status stored at the beginning. // Initially enable Undo / Redo stack to occupy maximum 10% of the total system physical memory. StackImpl() : m_memory_limit(std::min(Slic3r::total_physical_memory() / 10, size_t(1 * 16384 * 65536 / UNDO_REDO_DEBUG_LOW_MEM_FACTOR))), m_active_snapshot_time(0), m_current_time(0) {} void clear() { m_objects.clear(); m_shared_ptr_to_object_id.clear(); m_snapshots.clear(); m_active_snapshot_time = 0; m_current_time = 0; m_selection.clear(); } bool empty() const { assert(m_objects.empty() == m_snapshots.empty()); assert(! m_objects.empty() || (m_current_time == 0 && m_active_snapshot_time == 0)); return m_snapshots.empty(); } void set_memory_limit(size_t memsize) { m_memory_limit = memsize; } size_t get_memory_limit() const { return m_memory_limit; } size_t memsize() const { size_t memsize = 0; for (const auto &object : m_objects) memsize += object.second->memsize(); return memsize; } // Store the current application state onto the Undo / Redo stack, remove all snapshots after m_active_snapshot_time. void take_snapshot(const std::string& snapshot_name, const Slic3r::Model& model, const Slic3r::GUI::Selection& selection, const Slic3r::GUI::GLGizmosManager& gizmos, const SnapshotData &snapshot_data); void load_snapshot(size_t timestamp, Slic3r::Model& model, Slic3r::GUI::GLGizmosManager& gizmos); bool has_undo_snapshot() const; bool has_redo_snapshot() const; bool undo(Slic3r::Model &model, const Slic3r::GUI::Selection &selection, Slic3r::GUI::GLGizmosManager &gizmos, const SnapshotData &snapshot_data, size_t jump_to_time); bool redo(Slic3r::Model &model, Slic3r::GUI::GLGizmosManager &gizmos, size_t jump_to_time); void release_least_recently_used(); // Snapshot history (names with timestamps). const std::vector& snapshots() const { return m_snapshots; } // Timestamp of the active snapshot. size_t active_snapshot_time() const { return m_active_snapshot_time; } bool temp_snapshot_active() const { return m_snapshots.back().timestamp == m_active_snapshot_time && ! m_snapshots.back().is_topmost_captured(); } const Selection& selection_deserialized() const { return m_selection; } //protected: template ObjectID save_mutable_object(const T &object); template ObjectID save_immutable_object(std::shared_ptr &object, bool optional); template T* load_mutable_object(const Slic3r::ObjectID id); template std::shared_ptr load_immutable_object(const Slic3r::ObjectID id, bool optional); template void load_mutable_object(const Slic3r::ObjectID id, T &target); #ifdef SLIC3R_UNDOREDO_DEBUG std::string format() const { std::string out = "Objects\n"; for (const std::pair> &kvp : m_objects) out += std::string("ObjectID:") + std::to_string(kvp.first.id) + " " + kvp.second->format() + "\n"; out += "Snapshots\n"; for (const Snapshot &snapshot : m_snapshots) { if (snapshot.timestamp == m_active_snapshot_time) out += ">>> "; out += std::string("Name: \"") + snapshot.name + "\", timestamp: " + std::to_string(snapshot.timestamp) + ", Model ID:" + ((snapshot.model_id == 0) ? "Invalid" : std::to_string(snapshot.model_id)) + "\n"; } if (m_active_snapshot_time > m_snapshots.back().timestamp) out += ">>>\n"; out += "Current time: " + std::to_string(m_current_time) + "\n"; out += "Total memory occupied: " + std::to_string(this->memsize()) + "\n"; return out; } void print() const { std::cout << "Undo / Redo stack" << std::endl; std::cout << this->format() << std::endl; } #endif /* SLIC3R_UNDOREDO_DEBUG */ #ifndef NDEBUG bool valid() const { assert(! m_snapshots.empty()); assert(m_snapshots.back().is_topmost()); auto it = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); assert(it != m_snapshots.begin() && it != m_snapshots.end() && it->timestamp == m_active_snapshot_time); assert(m_active_snapshot_time <= m_snapshots.back().timestamp); for (auto it = m_objects.begin(); it != m_objects.end(); ++ it) assert(it->second->valid()); return true; } #endif /* NDEBUG */ private: template ObjectID immutable_object_id(const std::shared_ptr &ptr) { return this->immutable_object_id_impl((const void*)ptr.get()); } ObjectID immutable_object_id_impl(const void *ptr) { auto it = m_shared_ptr_to_object_id.find(ptr); if (it == m_shared_ptr_to_object_id.end()) { // Allocate a new temporary ObjectID for this shared pointer. ObjectBase object_with_id; it = m_shared_ptr_to_object_id.insert(it, std::make_pair(ptr, object_with_id.id())); } return it->second; } void collect_garbage(); // Maximum memory allowed to be occupied by the Undo / Redo stack. If the limit is exceeded, // least recently used snapshots will be released. size_t m_memory_limit; // Each individual object (Model, ModelObject, ModelInstance, ModelVolume, Selection, TriangleMesh) // is stored with its own history, referenced by the ObjectID. Immutable objects do not provide // their own IDs, therefore there are temporary IDs generated for them and stored to m_shared_ptr_to_object_id. std::map> m_objects; std::map m_shared_ptr_to_object_id; // Snapshot history (names with timestamps). std::vector m_snapshots; // Timestamp of the active snapshot. size_t m_active_snapshot_time; // Logical time counter. m_current_time is being incremented with each snapshot taken. size_t m_current_time; // Last selection serialized or deserialized. Selection m_selection; }; using InputArchive = cereal::UserDataAdapter; using OutputArchive = cereal::UserDataAdapter; } // namespace UndoRedo class Model; class ModelObject; class ModelVolume; class ModelInstance; class ModelMaterial; class DynamicPrintConfig; class TriangleMesh; } // namespace Slic3r namespace cereal { // Let cereal know that there are load / save non-member functions declared for ModelObject*, ignore serialization of pointers triggering // static assert, that cereal does not support serialization of raw pointers. template struct specialize {}; template struct specialize {}; template struct specialize {}; template struct specialize {}; template struct specialize {}; template struct specialize, cereal::specialization::non_member_load_save> {}; // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, // store just the ObjectID to this stream. template void save(BinaryOutputArchive& ar, T* const& ptr) { ar(cereal::get_user_data(ar).save_mutable_object(*ptr)); } // Load ObjectBase derived class from the Undo / Redo stack as a separate object // based on the ObjectID loaded from this stream. template void load(BinaryInputArchive& ar, T*& ptr) { Slic3r::UndoRedo::StackImpl& stack = cereal::get_user_data(ar); size_t id; ar(id); ptr = stack.load_mutable_object(Slic3r::ObjectID(id)); } // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, // store just the ObjectID to this stream. template void save(BinaryOutputArchive &ar, const std::unique_ptr &ptr) { ar(cereal::get_user_data(ar).save_mutable_object(*ptr.get())); } // Load ObjectBase derived class from the Undo / Redo stack as a separate object // based on the ObjectID loaded from this stream. template void load(BinaryInputArchive &ar, std::unique_ptr &ptr) { Slic3r::UndoRedo::StackImpl& stack = cereal::get_user_data(ar); size_t id; ar(id); ptr.reset(stack.load_mutable_object(Slic3r::ObjectID(id))); } // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, // store just the ObjectID to this stream. template void save_by_value(BinaryOutputArchive& ar, const T &cfg) { ar(cereal::get_user_data(ar).save_mutable_object(cfg)); } // Load ObjectBase derived class from the Undo / Redo stack as a separate object // based on the ObjectID loaded from this stream. template void load_by_value(BinaryInputArchive& ar, T &cfg) { Slic3r::UndoRedo::StackImpl& stack = cereal::get_user_data(ar); size_t id; ar(id); stack.load_mutable_object(Slic3r::ObjectID(id), cfg); } // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, // store just the ObjectID to this stream. template void save(BinaryOutputArchive &ar, const std::shared_ptr &ptr) { ar(cereal::get_user_data(ar).save_immutable_object(const_cast&>(ptr), false)); } template void save_optional(BinaryOutputArchive &ar, const std::shared_ptr &ptr) { ar(cereal::get_user_data(ar).save_immutable_object(const_cast&>(ptr), true)); } // Load ObjectBase derived class from the Undo / Redo stack as a separate object // based on the ObjectID loaded from this stream. template void load(BinaryInputArchive &ar, std::shared_ptr &ptr) { Slic3r::UndoRedo::StackImpl &stack = cereal::get_user_data(ar); size_t id; ar(id); ptr = stack.load_immutable_object(Slic3r::ObjectID(id), false); } template void load_optional(BinaryInputArchive &ar, std::shared_ptr &ptr) { Slic3r::UndoRedo::StackImpl &stack = cereal::get_user_data(ar); size_t id; ar(id); ptr = stack.load_immutable_object(Slic3r::ObjectID(id), true); } } #include #include #include #include namespace Slic3r { namespace UndoRedo { template std::shared_ptr& ImmutableObjectHistory::shared_ptr(StackImpl &stack) { if (m_shared_object.get() == nullptr && ! this->m_serialized.empty()) { // Deserialize the object. std::istringstream iss(m_serialized); { Slic3r::UndoRedo::InputArchive archive(stack, iss); typedef typename std::remove_const::type Type; std::unique_ptr mesh(new Type()); archive(*mesh.get()); m_shared_object = std::move(mesh); } } return m_shared_object; } template ObjectID StackImpl::save_mutable_object(const T &object) { // First find or allocate a history stack for the ObjectID of this object instance. auto it_object_history = m_objects.find(object.id()); if (it_object_history == m_objects.end()) it_object_history = m_objects.insert(it_object_history, std::make_pair(object.id(), std::unique_ptr>(new MutableObjectHistory()))); auto *object_history = static_cast*>(it_object_history->second.get()); // Then serialize the object into a string. std::ostringstream oss; { Slic3r::UndoRedo::OutputArchive archive(*this, oss); archive(object); } object_history->save(m_active_snapshot_time, m_current_time, oss.str()); return object.id(); } template ObjectID StackImpl::save_immutable_object(std::shared_ptr &object, bool optional) { // First allocate a temporary ObjectID for this pointer. ObjectID object_id = this->immutable_object_id(object); // and find or allocate a history stack for the ObjectID associated to this shared_ptr. auto it_object_history = m_objects.find(object_id); if (it_object_history == m_objects.end()) it_object_history = m_objects.emplace_hint(it_object_history, object_id, std::unique_ptr>(new ImmutableObjectHistory(object, optional))); else assert(it_object_history->second.get()->is_optional() == optional); // Then save the interval. static_cast*>(it_object_history->second.get())->save(m_active_snapshot_time, m_current_time); return object_id; } template T* StackImpl::load_mutable_object(const Slic3r::ObjectID id) { T *target = new T(); this->load_mutable_object(id, *target); return target; } template std::shared_ptr StackImpl::load_immutable_object(const Slic3r::ObjectID id, bool optional) { // First find a history stack for the ObjectID of this object instance. auto it_object_history = m_objects.find(id); assert(optional || it_object_history != m_objects.end()); if (it_object_history == m_objects.end()) return std::shared_ptr(); auto *object_history = static_cast*>(it_object_history->second.get()); assert(object_history->has_snapshot(m_active_snapshot_time)); object_history->restore_optional(); return object_history->shared_ptr(*this); } template void StackImpl::load_mutable_object(const Slic3r::ObjectID id, T &target) { // First find a history stack for the ObjectID of this object instance. auto it_object_history = m_objects.find(id); assert(it_object_history != m_objects.end()); auto *object_history = static_cast*>(it_object_history->second.get()); // Then get the data associated with the object history and m_active_snapshot_time. std::istringstream iss(object_history->load(m_active_snapshot_time)); Slic3r::UndoRedo::InputArchive archive(*this, iss); target.m_id = id; archive(target); } // Store the current application state onto the Undo / Redo stack, remove all snapshots after m_active_snapshot_time. void StackImpl::take_snapshot(const std::string& snapshot_name, const Slic3r::Model& model, const Slic3r::GUI::Selection& selection, const Slic3r::GUI::GLGizmosManager& gizmos, const SnapshotData &snapshot_data) { // Release old snapshot data. assert(m_active_snapshot_time <= m_current_time); for (auto &kvp : m_objects) kvp.second->release_after_timestamp(m_active_snapshot_time); { auto it = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); m_snapshots.erase(it, m_snapshots.end()); } // Take new snapshots. this->save_mutable_object(model); m_selection.volumes_and_instances.clear(); m_selection.volumes_and_instances.reserve(selection.get_volume_idxs().size()); m_selection.mode = selection.get_mode(); for (unsigned int volume_idx : selection.get_volume_idxs()) m_selection.volumes_and_instances.emplace_back(selection.get_volume(volume_idx)->geometry_id); this->save_mutable_object(m_selection); this->save_mutable_object(gizmos); // Save the snapshot info. m_snapshots.emplace_back(snapshot_name, m_current_time ++, model.id().id, snapshot_data); m_active_snapshot_time = m_current_time; // Save snapshot info of the last "current" aka "top most" state, that is only being serialized // if undoing an action. Such a snapshot has an invalid Model ID assigned if it was not taken yet. m_snapshots.emplace_back(topmost_snapshot_name, m_active_snapshot_time, 0, snapshot_data); // Release empty objects from the history. this->collect_garbage(); assert(this->valid()); #ifdef SLIC3R_UNDOREDO_DEBUG std::cout << "After snapshot" << std::endl; this->print(); #endif /* SLIC3R_UNDOREDO_DEBUG */ } void StackImpl::load_snapshot(size_t timestamp, Slic3r::Model& model, Slic3r::GUI::GLGizmosManager& gizmos) { // Find the snapshot by time. It must exist. const auto it_snapshot = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(timestamp)); if (it_snapshot == m_snapshots.end() || it_snapshot->timestamp != timestamp) throw std::runtime_error((boost::format("Snapshot with timestamp %1% does not exist") % timestamp).str()); m_active_snapshot_time = timestamp; model.clear_objects(); model.clear_materials(); this->load_mutable_object(ObjectID(it_snapshot->model_id), model); model.update_links_bottom_up_recursive(); m_selection.volumes_and_instances.clear(); this->load_mutable_object(m_selection.id(), m_selection); //gizmos.reset_all_states(); FIXME: is this really necessary? It is quite unpleasant for the gizmo undo/redo substack this->load_mutable_object(gizmos.id(), gizmos); // Sort the volumes so that we may use binary search. std::sort(m_selection.volumes_and_instances.begin(), m_selection.volumes_and_instances.end()); this->m_active_snapshot_time = timestamp; assert(this->valid()); } bool StackImpl::has_undo_snapshot() const { assert(this->valid()); auto it = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); return -- it != m_snapshots.begin(); } bool StackImpl::has_redo_snapshot() const { assert(this->valid()); auto it = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); return ++ it != m_snapshots.end(); } bool StackImpl::undo(Slic3r::Model &model, const Slic3r::GUI::Selection &selection, Slic3r::GUI::GLGizmosManager &gizmos, const SnapshotData &snapshot_data, size_t time_to_load) { assert(this->valid()); if (time_to_load == SIZE_MAX) { auto it_current = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); if (-- it_current == m_snapshots.begin()) return false; time_to_load = it_current->timestamp; } assert(time_to_load < m_active_snapshot_time); assert(std::binary_search(m_snapshots.begin(), m_snapshots.end(), Snapshot(time_to_load))); bool new_snapshot_taken = false; if (m_active_snapshot_time == m_snapshots.back().timestamp && ! m_snapshots.back().is_topmost_captured()) { // The current state is temporary. The current state needs to be captured to be redoable. this->take_snapshot(topmost_snapshot_name, model, selection, gizmos, snapshot_data); // The line above entered another topmost_snapshot_name. assert(m_snapshots.back().is_topmost()); assert(! m_snapshots.back().is_topmost_captured()); // Pop it back, it is not needed as there is now a captured topmost state. m_snapshots.pop_back(); // current_time was extended, but it should not cause any harm. Resetting it back may complicate the logic unnecessarily. //-- m_current_time; assert(m_snapshots.back().is_topmost()); assert(m_snapshots.back().is_topmost_captured()); new_snapshot_taken = true; } this->load_snapshot(time_to_load, model, gizmos); if (new_snapshot_taken) { // Release old snapshots if the memory allocated due to capturing the top most state is excessive. // Don't release the snapshots here, release them first after the scene and background processing gets updated, as this will release some references // to the shared TriangleMeshes. //this->release_least_recently_used(); } #ifdef SLIC3R_UNDOREDO_DEBUG std::cout << "After undo" << std::endl; this->print(); #endif /* SLIC3R_UNDOREDO_DEBUG */ return true; } bool StackImpl::redo(Slic3r::Model& model, Slic3r::GUI::GLGizmosManager& gizmos, size_t time_to_load) { assert(this->valid()); if (time_to_load == SIZE_MAX) { auto it_current = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); if (++ it_current == m_snapshots.end()) return false; time_to_load = it_current->timestamp; } assert(time_to_load > m_active_snapshot_time); assert(std::binary_search(m_snapshots.begin(), m_snapshots.end(), Snapshot(time_to_load))); this->load_snapshot(time_to_load, model, gizmos); #ifdef SLIC3R_UNDOREDO_DEBUG std::cout << "After redo" << std::endl; this->print(); #endif /* SLIC3R_UNDOREDO_DEBUG */ return true; } void StackImpl::collect_garbage() { // Purge objects with empty histories. for (auto it = m_objects.begin(); it != m_objects.end();) { if (it->second->empty()) { if (it->second->immutable_object_ptr() != nullptr) // Release the immutable object from the ptr to ObjectID map. m_shared_ptr_to_object_id.erase(it->second->immutable_object_ptr()); it = m_objects.erase(it); } else ++ it; } } void StackImpl::release_least_recently_used() { assert(this->valid()); size_t current_memsize = this->memsize(); #ifdef SLIC3R_UNDOREDO_DEBUG bool released = false; #endif // First try to release the optional immutable data (for example the convex hulls), // or the shared vertices of triangle meshes. for (auto it = m_objects.begin(); current_memsize > m_memory_limit && it != m_objects.end();) { const void *ptr = it->second->immutable_object_ptr(); size_t mem_released = it->second->release_optional(); if (it->second->empty()) { if (ptr != nullptr) // Release the immutable object from the ptr to ObjectID map. m_shared_ptr_to_object_id.erase(ptr); mem_released += it->second->memsize(); it = m_objects.erase(it); } else ++ it; assert(current_memsize >= mem_released); if (current_memsize >= mem_released) current_memsize -= mem_released; else current_memsize = 0; } while (current_memsize > m_memory_limit && m_snapshots.size() >= 3) { // From which side to remove a snapshot? assert(m_snapshots.front().timestamp < m_active_snapshot_time); size_t mem_released = 0; if (m_snapshots[1].timestamp == m_active_snapshot_time) { // Remove the last snapshot. #if 0 for (auto it = m_objects.begin(); it != m_objects.end();) { mem_released += it->second->release_after_timestamp(m_snapshots.back().timestamp); if (it->second->empty()) { if (it->second->immutable_object_ptr() != nullptr) // Release the immutable object from the ptr to ObjectID map. m_shared_ptr_to_object_id.erase(it->second->immutable_object_ptr()); mem_released += it->second->memsize(); it = m_objects.erase(it); } else ++ it; } m_snapshots.pop_back(); m_snapshots.back().name = topmost_snapshot_name; #else // Rather don't release the last snapshot as it will be very confusing to the user // as of why he cannot jump to the top most state. The Undo / Redo stack maximum size // should be set low enough to accomodate for the top most snapshot. break; #endif } else { // Remove the first snapshot. for (auto it = m_objects.begin(); it != m_objects.end();) { mem_released += it->second->release_before_timestamp(m_snapshots[1].timestamp); if (it->second->empty()) { if (it->second->immutable_object_ptr() != nullptr) // Release the immutable object from the ptr to ObjectID map. m_shared_ptr_to_object_id.erase(it->second->immutable_object_ptr()); mem_released += it->second->memsize(); it = m_objects.erase(it); } else ++ it; } m_snapshots.erase(m_snapshots.begin()); } assert(current_memsize >= mem_released); if (current_memsize >= mem_released) current_memsize -= mem_released; else current_memsize = 0; #ifdef SLIC3R_UNDOREDO_DEBUG released = true; #endif } assert(this->valid()); #ifdef SLIC3R_UNDOREDO_DEBUG std::cout << "After release_least_recently_used" << std::endl; this->print(); #endif /* SLIC3R_UNDOREDO_DEBUG */ } // Wrappers of the private implementation. Stack::Stack() : pimpl(new StackImpl()) {} Stack::~Stack() {} void Stack::clear() { pimpl->clear(); } bool Stack::empty() const { return pimpl->empty(); } void Stack::set_memory_limit(size_t memsize) { pimpl->set_memory_limit(memsize); } size_t Stack::get_memory_limit() const { return pimpl->get_memory_limit(); } size_t Stack::memsize() const { return pimpl->memsize(); } void Stack::release_least_recently_used() { pimpl->release_least_recently_used(); } void Stack::take_snapshot(const std::string& snapshot_name, const Slic3r::Model& model, const Slic3r::GUI::Selection& selection, const Slic3r::GUI::GLGizmosManager& gizmos, const SnapshotData &snapshot_data) { pimpl->take_snapshot(snapshot_name, model, selection, gizmos, snapshot_data); } bool Stack::has_undo_snapshot() const { return pimpl->has_undo_snapshot(); } bool Stack::has_redo_snapshot() const { return pimpl->has_redo_snapshot(); } bool Stack::undo(Slic3r::Model& model, const Slic3r::GUI::Selection& selection, Slic3r::GUI::GLGizmosManager& gizmos, const SnapshotData &snapshot_data, size_t time_to_load) { return pimpl->undo(model, selection, gizmos, snapshot_data, time_to_load); } bool Stack::redo(Slic3r::Model& model, Slic3r::GUI::GLGizmosManager& gizmos, size_t time_to_load) { return pimpl->redo(model, gizmos, time_to_load); } const Selection& Stack::selection_deserialized() const { return pimpl->selection_deserialized(); } const std::vector& Stack::snapshots() const { return pimpl->snapshots(); } size_t Stack::active_snapshot_time() const { return pimpl->active_snapshot_time(); } bool Stack::temp_snapshot_active() const { return pimpl->temp_snapshot_active(); } } // namespace UndoRedo } // namespace Slic3r //FIXME we should have unit tests for testing serialization of basic types as DynamicPrintConfig. #if 0 #include "libslic3r/Config.hpp" #include "libslic3r/PrintConfig.hpp" namespace Slic3r { bool test_dynamic_print_config_serialization() { FullPrintConfig full_print_config; DynamicPrintConfig cfg; cfg.apply(full_print_config, false); std::string serialized; try { std::ostringstream ss; cereal::BinaryOutputArchive oarchive(ss); oarchive(cfg); serialized = ss.str(); } catch (std::runtime_error e) { e.what(); } DynamicPrintConfig cfg2; try { std::stringstream ss(serialized); cereal::BinaryInputArchive iarchive(ss); iarchive(cfg2); } catch (std::runtime_error e) { e.what(); } if (cfg == cfg2) { printf("Yes!\n"); return true; } printf("No!\n"); return false; } } // namespace Slic3r #endif