diff --git a/src/user/CMakeLists.txt b/src/user/CMakeLists.txt index 035ef1fe..5aa8981c 100644 --- a/src/user/CMakeLists.txt +++ b/src/user/CMakeLists.txt @@ -15,6 +15,8 @@ set(MUJOCO_USER_SRCS user_api.cc user_api.h + user_cache.cc + user_cache.h user_composite.cc user_composite.h user_flexcomp.cc diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 6f779b4c..8068b1a0 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -13,8 +13,11 @@ // limitations under the License. #include "user/user_api.h" + +#include #include #include +#include #include #include #include @@ -22,10 +25,9 @@ #include #include "user/user_model.h" #include "user/user_objects.h" +#include "user/user_cache.h" #include "xml/xml_util.h" - - // create model mjSpec* mjm_createSpec() { mjCModel* modelC = new mjCModel; @@ -600,4 +602,17 @@ const char* mjm_setFullInertia(mjmBody* bodyspec, double quat[4], double inertia return body->FullInertia(quat, inertia); } +// -------------------------- GLOBAL ASSET CACHE ------------------------------- +void mj_setCacheSize(mjCache cache, std::size_t size) { + mjCCache* ccache = reinterpret_cast(cache); + if (ccache) { + ccache->SetMaxSize(size); + } +} + + + +mjCache mj_globalCache() { + return NULL; // currently disabled +} diff --git a/src/user/user_api.h b/src/user/user_api.h index 47b2541b..ce164cc3 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -983,6 +983,16 @@ MJAPI void mjm_defaultKey(mjmKey& key); // Default plugin attributes. MJAPI void mjm_defaultPlugin(mjmPlugin& plugin); +//------------------------- Cache functions ------------------------------------ + +typedef struct _mjCache* mjCache; + +// Set the size of the cache in bytes. +MJAPI void mj_setCacheSize(mjCache cache, size_t size); + +// Get internal global cache context. +MJAPI mjCache mj_globalCache(); + #ifdef __cplusplus } #endif diff --git a/src/user/user_cache.cc b/src/user/user_cache.cc index 64d1c9dd..64f125fc 100644 --- a/src/user/user_cache.cc +++ b/src/user/user_cache.cc @@ -39,7 +39,8 @@ template std::size_t mjCAsset::Add(const std::string& name, const uint8_t* ptr = reinterpret_cast(data); mjCAssetData& block = it->second; - block.bytes = std::make_shared(nbytes); + block.bytes = std::shared_ptr(new uint8_t[nbytes], + [](uint8_t *p) {delete [] p;}); std::copy(ptr, ptr + nbytes, block.bytes.get()); block.nbytes = nbytes; nbytes_ += nbytes; diff --git a/src/user/user_cache.h b/src/user/user_cache.h index ee0f1ef1..62af3580 100644 --- a/src/user/user_cache.h +++ b/src/user/user_cache.h @@ -30,7 +30,7 @@ // data associated with an asset struct mjCAssetData { - std::shared_ptr bytes; // raw serialized bytes of cached data + std::shared_ptr bytes; // raw serialized bytes of cached data std::size_t nbytes; // number of bytes stored }; @@ -80,6 +80,11 @@ class mjCAsset { return blocks_.find(name) != blocks_.end(); } + const std::string& Timestamp() const { return timestamp_; } + const std::string& Id() const { return id_; } + std::size_t InsertNum() const { return insert_num_; } + std::size_t AccessCount() const { return access_count_; } + private: mjCAsset() = default; @@ -105,10 +110,6 @@ class mjCAsset { void SetTimestamp(std::string timestamp) { timestamp_ = timestamp; } // accessors - const std::string& Id() const { return id_; } - const std::string& Timestamp() const { return timestamp_; } - std::size_t InsertNum() const { return insert_num_; } - std::size_t AccessCount() const { return access_count_; } std::size_t BytesCount() const { return nbytes_; } const std::unordered_map& Blocks() const { return blocks_; @@ -128,14 +129,23 @@ class mjCAsset { std::set references_; }; +struct mjCAssetCompare { + bool operator()(const mjCAsset* e1, const mjCAsset* e2) const { + if (e1->AccessCount() != e2->AccessCount()) { + return e1->AccessCount() < e2->AccessCount(); + } + return e1->InsertNum() < e2->InsertNum(); + } +}; + // the class container for a thread-safe asset cache class mjCCache { public: explicit mjCCache(std::size_t size) : max_size_(size) {} // move only - mjCCache(mjCCache&& other) = default; - mjCCache& operator=(mjCCache&& other) = default; + mjCCache(mjCCache&& other) = delete; + mjCCache& operator=(mjCCache&& other) = delete; mjCCache(const mjCCache& other) = delete; mjCCache& operator=(const mjCCache& other) = delete; @@ -186,20 +196,11 @@ class mjCCache { std::size_t size_ = 0; // current size of the cache in bytes std::size_t max_size_ = 0; // max size of the cache in bytes - // compare function for the priority queue - static constexpr auto compare_ = [](const mjCAsset* e1, - const mjCAsset* e2) { - if (e1->AccessCount() != e2->AccessCount()) { - return e1->AccessCount() < e2->AccessCount(); - } - return e1->InsertNum() < e2->InsertNum(); - }; - // internal constant look up table for assets std::unordered_map lookup_; // internal priority queue for the cache - std::set entries_; + std::set entries_; // models using the cache along with the assets they reference std::unordered_map> models_; diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 552cdb62..e678cbec 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -60,6 +60,7 @@ #include "engine/engine_util_misc.h" #include "engine/engine_util_solve.h" #include "engine/engine_util_spatial.h" +#include "user/user_cache.h" #include "user/user_model.h" #include "user/user_objects.h" #include "user/user_util.h" @@ -967,6 +968,19 @@ void mjCMesh::RemoveRepeated() { void mjCMesh::LoadOBJ(mjResource* resource) { tinyobj::ObjReader objReader; const void* bytes = nullptr; + + // try loading from cache + mjCCache *cache = reinterpret_cast(mj_globalCache()); + if (cache) { + auto asset = cache->Get(resource->name); + if (asset.has_value() && + !mju_isModifiedResource(resource, asset->Timestamp().c_str())) { + if (LoadCachedOBJ(asset.value())) { + return; + } + } + } + int buffer_sz = mju_readResource(resource, &bytes); if (buffer_sz < 0) { throw mjCError(this, "could not read OBJ file '%s'", resource->name); @@ -1030,9 +1044,117 @@ void mjCMesh::LoadOBJ(mjResource* resource) { for (int i=0; iname, resource->timestamp); + + asset.AddVector("uservert_", uservert_); + asset.AddVector("usernormal_", usernormal_); + asset.AddVector("usertexcoord_", usertexcoord_); + + if (!objReader.GetShapes().empty()) { + const auto& mesh = objReader.GetShapes()[0].mesh; + std::vector vertex_index; + vertex_index.reserve(mesh.indices.size()); + + std::vector normal_index; + normal_index.reserve(mesh.indices.size()); + + std::vector texcoord_index; + texcoord_index.reserve(mesh.indices.size()); + + for (tinyobj::index_t index : mesh.indices) { + vertex_index.push_back(index.vertex_index); + normal_index.push_back(index.normal_index); + texcoord_index.push_back(index.texcoord_index); + } + + asset.AddVector("num_face_vertices", mesh.num_face_vertices); + asset.AddVector("vertex_index", vertex_index); + asset.AddVector("normal_index", normal_index); + asset.AddVector("texcoord_index", texcoord_index); + } else { + asset.AddVector("num_face_vertices", std::vector()); + asset.AddVector("vertex_index", std::vector()); + asset.AddVector("normal_index", std::vector()); + asset.AddVector("texcoord_index", std::vector()); + } + cache->Insert(std::move(asset)); + } } + +// load OBJ from cached asset, return true on success +bool mjCMesh::LoadCachedOBJ(const mjCAsset& asset) { + // check that asset has all data + if (!asset.HasData("uservert_") || !asset.HasData("usernormal_") + || !asset.HasData("usertexcoord_") || !asset.HasData("num_face_vertices") + || !asset.HasData("vertex_index") || !asset.HasData("normal_index") + || !asset.HasData("texcoord_index")) { + return false; + } + uservert_ = asset.GetVector("uservert_").value(); + usernormal_ = asset.GetVector("usernormal_").value(); + usertexcoord_ = asset.GetVector("usertexcoord_").value(); + + vector vertex_index = asset.GetVector("vertex_index").value(); + vector normal_index = asset.GetVector("normal_index").value(); + vector texcoord_index = asset.GetVector("texcoord_index").value(); + vector num_face_vertices = + asset.GetVector("num_face_vertices").value(); + + bool righthand = (scale[0] * scale[1] * scale[2]) > 0; + + + for (int face = 0, i = 0; i < vertex_index.size();) { + int nfacevert = num_face_vertices[face]; + if (nfacevert < 3 || nfacevert > 4) { + throw mjCError( + this, "only tri or quad meshes are supported for OBJ (file '%s')", + asset.Id().c_str()); + } + + userface_.push_back(vertex_index[i]); + userface_.push_back(vertex_index[i + (righthand == 1 ? 1 : 2)]); + userface_.push_back(vertex_index[i + (righthand == 1 ? 2 : 1)]); + + if (!usernormal_.empty()) { + userfacenormal_.push_back(normal_index[i]); + userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 1 : 2)]); + userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 2 : 1)]); + } + + if (!usertexcoord_.empty()) { + userfacetexcoord_.push_back(texcoord_index[i]); + userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 1 : 2)]); + userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 2 : 1)]); + } + + if (nfacevert == 4) { + userface_.push_back(vertex_index[i]); + userface_.push_back(vertex_index[i + (righthand == 1 ? 2 : 3)]); + userface_.push_back(vertex_index[i + (righthand == 1 ? 3 : 2)]); + + if (!usernormal_.empty()) { + userfacenormal_.push_back(normal_index[i]); + userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 1 : 2)]); + userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 2 : 1)]); + } + + if (!usertexcoord_.empty()) { + userfacetexcoord_.push_back(texcoord_index[i]); + userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 1 : 2)]); + userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 2 : 1)]); + } + } + i += nfacevert; + ++face; + } + return true; +} + // load STL binary mesh void mjCMesh::LoadSTL(mjResource* resource) { bool righthand = (scale[0]*scale[1]*scale[2]>0); diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 2d24ea85..a62436e1 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -26,6 +26,7 @@ #include #include #include "user/user_api.h" +#include "user/user_cache.h" // forward declarations of all mjC/X classes class mjCError; @@ -827,6 +828,7 @@ class mjCMesh: public mjCMesh_, private mjmMesh { private: void LoadOBJ(mjResource* resource); // load mesh in wavefront OBJ format + bool LoadCachedOBJ(const mjCAsset& asset); // load OBJ from cache asset, return true on success void LoadSTL(mjResource* resource); // load mesh in STL BIN format void LoadMSH(mjResource* resource); // load mesh in MSH BIN format void LoadSDF(); // generate mesh using marching cubes diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index acfa8501..926e8dc3 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -116,5 +117,58 @@ TEST_F(PluginTest, RecompileCompare) { } } +// ------------------- test cache with modified assets ------------------------- +TEST_F(PluginTest, RecompileCompareCache) { + static constexpr char xml[] = R"( + + + + + + + + )"; + + static constexpr char cube1[] = R"( + v -0.500000 -0.500000 0.500000 + v 0.500000 -0.500000 0.500000 + v -0.500000 0.500000 0.500000 + v 0.500000 0.500000 0.500000 + v -0.500000 0.500000 -0.500000 + v 0.500000 0.500000 -0.500000 + v -0.500000 -0.500000 -0.500000 + v 0.500000 -0.500000 -0.500000)"; + + static constexpr char cube2[] = R"( + v -1 -1 1 + v 1 -1 1 + v -1 1 1 + v 1 1 1 + v -1 1 -1 + v 1 1 -1 + v -1 -1 -1 + v 1 -1 -1)"; + + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + mj_addBufferVFS(vfs.get(), "cube.obj", cube1, sizeof(cube1)); + + std::array error; + + // load model once + mjModel* m = LoadModelFromString(xml, error.data(), error.size(), vfs.get()); + EXPECT_EQ(m->mesh_vert[0], -0.5); + mj_deleteModel(m); + + // update cube.obj, load again + mj_deleteFileVFS(vfs.get(), "cube.obj"); + mj_addBufferVFS(vfs.get(), "cube.obj", cube2, sizeof(cube2)); + m = LoadModelFromString(xml, error.data(), error.size(), vfs.get()); + EXPECT_EQ(m->mesh_vert[0], -1); + mj_deleteModel(m); + + mj_deleteVFS(vfs.get()); +} + } // namespace } // namespace mujoco