From 3a74e7ab69fab1080f4245bfbf972cfe0b851129 Mon Sep 17 00:00:00 2001 From: bubnikv Date: Wed, 17 Jul 2019 15:48:53 +0200 Subject: [PATCH] WIP: Undo / Redo memory limiting by releasing the least recently used snapshots. Memory limit set to 10% of physical system memory. --- src/admesh/stl.h | 8 ++ src/libslic3r/TriangleMesh.cpp | 6 + src/libslic3r/TriangleMesh.hpp | 2 + src/libslic3r/Utils.hpp | 2 + src/libslic3r/utils.cpp | 82 +++++++++++- src/slic3r/GUI/GUI_App.cpp | 4 +- src/slic3r/GUI/GUI_App.hpp | 4 +- src/slic3r/GUI/GUI_ObjectList.cpp | 16 +-- src/slic3r/GUI/Plater.cpp | 112 ++++++++++------ src/slic3r/GUI/Plater.hpp | 38 +++++- src/slic3r/Utils/UndoRedo.cpp | 207 ++++++++++++++++++++++++++---- src/slic3r/Utils/UndoRedo.hpp | 12 ++ 12 files changed, 411 insertions(+), 82 deletions(-) diff --git a/src/admesh/stl.h b/src/admesh/stl.h index 2ac6c7fd2..9b1146f8d 100644 --- a/src/admesh/stl.h +++ b/src/admesh/stl.h @@ -127,6 +127,10 @@ struct stl_file { this->stats.reset(); } + size_t memsize() const { + return sizeof(*this) + sizeof(stl_facet) * facet_start.size() + sizeof(stl_neighbors) * neighbors_start.size(); + } + std::vector facet_start; std::vector neighbors_start; // Statistics @@ -139,6 +143,10 @@ struct indexed_triangle_set void clear() { indices.clear(); vertices.clear(); } + size_t memsize() const { + return sizeof(*this) + sizeof(stl_triangle_vertex_indices) * indices.size() + sizeof(stl_vertex) * vertices.size(); + } + std::vector indices; std::vector vertices; //FIXME add normals once we get rid of the stl_file from TriangleMesh completely. diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index fbfff90fb..d782bc5ac 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -607,6 +607,12 @@ void TriangleMesh::require_shared_vertices() BOOST_LOG_TRIVIAL(trace) << "TriangleMeshSlicer::require_shared_vertices - end"; } +size_t TriangleMesh::memsize() const +{ + size_t memsize = 8 + this->stl.memsize() + this->its.memsize(); + return memsize; +} + void TriangleMeshSlicer::init(const TriangleMesh *_mesh, throw_on_cancel_callback_type throw_on_cancel) { mesh = _mesh; diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 5dd2597a5..d2b688638 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -67,6 +67,8 @@ public: size_t facets_count() const { return this->stl.stats.number_of_facets; } bool empty() const { return this->facets_count() == 0; } bool is_splittable() const; + // Estimate of the memory occupied by this structure. + size_t memsize() const; stl_file stl; indexed_triangle_set its; diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 3b30e981c..09e24a475 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -21,6 +21,8 @@ extern std::string format_memsize_MB(size_t n); // The string is non-empty only if the loglevel >= info (3). extern std::string log_memory_info(); extern void disable_multi_threading(); +// Returns the size of physical memory (RAM) in bytes. +extern size_t total_physical_memory(); // Set a path with GUI resource files. void set_var_dir(const std::string &path); diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 519763731..f9a044338 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -7,10 +7,15 @@ #include #ifdef WIN32 -#include -#include + #include + #include #else -#include + #include + #include + #include + #ifdef BSD + #include + #endif #endif #include @@ -467,4 +472,75 @@ std::string log_memory_info() } #endif +// Returns the size of physical memory (RAM) in bytes. +// http://nadeausoftware.com/articles/2012/09/c_c_tip_how_get_physical_memory_size_system +size_t total_physical_memory() +{ +#if defined(_WIN32) && (defined(__CYGWIN__) || defined(__CYGWIN32__)) + // Cygwin under Windows. ------------------------------------ + // New 64-bit MEMORYSTATUSEX isn't available. Use old 32.bit + MEMORYSTATUS status; + status.dwLength = sizeof(status); + GlobalMemoryStatus( &status ); + return (size_t)status.dwTotalPhys; +#elif defined(_WIN32) + // Windows. ------------------------------------------------- + // Use new 64-bit MEMORYSTATUSEX, not old 32-bit MEMORYSTATUS + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + GlobalMemoryStatusEx( &status ); + return (size_t)status.ullTotalPhys; +#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__)) + // UNIX variants. ------------------------------------------- + // Prefer sysctl() over sysconf() except sysctl() HW_REALMEM and HW_PHYSMEM + +#if defined(CTL_HW) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM64)) + int mib[2]; + mib[0] = CTL_HW; +#if defined(HW_MEMSIZE) + mib[1] = HW_MEMSIZE; // OSX. --------------------- +#elif defined(HW_PHYSMEM64) + mib[1] = HW_PHYSMEM64; // NetBSD, OpenBSD. --------- +#endif + int64_t size = 0; // 64-bit + size_t len = sizeof( size ); + if ( sysctl( mib, 2, &size, &len, NULL, 0 ) == 0 ) + return (size_t)size; + return 0L; // Failed? + +#elif defined(_SC_AIX_REALMEM) + // AIX. ----------------------------------------------------- + return (size_t)sysconf( _SC_AIX_REALMEM ) * (size_t)1024L; + +#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE) + // FreeBSD, Linux, OpenBSD, and Solaris. -------------------- + return (size_t)sysconf( _SC_PHYS_PAGES ) * + (size_t)sysconf( _SC_PAGESIZE ); + +#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGE_SIZE) + // Legacy. -------------------------------------------------- + return (size_t)sysconf( _SC_PHYS_PAGES ) * + (size_t)sysconf( _SC_PAGE_SIZE ); + +#elif defined(CTL_HW) && (defined(HW_PHYSMEM) || defined(HW_REALMEM)) + // DragonFly BSD, FreeBSD, NetBSD, OpenBSD, and OSX. -------- + int mib[2]; + mib[0] = CTL_HW; +#if defined(HW_REALMEM) + mib[1] = HW_REALMEM; // FreeBSD. ----------------- +#elif defined(HW_PYSMEM) + mib[1] = HW_PHYSMEM; // Others. ------------------ +#endif + unsigned int size = 0; // 32-bit + size_t len = sizeof( size ); + if ( sysctl( mib, 2, &size, &len, NULL, 0 ) == 0 ) + return (size_t)size; + return 0L; // Failed? +#endif // sysctl and sysconf variants + +#else + return 0L; // Unknown OS. +#endif +} + }; // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 8a376c3a3..b4e7bc2b2 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -534,7 +534,7 @@ void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_max }); } -void GUI_App::load_project(wxWindow *parent, wxString& input_file) +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const { input_file.Clear(); wxFileDialog dialog(parent ? parent : GetTopWindow(), @@ -546,7 +546,7 @@ void GUI_App::load_project(wxWindow *parent, wxString& input_file) input_file = dialog.GetPath(); } -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const { input_files.Clear(); wxFileDialog dialog(parent ? parent : GetTopWindow(), diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index e69503ff8..2f0e8bcf3 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -121,8 +121,8 @@ public: void recreate_GUI(); void system_info(); void keyboard_shortcuts(); - void load_project(wxWindow *parent, wxString& input_file); - void import_model(wxWindow *parent, wxArrayString& input_files); + void load_project(wxWindow *parent, wxString& input_file) const; + void import_model(wxWindow *parent, wxArrayString& input_files) const; static bool catch_error(std::function cb, const std::string& err); void persist_window_geometry(wxTopLevelWindow *window, bool default_maximized = false); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 9e681257c..3f3256c42 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -71,9 +71,6 @@ static void take_snapshot(const wxString& snapshot_name) wxGetApp().plater()->take_snapshot(snapshot_name); } -static void suppress_snapshots(){ wxGetApp().plater()->suppress_snapshots(); } -static void allow_snapshots() { wxGetApp().plater()->allow_snapshots(); } - ObjectList::ObjectList(wxWindow* parent) : wxDataViewCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxDV_MULTIPLE), m_parent(parent) @@ -2406,8 +2403,7 @@ void ObjectList::remove() wxDataViewItem parent = wxDataViewItem(0); - take_snapshot(_(L("Delete Selected"))); - suppress_snapshots(); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete Selected"))); for (auto& item : sels) { @@ -2429,8 +2425,6 @@ void ObjectList::remove() if (parent) select_item(parent); - - allow_snapshots(); } void ObjectList::del_layer_range(const t_layer_height_range& range) @@ -2505,8 +2499,7 @@ void ObjectList::add_layer_range_after_current(const t_layer_height_range& curre t_layer_height_range new_range = { midl_layer, next_range.second }; - take_snapshot(_(L("Add New Layers Range"))); - suppress_snapshots(); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add New Layers Range"))); // create new 2 layers instead of deleted one @@ -2521,7 +2514,6 @@ void ObjectList::add_layer_range_after_current(const t_layer_height_range& curre new_range = { current_range.second, midl_layer }; ranges[new_range] = get_default_layer_config(obj_idx); add_layer_item(new_range, layers_item, layer_idx); - allow_snapshots(); } else { @@ -3531,7 +3523,7 @@ void ObjectList::update_after_undo_redo() m_prevent_list_events = true; m_prevent_canvas_selection_update = true; - suppress_snapshots(); + Plater::SuppressSnapshots suppress(wxGetApp().plater()); // Unselect all objects before deleting them, so that no change of selection is emitted during deletion. this->UnselectAll(); @@ -3543,8 +3535,6 @@ void ObjectList::update_after_undo_redo() ++obj_idx; } - allow_snapshots(); - #ifndef __WXOSX__ selection_changed(); #endif /* __WXOSX__ */ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f5f245471..11055af11 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1255,13 +1255,9 @@ const std::regex PlaterDropTarget::pattern_drop(".*[.](stl|obj|amf|3mf|prusa)", bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { - plater->take_snapshot(_(L("Load Files"))); - std::vector paths; - for (const auto &filename : filenames) { fs::path path(into_path(filename)); - if (std::regex_match(path.string(), pattern_drop)) { paths.push_back(std::move(path)); } else { @@ -1269,6 +1265,23 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi } } + wxString snapshot_label; + assert(! paths.empty()); + if (paths.size() == 1) { + snapshot_label = _(L("Load File")); + snapshot_label += ": "; + snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str()); + } else { + snapshot_label = _(L("Load Files")); + snapshot_label += ": "; + snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str()); + for (size_t i = 1; i < paths.size(); ++ i) { + snapshot_label += ", "; + snapshot_label += wxString::FromUTF8(paths[i].filename().string().c_str()); + } + } + Plater::TakeSnapshot snapshot(plater, snapshot_label); + // FIXME: when drag and drop is done on a .3mf or a .amf file we should clear the plater for consistence with the open project command // (the following call to plater->load_files() will load the config data, if present) @@ -1595,7 +1608,7 @@ struct Plater::priv priv(Plater *q, MainFrame *main_frame); ~priv(); - void update(bool force_full_scene_refresh = false); + void update(bool force_full_scene_refresh = false, bool force_background_processing_update = false); void select_view(const std::string& direction); void select_view_3D(const std::string& name); void select_next_view_3D(); @@ -1634,6 +1647,8 @@ struct Plater::priv return; assert(this->m_prevent_snapshots >= 0); this->undo_redo_stack.take_snapshot(snapshot_name, model, view3D->get_canvas3d()->get_selection()); + this->undo_redo_stack.release_least_recently_used(); + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack.memsize()) << log_memory_info(); } void take_snapshot(const wxString& snapshot_name) { this->take_snapshot(std::string(snapshot_name.ToUTF8().data())); } int get_active_snapshot_index(); @@ -1734,7 +1749,7 @@ private: void update_fff_scene(); void update_sla_scene(); - void update_after_undo_redo(); + void update_after_undo_redo(bool temp_snapshot_was_taken = false); // path to project file stored with no extension wxString m_project_filename; @@ -1888,7 +1903,7 @@ Plater::priv::~priv() delete config; } -void Plater::priv::update(bool force_full_scene_refresh) +void Plater::priv::update(bool force_full_scene_refresh, bool force_background_processing_update) { // the following line, when enabled, causes flickering on NVIDIA graphics cards // wxWindowUpdateLocker freeze_guard(q); @@ -1901,7 +1916,7 @@ void Plater::priv::update(bool force_full_scene_refresh) } unsigned int update_status = 0; - if (this->printer_technology == ptSLA) + if (this->printer_technology == ptSLA || force_background_processing_update) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data. update_status = this->update_background_process(false); @@ -2459,7 +2474,10 @@ void Plater::priv::remove(size_t obj_idx) void Plater::priv::delete_object_from_model(size_t obj_idx) { - this->take_snapshot(_(L("Delete Object"))); + wxString snapshot_label = _(L("Delete Object")); + if (! model.objects[obj_idx]->name.empty()) + snapshot_label += ": " + wxString::FromUTF8(model.objects[obj_idx]->name.c_str()); + Plater::TakeSnapshot snapshot(q, snapshot_label); model.delete_object(obj_idx); update(); object_list_changed(); @@ -2467,7 +2485,7 @@ void Plater::priv::delete_object_from_model(size_t obj_idx) void Plater::priv::reset() { - this->take_snapshot(_(L("Reset Project"))); + Plater::TakeSnapshot snapshot(q, _(L("Reset Project"))); set_project_filename(wxEmptyString); @@ -2663,7 +2681,7 @@ void Plater::priv::split_object() Slic3r::GUI::warning_catcher(q, _(L("The selected object couldn't be split because it contains only one part."))); else { - this->take_snapshot(_(L("Split to Objects"))); + Plater::TakeSnapshot snapshot(q, _(L("Split to Objects"))); unsigned int counter = 1; for (ModelObject* m : new_objects) @@ -2903,7 +2921,7 @@ void Plater::priv::update_sla_scene() void Plater::priv::reload_from_disk() { - this->take_snapshot(_(L("Reload from Disk"))); + Plater::TakeSnapshot snapshot(q, _(L("Reload from Disk"))); const auto &selection = get_selection(); const auto obj_orig_idx = selection.get_object_idx(); @@ -2939,7 +2957,7 @@ void Plater::priv::fix_through_netfabb(const int obj_idx, const int vol_idx/* = if (obj_idx < 0) return; - this->take_snapshot(_(L("Fix Throught NetFabb"))); + Plater::TakeSnapshot snapshot(q, _(L("Fix Throught NetFabb"))); fix_model_by_win10_sdk_gui(*model.objects[obj_idx], vol_idx); this->update(); @@ -3603,7 +3621,7 @@ void Plater::priv::show_action_buttons(const bool is_ready_to_slice) const int Plater::priv::get_active_snapshot_index() { - const size_t& active_snapshot_time = this->undo_redo_stack.active_snapshot_time(); + const size_t active_snapshot_time = this->undo_redo_stack.active_snapshot_time(); const std::vector& ss_stack = this->undo_redo_stack.snapshots(); const auto it = std::lower_bound(ss_stack.begin(), ss_stack.end(), UndoRedo::Snapshot(active_snapshot_time)); return it - ss_stack.begin(); @@ -3611,8 +3629,9 @@ int Plater::priv::get_active_snapshot_index() void Plater::priv::undo() { + bool temp_snapshot_was_taken = this->undo_redo_stack.temp_snapshot_active(); if (this->undo_redo_stack.undo(model, this->view3D->get_canvas3d()->get_selection())) - this->update_after_undo_redo(); + this->update_after_undo_redo(temp_snapshot_was_taken); } void Plater::priv::redo() @@ -3623,8 +3642,9 @@ void Plater::priv::redo() void Plater::priv::undo_to(size_t time_to_load) { + bool temp_snapshot_was_taken = this->undo_redo_stack.temp_snapshot_active(); if (this->undo_redo_stack.undo(model, this->view3D->get_canvas3d()->get_selection(), time_to_load)) - this->update_after_undo_redo(); + this->update_after_undo_redo(temp_snapshot_was_taken); } void Plater::priv::redo_to(size_t time_to_load) @@ -3633,10 +3653,16 @@ void Plater::priv::redo_to(size_t time_to_load) this->update_after_undo_redo(); } -void Plater::priv::update_after_undo_redo() +void Plater::priv::update_after_undo_redo(bool /* temp_snapshot_was_taken */) { this->view3D->get_canvas3d()->get_selection().clear(); - this->update(false); // update volumes from the deserializd model + // Update volumes from the deserializd model, always stop / update the background processing (for both the SLA and FFF technologies). + this->update(false, true); + // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first snapshot. + //if (temp_snapshot_was_taken) + // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while some + // triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the Undo / Redo stack size. + this->undo_redo_stack.release_least_recently_used(); //YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack.selection_deserialized().mode), this->undo_redo_stack.selection_deserialized().volumes_and_instances); @@ -3644,6 +3670,8 @@ void Plater::priv::update_after_undo_redo() //FIXME what about the state of the manipulators? //FIXME what about the focus? Cursor in the side panel? + + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack.memsize()) << log_memory_info(); } void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& label) const @@ -3683,10 +3711,12 @@ void Plater::new_project() void Plater::load_project() { - this->take_snapshot(_(L("Load Project"))); - + // Ask user for a project file name. wxString input_file; wxGetApp().load_project(this, input_file); + // Take the Undo / Redo snapshot. + Plater::TakeSnapshot snapshot(this, _(L("Load Project")) + ": " + wxString::FromUTF8(into_path(input_file).stem().string().c_str())); + // And finally load the new project. load_project(input_file); } @@ -3710,13 +3740,28 @@ void Plater::add_model() if (input_files.empty()) return; - this->take_snapshot(_(L("Add object(s)"))); + std::vector paths; + for (const auto &file : input_files) + paths.push_back(into_path(file)); - std::vector input_paths; - for (const auto &file : input_files) { - input_paths.push_back(into_path(file)); - } - load_files(input_paths, true, false); + wxString snapshot_label; + assert(! paths.empty()); + if (paths.size() == 1) { + snapshot_label = _(L("Import Object")); + snapshot_label += ": "; + snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str()); + } else { + snapshot_label = _(L("Import Objects")); + snapshot_label += ": "; + snapshot_label += wxString::FromUTF8(paths.front().filename().string().c_str()); + for (size_t i = 1; i < paths.size(); ++ i) { + snapshot_label += ", "; + snapshot_label += wxString::FromUTF8(paths[i].filename().string().c_str()); + } + } + + Plater::TakeSnapshot snapshot(this, snapshot_label); + load_files(paths, true, false); } void Plater::extract_config_from_project() @@ -3769,17 +3814,15 @@ void Plater::delete_object_from_model(size_t obj_idx) { p->delete_object_from_mo void Plater::remove_selected() { - this->take_snapshot(_(L("Delete Selected Objects"))); - this->suppress_snapshots(); + Plater::TakeSnapshot snapshot(this, _(L("Delete Selected Objects"))); this->p->view3D->delete_selected(); - this->allow_snapshots(); } void Plater::increase_instances(size_t num) { if (! can_increase_instances()) { return; } - this->take_snapshot(_(L("Increase Instances"))); + Plater::TakeSnapshot snapshot(this, _(L("Increase Instances"))); int obj_idx = p->get_selected_object_idx(); @@ -3815,7 +3858,7 @@ void Plater::decrease_instances(size_t num) { if (! can_decrease_instances()) { return; } - this->take_snapshot(_(L("Decrease Instances"))); + Plater::TakeSnapshot snapshot(this, _(L("Decrease Instances"))); int obj_idx = p->get_selected_object_idx(); @@ -3851,16 +3894,13 @@ void Plater::set_number_of_copies(/*size_t num*/) if (num < 0) return; - this->take_snapshot(wxString::Format(_(L("Set numbers of copies to %d")), num)); - this->suppress_snapshots(); + Plater::TakeSnapshot snapshot(this, wxString::Format(_(L("Set numbers of copies to %d")), num)); int diff = (int)num - (int)model_object->instances.size(); if (diff > 0) increase_instances(diff); else if (diff < 0) decrease_instances(-diff); - - this->allow_snapshots(); } bool Plater::is_selection_empty() const @@ -3884,7 +3924,7 @@ void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_uppe return; } - this->take_snapshot(_(L("Cut"))); + Plater::TakeSnapshot snapshot(this, _(L("Cut"))); wxBusyCursor wait; const auto new_objects = object->cut(instance_idx, z, keep_upper, keep_lower, rotate_lower); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e73f88ef3..a4ced64a0 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -186,8 +186,6 @@ public: void take_snapshot(const std::string &snapshot_name); void take_snapshot(const wxString &snapshot_name); - void suppress_snapshots(); - void allow_snapshots(); void undo(); void redo(); void undo_to(int selection); @@ -235,10 +233,46 @@ public: const Camera& get_camera() const; + // ROII wrapper for suppressing the Undo / Redo snapshot to be taken. + class SuppressSnapshots + { + public: + SuppressSnapshots(Plater *plater) : m_plater(plater) + { + m_plater->suppress_snapshots(); + } + ~SuppressSnapshots() + { + m_plater->allow_snapshots(); + } + private: + Plater *m_plater; + }; + + // ROII wrapper for taking an Undo / Redo snapshot while disabling the snapshot taking by the methods called from inside this snapshot. + class TakeSnapshot + { + public: + TakeSnapshot(Plater *plater, const wxString &snapshot_name) : m_plater(plater) + { + m_plater->take_snapshot(snapshot_name); + m_plater->suppress_snapshots(); + } + ~TakeSnapshot() + { + m_plater->allow_snapshots(); + } + private: + Plater *m_plater; + }; + private: struct priv; std::unique_ptr p; + void suppress_snapshots(); + void allow_snapshots(); + friend class SuppressBackgroundProcessingUpdate; }; diff --git a/src/slic3r/Utils/UndoRedo.cpp b/src/slic3r/Utils/UndoRedo.cpp index 058062502..6910fa87b 100644 --- a/src/slic3r/Utils/UndoRedo.cpp +++ b/src/slic3r/Utils/UndoRedo.cpp @@ -18,6 +18,7 @@ #include #include +#include #include @@ -28,11 +29,11 @@ namespace Slic3r { namespace UndoRedo { -static std::string topmost_snapsnot_name = "@@@ Topmost @@@"; +static std::string topmost_snapshot_name = "@@@ Topmost @@@"; bool Snapshot::is_topmost() const { - return this->name == topmost_snapsnot_name; + return this->name == topmost_snapshot_name; } // Time interval, start is closed, end is open. @@ -53,9 +54,12 @@ public: 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; @@ -75,8 +79,15 @@ public: // 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. - virtual void release_after_timestamp(size_t timestamp) = 0; + // Return the amount of memory released. + virtual size_t release_after_timestamp(size_t timestamp) = 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. @@ -96,21 +107,54 @@ public: // If the history is empty, the ObjectHistory object could be released. bool empty() override { return m_history.empty(); } - // Release all data after the given timestamp. The shared pointer is NOT released. - void release_after_timestamp(size_t timestamp) override { - assert(! 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); + // 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()); } - m_history.erase(it, m_history.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: @@ -138,6 +182,18 @@ public: bool is_immutable() const override { return true; } 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. @@ -230,7 +286,8 @@ public: 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_end(size_t timestamp) { m_interval.trim_end(timestamp); } + 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; } @@ -240,6 +297,13 @@ public: 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); @@ -270,6 +334,15 @@ public: 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) { @@ -356,7 +429,17 @@ 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. - StackImpl() : m_active_snapshot_time(0), m_current_time(0) {} + // 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))), m_active_snapshot_time(0), m_current_time(0) {} + + void set_memory_limit(size_t memsize) { m_memory_limit = memsize; } + + size_t memsize() const { + size_t memsize = 0; + for (const auto &object : m_objects) + memsize += object.second->memsize(); + return memsize; + } // The Undo / Redo stack is being initialized with an empty model and an empty selection. // The first snapshot cannot be removed. @@ -370,13 +453,15 @@ public: bool has_redo_snapshot() const; bool undo(Slic3r::Model &model, const Slic3r::GUI::Selection &selection, size_t jump_to_time); bool redo(Slic3r::Model &model, 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; } + const Selection& selection_deserialized() const { return m_selection; } //protected: template ObjectID save_mutable_object(const T &object); @@ -400,6 +485,7 @@ public: 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 { @@ -412,9 +498,10 @@ public: #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 || m_snapshots.back().is_topmost()); + 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; @@ -436,6 +523,9 @@ private: } 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. @@ -655,7 +745,7 @@ void StackImpl::take_snapshot(const std::string &snapshot_name, const Slic3r::Mo 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_snapsnot_name, m_active_snapshot_time, 0); + m_snapshots.emplace_back(topmost_snapshot_name, m_active_snapshot_time, 0); // Release empty objects from the history. this->collect_garbage(); assert(this->valid()); @@ -710,9 +800,10 @@ bool StackImpl::undo(Slic3r::Model &model, const Slic3r::GUI::Selection &selecti } 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_snapsnot_name, model, selection); + this->take_snapshot(topmost_snapshot_name, model, selection); // The line above entered another topmost_snapshot_name. assert(m_snapshots.back().is_topmost()); assert(! m_snapshots.back().is_topmost_captured()); @@ -722,8 +813,15 @@ bool StackImpl::undo(Slic3r::Model &model, const Slic3r::GUI::Selection &selecti //-- 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); + 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(); @@ -764,10 +862,70 @@ void StackImpl::collect_garbage() } } +void StackImpl::release_least_recently_used() +{ + assert(this->valid()); + size_t current_memsize = this->memsize(); +#ifdef SLIC3R_UNDOREDO_DEBUG + bool released = false; +#endif + 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. + 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 { + // 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::take_snapshot(const std::string &snapshot_name, const Slic3r::Model &model, const Slic3r::GUI::Selection &selection) { pimpl->take_snapshot(snapshot_name, model, selection); } +void Stack::set_memory_limit(size_t memsize) { pimpl->set_memory_limit(memsize); } +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) { pimpl->take_snapshot(snapshot_name, model, selection); } 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, size_t time_to_load) { return pimpl->undo(model, selection, time_to_load); } @@ -776,6 +934,7 @@ const Selection& Stack::selection_deserialized() const { return pimpl->selection 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 diff --git a/src/slic3r/Utils/UndoRedo.hpp b/src/slic3r/Utils/UndoRedo.hpp index f8bfda08c..606b8de74 100644 --- a/src/slic3r/Utils/UndoRedo.hpp +++ b/src/slic3r/Utils/UndoRedo.hpp @@ -55,6 +55,15 @@ public: Stack(); ~Stack(); + // Set maximum memory threshold. If the threshold is exceeded, least recently used snapshots are released. + void set_memory_limit(size_t memsize); + + // Estimate size of the RAM consumed by the Undo / Redo stack. + size_t memsize() const; + + // Release least recently used snapshots up to the memory limit set above. + void release_least_recently_used(); + // 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); @@ -78,6 +87,9 @@ public: // The snapshot time indicates start of an operation, which is finished at the time of the following snapshot, therefore // the active snapshot is the successive snapshot. The same logic applies to the time_to_load parameter of undo() and redo() operations. size_t active_snapshot_time() const; + // Temporary snapshot is active if the topmost snapshot is active and it has not been captured yet. + // In that case the Undo action will capture the last snapshot. + bool temp_snapshot_active() const; // After load_snapshot() / undo() / redo() the selection is deserialized into a list of ObjectIDs, which needs to be converted // into the list of GLVolume pointers once the 3D scene is updated.