diff --git a/CHANGELOG.md b/CHANGELOG.md index de8adb4532..07af30055a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,8 @@ v4.6 - If an `Object` cannot be found when loading a list property from XML, a warning will now be emitted to the log (previously: it was emitted to `std::cerr`, #4009). - Added the property `activation_dynamics_smoothing` to `DeGrooteFregly2016Muscle`. This property uses the model's original value of 0.1 as a default, but users may consider increasing this value (e.g., 10.0) so that the activation and deactivation speeds of the model better match the - activation and deactivation time constants. +- `OpenSim::Mesh` now retains a reference-counted copy of the mesh data when it's copied, which should make + copying + re-finalizing `OpenSim::Model`s faster (#4010). v4.5.1 ====== diff --git a/OpenSim/Simulation/Model/Geometry.cpp b/OpenSim/Simulation/Model/Geometry.cpp index ed97c3b14a..b811f6de8b 100644 --- a/OpenSim/Simulation/Model/Geometry.cpp +++ b/OpenSim/Simulation/Model/Geometry.cpp @@ -24,10 +24,17 @@ //============================================================================= // INCLUDES //============================================================================= -#include -#include "Frame.h" #include "Geometry.h" + +#include "Frame.h" #include "Model.h" + +#include +#include +#include +#include +#include + //============================================================================= // STATICS //============================================================================= @@ -37,6 +44,69 @@ using namespace SimTK; OpenSim_DEFINE_SOCKET_FD(frame, Geometry); +namespace +{ + // Returns a pointer to `c`'s owner, or `nullptr` if `c` does not have an owner. + const OpenSim::Component* tryGetOwner(const OpenSim::Component& c) + { + return c.hasOwner() ? &c.getOwner() : nullptr; + } + + // Returns a pointer to the closest ancestor of `c` that has type `T`, or + // `nullptr` if no such owner exists. + template + const T* findFirstOwnerOfType(const OpenSim::Component& c) + { + for (const OpenSim::Component* cur = tryGetOwner(c); cur; cur = tryGetOwner(*cur)) { + if (const T* downcasted = dynamic_cast(cur)) { + return downcasted; + } + } + return nullptr; + } + + // Returns `true` if `str` has a suffix of `suffix`, ignoring case. + bool hasSuffixCaseInsensitive(const std::string& str, const std::string& suffix) + { + if (str.size() < suffix.size()) { + return false; + } + for (std::string::size_type i = 0; i < suffix.size(); ++i) { + if (std::tolower(str.rbegin()[i]) != std::tolower(suffix.rbegin()[i])) { + return false; + } + } + return true; + } + + // Returns an absolute path to the underlying geometry file that can be + // associated with `file`; otherwise, returns `std::nullopt`. + // + // Prints search errors to the log if `warningGiven` is `false` and then + // flips `warningGiven` to `true` (i.e. it's a one-time flag). + std::optional findGeometry( + const OpenSim::Model& model, + const std::string& file, + bool& warningGiven) + { + Array_ attempts; + bool isAbsolutePath = false; + if (ModelVisualizer::findGeometryFile(model, file, isAbsolutePath, attempts)) { + return std::move(attempts.back()); + } + + // Else: geometry file could not be found, print warning + if (!std::exchange(warningGiven, true)) { + log_warn("Couldn't find file '{}'.", file); + log_debug( "The following locations were tried:"); + for (const auto& attempt : attempts) { + log_debug(attempt); + } + } + return std::nullopt; + } +} + Geometry::Geometry() { setNull(); constructProperties(); @@ -216,106 +286,111 @@ void FrameGeometry::implementCreateDecorativeGeometry(SimTK::Array_(owner) != nullptr) { - rootModel = owner; - break; - } - if (owner->hasOwner()) - owner = &(owner->getOwner()); // traverse up Component tree - else - break; // can't traverse up. - } + const std::string& getMeshFilePath() const { return _meshFile.getMeshFile(); } + const std::filesystem::file_time_type& getModificationTime() const { return _meshFileModificationTime; } + const SimTK::Vec3& getScaleFactors() const { return _meshFile.getScaleFactors(); } + void setScaleFactors(const SimTK::Vec3& newScaleFactors) { _meshFile.setScaleFactors(newScaleFactors); } + const SimTK::DecorativeGeometry& getGeometry() const { return _meshFile; } +private: + std::filesystem::file_time_type _meshFileModificationTime; + SimTK::DecorativeMeshFile _meshFile; +}; + +Mesh::Mesh() +{ + constructProperty_mesh_file(""); +} - if (rootModel == nullptr) { - log_error("Mesh {} not connected to model...ignoring", - get_mesh_file()); - return; // Orphan Mesh not descendant of a model - } +Mesh::Mesh(const std::string& geomFile) +{ + constructProperty_mesh_file(""); + upd_mesh_file() = geomFile; +} - // Current interface to Visualizer calls generateDecorations on every - // frame. On first time through, load file and create DecorativeMeshFile - // and cache it so we don't load files from disk during live rendering. - const Model* mdl = dynamic_cast(rootModel); - const std::string& file = get_mesh_file(); - if (file.empty() || file.compare(PropertyStr::getDefaultStr()) == 0 || - !mdl->getDisplayHints().isVisualizationEnabled()) - return; // Return immediately if no file has been specified - // or display is disabled altogether. - - bool isAbsolutePath; string directory, fileName, extension; - SimTK::Pathname::deconstructPathname(file, - isAbsolutePath, directory, fileName, extension); - const string lowerExtension = SimTK::String::toLower(extension); - if (lowerExtension != ".vtp" && lowerExtension != ".obj" && lowerExtension != ".stl") { - log_error("ModelVisualizer ignoring '{}'; only .vtp, .stl, and " - ".obj files currently supported.", - file); - return; - } +void Mesh::extendFinalizeFromProperties() +{ + if (isObjectUpToDateWithProperties()) { + return; // No need to re-finalize. + } - // File is a .vtp, .stl, or .obj; attempt to find it. - Array_ attempts; - const Model& model = dynamic_cast(*rootModel); - bool foundIt = ModelVisualizer::findGeometryFile(model, file, isAbsolutePath, attempts); + const std::string& meshPath = get_mesh_file(); + if (meshPath.empty() || meshPath == PropertyStr::getDefaultStr()) { + _mesh.reset(); + return; // No mesh specified. + } - if (!foundIt) { - if (!warningGiven) { - log_warn("Couldn't find file '{}'.", file); - warningGiven = true; - } - - log_debug( "The following locations were tried:"); - for (unsigned i = 0; i < attempts.size(); ++i) - log_debug(attempts[i]); - - } + if (!(hasSuffixCaseInsensitive(meshPath, ".vtp") || + hasSuffixCaseInsensitive(meshPath, ".obj") || + hasSuffixCaseInsensitive(meshPath, ".stl"))) { + + log_error("ModelVisualizer ignoring '{}'; only .vtp, .stl, and .obj files currently supported.", meshPath); + _mesh.reset(); + return; // Unsupported file format. + } + + const auto* model = findFirstOwnerOfType(*this); + if (!model) { + log_error("Mesh {} not connected to a model...ignoring", get_mesh_file()); + _mesh.reset(); + return; // This component isn't connected to a model. + } + + if (!model->getDisplayHints().isVisualizationEnabled()) { + _mesh.reset(); + return; // Visualization is disabled. + } + + const std::optional meshAbsPath = findGeometry(*model, meshPath, _warningGiven); + if (!meshAbsPath) { + _mesh.reset(); + return; // Couldn't find the mesh. + } + + // Completely reset the cached mesh if the underlying filepath/modification + // time has changed. + if (_mesh && + (_mesh->getMeshFilePath() != *meshAbsPath || + std::filesystem::last_write_time(*meshAbsPath) != _mesh->getModificationTime())) { + _mesh.reset(); + } + + if (!_mesh) { + // There is no cached mesh, load a new one from scratch. try { - std::ifstream objFile; - objFile.open(attempts.back().c_str()); - // objFile closes when destructed - // if the file can be opened but had bad contents e.g. binary vtp - // it will be handled downstream + _mesh = std::make_shared(*meshAbsPath, get_scale_factors()); } - catch (const std::exception& e) { - log_warn("Visualizer couldn't open {} because: {}", - attempts.back(), e.what()); - return; + catch (const std::exception& ex) { + log_warn("Visualizer couldn't open {} because: {}", get_mesh_file(), ex.what()); } - - cachedMesh.reset(new DecorativeMeshFile(attempts.back().c_str())); + } + else if (_mesh->getScaleFactors() != get_scale_factors()) { + // There is a cached mesh, but it has invalid scale factors, copy the mesh + // data, update the scale factors, but don't reload from the filesystem. + auto meshCopy = std::make_shared(*_mesh); + meshCopy->setScaleFactors(get_scale_factors()); + _mesh = std::move(meshCopy); } } - void Mesh::implementCreateDecorativeGeometry(SimTK::Array_& decoGeoms) const { - if (cachedMesh.get() != nullptr) { - try { - // Force the loading of the mesh to see if it has bad contents - // (e.g., binary vtp). - // We do not want to do this in extendFinalizeFromProperties b/c - // it's expensive to repeatedly load meshes. - cachedMesh->getMesh(); - } catch (const std::exception& e) { - log_warn("Visualizer couldn't open {} because: {}", - get_mesh_file(), e.what()); - // No longer try to visualize this mesh. - cachedMesh.reset(); - return; - } - cachedMesh->setScaleFactors(get_scale_factors()); - decoGeoms.push_back(*cachedMesh); + if (_mesh) { + decoGeoms.push_back(_mesh->getGeometry()); } } diff --git a/OpenSim/Simulation/Model/Geometry.h b/OpenSim/Simulation/Model/Geometry.h index 99ecb79e7e..b3d0e0660b 100644 --- a/OpenSim/Simulation/Model/Geometry.h +++ b/OpenSim/Simulation/Model/Geometry.h @@ -26,6 +26,8 @@ #include #include "Appearance.h" +#include + namespace OpenSim { class Frame; @@ -557,43 +559,30 @@ class OSIMSIMULATION_API Mesh : public Geometry public: /// Default constructor - Mesh() : - Geometry(), - cachedMesh(nullptr), - warningGiven(false) - { - constructProperty_mesh_file(""); - } + Mesh(); + /// Constructor that takes a mesh file name - Mesh(const std::string& geomFile) : - Geometry(), - cachedMesh(nullptr), - warningGiven(false) - { - constructProperty_mesh_file(""); - upd_mesh_file() = geomFile; - } - /// destructor - virtual ~Mesh() {}; + Mesh(const std::string& geomFile); + /// Retrieve file name const std::string& getGeometryFilename() const { return get_mesh_file(); }; protected: - // ModelComponent interface. void extendFinalizeFromProperties() override; protected: - /// Method to map Mesh to Array of SimTK::DecorativeGeometry. void implementCreateDecorativeGeometry( - SimTK::Array_& decoGeoms) const override; + SimTK::Array_&) const override; + private: - // We cache the DecorativeMeshFile if we successfully - // load the mesh from file so we don't try loading from disk every frame. - // This is mutable since it is not part of the public interface. - mutable SimTK::ResetOnCopy> cachedMesh; - mutable bool warningGiven; + // The mesh data is cached and reference-counted for copies of this `Mesh` + // until it's detected that its on-disk location, or scale factors, have + // changed. + class CachedDecorativeMeshFile; + std::shared_ptr _mesh; + bool _warningGiven = false; }; /**