From c2138c3fb0ec400893cd19fa1cc2c2b4083b7be1 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Thu, 16 Jan 2025 03:52:31 -0800 Subject: [PATCH] Do not copy the spec during attach. Use a reference count for managing the memory. PiperOrigin-RevId: 716169486 Change-Id: Id270c4858c17b9250115e9544d5ea143584e2d5f --- doc/APIreference/functions.rst | 9 ++ doc/changelog.rst | 9 +- doc/includes/references.h | 1 + doc/programming/modeledit.rst | 24 ++-- doc/python.rst | 10 +- include/mujoco/mujoco.h | 3 + introspect/functions.py | 18 +++ python/mujoco/specs.cc | 8 ++ python/mujoco/specs_test.py | 40 ++++--- src/user/user_api.cc | 13 ++- src/user/user_api.h | 3 + src/user/user_mesh.cc | 3 - src/user/user_model.cc | 128 +++++++++++++++----- src/user/user_model.h | 9 +- src/user/user_objects.cc | 208 +++++++++++++++++++++------------ src/user/user_objects.h | 26 ++++- src/xml/xml_native_reader.cc | 6 + test/user/user_api_test.cc | 159 +++++++++++++++++++------ test/user/user_model_test.cc | 1 + 19 files changed, 505 insertions(+), 173 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 91fdbd7b..19d792fd 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -1510,6 +1510,15 @@ Free memory allocation in mjSpec. Activate plugin. Returns 0 on success. +.. _mjs_setDeepCopy: + +`mjs_setDeepCopy <#mjs_setDeepCopy>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_setDeepCopy + +Turn deep copy on or off attach. Returns 0 on success. + .. _Errorandmemory: Error and memory diff --git a/doc/changelog.rst b/doc/changelog.rst index d9a9936d..c3a0cac9 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,8 +2,13 @@ Changelog ========= -Version 3.2.7 (Jan 14, 2025) ----------------------------- +Upcoming version (not yet released) +----------------------------------- + +- Added ``mjs_setDeepCopy`` API function. When the deep copy flag is 0, attaching a model will not copy it to the + parent, so the original references to the child allow to modify the parent as well. The default behavior is to perform + such a shallow copy. The old behavioud of creating a deep copy of the child model while attaching can be restored by + setting the deep copy flag to 1. Python bindings ^^^^^^^^^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index d9742797..26513c8d 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3181,6 +3181,7 @@ mjSpec* mj_makeSpec(void); mjSpec* mj_copySpec(const mjSpec* s); void mj_deleteSpec(mjSpec* s); int mjs_activatePlugin(mjSpec* s, const char* name); +int mjs_setDeepCopy(mjSpec* s, int deepcopy); void mj_printFormattedModel(const mjModel* m, const char* filename, const char* float_format); void mj_printModel(const mjModel* m, const char* filename); void mj_printFormattedData(const mjModel* m, mjData* d, const char* filename, diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index eb65b6e7..fa800b67 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -106,12 +106,14 @@ procedurally, default classes are passed in explicitly to element constructors. Attachment ^^^^^^^^^^ -This framework introduces a powerful new feature: attaching and detaching model subtrees. Attachment allows the user -copy a subtree from one model into another, while also copying related referenced assets and referencing elements from -outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will remove all associated -elements from the model. This feature is already used to power the :ref:`attach` and -:ref:`replicate` meta-elements in MJCF. It is possible to :ref:`attach a body to a frame` and -to :ref:`attach a body to a site`: + +This framework introduces a powerful new feature: attaching and detaching model subtrees. This feature is already used +to power the :ref:`attach` an :ref:`replicate` meta-elements in MJCF. Attachment allows the user +to move or copy a subtree from one model into another, while also copying or moving related referenced assets and +referencing elements from outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will +remove all associated elements from the model. The default behavior is to move during attach. The user can select to +instead copy by passing the corresponding flag to ``mjs_setDeepCopy``. This flag is temporary set to true while parsing +XMLs. It is possible to :ref:`attach a body to a frame`: .. code-block:: C @@ -120,9 +122,17 @@ to :ref:`attach a body to a site`: parent->compiler.degree = 0; child->compiler.degree = 1; mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL); - mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL); mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL); mjsBody* attached_body_1 = mjs_attachBody(frame, body, "attached-", "-1"); + +or :ref:`attach a body to a site`: + +.. code-block:: C + + mjSpec* parent = mj_makeSpec(); + mjSpec* child = mj_makeSpec(); + mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL); + mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL); mjsBody* attached_body_2 = mjs_attachToSite(site, body, "attached-", "-2"); or :ref:`attach a frame to a body`: diff --git a/doc/python.rst b/doc/python.rst index 90f861a2..95bd409a 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -531,15 +531,19 @@ Attachment It is possible to combine multiple specs by using attachments. The following options are possible: - Attach a body from the child spec to a frame in the parent spec: ``body.attach_body(body, prefix, suffix)``, returns - the newly createdbody in the parent spec. + the reference to the attached body, which should be identical to the body used as input. - Attach a frame from the child spec to a body in the parent spec: ``body.attach_frame(frame, prefix, suffix)``, - returns the newly created frame in the parent spec. + returns the reference to the attached frame, which should be identical to the frame used as input. - Attach a body from the child spec to a site in the parent spec: ``site.attach(body, prefix, suffix)``, returns the - newly created body in the parent spec. + reference to the attached body, which should be identical to the body used as input. - Attach the worldbody from the child spec to a frame in the parent spec and transform it to a frame: ``body.attach(spec, prefix, suffix)``, returns the newly created frame that the child worldbody was transformed into. +Attaching does not copy, so all the child reference are still valid in the parent and therefore modifying the child will +modify the parent. This is not true for the attach :ref:`attach` an :ref:`replicate` +meta-elements in MJCF, which create deep copies while attaching. + .. code-block:: python import mujoco diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 8fb5d3aa..1fefa52f 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -242,6 +242,9 @@ MJAPI void mj_deleteSpec(mjSpec* s); // Activate plugin. Returns 0 on success. MJAPI int mjs_activatePlugin(mjSpec* s, const char* name); +// Turn deep copy on or off attach. Returns 0 on success. +MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy); + //---------------------------------- Printing ------------------------------------------------------ diff --git a/introspect/functions.py b/introspect/functions.py index 4fd2374a..cdde54ff 100644 --- a/introspect/functions.py +++ b/introspect/functions.py @@ -1045,6 +1045,24 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Activate plugin. Returns 0 on success.', )), + ('mjs_setDeepCopy', + FunctionDecl( + name='mjs_setDeepCopy', + return_type=ValueType(name='int'), + parameters=( + FunctionParameterDecl( + name='s', + type=PointerType( + inner_type=ValueType(name='mjSpec'), + ), + ), + FunctionParameterDecl( + name='deepcopy', + type=ValueType(name='int'), + ), + ), + doc='Turn deep copy on or off attach. Returns 0 on success.', + )), ('mj_printFormattedModel', FunctionDecl( name='mj_printFormattedModel', diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index a0dc13e4..2f37f64f 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -350,6 +350,14 @@ PYBIND11_MODULE(_specs, m) { mjSpec.def("copy", [](const MjSpec& self) -> MjSpec { return MjSpec(self); }); + mjSpec.def_property( + "copy_during_attach", + [](MjSpec& self) { + throw pybind11::value_error("copy_during_attach can only be set."); + }, + [](MjSpec& self, bool deepcopy) { + return mjs_setDeepCopy(self.ptr, deepcopy); + }); mjSpec.def_property_readonly( "worldbody", [](MjSpec& self) -> raw::MjsBody* { diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index dd8a6df7..d7b86481 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -918,27 +918,31 @@ class SpecsTest(absltest.TestCase): model = parent.compile() np.testing.assert_almost_equal(model.body_quat[1], [1, 0, 0, 0]) - def test_attach_body_to_site(self): - child = mujoco.MjSpec() + def test_attach_to_site(self): parent = mujoco.MjSpec() site = parent.worldbody.add_site(pos=[1, 2, 3], quat=[0, 0, 0, 1]) - body = child.worldbody.add_body() # Attach body to site and compile. - self.assertIsNotNone(site.attach_body(body, prefix='_')) + child1 = mujoco.MjSpec() + body1 = child1.worldbody.add_body() + self.assertIs(body1, site.attach_body(body1, prefix='_')) + body1.pos = [1, 1, 1] model1 = parent.compile() self.assertIsNotNone(model1) self.assertEqual(model1.nbody, 2) - np.testing.assert_array_equal(model1.body_pos[1], [1, 2, 3]) + np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4]) np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1]) # Attach entire spec to site and compile again. - self.assertIsNotNone(site.attach(child, prefix='child-')) + child2 = mujoco.MjSpec() + body2 = child2.worldbody.add_body(name='body') + self.assertIsNotNone(site.attach(child2, prefix='child-')) + body2.pos = [-1, -1, -1] model2 = parent.compile() self.assertIsNotNone(model2) self.assertEqual(model2.nbody, 3) - np.testing.assert_array_equal(model2.body_pos[1], [1, 2, 3]) - np.testing.assert_array_equal(model2.body_pos[2], [1, 2, 3]) + np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4]) + np.testing.assert_array_equal(model2.body_pos[2], [2, 3, 2]) np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1]) np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1]) @@ -949,27 +953,31 @@ class SpecsTest(absltest.TestCase): frame = body.to_frame() np.testing.assert_array_equal(frame.pos, [1, 2, 3]) - def test_attach_spec_to_frame(self): - child = mujoco.MjSpec() + def test_attach_to_frame(self): parent = mujoco.MjSpec() frame = parent.worldbody.add_frame(pos=[1, 2, 3], quat=[0, 0, 0, 1]) - body = child.worldbody.add_body() # Attach body to frame and compile. - self.assertIsNotNone(frame.attach_body(body, prefix='_')) + child1 = mujoco.MjSpec() + body1 = child1.worldbody.add_body() + self.assertIs(body1, frame.attach_body(body1, prefix='_')) + body1.pos = [1, 1, 1] model1 = parent.compile() self.assertIsNotNone(model1) self.assertEqual(model1.nbody, 2) - np.testing.assert_array_equal(model1.body_pos[1], [1, 2, 3]) + np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4]) np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1]) # Attach entire spec to frame and compile again. - self.assertIsNotNone(frame.attach(child, prefix='child-')) + child2 = mujoco.MjSpec() + body2 = child2.worldbody.add_body(name='body') + self.assertIsNotNone(frame.attach(child2, prefix='child-')) + body2.pos = [-1, -1, -1] model2 = parent.compile() self.assertIsNotNone(model2) self.assertEqual(model2.nbody, 3) - np.testing.assert_array_equal(model2.body_pos[1], [1, 2, 3]) - np.testing.assert_array_equal(model2.body_pos[2], [1, 2, 3]) + np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4]) + np.testing.assert_array_equal(model2.body_pos[2], [2, 3, 2]) np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1]) np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1]) diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 8eb612a7..5c9bb28f 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -208,7 +208,7 @@ int mjs_detachBody(mjSpec* s, mjsBody* b) { model->SetError(e); return -1; } - delete body; + model->Detach(body); return 0; } @@ -254,6 +254,15 @@ int mjs_activatePlugin(mjSpec* s, const char* name) { +// set deep copy flag +int mjs_setDeepCopy(mjSpec* s, int deepcopy) { + mjCModel* model = static_cast(s->element); + model->SetDeepCopy(deepcopy); + return 0; +} + + + // delete object, return 0 if success int mjs_delete(mjsElement* element) { mjCBase* object = static_cast(element); @@ -705,7 +714,7 @@ const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* s mjsFrame* mjs_bodyToFrame(mjsBody** body) { mjCBody* bodyC = static_cast((*body)->element); mjCFrame* frameC = bodyC->ToFrame(); - delete bodyC; + bodyC->model->Detach(bodyC); *body = nullptr; return &frameC->spec; } diff --git a/src/user/user_api.h b/src/user/user_api.h index b6722710..89079048 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -63,6 +63,9 @@ MJAPI void mjs_addSpec(mjSpec* s, mjSpec* child); // Activate plugin, return 0 on success. MJAPI int mjs_activatePlugin(mjSpec* s, const char* name); +// Turn deep copy on or off attach. Returns 0 on success. +MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy); + //---------------------------------- Attachment ---------------------------------------------------- diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 4de4e2e3..2e349459 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -278,9 +278,6 @@ void mjCMesh::CopyPlugin() { mjCMesh::~mjCMesh() { if (center_) mju_free(center_); if (graph_) mju_free(graph_); - if (spec.plugin.active && spec.plugin.name->empty() && model) { - model->DeleteElement(spec.plugin.element); - } } diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 55fcb0d8..532eece0 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -139,6 +139,7 @@ mjCModel::mjCModel() { center_auto[0] = center_auto[1] = center_auto[2] = 0; #endif + deepcopy_ = false; nplugin = 0; Clear(); @@ -181,6 +182,7 @@ mjCModel::mjCModel(const mjCModel& other) { mjCModel& mjCModel::operator=(const mjCModel& other) { + deepcopy_ = true; if (this != &other) { this->spec = other.spec; *static_cast(this) = static_cast(other); @@ -210,6 +212,7 @@ mjCModel& mjCModel::operator=(const mjCModel& other) { ids[i] = other.ids[i]; } } + deepcopy_ = other.deepcopy_; return *this; } @@ -222,20 +225,29 @@ void mjCModel::CopyList(std::vector& dest, // loop over the elements from the other model int nsource = (int)source.size(); for (int i = 0; i < nsource; i++) { - T* candidate = new T(*source[i]); + T* candidate = deepcopy_ ? new T(*source[i]) : source[i]; try { // try to find the referenced object in this model - candidate->NameSpace(source[i]->model); + mjCModel* source_model = source[i]->model; + candidate->model = this; + candidate->NameSpace(source_model); candidate->CopyFromSpec(); candidate->ResolveReferences(this); } catch (mjCError err) { // if not present, skip the element // TODO: do not skip elements that contain user errors - delete candidate; + if (deepcopy_) { + candidate->model = nullptr; + delete candidate; + } continue; } // copy the element from the other model to this model - source[i]->ForgetKeyframes(); + if (deepcopy_) { + source[i]->ForgetKeyframes(); + } else { + candidate->AddRef(); + } mjSpec* origin = FindSpec(source[i]->compiler); dest.push_back(candidate); dest.back()->model = this; @@ -324,9 +336,12 @@ void mjCModel::CopyExplicitPlugin(T* obj) { return; } mjCPlugin* origin = static_cast(obj->spec.plugin.element); - mjCPlugin* candidate = new mjCPlugin(*origin); + mjCPlugin* candidate = deepcopy_ ? new mjCPlugin(*origin) : origin; candidate->id = plugins_.size(); candidate->model = this; + if (!deepcopy_) { + candidate->AddRef(); + } plugins_.push_back(candidate); obj->spec.plugin.element = candidate; } @@ -566,7 +581,7 @@ void deletefromlist(std::vector* list, mjsElement* element) { for (int j = 0; j < list->size(); ++j) { list->at(j)->id = -1; if (list->at(j) == element) { - delete list->at(j); + list->at(j)->Release(); list->erase(list->begin() + j); j--; } @@ -577,8 +592,9 @@ void deletefromlist(std::vector* list, mjsElement* element) { // discard all invalid elements from all lists void mjCModel::DeleteElement(mjsElement* el) { - mjCBody *world = bodies_[0]; + mjCBody *world = nullptr; if (compiled) { + world = bodies_[0]; ResetTreeLists(); } @@ -588,8 +604,14 @@ void mjCModel::DeleteElement(mjsElement* el) { break; case mjOBJ_GEOM: - deletefromlist(&(static_cast(el)->body->geoms), el); + { + mjCGeom* geom = static_cast(el); + if (geom->plugin.active && geom->plugin.name->empty() && geom->GetRef() == 1) { + DeleteElement(geom->plugin.element); + } + deletefromlist(&(geom->body->geoms), el); break; + } case mjOBJ_SITE: deletefromlist(&(static_cast(el)->body->sites), el); @@ -607,6 +629,36 @@ void mjCModel::DeleteElement(mjsElement* el) { deletefromlist(&(static_cast(el)->body->cameras), el); break; + case mjOBJ_MESH: + { + mjCMesh* mesh = static_cast(el); + if (mesh->plugin.active && mesh->plugin.name->empty() && mesh->GetRef() == 1) { + DeleteElement(mesh->plugin.element); + } + deletefromlist(object_lists_[mjOBJ_MESH], el); + break; + } + + case mjOBJ_ACTUATOR: + { + mjCActuator* actuator = static_cast(el); + if (actuator->plugin.active && actuator->plugin.name->empty() && actuator->GetRef() == 1) { + DeleteElement(actuator->plugin.element); + } + deletefromlist(object_lists_[mjOBJ_ACTUATOR], el); + break; + } + + case mjOBJ_SENSOR: + { + mjCSensor* sensor = static_cast(el); + if (sensor->plugin.active && sensor->plugin.name->empty() && sensor->GetRef() == 1) { + DeleteElement(sensor->plugin.element); + } + deletefromlist(object_lists_[mjOBJ_SENSOR], el); + break; + } + default: deletefromlist(object_lists_[el->elemtype], el); break; @@ -621,6 +673,29 @@ void mjCModel::DeleteElement(mjsElement* el) { +// recursively delete all plugins in the subtree +void deletesubtreeplugin(mjCBody* subtree, mjCModel* model) { + mjsPlugin* plugin = &(subtree->spec.plugin); + if (plugin->active && plugin->name->empty()) { + model->DeleteElement(plugin->element); + } + for (auto* body : subtree->Bodies()) { + deletesubtreeplugin(body, model); + } +} + + + +// deletes all plugins in the subtree and then the subtree itself +void mjCModel::Detach(mjCBody* subtree) { + if (subtree->GetRef() == 1) { + deletesubtreeplugin(subtree, this); + } + subtree->Release(); +} + + + // TODO: we should not use C-type casting with multiple C++ inheritance void mjCModel::CreateObjectLists() { for (int i = 0; i < mjNOBJECT; ++i) { @@ -688,28 +763,28 @@ mjCModel::~mjCModel() { compiled = false; // delete kinematic tree and all objects allocated in it - delete bodies_[0]; + bodies_[0]->Release(); // delete objects allocated in mjCModel - for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); // also deletes wraps + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); for (int i=0; iRelease(); // clear sizes and pointer lists created in Compile Clear(); @@ -933,6 +1008,7 @@ mjCPlugin* mjCModel::AddPlugin() { // append spec to spec void mjCModel::AppendSpec(mjSpec* spec) { + // TODO: check if the spec is already in the list specs_.push_back(spec); } diff --git a/src/user/user_model.h b/src/user/user_model.h index 9109018c..f1023e48 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -221,6 +221,9 @@ class mjCModel : public mjCModel_, private mjSpec { // delete object from the corresponding list void DeleteElement(mjsElement* el); + // detach subtree from model + void Detach(mjCBody* subtree); + // API for access to model elements (outside tree) int NumObjects(mjtObj type); // number of objects in specified list mjCBase* GetObject(mjtObj type, int id); // pointer to specified object @@ -307,6 +310,9 @@ class mjCModel : public mjCModel_, private mjSpec { // get the spec from which this model was created mjSpec* GetSourceSpec() const; + // set deepcopy flag + void SetDeepCopy(bool deepcopy) { deepcopy_ = deepcopy; } + private: // settings for each defaults class std::vector defaults_; @@ -351,7 +357,7 @@ class mjCModel : public mjCModel_, private mjSpec { std::vector tuples_; // list of tuple fields std::vector keys_; // list of keyframe fields std::vector plugins_; // list of plugin instances - std::vector specs_; // list of specs + std::vector specs_; // list of attached specs // pointers to objects created inside kinematic tree std::vector bodies_; // list of bodies @@ -410,5 +416,6 @@ class mjCModel : public mjCModel_, private mjSpec { mjListKeyMap ids; // map from object names to ids mjCError errInfo; // last error info std::vector key_pending_; // attached keyframes + bool deepcopy_; // copy objects when attaching }; #endif // MUJOCO_SRC_USER_USER_MODEL_H_ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index c96872f5..82c46acd 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -763,6 +763,7 @@ mjCBody::mjCBody(mjCModel* _model) { model = _model; if (_model) compiler = &_model->spec.compiler; + refcount = 1; mjs_defaultBody(&spec); elemtype = mjOBJ_BODY; parent = nullptr; @@ -872,22 +873,28 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { other.model->prefix = other.prefix; other.model->suffix = other.suffix; other.model->StoreKeyframes(model); + mjCModel* other_model = other.model; // attach defaults - if (other.model != model) { - mjCDef* subdef = new mjCDef(*other.model->Default()); - subdef->NameSpace(other.model); + if (other_model != model) { + mjCDef* subdef = new mjCDef(*other_model->Default()); + subdef->NameSpace(other_model); *model += *subdef; } // copy input frame mjSpec* origin = model->FindSpec(other.compiler); - frames.push_back(new mjCFrame(other)); + mjCFrame* newframe(model->deepcopy_ ? new mjCFrame(other) : (mjCFrame*)&other); + frames.push_back(newframe); frames.back()->body = this; frames.back()->model = model; frames.back()->compiler = origin ? &origin->compiler : &model->spec.compiler; frames.back()->frame = other.frame; - frames.back()->NameSpace(other.model); + if (model->deepcopy_) { + frames.back()->NameSpace(other_model); + } else { + frames.back()->AddRef(); + } int i = frames.size(); last_attached = &frames.back()->spec; @@ -909,30 +916,43 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { CopyList(cameras, subtree->cameras, fmap, &other); CopyList(lights, subtree->lights, fmap, &other); + if (!model->deepcopy_) { + subtree->SetModel(model); + subtree->NameSpace(other_model); + } + int nbodies = (int)subtree->bodies.size(); for (int i=0; ibodies[i]->frame)) { continue; } - bodies.push_back(new mjCBody(*subtree->bodies[i], model)); // triggers recursive call + if (model->deepcopy_) { + mjCBody* newbody(new mjCBody(*subtree->bodies[i], model)); // triggers recursive call + bodies.push_back(newbody); + subtree->bodies[i]->ForgetKeyframes(); + bodies.back()->NameSpace_(other_model, /*propagate=*/ false); + } else { + bodies.push_back(subtree->bodies[i]); + bodies.back()->SetModel(model); + bodies.back()->ResetId(); + bodies.back()->AddRef(); + } bodies.back()->parent = this; bodies.back()->frame = subtree->bodies[i]->frame ? frames[fmap[subtree->bodies[i]->frame]] : nullptr; - bodies.back()->NameSpace_(other.model, /*propagate=*/ false); - subtree->bodies[i]->ForgetKeyframes(); } // attach referencing elements - *model += *other.model; + *model += *other_model; // leave the source model in a clean state - if (other.model != model) { - other.model->key_pending_.clear(); + if (other_model != model) { + other_model->key_pending_.clear(); } // clear namespace and return body - other.model->prefix.clear(); - other.model->suffix.clear(); + other_model->prefix.clear(); + other_model->suffix.clear(); return *this; } @@ -948,7 +968,8 @@ void mjCBody::CopyList(std::vector& dst, const std::vector& src, continue; // skip if the element is not inside pframe } mjSpec* origin = model->FindSpec(src[i]->compiler); - dst.push_back(new T(*src[i])); + T* new_obj = model->deepcopy_ ? new T(*src[i]) : src[i]; + dst.push_back(new_obj); dst.back()->body = this; dst.back()->model = model; dst.back()->compiler = origin ? &origin->compiler : &model->spec.compiler; @@ -956,6 +977,11 @@ void mjCBody::CopyList(std::vector& dst, const std::vector& src, dst.back()->CopyPlugin(); dst.back()->classname = src[i]->classname; + // increment refcount if shallow copy is made + if (!model->deepcopy_) { + dst.back()->AddRef(); + } + // assign dst frame to src frame dst.back()->frame = src[i]->frame ? frames[fmap[src[i]->frame]] : nullptr; @@ -981,6 +1007,73 @@ mjCBody& mjCBody::operator-=(const mjCBody& subtree) { +// set model of this body and its subtree +void mjCBody::SetModel(mjCModel* _model) { + model = _model; + mjSpec* origin = model->FindSpec(mjs_getString(model->spec.modelname)); + compiler = origin ? &origin->compiler : &model->spec.compiler; + + for (auto& body : bodies) { + body->SetModel(_model); + } + for (auto& frame : frames) { + frame->model = _model; + frame->compiler = compiler; + } + for (auto& geom : geoms) { + geom->model = _model; + geom->compiler = compiler; + } + for (auto& joint : joints) { + joint->model = _model; + joint->compiler = compiler; + } + for (auto& site : sites) { + site->model = _model; + site->compiler = compiler; + } + for (auto& camera : cameras) { + camera->model = _model; + camera->compiler = compiler; + } + for (auto& light : lights) { + light->model = _model; + light->compiler = compiler; + } +} + + + +// reset ids of all objects in this body +void mjCBody::ResetId() { + id = -1; + for (auto& body : bodies) { + body->ResetId(); + } + for (auto& frame : frames) { + frame->id = -1; + } + for (auto& geom : geoms) { + geom->id = -1; + } + for (auto& joint : joints) { + joint->id = -1; + joint->qposadr_ = -1; + joint->dofadr_ = -1; + } + for (auto& site : sites) { + site->id = -1; + } + for (auto& camera : cameras) { + camera->id = -1; + } + for (auto& light : lights) { + light->id = -1; + } +} + + + void mjCBody::PointToLocal() { spec.element = static_cast(this); spec.name = &name; @@ -1012,26 +1105,13 @@ void mjCBody::CopyPlugin() { // destructor mjCBody::~mjCBody() { - // delete objects allocated here - for (int i=0; iempty() && model) { - model->DeleteElement(spec.plugin.element); - } + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); + for (int i=0; iRelease(); } @@ -1840,19 +1920,27 @@ mjCFrame& mjCFrame::operator+=(const mjCBody& other) { other.model->StoreKeyframes(model); other.model->prefix = ""; other.model->suffix = ""; + mjCModel* other_model = other.model; - mjCBody* subtree = new mjCBody(other, model); - other.ForgetKeyframes(); - other.model->prefix = subtree->prefix; - other.model->suffix = subtree->suffix; + // attach or copy the subtree + mjCBody* subtree = model->deepcopy_ ? new mjCBody(other, model) : (mjCBody*)&other; + if (model->deepcopy_) { + other.ForgetKeyframes(); + } else { + subtree->SetModel(model); + subtree->ResetId(); + subtree->AddRef(); + } + other_model->prefix = subtree->prefix; + other_model->suffix = subtree->suffix; subtree->SetParent(body); subtree->SetFrame(this); - subtree->NameSpace(other.model); + subtree->NameSpace(other_model); // attach defaults - if (other.model != model) { - mjCDef* subdef = new mjCDef(*other.model->Default()); - subdef->NameSpace(other.model); + if (other_model != model) { + mjCDef* subdef = new mjCDef(*other_model->Default()); + subdef->NameSpace(other_model); *model += *subdef; } @@ -1861,16 +1949,16 @@ mjCFrame& mjCFrame::operator+=(const mjCBody& other) { last_attached = &body->bodies.back()->spec; // attach referencing elements - *model += *other.model; + *model += *other_model; // leave the source model in a clean state - if (other.model != model) { - other.model->key_pending_.clear(); + if (other_model != model) { + other_model->key_pending_.clear(); } // clear suffixes and return - other.model->suffix.clear(); - other.model->prefix.clear(); + other_model->suffix.clear(); + other_model->prefix.clear(); return *this; } @@ -2221,14 +2309,6 @@ mjCGeom::mjCGeom(const mjCGeom& other) { -mjCGeom::~mjCGeom() { - if (spec.plugin.active && spec.plugin.name->empty() && model) { - model->DeleteElement(spec.plugin.element); - } -} - - - mjCGeom& mjCGeom::operator=(const mjCGeom& other) { if (this != &other) { this->spec = other.spec; @@ -4591,7 +4671,7 @@ void mjCMaterial::CopyFromSpec() { void mjCMaterial::NameSpace(const mjCModel* m) { mjCBase::NameSpace(m); for (int i=0; iprefix + spec_textures_[i] + m->suffix; } } @@ -5657,14 +5737,6 @@ mjCActuator::mjCActuator(const mjCActuator& other) { -mjCActuator::~mjCActuator() { - if (spec.plugin.active && spec.plugin.name->empty() && model) { - model->DeleteElement(spec.plugin.element); - } -} - - - mjCActuator& mjCActuator::operator=(const mjCActuator& other) { if (this != &other) { this->spec = other.spec; @@ -6033,14 +6105,6 @@ mjCSensor::mjCSensor(const mjCSensor& other) { -mjCSensor::~mjCSensor() { - if (spec.plugin.active && spec.plugin.name->empty() && model) { - model->DeleteElement(spec.plugin.element); - } -} - - - mjCSensor& mjCSensor::operator=(const mjCSensor& other) { if (this != &other) { this->spec = other.spec; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index ca13a741..0d093c0e 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -224,9 +224,23 @@ class mjCBase : public mjCBase_ { virtual void ForgetKeyframes() {} virtual void ForgetKeyframes() const {} + // increment and decrement reference count + // release uses the argument to delete the plugin + // which may be still owned by the source spec during shallow attach + virtual void AddRef() { ++refcount; } + virtual int GetRef() { return refcount; } + virtual void Release() { + if (--refcount == 0) { + delete this; + } + } + protected: mjCBase(); // constructor mjCBase(const mjCBase& other); // copy constructor + + // reference count for allowing deleting an attached object + int refcount = 1; }; @@ -349,6 +363,15 @@ class mjCBody : public mjCBody_, private mjsBody { void SetParent(mjCBody* _body) { parent = _body; } mjCBody* GetParent() const { return parent; } + // set model of this body + void SetModel(mjCModel* _model); + + // reset ids of all objects in this body + void ResetId(); + + // getters + std::vector Bodies() const { return bodies; } + private: mjCBody(const mjCBody& other, mjCModel* _model); // copy constructor mjCBody& operator=(const mjCBody& other); // copy assignment @@ -537,7 +560,6 @@ class mjCGeom : public mjCGeom_, private mjsGeom { mjCGeom(mjCModel* = nullptr, mjCDef* = nullptr); mjCGeom(const mjCGeom& other); mjCGeom& operator=(const mjCGeom& other); - ~mjCGeom(); using mjCBase::name; mjsGeom spec; // variables set by user @@ -1505,7 +1527,6 @@ class mjCActuator : public mjCActuator_, private mjsActuator { mjCActuator(mjCModel* = nullptr, mjCDef* = nullptr); mjCActuator(const mjCActuator& other); mjCActuator& operator=(const mjCActuator& other); - ~mjCActuator(); mjsActuator spec; using mjCBase::name; @@ -1567,7 +1588,6 @@ class mjCSensor : public mjCSensor_, private mjsSensor { mjCSensor(mjCModel*); mjCSensor(const mjCSensor& other); mjCSensor& operator=(const mjCSensor& other); - ~mjCSensor(); mjsSensor spec; using mjCBase::name; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index f07fa341..6fe26b27 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -945,10 +945,16 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { Keyframe(section); } + // set deepcopy flag to true to copy child specs during attach calls + mjs_setDeepCopy(spec, true); + for (XMLElement* section = FirstChildElement(root, "worldbody"); section; section = NextSiblingElement(section, "worldbody")) { Body(section, mjs_findBody(spec, "world"), nullptr, vfs); } + + // set deepcopy flag to false to disable copying during attach in all future calls + mjs_setDeepCopy(spec, false); } diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 7394c7f1..31b7af43 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -262,35 +262,48 @@ TEST_F(PluginTest, AttachPlugin) { )"; std::array err; - mjSpec* spec_1 = mj_parseXMLString(xml_1, 0, err.data(), err.size()); + mjSpec* parent = mj_parseXMLString(xml_1, 0, err.data(), err.size()); + ASSERT_THAT(parent, NotNull()) << err.data(); + mjSpec* spec_1 = mj_parseXMLString(xml_2, 0, err.data(), err.size()); ASSERT_THAT(spec_1, NotNull()) << err.data(); - mjSpec* spec_2 = mj_parseXMLString(xml_2, 0, err.data(), err.size()); - ASSERT_THAT(spec_2, NotNull()) << err.data(); - mjsBody* body_1 = mjs_findBody(spec_1, "body"); + // do a copy before attaching + mjSpec* spec_2 = mj_copySpec(spec_1); + mjs_setString(spec_2->modelname, "first_copy"); + mjSpec* spec_3 = mj_copySpec(spec_1); + mjs_setString(spec_3->modelname, "second_copy"); + ASSERT_THAT(spec_3, NotNull()) << err.data(); + + // attach a body referencing the plugin to the frame and compile + mjsBody* body_1 = mjs_findBody(parent, "body"); EXPECT_THAT(body_1, NotNull()); mjsFrame* attachment_frame = mjs_addFrame(body_1, 0); EXPECT_THAT(attachment_frame, NotNull()); - - mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "body"), "child-", ""); - mjModel* model_1 = mj_compile(spec_1, nullptr); + mjs_attachBody(attachment_frame, mjs_findBody(spec_1, "body"), "child-", ""); + mjModel* model_1 = mj_compile(parent, nullptr); EXPECT_THAT(model_1, NotNull()); + EXPECT_THAT(model_1->nbody, 3); - // attach it a second time to test namespacing + // attach it a second time to test namespacing and compile + ASSERT_THAT(spec_2, NotNull()) << err.data(); mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "body"), "copy-", ""); - mjModel* model_2 = mj_compile(spec_1, nullptr); + mjModel* model_2 = mj_compile(parent, nullptr); EXPECT_THAT(model_2, NotNull()); + EXPECT_THAT(model_2->nbody, 4); - // attach a body not referencing the plugin - mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "empty"), "empty-", ""); - mjModel* model_3 = mj_compile(spec_1, nullptr); + // attach a body not referencing the plugin and compile + mjs_attachBody(attachment_frame, mjs_findBody(spec_3, "empty"), "empty-", ""); + mjModel* model_3 = mj_compile(parent, nullptr); EXPECT_THAT(model_3, NotNull()); + EXPECT_THAT(model_3->nbody, 5); mj_deleteModel(model_1); mj_deleteModel(model_2); mj_deleteModel(model_3); + mj_deleteSpec(parent); mj_deleteSpec(spec_1); mj_deleteSpec(spec_2); + mj_deleteSpec(spec_3); } TEST_F(PluginTest, AttachExplicitPlugin) { @@ -858,6 +871,7 @@ TEST_F(MujocoTest, AttachSame) { // create parent mjSpec* parent = mj_parseXMLString(xml_child, 0, er.data(), er.size()); EXPECT_THAT(parent, NotNull()) << er.data(); + mjs_setDeepCopy(parent, true); // needed for self-attach // get frame mjsFrame* frame = mjs_findFrame(parent, "frame"); @@ -903,7 +917,7 @@ TEST_F(MujocoTest, AttachDifferent) { std::string field = ""; static constexpr char xml_parent[] = R"( - + @@ -924,7 +938,7 @@ TEST_F(MujocoTest, AttachDifferent) { )"; static constexpr char xml_result[] = R"( - + @@ -1048,7 +1062,7 @@ TEST_F(MujocoTest, AttachFrame) { std::string field = ""; static constexpr char xml_parent[] = R"( - + @@ -1063,7 +1077,7 @@ TEST_F(MujocoTest, AttachFrame) { )"; static constexpr char xml_result[] = R"( - + @@ -1371,13 +1385,15 @@ TEST_F(MujocoTest, AttachWorld) { mjSpec* parent = mj_parseXMLString(xml_parent, 0, er.data(), er.size()); EXPECT_THAT(parent, NotNull()) << er.data(); - mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size()); - EXPECT_THAT(child, NotNull()) << er.data(); + mjSpec* child1 = mj_parseXMLString(xml_child, 0, er.data(), er.size()); + EXPECT_THAT(child1, NotNull()) << er.data(); + mjSpec* child2 = mj_parseXMLString(xml_child, 0, er.data(), er.size()); + EXPECT_THAT(child2, NotNull()) << er.data(); // attach a body to the frame mjsFrame* frame = mjs_findFrame(parent, "frame"); EXPECT_THAT(frame, NotNull()); - mjsBody* body = mjs_findBody(child, "sphere"); + mjsBody* body = mjs_findBody(child1, "sphere"); EXPECT_THAT(body, NotNull()); mjsBody* attached = mjs_attachBody(frame, body, "attached-", "-1"); EXPECT_THAT(attached, NotNull()); @@ -1385,7 +1401,7 @@ TEST_F(MujocoTest, AttachWorld) { EXPECT_THAT(model1, NotNull()); // attach the world to the same frame and convert it to a frame - mjsBody* world = mjs_findBody(child, "world"); + mjsBody* world = mjs_findBody(child2, "world"); EXPECT_THAT(world, NotNull()); mjsBody* child_world = mjs_attachBody(frame, world, "attached-", "-2"); EXPECT_THAT(child_world, NotNull()); @@ -1403,7 +1419,8 @@ TEST_F(MujocoTest, AttachWorld) { << "Different field: " << field << '\n'; mj_deleteSpec(parent); - mj_deleteSpec(child); + mj_deleteSpec(child1); + mj_deleteSpec(child2); mj_deleteModel(model1); mj_deleteModel(model2); mj_deleteModel(expected); @@ -1577,11 +1594,13 @@ TEST_F(MujocoTest, RecompileAttach) { mjSpec* parent = mj_makeSpec(); EXPECT_THAT(parent, NotNull()); - mjSpec* child = mj_parseXMLString(xml, 0, er.data(), er.size()); - EXPECT_THAT(child, NotNull()); + mjSpec* child1 = mj_parseXMLString(xml, 0, er.data(), er.size()); + EXPECT_THAT(child1, NotNull()); + mjSpec* child2 = mj_parseXMLString(xml, 0, er.data(), er.size()); + EXPECT_THAT(child2, NotNull()); mjsFrame* frame1 = mjs_addFrame(mjs_findBody(parent, "world"), 0); - mjs_attachBody(frame1, mjs_findBody(child, "body"), "child-", "-1"); + mjs_attachBody(frame1, mjs_findBody(child1, "body"), "child-", "-1"); mjModel* model = mj_compile(parent, 0); EXPECT_THAT(model, NotNull()); @@ -1594,7 +1613,7 @@ TEST_F(MujocoTest, RecompileAttach) { } mjsFrame* frame2 = mjs_addFrame(mjs_findBody(parent, "world"), 0); - mjs_attachBody(frame2, mjs_findBody(child, "body"), "child-", "-2"); + mjs_attachBody(frame2, mjs_findBody(child2, "body"), "child-", "-2"); EXPECT_EQ(mj_recompile(parent, 0, model, data), 0); EXPECT_THAT(model, NotNull()); @@ -1604,7 +1623,8 @@ TEST_F(MujocoTest, RecompileAttach) { mj_deleteData(data); mj_deleteModel(model); - mj_deleteSpec(child); + mj_deleteSpec(child1); + mj_deleteSpec(child2); mj_deleteSpec(parent); } @@ -1637,6 +1657,7 @@ TEST_F(MujocoTest, AttachMocap) { mjSpec* spec = mj_parseXMLString(xml, 0, er.data(), er.size()); EXPECT_THAT(spec, NotNull()) << er.data(); + mjs_setDeepCopy(spec, true); // needed for self-attach mjsBody* body = mjs_findBody(spec, "mocap"); EXPECT_THAT(body, NotNull()); @@ -1732,8 +1753,8 @@ TEST_F(MujocoTest, AttachUnnamedAssets) { EXPECT_STREQ(mj_id2name(model, mjOBJ_MESH, 0), "_cube"); mj_deleteVFS(vfs.get()); - mj_deleteSpec(child); mj_deleteSpec(spec); + mj_deleteSpec(child); mj_deleteModel(model); } @@ -1866,6 +1887,9 @@ void AttachNestedKeyframe(bool compile) { mjSpec* gchild = mj_parseXMLString(gchild_xml, 0, er.data(), er.size()); EXPECT_THAT(gchild, NotNull()) << er.data(); + mjs_setDeepCopy(parent, true); + mjs_setDeepCopy(child, true); + // attach gchild to child mjs_attachBody(mjs_findFrame(child, "frame"), mjs_findBody(gchild, "body"), "gchild-", ""); @@ -1937,18 +1961,18 @@ TEST_F(MujocoTest, RepeatedAttachKeyframe) { )"; std::array er; - mjSpec* spec_1 = mj_parseXMLString(xml_1, 0, er.data(), er.size()); - EXPECT_THAT(spec_1, NotNull()) << er.data(); - mjSpec* spec_2 = mj_parseXMLString(xml_2, 0, er.data(), er.size()); - EXPECT_THAT(spec_2, NotNull()) << er.data(); + mjSpec* parent = mj_parseXMLString(xml_1, 0, er.data(), er.size()); + EXPECT_THAT(parent, NotNull()) << er.data(); + mjSpec* child = mj_parseXMLString(xml_2, 0, er.data(), er.size()); + EXPECT_THAT(child, NotNull()) << er.data(); - mjsBody* body_1 = mjs_findBody(spec_1, "body"); + mjsBody* body_1 = mjs_findBody(parent, "body"); mjsFrame* attachment_frame = mjs_addFrame(body_1, 0); - mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "b1"), "b1-", ""); - mjModel* model_1 = mj_compile(spec_1, 0); + mjs_attachBody(attachment_frame, mjs_findBody(child, "b1"), "b1-", ""); + mjModel* model_1 = mj_compile(parent, 0); EXPECT_THAT(model_1, NotNull()); - mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "b2"), "b2-", ""); - mjModel* model_2 = mj_compile(spec_1, 0); + mjs_attachBody(attachment_frame, mjs_findBody(child, "b2"), "b2-", ""); + mjModel* model_2 = mj_compile(parent, 0); EXPECT_THAT(model_2, NotNull()); EXPECT_EQ(model_1->nkey, 1); @@ -1956,8 +1980,8 @@ TEST_F(MujocoTest, RepeatedAttachKeyframe) { EXPECT_STREQ(mj_id2name(model_2, mjOBJ_KEY, 0), "b1-home"); EXPECT_STREQ(mj_id2name(model_2, mjOBJ_KEY, 1), "b2-home"); - mj_deleteSpec(spec_1); - mj_deleteSpec(spec_2); + mj_deleteSpec(parent); + mj_deleteSpec(child); mj_deleteModel(model_1); mj_deleteModel(model_2); } @@ -2177,5 +2201,64 @@ TEST_F(MujocoTest, CopyAttachedSpec) { mj_deleteVFS(vfs.get()); } +TEST_F(MujocoTest, ApplyNameSpaceToDefaults) { + static constexpr char xml_c[] = R"( + + + + + + + + + + + + + + + )"; + + static constexpr char xml_p[] = R"( + + + + + + )"; + + static constexpr char cube[] = 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)"; + + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + mj_addBufferVFS(vfs.get(), "cube.obj", cube, sizeof(cube)); + + std::array err; + mjSpec* child = mj_parseXMLString(xml_c, vfs.get(), err.data(), err.size()); + EXPECT_THAT(child, NotNull()) << err.data(); + mjSpec* parent = mj_parseXMLString(xml_p, 0, err.data(), err.size()); + EXPECT_THAT(parent, NotNull()) << err.data(); + + mjsBody* attached = mjs_attachBody(mjs_findFrame(parent, "parent"), + mjs_findBody(child, "body"), "child-", ""); + EXPECT_THAT(attached, NotNull()); + + mjModel* model = mj_compile(parent, vfs.get()); + EXPECT_THAT(model, NotNull()); + + mj_deleteSpec(child); + mj_deleteSpec(parent); + mj_deleteModel(model); + mj_deleteVFS(vfs.get()); +} + } // namespace } // namespace mujoco diff --git a/test/user/user_model_test.cc b/test/user/user_model_test.cc index 811004a7..0c3cddac 100644 --- a/test/user/user_model_test.cc +++ b/test/user/user_model_test.cc @@ -545,6 +545,7 @@ TEST_F(MujocoTest, Modeldir) { // parent attaching the child mjSpec* spec = mj_makeSpec(); + mjs_setDeepCopy(spec, true); mjs_setString(spec->meshdir, "asset"); mjs_attachFrame(mjs_findBody(spec, "world"), frame, "_", ""); mjModel* model = mj_compile(spec, vfs.get());