diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 0b85227a..cbe5bb41 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -323,7 +323,7 @@ frame (``point``) treated as attached to the body, the Jacobian has both transla (``jacr``) components. Passing ``NULL`` for either pointer will skip that part of the computation. Each component is a 3-by-nv matrix. Each row of this matrix is the gradient of the corresponding coordinate of the specified point with respect to the degrees-of-freedom. The :ref:`pipeline stages` required for Jacobian computations to be -consistenst with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. +consistent with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. .. _mj_jacBody: @@ -1882,7 +1882,7 @@ Twice continuously differentiable sigmoid function using a quintic polynomial: Interaction ^^^^^^^^^^^ -These function implement abstract mouse interactions, allowing control over cameras and perturbations. Their use is well +These functions implement abstract mouse interactions, allowing control over cameras and perturbations. Their use is well illustrated in :ref:`simulate`. .. _mjv_defaultCamera: @@ -2066,8 +2066,8 @@ an illustration. Visualization ^^^^^^^^^^^^^ -The functions in this section implement abstract visualization. The results are used by the OpenGL rendered, and can -also be used by users wishing to implement their own rendered, or hook up MuJoCo to advanced rendering tools such as +The functions in this section implement abstract visualization. The results are used by the OpenGL renderer, and can +also be used by users wishing to implement their own renderer, or hook up MuJoCo to advanced rendering tools such as Unity or Unreal Engine. See :ref:`simulate` for illustration of how to use these functions. .. _mjv_defaultOption: diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index a63bf214..b7e62d78 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -180,7 +180,7 @@ frame (``point``) treated as attached to the body, the Jacobian has both transla (``jacr``) components. Passing ``NULL`` for either pointer will skip that part of the computation. Each component is a 3-by-nv matrix. Each row of this matrix is the gradient of the corresponding coordinate of the specified point with respect to the degrees-of-freedom. The :ref:`pipeline stages` required for Jacobian computations to be -consistenst with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. +consistent with the current generalized positions ``mjData.qpos`` are :ref:`mj_kinematics` and :ref:`mj_comPos`. .. _mj_jacBody: @@ -270,7 +270,7 @@ bodyexclude=-1 can be used to indicate that all bodies are included. .. _Interaction: -These function implement abstract mouse interactions, allowing control over cameras and perturbations. Their use is well +These functions implement abstract mouse interactions, allowing control over cameras and perturbations. Their use is well illustrated in :ref:`simulate`. .. _mjv_select: @@ -283,8 +283,8 @@ an illustration. .. _Visualization-api: -The functions in this section implement abstract visualization. The results are used by the OpenGL rendered, and can -also be used by users wishing to implement their own rendered, or hook up MuJoCo to advanced rendering tools such as +The functions in this section implement abstract visualization. The results are used by the OpenGL renderer, and can +also be used by users wishing to implement their own renderer, or hook up MuJoCo to advanced rendering tools such as Unity or Unreal Engine. See :ref:`simulate` for illustration of how to use these functions. .. _OpenGLrendering: diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index ba447d4b..56c026a4 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -577,6 +577,11 @@ from its default. This flag disables implicit integration with respect to joint damping in the Euler integrator. See the :ref:`Numerical Integration` section for more details. +.. _option-flag-autoreset: + +:at:`autoreset`: :at-val:`[disable, enable], "enable"` + This flag disables the automatic reseting of the simulation state when numerical issues are detected. + .. _option-flag-override: :at:`override`: :at-val:`[disable, enable], "disable"` @@ -1751,7 +1756,7 @@ properties are grouped together. .. _material-rgb: :el-prefix:`material/` |-| **rgb** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify base color / albedo values. @@ -1763,7 +1768,7 @@ This element references a texture asset used to specify base color / albedo valu .. _material-normal: :el-prefix:`material/` |-| **normal** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify the bump map (surface normals). @@ -1775,7 +1780,7 @@ This element references a texture asset used to specify the bump map (surface no .. _material-occlusion: :el-prefix:`material/` |-| **occlusion** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify ambient occlusion. @@ -1787,7 +1792,7 @@ This element references a texture asset used to specify ambient occlusion. .. _material-roughness: :el-prefix:`material/` |-| **roughness** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify the roughness map. @@ -1799,7 +1804,7 @@ This element references a texture asset used to specify the roughness map. .. _material-metallic: :el-prefix:`material/` |-| **metallic** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify the metallic map. @@ -1811,7 +1816,7 @@ This element references a texture asset used to specify the metallic map. .. _material-opacity: :el-prefix:`material/` |-| **opacity** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify the opacity map (alpha channel, transparency). @@ -1823,7 +1828,7 @@ This element references a texture asset used to specify the opacity map (alpha c .. _material-emissive: :el-prefix:`material/` |-| **emissive** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify light emission. @@ -1835,7 +1840,7 @@ This element references a texture asset used to specify light emission. .. _material-orm: :el-prefix:`material/` |-| **orm** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify a packed ORM map, where occlusion, roughness, and metallic are joined into the corresponding RGB values of a single texture. @@ -1848,7 +1853,7 @@ are joined into the corresponding RGB values of a single texture. .. _material-rgba: :el-prefix:`material/` |-| **rgba** (?) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element references a texture asset used to specify a packed map where albedo and opacity are joined into the same 4-channel texture. diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 9fdd9dcd..ae9f43c7 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -35,11 +35,11 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`warmstart` | :ref:`filterparent` | :ref:`actuation` | :ref:`refsafe` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`sensor` | :ref:`midphase` | :ref:`eulerdamp` | :ref:`override` | | +| | | | :ref:`sensor` | :ref:`midphase` | :ref:`eulerdamp` | :ref:`autoreset` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`energy` | :ref:`fwdinv` | :ref:`invdiscrete` | :ref:`multiccd` | | +| | | | :ref:`override` | :ref:`energy` | :ref:`fwdinv` | :ref:`invdiscrete` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`island` | | | | | +| | | | :ref:`multiccd` | :ref:`island` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | mujoco |br| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index b19bcc28..a574c451 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,16 +7,15 @@ Upcoming version (not yet released) General ^^^^^^^ - 1. Renamed ``mjModel.tex_rbg`` to ``mjModel.tex_data``. -2. Added a new disable flag ``mjDSBL_AUTORESETNAN`` to disable automatic reset when NaNs or infinities are produced. +2. Added a new :ref:`autoreset` flag to disable automatic reset when NaNs or infinities are + detected. 3. Added sub-elements to the MJCF :ref:`material` element, to allow specification of multiple textures -for rendering (e.g., :ref:`occlusion-roughness-metallic`). Note that the MuJoCo renderer doesn't support -these new features, and they are made available for use with external renderers. + for rendering (e.g., :ref:`occlusion-roughness-metallic`). Note that the MuJoCo renderer doesn't + support these new features, and they are made available for use with external renderers. MJX ^^^ - 4. Added more fields to ``mjx.Model`` and ``mjx.Data`` for further compatibility with the corresponding MuJoCo structs. 5. Added support for :ref:`fixed tendons `. 6. Added support for tendon length limits (``mjCNSTR_LIMIT_TENDON`` in :ref:`mjtConstraint`). @@ -28,6 +27,10 @@ Python bindings 9. Added support for asset dictionary argument in ``mujoco.spec.from_file``, ``mujoco.spec.from_string`` and ``mujoco.spec.compile``. +Bug fixes +^^^^^^^^^ +10. Fixed a bug where implicit integrators did not take into account disabled actuators (:github:issue:`1838`). + Version 3.2.0 (Jul 15, 2024) ---------------------------- diff --git a/doc/computation/index.rst b/doc/computation/index.rst index eb1bf285..06e0da68 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -1328,7 +1328,7 @@ representations of the constraint Jacobian and related matrices. **PGS** : Projected Gauss-Seidel method This is the most common algorithm used in physics simulators, and used to be the default in MuJoCo, until we developed the Newton method which appears to be better in every way. PGS uses the dual formulation. Unlike - gradient-based method which improve the solution along oblique directions, Gauss-Seidel works on one scalar component + gradient-based methods which improve the solution along oblique directions, Gauss-Seidel works on one scalar component at a time, and sets it to its optimal value given the current values of all other components. One sweep of PGS has the computational complexity of one matrix-vector multiplication (although the constants are larger). It has first-order convergence but nevertheless makes rapid progress in a few iterations. diff --git a/doc/includes/references.h b/doc/includes/references.h index 871069f2..d301a2c2 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -418,7 +418,7 @@ typedef enum mjtDisableBit_ { // disable default feature bitflags mjDSBL_SENSOR = 1<<12, // sensors mjDSBL_MIDPHASE = 1<<13, // mid-phase collision filtering mjDSBL_EULERDAMP = 1<<14, // implicit integration of joint damping in Euler integrator - mjDSBL_AUTORESETNAN = 1<<15, // automatic reset when numerical issues are detected + mjDSBL_AUTORESET = 1<<15, // automatic reset when numerical issues are detected mjNDISABLE = 16 // number of disable flags } mjtDisableBit; diff --git a/doc/modeling.rst b/doc/modeling.rst index ed12659e..325317f1 100644 --- a/doc/modeling.rst +++ b/doc/modeling.rst @@ -1407,7 +1407,7 @@ Using the :ref:`flexcomp` element, we can create flexes from mesh automatically generate all the bodies/vertices and connect them with suitable elements. We can also create grids and other topologies automatically. This machinery makes it easy to create very large flexes, involving thousands or even tens of thousands of bodies, elements and edges. Obviously such simulations will not be fast. Even for medium-sized -flexes, pruning of collision pairs and essential. This is why we have developed elaborate methods for pruning +flexes, pruning of collision pairs is essential. This is why we have developed elaborate methods for pruning self-collisions; see XML reference. In case of 3D flexes made of tetrahedra, it may be useful to examine how the flex is "triangulated" internally. We have @@ -1724,7 +1724,7 @@ better visualize and understand the contact configuration and resulting forces. model by design, since without it the inverse dynamics are not defined. This is discussed in detail in the :ref:`softness and slip` clarification. This type of slippage can be addressed in two ways. - a. Increase the :ref:`impration` parameter. This will reduce (but not entirely prevent) slow + a. Increase the :ref:`impratio` parameter. This will reduce (but not entirely prevent) slow slippage. Note that high impratio values work well only with :ref:`elliptic cones`. b. Enable the noslip solver by increasing :ref:`noslip_iterations` to a positive integer. A small number (1, 2 or 3) is usually sufficient. The noslip post-processing solver will entirely prevent slip, diff --git a/doc/overview.rst b/doc/overview.rst index 337023a3..a88fb7bc 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -9,7 +9,7 @@ aims to facilitate research and development in robotics, biomechanics, graphics other areas that demand fast and accurate simulation of articulated structures interacting with their environment. Initially developed by Roboti LLC, it was acquired and made `freely available `__ by DeepMind in October 2021, and open sourced in May -2022. The MuJoCo codebase is available at the `deepmind/mujoco `__ repository +2022. The MuJoCo codebase is available at the `google-deepmind/mujoco `__ repository on GitHub. MuJoCo is a C/C++ library with a C API, intended for researchers and developers. The runtime simulation module is tuned diff --git a/doc/programming/extension.rst b/doc/programming/extension.rst index 33e1002b..d61139fa 100644 --- a/doc/programming/extension.rst +++ b/doc/programming/extension.rst @@ -185,7 +185,7 @@ faithfully restored. Plugins must declare the number of floating point values required for each instance via the ``nstate`` callback of its :ref:`mjpPlugin` struct. Note that this number can depend on the exact configuration of the instance. During -:ref:`mj_makeData`, MuJoCo allocate the requisite number of slots in the ``plugin_state`` field of :ref:`mjData` for +:ref:`mj_makeData`, MuJoCo allocates the requisite number of slots in the ``plugin_state`` field of :ref:`mjData` for each plugin instance. The ``plugin_stateadr`` field in :ref:`mjModel` indicates the position within the overall ``plugin_state`` array at which each plugin instance can find its state values. diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index 24aa64ae..8968d4cc 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -20,7 +20,7 @@ Overview The new API augments the traditional workflow of creating and editing models using XML files, breaking up the *parse* and *compile* steps. As summarized in the the :ref:`Overview chapter`, the traditional workflow is: - 1. Create an XML model description file (MJCF or URDF) and ascociated assets. |br| + 1. Create an XML model description file (MJCF or URDF) and associated assets. |br| 2. Call :ref:`mj_loadXML`, obtain an :ref:`mjModel` instance. The new workflow is: @@ -107,7 +107,7 @@ Known issues to `user_api_test.cc `__ and the MJCF parser in `xml_native_reader.cc `__, which is already using this API. -- One of the central design consideration of the new API is incremental compilation, meaning that after making small +- One of the central design considerations of the new API is incremental compilation, meaning that after making small changes to a spec that has already been compiled, subsequent re-compilation will be very fast. While the code is written to support incremental compilation, this functionality is not fully implemented and will be added in the future, resulting in faster re-compilation times. diff --git a/doc/programming/simulation.rst b/doc/programming/simulation.rst index 9233f6df..ca1d0b60 100644 --- a/doc/programming/simulation.rst +++ b/doc/programming/simulation.rst @@ -945,7 +945,7 @@ this body quaternion, the quaternions of all other objects attached to the body multiplication. The function :ref:`mj_local2Global` converts from local body coordinates to global Cartesian coordinates. -:ref:`mju_negPose` and :ref:`mju_trnVecPose`. A pose is a grouping of a 3D position and a unit quaternion orientation. +A pose is a grouping of a 3D position and a unit quaternion orientation. There is no separate data structure; the grouping is in terms of logic. This represents a position and orientation in space, or in other words a spatial frame. Note that OpenGL uses 4-by-4 matrices to represent the same information, except here we use a quaternion for orientation. The function mju_mulPose multiplies two poses, meaning that it diff --git a/doc/unity.rst b/doc/unity.rst index 80ba500c..6177ef02 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -328,7 +328,7 @@ ___________________________________ Roboti’s `MuJoCo plug-in for Unity `_ steps the simulation in an external Python process, and uses Unity only for rendering. In contrast, our plug-in relies on Unity to step the simulation. It should -be possible to use our plug-in while an external process "drives" the simulation, for example by seting ``qpos``, +be possible to use our plug-in while an external process "drives" the simulation, for example by setting ``qpos``, calling ``mj_kinematics``, synchronizing the transforms, and then using Unity to render or compute game logic. In order to establish communication with an external process, you can use Unity's `ML-Agents `_ package. diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 73697a66..5f3fe4bd 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -61,7 +61,7 @@ typedef enum mjtDisableBit_ { // disable default feature bitflags mjDSBL_SENSOR = 1<<12, // sensors mjDSBL_MIDPHASE = 1<<13, // mid-phase collision filtering mjDSBL_EULERDAMP = 1<<14, // implicit integration of joint damping in Euler integrator - mjDSBL_AUTORESETNAN = 1<<15, // automatic reset when numerical issues are detected + mjDSBL_AUTORESET = 1<<15, // automatic reset when numerical issues are detected mjNDISABLE = 16 // number of disable flags } mjtDisableBit; diff --git a/introspect/enums.py b/introspect/enums.py index 52bf4c6a..f040505c 100644 --- a/introspect/enums.py +++ b/introspect/enums.py @@ -42,7 +42,7 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjDSBL_SENSOR', 4096), ('mjDSBL_MIDPHASE', 8192), ('mjDSBL_EULERDAMP', 16384), - ('mjDSBL_AUTORESETNAN', 32768), + ('mjDSBL_AUTORESET', 32768), ('mjNDISABLE', 16), ]), )), diff --git a/model/balloons/balloons.xml b/model/balloons/balloons.xml index 31659151..4aab660f 100644 --- a/model/balloons/balloons.xml +++ b/model/balloons/balloons.xml @@ -57,7 +57,7 @@ - + diff --git a/plugin/actuator/README.md b/plugin/actuator/README.md index 9139aed1..a6b1e862 100644 --- a/plugin/actuator/README.md +++ b/plugin/actuator/README.md @@ -4,7 +4,7 @@ The `mujoco.pid` actuator plugin implements a configurable [PID controller](https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller): -$$f(t) = K_\text{p} e(t) + K_\text{i} \int_0^t e(\tau) \,\mathrm{d}\tau + K_\text{d} \frac{\mathrm{d}e(t)}{\mathrm{d}t},$$ +$$f(t) = K_\text{p} e(t) + K_\text{i} \int_0^t e(\tau) \mathrm{d}\tau + K_\text{d} \frac{\mathrm{d}e(t)}{\mathrm{d}t},$$ where $e(t) = u(t) - \ell(t)$ is the difference between the control $u$ and the actuator length $\ell$. You can use it like: @@ -43,5 +43,5 @@ The available options are: |`kp` | 0 | **P** gain for the controller. | |`ki` | 0 | **I** gain for the controller.

If nonzero, one activation variable will be added to `mjData.act`, containing the current I term (in units of force). | |`kd` | 0 | **D** gain for the controller. | -|`imax` | Optional | If specified, the force produced by the I term will be clipped to the range `[-imax, -imax]`. | +|`imax` | Optional | If specified, the force produced by the I term will be clipped to the range `[-imax, imax]`. | |`slewmax` | Optional | The maximum rate at which the setpoint for the PID controller can change.

If a bigger change is requested between two timesteps, it will be clipped to the range `[ctrl - slewmax * dt, ctrl + slewmax * dt]`

If specified, one activation variable will be added to `mjData.act` containing the previous value of `ctrl`. | diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 70bbc18f..c280911e 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -568,7 +568,6 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSBODY ===================================== mjsBody.def_property_readonly( "id", [](raw::MjsBody& self) -> int { return mjs_getId(self.element); }); - mjsBody.def("delete", [](raw::MjsBody& self) { mjs_delete(self.element); }); mjsBody.def( "add_body", [](raw::MjsBody& self, raw::MjsDefault* default_) -> raw::MjsBody* { diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 0459b013..ad068f91 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -98,6 +98,34 @@ class SpecsTest(absltest.TestCase): """),) + def test_load_xml(self): + filename = '../../test/testdata/model.xml' + state_type = mujoco.mjtState.mjSTATE_INTEGRATION + + # Load from file. + spec1 = mujoco.MjSpec() + spec1.from_file(filename) + model1 = spec1.compile() + data1 = mujoco.MjData(model1) + mujoco.mj_step(model1, data1) + size1 = mujoco.mj_stateSize(model1, state_type) + state1 = np.empty(size1, np.float64) + mujoco.mj_getState(model1, data1, state1, state_type) + + # Load from string. + spec2 = mujoco.MjSpec() + with open(filename, 'r') as file: + spec2.from_string(file.read().rstrip()) + model2 = spec2.compile() + data2 = mujoco.MjData(model2) + mujoco.mj_step(model2, data2) + size2 = mujoco.mj_stateSize(model2, state_type) + state2 = np.empty(size2, np.float64) + mujoco.mj_getState(model2, data2, state2, state_type) + + # Check that the state is the same. + np.testing.assert_array_equal(state1, state2) + def test_compile_errors_with_line_info(self): spec = mujoco.MjSpec() @@ -269,6 +297,30 @@ class SpecsTest(absltest.TestCase): model = spec.compile({'cube.obj': cube}) self.assertEqual(model.nmeshvert, 8) + def test_delete(self): + filename = '../../test/testdata/model.xml' + + spec = mujoco.MjSpec() + spec.from_file(filename) + + model = spec.compile() + self.assertIsNotNone(model) + self.assertEqual(model.nsite, 11) + self.assertEqual(model.nsensor, 11) + + head = spec.find_body('head') + self.assertIsNotNone(head) + site = head.first_site() + self.assertIsNotNone(site) + + site.delete() + spec.sensors[-1].delete() + spec.sensors[-1].delete() + + model = spec.compile() + self.assertIsNotNone(model) + self.assertEqual(model.nsite, 10) + self.assertEqual(model.nsensor, 9) if __name__ == '__main__': absltest.main() diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 373593c7..66566fe7 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -16,7 +16,9 @@ #include +#include #include +#include #include #include #include @@ -84,6 +86,22 @@ constexpr auto XArrayShapeImpl(const std::string_view dim1_str) { inline std::size_t NConMax(const mjData* d) { return d->narena / sizeof(mjContact); } + +// strip path prefix from filename and make lowercase +std::string StripPath(const char* name) { + std::string filename(name); + size_t start = filename.find_last_of("/\\"); + + // get name without path + if (start != std::string::npos) { + filename = filename.substr(start + 1, filename.size() - start - 1); + } + + // make lowercase + std::transform(filename.begin(), filename.end(), filename.begin(), + [](unsigned char c) { return std::tolower(c); }); + return filename; +} } // namespace // ==================== MJOPTION =============================================== @@ -323,8 +341,9 @@ static raw::MjModel* LoadModelFileImpl( mj_defaultVFS(&vfs); vfs_ptr = &vfs; for (const auto& asset : assets) { + std::string buffer_name = StripPath(asset.name); const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( - vfs_ptr, asset.name, asset.content, asset.content_size); + vfs_ptr, buffer_name.c_str(), asset.content, asset.content_size); if (vfs_error) { throw py::value_error("assets dict is too big"); } diff --git a/simulate/main.cc b/simulate/main.cc index 17c3ff5e..15316383 100644 --- a/simulate/main.cc +++ b/simulate/main.cc @@ -199,6 +199,16 @@ void scanPluginLibraries() { //------------------------------------------- simulation ------------------------------------------- +const char* Diverged(int disableflags, const mjData* d) { + if (disableflags & mjDSBL_AUTORESET) { + for (mjtWarning w : {mjWARN_BADQACC, mjWARN_BADQVEL, mjWARN_BADQPOS}) { + if (d->warning[w].number > 0) { + return mju_warningText(w, d->warning[w].lastinfo); + } + } + } + return nullptr; +} mjModel* LoadModel(const char* file, mj::Simulate& sim) { // this copy is needed so that the mju::strlen call below compiles @@ -356,7 +366,13 @@ void PhysicsLoop(mj::Simulate& sim) { // run single step, let next iteration deal with timing mj_step(m, d); - stepped = true; + const char* message = Diverged(m->opt.disableflags, d); + if (message) { + sim.run = 0; + mju::strcpy_arr(sim.load_error, message); + } else { + stepped = true; + } } // in-sync: step until ahead of cpu @@ -381,7 +397,13 @@ void PhysicsLoop(mj::Simulate& sim) { // call mj_step mj_step(m, d); - stepped = true; + const char* message = Diverged(m->opt.disableflags, d); + if (message) { + sim.run = 0; + mju::strcpy_arr(sim.load_error, message); + } else { + stepped = true; + } // break if reset if (d->time < prevSim) { diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 9fe5ae7c..d676129b 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -739,18 +739,12 @@ void MakePhysicsSection(mj::Simulate* sim) { for (int i=0; idisable + i; - if ((1 << i) == mjDSBL_AUTORESETNAN) { - defFlag[0].state = 0; - } else { - defFlag[0].state = 2; - } mjui_add(&sim->ui0, defFlag); } mjui_add(&sim->ui0, defEnableFlags); for (int i=0; ienable + i; - defFlag[0].state = 2; mjui_add(&sim->ui0, defFlag); } // add contact override @@ -1956,6 +1950,7 @@ void Simulate::Sync() { if (pending_.reset) { mj_resetData(m_, d_); mj_forward(m_, d_); + load_error[0] = '\0'; update_profiler = true; update_sensor = true; scrub_index = 0; diff --git a/src/engine/engine_derivative.c b/src/engine/engine_derivative.c index 73c64b0f..dec37eb8 100644 --- a/src/engine/engine_derivative.c +++ b/src/engine/engine_derivative.c @@ -834,6 +834,11 @@ void mjd_actuator_vel(const mjModel* m, mjData* d) { // process actuators for (int i=0; i < m->nu; i++) { + // skip if disabled + if (mj_actuatorDisabled(m, i)) { + continue; + } + mjtNum bias_vel = 0, gain_vel = 0; // affine bias diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 6c78663e..cb326594 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -52,7 +52,7 @@ void mj_checkPos(const mjModel* m, mjData* d) { for (int i=0; i < m->nq; i++) { if (mju_isBad(d->qpos[i])) { mj_warning(d, mjWARN_BADQPOS, i); - if (!(m->opt.disableflags & mjDSBL_AUTORESETNAN)) { + if (!mjDISABLED(mjDSBL_AUTORESET)) { mj_resetData(m, d); } d->warning[mjWARN_BADQPOS].number++; @@ -69,7 +69,7 @@ void mj_checkVel(const mjModel* m, mjData* d) { for (int i=0; i < m->nv; i++) { if (mju_isBad(d->qvel[i])) { mj_warning(d, mjWARN_BADQVEL, i); - if (!(m->opt.disableflags & mjDSBL_AUTORESETNAN)) { + if (!mjDISABLED(mjDSBL_AUTORESET)) { mj_resetData(m, d); } d->warning[mjWARN_BADQVEL].number++; @@ -86,12 +86,12 @@ void mj_checkAcc(const mjModel* m, mjData* d) { for (int i=0; i < m->nv; i++) { if (mju_isBad(d->qacc[i])) { mj_warning(d, mjWARN_BADQACC, i); - if (!(m->opt.disableflags & mjDSBL_AUTORESETNAN)) { + if (!mjDISABLED(mjDSBL_AUTORESET)) { mj_resetData(m, d); } d->warning[mjWARN_BADQACC].number++; d->warning[mjWARN_BADQACC].lastinfo = i; - if (!(m->opt.disableflags & mjDSBL_AUTORESETNAN)) { + if (!mjDISABLED(mjDSBL_AUTORESET)) { mj_forward(m, d); } return; diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index b0e86887..a3f269c0 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -58,7 +58,7 @@ const char* mjDISABLESTRING[mjNDISABLE] = { "Sensor", "Midphase", "Eulerdamp", - "AutoResetNaN" + "AutoReset" }; diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 2c3b5aee..ba5890fe 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -148,7 +148,7 @@ int mjs_detachBody(mjSpec* s, mjsBody* b) { mjCModel* model = static_cast(s->element); mjCBody* body = static_cast(b->element); *model -= *body; - mjs_delete(b->element); + delete body; return 0; } @@ -181,7 +181,7 @@ void mjs_addSpec(mjSpec* s, mjSpec* child) { // delete object, it will call the appropriate destructor since ~mjCBase is virtual void mjs_delete(mjsElement* element) { mjCBase* object = static_cast(element); - delete object; + object->model->DeleteElement(element); } diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 1662fbd8..5dfc6943 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -900,12 +900,12 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, char* error, int error_sz) { } // load resource - std::string filename = mjuu_combinePaths(mjs_getString(model->spec.modelfiledir), - mjs_getString(model->spec.meshdir), file); + std::string filename = mjuu_combinePaths(mjs_getString(model->spec.meshdir), file); mjResource* resource = nullptr; try { - resource = mjCBase::LoadResource(filename, 0); + resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir), + filename, 0); } catch (mjCError err) { return comperr(error, err.message, error_sz); } @@ -999,12 +999,11 @@ bool mjCFlexcomp::MakeGMSH(mjCModel* model, char* error, int error_sz) { } // open resource - std::string filename = mjuu_combinePaths(mjs_getString(model->spec.modelfiledir), - mjs_getString(model->spec.meshdir), file); mjResource* resource = nullptr; - try { - resource = mjCBase::LoadResource(filename, 0); + std::string filename = mjuu_combinePaths(mjs_getString(model->spec.meshdir), file); + resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir), + filename, 0); } catch (mjCError err) { return comperr(error, err.message, error_sz); } diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 1b2d1022..c92a6f67 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -402,8 +402,8 @@ void mjCMesh::Compile(const mjVFS* vfs) { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "model/stl") { @@ -2095,8 +2095,8 @@ void mjCSkin::Compile(const mjVFS* vfs) { throw mjCError(this, "Unknown skin file type: %s", file_.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { LoadSKN(resource); @@ -2152,7 +2152,7 @@ void mjCSkin::Compile(const mjVFS* vfs) { if (pmat) { matid = pmat->id; } else if (!material_.empty()) { - throw mjCError(this, "unkown material '%s' in skin", material_.c_str()); + throw mjCError(this, "unknown material '%s' in skin", material_.c_str()); } // set total vertex weights to 0 @@ -2452,7 +2452,7 @@ void mjCFlex::ResolveReferences(const mjCModel* m) { if (pbody) { vertbodyid.push_back(pbody->id); } else { - throw mjCError(this, "unkown body '%s' in flex", vertbody.c_str()); + throw mjCError(this, "unknown body '%s' in flex", vertbody.c_str()); } } } @@ -2515,7 +2515,7 @@ void mjCFlex::Compile(const mjVFS* vfs) { if (pmat) { matid = pmat->id; } else if (!material_.empty()) { - throw mjCError(this, "unkown material '%s' in flex", material_.c_str()); + throw mjCError(this, "unknown material '%s' in flex", material_.c_str()); } // resolve body ids diff --git a/src/user/user_model.cc b/src/user/user_model.cc index b6ec8086..c7fd5ccb 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -250,15 +250,22 @@ mjCModel& mjCModel::operator+=(const mjCModel& other) { template -void mjCModel::RemoveFromList(std::vector& list) { +void mjCModel::RemoveFromList(std::vector& list, const mjCModel& other) { int nlist = (int)list.size(); int removed = 0; for (int i = 0; i < nlist; i++) { T* element = list[i]; element->id -= removed; + try { + // check if the element contains an error + element->NameSpace(&other); + element->CopyFromSpec(); + element->ResolveReferences(&other); + } catch (mjCError err) { + continue; + } try { // check if the element references something that was removed - // TODO: do not remove elements that contain user errors element->NameSpace(this); element->CopyFromSpec(); element->ResolveReferences(this); @@ -296,12 +303,12 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) { ProcessLists(/*checkrepeat=*/false); // check if we have to remove anything else - RemoveFromList(pairs_); - RemoveFromList(excludes_); - RemoveFromList(tendons_); - RemoveFromList(equalities_); - RemoveFromList(actuators_); - RemoveFromList(sensors_); + RemoveFromList(pairs_, oldmodel); + RemoveFromList(excludes_, oldmodel); + RemoveFromList(tendons_, oldmodel); + RemoveFromList(equalities_, oldmodel); + RemoveFromList(actuators_, oldmodel); + RemoveFromList(sensors_, oldmodel); // restore to the original state if (!compiled) { @@ -333,6 +340,68 @@ mjCModel_& mjCModel::operator+=(mjCDef& subtree) { +template +void deletefromlist(std::vector* list, mjsElement* element) { + if (!list) { + return; + } + for (int j = 0; j < list->size(); ++j) { + list->at(j)->id = -1; + if (list->at(j) == element) { + delete list->at(j); + list->erase(list->begin() + j); + j--; + } + } +} + + + +// discard all invalid elements from all lists +void mjCModel::DeleteElement(mjsElement* el) { + mjCBody *world = bodies_[0]; + if (compiled) { + ResetTreeLists(); + } + + switch (el->elemtype) { + case mjOBJ_BODY: + throw mjCError(NULL, "bodies cannot be deleted, use detach instead"); + break; + + case mjOBJ_GEOM: + deletefromlist(&(static_cast(el)->body->geoms), el); + break; + + case mjOBJ_SITE: + deletefromlist(&(static_cast(el)->body->sites), el); + break; + + case mjOBJ_JOINT: + deletefromlist(&(static_cast(el)->body->joints), el); + break; + + case mjOBJ_LIGHT: + deletefromlist(&(static_cast(el)->body->lights), el); + break; + + case mjOBJ_CAMERA: + deletefromlist(&(static_cast(el)->body->cameras), el); + break; + + default: + deletefromlist(object_lists_[el->elemtype], el); + break; + } + + if (compiled) { + MakeLists(world); + ProcessLists(/*checkrepeat=*/false); + } +} + + + // TODO: we should not use C-type casting with multiple C++ inheritance void mjCModel::CreateObjectLists() { for (int i = 0; i < mjNOBJECT; ++i) { diff --git a/src/user/user_model.h b/src/user/user_model.h index f6c1c782..9f88bb5b 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -202,6 +202,9 @@ class mjCModel : public mjCModel_, private mjSpec { // delete all elements template void DeleteAll(std::vector& elements); + // delete object from the corresponding list + void DeleteElement(mjsElement* el); + // 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 @@ -345,7 +348,7 @@ class mjCModel : public mjCModel_, private mjSpec { const std::vector& sources); // delete from list the elements that cause an error - template void RemoveFromList(std::vector& list); + template void RemoveFromList(std::vector& list, const mjCModel& other); // create mjCBase lists from children lists void CreateObjectLists(); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index c647f4b3..9ff645b2 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -701,10 +701,12 @@ void mjCBase::NameSpace(const mjCModel* m) { // load resource if found (fallback to OS filesystem) -mjResource* mjCBase::LoadResource(std::string filename, const mjVFS* vfs) { +mjResource* mjCBase::LoadResource(const std::string& modelfiledir, + const std::string& filename, + const mjVFS* vfs) { // try reading from provided VFS or fallback to OS filesystem std::array error; - mjResource* resource = mju_openResource(filename.c_str(), vfs, + mjResource* resource = mju_openResource(modelfiledir.c_str(), filename.c_str(), vfs, error.data(), error.size()); if (!resource) { throw mjCError(nullptr, "%s", error.data()); @@ -3159,8 +3161,8 @@ void mjCHField::Compile(const mjVFS* vfs) { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "image/png") { @@ -3594,7 +3596,7 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - mjResource* resource = LoadResource(filename, vfs); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "image/png") { @@ -3797,7 +3799,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { } // make filename - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->texturedir_, cubefiles_[i]); + std::string filename = mjuu_combinePaths(model->texturedir_, cubefiles_[i]); // load PNG or custom unsigned int w, h; @@ -3895,7 +3897,7 @@ void mjCTexture::Compile(const mjVFS* vfs) { } // make filename - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->texturedir_, file_); + std::string filename = mjuu_combinePaths(model->texturedir_, file_); // dispatch if (type==mjTEXTURE_2D) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 4e508b04..18d9b886 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -181,7 +181,8 @@ class mjCBase : public mjCBase_ { public: // load resource if found (fallback to OS filesystem) - static mjResource* LoadResource(std::string filename, const mjVFS* vfs); + static mjResource* LoadResource(const std::string& modelfiledir, + const std::string& filename, const mjVFS* vfs); // Get and sanitize content type from raw_text if not empty, otherwise parse // content type from resource_name; throw on failure diff --git a/src/user/user_resource.cc b/src/user/user_resource.cc index 805a3156..980d7ee9 100644 --- a/src/user/user_resource.cc +++ b/src/user/user_resource.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) @@ -117,8 +118,8 @@ int FileModified(const mjResource* resource, const char*timestamp) { // open the given resource; if the name doesn't have a prefix matching with a // resource provider, then the OS filesystem is used -mjResource* mju_openResource(const char* name, const mjVFS* vfs, - char* error, size_t nerror) { +mjResource* mju_openResource(const char* dir, const char* name, + const mjVFS* vfs, char* error, size_t nerror) { // no error so far if (error) { error[0] = '\0'; @@ -136,8 +137,10 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, // clear out resource memset(resource, 0, sizeof(mjResource)); - // copy name - resource->name = (char*) mju_malloc(sizeof(char) * (strlen(name) + 1)); + // make space for filename + std::string fullname = mjuu_combinePaths(dir, name); + std::size_t n = fullname.size(); + resource->name = (char*) mju_malloc(sizeof(char) * (n + 1)); if (resource->name == nullptr) { if (error) { strncpy(error, "could not allocate memory", nerror); @@ -146,10 +149,11 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, mju_closeResource(resource); return nullptr; } - memcpy(resource->name, name, sizeof(char) * (strlen(name) + 1)); // first priority is to check the VFS if (vfs != nullptr) { + memcpy(resource->name, name, + sizeof(char) * (std::strlen(name) + 1)); const mjpResourceProvider* provider = GetVfsResourceProvider(); resource->data = (void*) vfs; resource->provider = provider; @@ -158,8 +162,11 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, } } + // copy full path over + memcpy(resource->name, fullname.c_str(), sizeof(char) * (n + 1)); + // find provider based off prefix of name - const mjpResourceProvider* provider = mjp_getResourceProvider(name); + const mjpResourceProvider* provider = mjp_getResourceProvider(resource->name); if (provider != nullptr) { resource->provider = provider; resource->data = nullptr; @@ -170,7 +177,7 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, if (error) { snprintf(error, nerror, "could not open '%s'" "using a resource provider matching prefix '%s'", - name, provider->prefix); + resource->name, provider->prefix); } mju_closeResource(resource); diff --git a/src/user/user_resource.h b/src/user/user_resource.h index e64874cb..82a70ed6 100644 --- a/src/user/user_resource.h +++ b/src/user/user_resource.h @@ -18,8 +18,6 @@ #define MUJOCO_SRC_ENGINE_ENGINE_RESOURCE_H_ #include -#include -#include #include #include @@ -30,8 +28,8 @@ extern "C" { // open the given resource; if the name doesn't have a prefix matching with a // resource provider, then the OS filesystem is used -MJAPI mjResource* mju_openResource(const char* name, const mjVFS* vfs, - char* error, std::size_t nerror); +MJAPI mjResource* mju_openResource(const char* dir, const char* name, + const mjVFS* vfs, char* error, size_t nerror); // close the given resource; no-op if resource is NULL MJAPI void mju_closeResource(mjResource* resource); diff --git a/src/user/user_util.cc b/src/user/user_util.cc index 842229e8..803c22d4 100644 --- a/src/user/user_util.cc +++ b/src/user/user_util.cc @@ -996,7 +996,7 @@ std::string FilePath::PathReduce(const std::string& str) { if (IsSeperator(str[i])) { std::string temp = str.substr(j, i - j); j = i + 1; - if (temp == ".." && !dirs.empty()) { + if (temp == ".." && !dirs.empty() && dirs.back() != "..") { dirs.pop_back(); } else if (temp != ".") { dirs.push_back(std::move(temp)); diff --git a/src/user/user_vfs.cc b/src/user/user_vfs.cc index 8bc484a2..02c8829d 100644 --- a/src/user/user_vfs.cc +++ b/src/user/user_vfs.cc @@ -142,9 +142,13 @@ int Open(mjResource* resource) { const VFS* cvfs = GetVFSImpl(vfs); const VFSFile* file = cvfs->GetFile(StripPath(resource->name)); if (file == nullptr) { - return 0; + file = cvfs->GetFile(FilePath(resource->name)); + if (file == nullptr) { + return 0; + } } + resource->data = (void*) file; resource->timestamp[0] = '\0'; if (file->filestamp) { mju_encodeBase64(resource->timestamp, (uint8_t*) &file->filestamp, @@ -160,8 +164,7 @@ int Read(mjResource* resource, const void** buffer) { return -1; } - const VFS* vfs = GetVFSImpl(static_cast(resource->data)); - const VFSFile* file = vfs->GetFile(StripPath(resource->name)); + const VFSFile* file = static_cast(resource->data); if (file == nullptr) { *buffer = nullptr; return -1; @@ -193,8 +196,7 @@ int Modified(const mjResource* resource, const char* timestamp) { if (!filestamp) return 3; // no hash (assume modified) if (resource) { - const VFS* cvfs = GetVFSImpl(static_cast(resource->data)); - const VFSFile* file = cvfs->GetFile(StripPath(resource->name)); + const VFSFile* file = static_cast(resource->data); if (file == nullptr) return 4; // missing file (assume modified) if (!file->filestamp) return 5; // missing filestamp (assume modified) @@ -245,7 +247,7 @@ int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, std::vector inbuffer; VFS* cvfs = GetVFSImpl(vfs); VFSFile* file; - if (!(file = cvfs->AddFile(StripPath(name), std::move(inbuffer), 0))) { + if (!(file = cvfs->AddFile(FilePath(name), std::move(inbuffer), 0))) { return 2; // AddFile failed, repeated name } file->filedata.reserve(nbuffer); @@ -256,7 +258,10 @@ int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, // delete file from VFS, return 0: success, -1: not found in VFS int mj_deleteFileVFS(mjVFS* vfs, const char* filename) { VFS* cvfs = GetVFSImpl(vfs); - return cvfs->DeleteFile(StripPath(filename)); + if (cvfs->DeleteFile(StripPath(filename))) { + return cvfs->DeleteFile(FilePath(filename)); + } + return 0; } // delete all files from VFS diff --git a/src/xml/xml.cc b/src/xml/xml.cc index 9c64ce2d..e299cea2 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -48,9 +48,11 @@ namespace { using tinyxml2::XMLDocument; using tinyxml2::XMLElement; using tinyxml2::XMLNode; +using mujoco::user::FilePath; namespace mju = ::mujoco::util; + // We are using "locale-sensitive" sprintf to read and write XML. // When MuJoCo is being used as a plug-in for an application that respects the system locale // (e.g. Unity), the user's locale setting can affect the formatting of numbers into strings. @@ -97,29 +99,30 @@ class LocaleOverride { }; #endif -} // namespace - -// Main writer function - calls mjXWrite -std::string mjWriteXML(const mjSpec* spec, char* error, int error_sz) { - LocaleOverride locale_override; - - // check for empty model - if (!spec) { - mjCopyError(error, "Cannot write empty model", error_sz); - return ""; +void RegisterResourceProvider() { + // register string resource provider if not registered before + if (mjp_getResourceProvider("LoadModelFromString:") == nullptr) { + mjpResourceProvider resourceProvider; + mjp_defaultResourceProvider(&resourceProvider); + resourceProvider.prefix = "LoadModelFromString"; + resourceProvider.open = +[](mjResource* resource) { + resource->data = &(resource->name[strlen("LoadModelFromString:")]); + return 1; + }; + resourceProvider.read = + +[](mjResource* resource, const void** buffer) { + *buffer = resource->data; + return (int) strlen((const char*) resource->data); + }; + resourceProvider.close = +[](mjResource* resource) {}; + mjp_registerResourceProvider(&resourceProvider); } - - mjXWriter writer; - writer.SetModel(spec); - return writer.Write(error, error_sz); } - - // find include elements recursively, replace them with subtree from xml file -static void mjIncludeXML(mjXReader& reader, XMLElement* elem, - std::string_view dir, const mjVFS* vfs, - std::unordered_set& included) { +void IncludeXML(mjXReader& reader, XMLElement* elem, + const FilePath& dir, const mjVFS* vfs, + std::unordered_set& included) { // capture directory defaults on first pass of XML tree if (!strcasecmp(elem->Value(), "compiler")) { auto assetdir_attr = mjXUtil::ReadAttrStr(elem, "assetdir"); @@ -142,7 +145,7 @@ static void mjIncludeXML(mjXReader& reader, XMLElement* elem, if (strcasecmp(elem->Value(), "include")) { XMLElement* child = elem->FirstChildElement(); for (; child; child = child->NextSiblingElement()) { - mjIncludeXML(reader, child, dir, vfs, included); + IncludeXML(reader, child, dir, vfs, included); } return; } @@ -153,36 +156,32 @@ static void mjIncludeXML(mjXReader& reader, XMLElement* elem, } // get filename - auto file_attr = mjXUtil::ReadAttrStr(elem, "file", true); + auto file_attr = mjXUtil::ReadAttrFile(elem, "file", vfs, + reader.ModelFileDir(), true); if (!file_attr.has_value()) { throw mjXError(elem, "Include element missing file attribute"); } - std::string filename = file_attr.value(); + FilePath filename = file_attr.value(); // block repeated include files - if (included.find(filename) != included.end()) { + if (included.find(filename.Str()) != included.end()) { throw mjXError(elem, "File '%s' already included", filename.c_str()); } // TODO: b/325905702 - We have a messy wrapper here to remain backwards // compatible, which will be removed in the near future. - std::string fullname; - if (!mjuu_isabspath(filename)) { - fullname = reader.ModelFileDir() + filename; - } else { - fullname = filename; - } - // legacy behavior: try to load in top level directory std::array error; - mjResource *resource = mju_openResource(fullname.c_str(), vfs, + mjResource *resource = mju_openResource(reader.ModelFileDir().c_str(), + filename.c_str(), vfs, error.data(), error.size()); if (resource == nullptr) { // new behavior: try to load in relative directory - if (!mjuu_isabspath(filename)) { - fullname = std::string(dir) + filename; - resource = mju_openResource(fullname.c_str(), vfs, error.data(), error.size()); + if (!filename.IsAbs()) { + FilePath fullname = dir + filename; + resource = mju_openResource(reader.ModelFileDir().c_str(), + fullname.c_str(), vfs, error.data(), error.size()); } } @@ -190,18 +189,13 @@ static void mjIncludeXML(mjXReader& reader, XMLElement* elem, throw mjXError(elem, "%s", error.data()); } - if (!mjuu_isabspath(filename)) { - filename = std::string(dir) + filename; - } + filename = dir + filename; const char* include_dir = nullptr; int ninclude_dir = 0; mju_getResourceDir(resource, &include_dir, &ninclude_dir); - std::string next_dir = std::string(include_dir, ninclude_dir); - if (!mjuu_isabspath(filename)) { - next_dir = std::string(dir) + next_dir; - } - elem->SetAttribute("dir", next_dir.data()); + FilePath next_dir = FilePath(std::string(include_dir, ninclude_dir)); + elem->SetAttribute("dir", next_dir.c_str()); const char* xmlstring = nullptr; int buffer_size = mju_readResource(resource, (const void**) &xmlstring); @@ -228,7 +222,7 @@ static void mjIncludeXML(mjXReader& reader, XMLElement* elem, } // remember that file was included - included.insert(filename); + included.insert(filename.Str()); // get and check root element XMLElement* docroot = doc.RootElement(); @@ -262,21 +256,21 @@ static void mjIncludeXML(mjXReader& reader, XMLElement* elem, // recursively run include child = include->FirstChildElement(); for (; child; child = child->NextSiblingElement()) { - mjIncludeXML(reader, child, next_dir, vfs, included); + IncludeXML(reader, child, next_dir, vfs, included); } } - +} // namespace // Main parser function -mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, - char* error, int error_sz) { +mjSpec* ParseXML(const char* filename, const mjVFS* vfs, + char* error, int nerror) { LocaleOverride locale_override; // check arguments if (!filename) { if (error) { - std::snprintf(error, error_sz, "mjParseXML: filename argument required\n"); + std::snprintf(error, nerror, "ParseXML: filename argument required\n"); } return nullptr; } @@ -290,24 +284,24 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, // get data source const char* xmlstring = nullptr; std::array rerror; - mjResource* resource = mju_openResource(filename, vfs, + mjResource* resource = mju_openResource("", filename, vfs, rerror.data(), rerror.size()); if (resource == nullptr) { - std::snprintf(error, error_sz, "mjParseXML: %s", rerror.data()); + std::snprintf(error, nerror, "ParseXML: %s", rerror.data()); return nullptr; } int buffer_size = mju_readResource(resource, (const void**) &xmlstring); if (buffer_size < 0) { if (error) { - std::snprintf(error, error_sz, - "mjParseXML: error reading file '%s'", filename); + std::snprintf(error, nerror, + "ParseXML: error reading file '%s'", filename); } mju_closeResource(resource); return nullptr; } else if (!buffer_size) { if (error) { - std::snprintf(error, error_sz, "mjParseXML: empty file '%s'", filename); + std::snprintf(error, nerror, "ParseXML: empty file '%s'", filename); } mju_closeResource(resource); return nullptr; @@ -321,7 +315,7 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, // error checking if (doc.Error()) { if (error) { - snprintf(error, error_sz, "XML parse error %d:\n%s\n", + snprintf(error, nerror, "XML parse error %d:\n%s\n", doc.ErrorID(), doc.ErrorStr()); } mju_closeResource(resource); @@ -332,7 +326,7 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, XMLElement* root = doc.RootElement(); if (!root) { mju_closeResource(resource); - mjCopyError(error, "XML root element not found", error_sz); + mjCopyError(error, "XML root element not found", nerror); return nullptr; } @@ -357,7 +351,7 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, std::unordered_set included = {filename}; mjXReader parser; parser.SetModelFileDir(mjs_getString(spec->modelfiledir)); - mjIncludeXML(parser, root, mjs_getString(spec->modelfiledir), vfs, included); + IncludeXML(parser, root, FilePath(), vfs, included); // parse MuJoCo model parser.SetModel(spec); @@ -385,7 +379,7 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, // catch known errors catch (mjXError err) { - mjCopyError(error, err.message, error_sz); + mjCopyError(error, err.message, nerror); mj_deleteSpec(spec); return nullptr; } @@ -393,32 +387,24 @@ mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, return spec; } - -static void RegisterResourceProvider() { - // register string resource provider if not registered before - if (mjp_getResourceProvider("LoadModelFromString:") == nullptr) { - mjpResourceProvider resourceProvider; - mjp_defaultResourceProvider(&resourceProvider); - resourceProvider.prefix = "LoadModelFromString"; - resourceProvider.open = +[](mjResource* resource) { - resource->data = &(resource->name[strlen("LoadModelFromString:")]); - return 1; - }; - resourceProvider.read = - +[](mjResource* resource, const void** buffer) { - *buffer = resource->data; - return (int) strlen((const char*) resource->data); - }; - resourceProvider.close = +[](mjResource* resource) {}; - mjp_registerResourceProvider(&resourceProvider); - } -} - - -mjSpec* ParseSpecFromString(std::string_view xml, char* error, - int error_size) { +mjSpec* ParseSpecFromString(std::string_view xml, char* error, int nerror) { RegisterResourceProvider(); std::string xml2 = {xml.begin(), xml.end()}; std::string str = "LoadModelFromString:" + xml2; - return mjParseXML(str.c_str(), nullptr, error, error_size); + return ParseXML(str.c_str(), nullptr, error, nerror); +} + +// Main writer function - calls mjXWrite +std::string WriteXML(const mjSpec* spec, char* error, int nerror) { + LocaleOverride locale_override; + + // check for empty model + if (!spec) { + mjCopyError(error, "Cannot write empty model", nerror); + return ""; + } + + mjXWriter writer; + writer.SetModel(spec); + return writer.Write(error, nerror); } diff --git a/src/xml/xml.h b/src/xml/xml.h index dadc2451..d83f026a 100644 --- a/src/xml/xml.h +++ b/src/xml/xml.h @@ -16,22 +16,19 @@ #define MUJOCO_SRC_XML_XML_H_ #include +#include -#include #include #include -// Top level API - -// Main writer function -std::string mjWriteXML(const mjSpec* spec, char* error, int error_sz); - // Main parser function -mjSpec* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz); +mjSpec* ParseXML(const char* filename, const mjVFS* vfs, char* error, int nerror); // Returns a newly-allocated mjSpec, loaded from the contents of xml. // On failure returns nullptr and populates the error array if present. -mjSpec* ParseSpecFromString(std::string_view xml, char* error = nullptr, int error_size = 0); +mjSpec* ParseSpecFromString(std::string_view xml, char* error = nullptr, int nerror = 0); +// Main writer function +std::string WriteXML(const mjSpec* spec, char* error, int nerror); #endif // MUJOCO_SRC_XML_XML_H_ diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index 623466ed..31deeec8 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -37,6 +37,8 @@ //---------------------------------- Globals ------------------------------------------------------- +namespace { + // global user model class class GlobalModel { public: @@ -61,7 +63,7 @@ std::optional GlobalModel::ToXML(const mjModel* m, char* error, return std::nullopt; } mj_copyBack(spec_, m); - std::string result = mjWriteXML(spec_, error, error_sz); + std::string result = WriteXML(spec_, error, error_sz); if (result.empty()) { return std::nullopt; } @@ -86,6 +88,8 @@ GlobalModel& GetGlobalModel() { return global_model; } +} // namespace + //---------------------------------- Functions ----------------------------------------------------- // parse XML file in MJCF or URDF format, compile it, return low-level model @@ -96,7 +100,7 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, // parse new model std::unique_ptr> spec( - mjParseXML(filename, vfs, error, error_sz), + ParseXML(filename, vfs, error, error_sz), [](mjSpec* s) { mj_deleteSpec(s); }); if (!spec) { return nullptr; @@ -188,7 +192,7 @@ int mj_printSchema(const char* filename, char* buffer, int buffer_sz, int flg_ht // load model from binary MJB resource mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { std::array error; - mjResource* resource = mju_openResource(filename, vfs, + mjResource* resource = mju_openResource("", filename, vfs, error.data(), error.size()); if (resource == nullptr) { mju_warning("%s", error.data()); @@ -211,7 +215,7 @@ mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { // parse spec from file mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz) { - return mjParseXML(filename, vfs, error, error_sz); + return ParseXML(filename, vfs, error, error_sz); } @@ -225,7 +229,7 @@ mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int er // save spec to XML file, return 1 on success, 0 otherwise int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz) { - std::string result = mjWriteXML(s, error, error_sz); + std::string result = WriteXML(s, error, error_sz); if (result.empty()) { return 0; } @@ -241,7 +245,7 @@ int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz) // save spec to string, return 1 on success, 0 otherwise int mj_saveXMLString(const mjSpec* s, char* xml, int xml_sz, char* error, int error_sz) { - std::string result = mjWriteXML(s, error, error_sz); + std::string result = WriteXML(s, error, error_sz); if (result.size() >= xml_sz) { std::string error_msg = "Output string too short, should be at least " + std::to_string(result.size()+1); diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 6ec5a6a0..46b13c4f 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -49,6 +49,7 @@ namespace { using std::string; using std::vector; +using mujoco::user::FilePath; using tinyxml2::XMLElement; void ReadPluginConfigs(tinyxml2::XMLElement* elem, mjsPlugin* p) { @@ -114,9 +115,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { "solver", "iterations", "ls_iterations", "noslip_iterations", "mpr_iterations", "sdf_iterations", "sdf_initpoints", "actuatorgroupdisable"}, {"<"}, - {"flag", "?", "21", "constraint", "equality", "frictionloss", "limit", "contact", + {"flag", "?", "22", "constraint", "equality", "frictionloss", "limit", "contact", "passive", "gravity", "clampctrl", "warmstart", - "filterparent", "actuation", "refsafe", "sensor", "midphase", "eulerdamp", + "filterparent", "actuation", "refsafe", "sensor", "midphase", "eulerdamp", "autoreset", "override", "energy", "fwdinv", "invdiscrete", "multiccd", "island"}, {">"}, @@ -892,7 +893,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { readingdefaults = true; for (XMLElement* section = FirstChildElement(root, "default"); section; section = NextSiblingElement(section, "default")) { - Default(section, nullptr); + Default(section, nullptr, vfs); } readingdefaults = false; @@ -918,7 +919,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { for (XMLElement* section = FirstChildElement(root, "deformable"); section; section = NextSiblingElement(section, "deformable")) { - Deformable(section); + Deformable(section, vfs); } for (XMLElement* section = FirstChildElement(root, "equality"); section; @@ -948,7 +949,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { for (XMLElement* section = FirstChildElement(root, "worldbody"); section; section = NextSiblingElement(section, "worldbody")) { - Body(section, mjs_findBody(spec, "world"), nullptr); + Body(section, mjs_findBody(spec, "world"), nullptr, vfs); } } @@ -1121,6 +1122,7 @@ void mjXReader::Option(XMLElement* section, mjOption* opt) { READDSBL("sensor", mjDSBL_SENSOR) READDSBL("midphase", mjDSBL_MIDPHASE) READDSBL("eulerdamp", mjDSBL_EULERDAMP) + READDSBL("autoreset", mjDSBL_AUTORESET) #undef READDSBL #define READENBL(NAME, MASK) \ @@ -1388,7 +1390,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjsFlex* pflex) { // mesh element parser -void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { +void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh, const mjVFS* vfs) { int n; string text, name, content_type; @@ -1399,7 +1401,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { if (ReadAttrTxt(elem, "content_type", content_type)) { *pmesh->content_type = content_type; } - auto file = ReadAttrFile(elem, "file", MeshDir()); + auto file = ReadAttrFile(elem, "file", vfs, MeshDir()); if (file) { mjs_setString(pmesh->file, file->c_str()); } @@ -1460,7 +1462,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { // skin element parser -void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin) { +void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin, const mjVFS* vfs) { string text, name, material; float data[4]; @@ -1468,7 +1470,7 @@ void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin) { if (ReadAttrTxt(elem, "name", name)) { mjs_setString(pskin->name, name.c_str()); } - auto file = ReadAttrFile(elem, "file", AssetDir()); + auto file = ReadAttrFile(elem, "file", vfs, AssetDir()); if (file.has_value()) { mjs_setString(pskin->file, file->c_str()); } @@ -2535,7 +2537,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjsBody* pbody, mjsDefault* def) // make flexcomp -void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody) { +void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody, const mjVFS* vfs) { string text, material; int n; @@ -2553,7 +2555,12 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody) { ReadAttr(elem, "scale", 3, fcomp.scale, text); ReadAttr(elem, "mass", 1, &fcomp.mass, text); ReadAttr(elem, "inertiabox", 1, &fcomp.inertiabox, text); - fcomp.file = ReadAttrFile(elem, "file", modelfiledir_).value_or(""); + auto maybe_file = ReadAttrFile(elem, "file", vfs, modelfiledir_); + if (maybe_file.has_value()) { + fcomp.file = std::move(maybe_file.value().Str()); + } else { + fcomp.file = ""; + } if (ReadAttrTxt(elem, "material", material)) { mjs_setString(dflex.material, material.c_str()); } @@ -2684,7 +2691,7 @@ void mjXReader::OnePlugin(XMLElement* elem, mjsPlugin* plugin) { //------------------ MJCF-specific sections -------------------------------------------------------- // default section parser -void mjXReader::Default(XMLElement* section, const mjsDefault* def) { +void mjXReader::Default(XMLElement* section, const mjsDefault* def, const mjVFS* vfs) { XMLElement* elem; string text, name; @@ -2715,7 +2722,7 @@ void mjXReader::Default(XMLElement* section, const mjsDefault* def) { name = elem->Value(); // read mesh - if (name=="mesh") OneMesh(elem, def->mesh); + if (name=="mesh") OneMesh(elem, def->mesh, vfs); // read material else if (name=="material") OneMaterial(elem, def->material); @@ -2769,7 +2776,7 @@ void mjXReader::Default(XMLElement* section, const mjsDefault* def) { // read default if (name=="default") { - Default(elem, def); + Default(elem, def, vfs); } // advance @@ -3130,7 +3137,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjs_setString(ptex->content_type, content_type.c_str()); } - auto file = ReadAttrFile(elem, "file", TextureDir()); + auto file = ReadAttrFile(elem, "file", vfs, TextureDir()); if (file.has_value()) { mjs_setString(ptex->file, file->c_str()); } @@ -3171,14 +3178,18 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { } // separate files - std::vector cubefiles(6); - cubefiles[0] = ReadAttrFile(elem, "fileright", TextureDir()).value_or(""); - cubefiles[1] = ReadAttrFile(elem, "fileleft", TextureDir()).value_or(""); - cubefiles[2] = ReadAttrFile(elem, "fileup", TextureDir()).value_or(""); - cubefiles[3] = ReadAttrFile(elem, "filedown", TextureDir()).value_or(""); - cubefiles[4] = ReadAttrFile(elem, "filefront", TextureDir()).value_or(""); - cubefiles[5] = ReadAttrFile(elem, "fileback", TextureDir()).value_or(""); + std::vector cubefiles(6); + std::vector cubefile_names = {"fileright", "fileleft", + "fileup", "filedown", + "filefront", "fileback"}; for (int i = 0; i < cubefiles.size(); i++) { + auto maybe_file = ReadAttrFile(elem, cubefile_names[i].c_str(), vfs, + TextureDir()); + if (maybe_file.has_value()) { + cubefiles[i] = maybe_file.value().Str(); + } else { + cubefiles[i] = ""; + } mjs_setInStringVec(ptex->cubefiles, i, cubefiles[i].c_str()); } } @@ -3194,14 +3205,14 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { else if (name=="mesh") { // create mesh and parse mjsMesh* pmesh = mjs_addMesh(spec, def); - OneMesh(elem, pmesh); + OneMesh(elem, pmesh, vfs); } // skin sub-element... deprecate ??? else if (name=="skin") { // create skin and parse mjsSkin* pskin = mjs_addSkin(spec); - OneSkin(elem, pskin); + OneSkin(elem, pskin, vfs); } // hfield sub-element @@ -3220,7 +3231,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjs_setString(phf->content_type, content_type.c_str()); } - auto file = ReadAttrFile(elem, "file", AssetDir()); + auto file = ReadAttrFile(elem, "file", vfs, AssetDir()); if (file.has_value()) { mjs_setString(phf->file, file->c_str()); } @@ -3264,7 +3275,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { // model sub-element else if (name=="model") { - auto filename = modelfiledir_ + ReadAttrFile(elem, "file", "").value(); + auto filename = modelfiledir_ + ReadAttrFile(elem, "file", vfs).value(); // parse the child std::array error; @@ -3291,7 +3302,8 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { // body/world section parser; recursive -void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { +void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame, + const mjVFS* vfs) { string text, name; XMLElement* elem; int n; @@ -3417,7 +3429,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { // flexcomp sub-element else if (name=="flexcomp") { // parse flexcomp - OneFlexcomp(elem, pbody); + OneFlexcomp(elem, pbody, vfs); } // frame sub-element @@ -3449,7 +3461,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { ReadQuat(elem, "quat", pframe->quat, text); ReadAlternative(elem, pframe->alt); - Body(elem, pbody, pframe); + Body(elem, pbody, pframe, vfs); } // replicate sub-element @@ -3486,12 +3498,16 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { double pos[3] = {0, 0, 0}; double quat[4] = {1, 0, 0, 0}; - for (int i = 0; i < count; i++) { - // create parent frame - mjsFrame* pframe = mjs_addFrame(subtree, frame); - mjs_setString(pframe->info, ("line = " + std::to_string(elem->GetLineNum())).c_str()); - mjs_setDefault(pframe->element, childdef ? childdef : def); + // parent frame that will be used to attach the subtree + mjsFrame* pframe = mjs_addFrame(subtree, frame); + mjs_setDefault(pframe->element, childdef ? childdef : def); + mjs_setString(pframe->info, ("line = " + std::to_string(elem->GetLineNum())).c_str()); + // parse subtree + Body(elem, subtree, pframe, vfs); + + // update pframe and attach + for (int i = 0; i < count; i++) { // accumulate rotation mjuu_setvec(pframe->pos, pos[0], pos[1], pos[2]); mjuu_frameaccum(pos, quat, offset, rotation); @@ -3507,9 +3523,6 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { std::string suffix = separator; UpdateString(suffix, count, i); - // process subtree - Body(elem, subtree, pframe); - // attach to parent if (mjs_attachFrame(pbody, pframe, /*prefix=*/"", suffix.c_str()) != 0) { throw mjXError(elem, mjs_getError(spec)); @@ -3567,7 +3580,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { mjs_setFrame(pchild->element, frame); // make recursive call - Body(elem, pchild, nullptr); + Body(elem, pchild, nullptr, vfs); } // attachment @@ -3685,7 +3698,7 @@ void mjXReader::Equality(XMLElement* section) { // deformable section parser -void mjXReader::Deformable(XMLElement* section) { +void mjXReader::Deformable(XMLElement* section, const mjVFS* vfs) { string name; XMLElement* elem; @@ -3712,7 +3725,7 @@ void mjXReader::Deformable(XMLElement* section) { else if (name=="skin") { // create skin and parse mjsSkin* pskin = mjs_addSkin(spec); - OneSkin(elem, pskin); + OneSkin(elem, pskin, vfs); } // advance to next element @@ -4227,36 +4240,35 @@ mjsDefault* mjXReader::GetClass(XMLElement* section) { return def; } -void mjXReader::SetModelFileDir(std::string modelfiledir) { - modelfiledir_ = modelfiledir; +void mjXReader::SetModelFileDir(const std::string& modelfiledir) { + modelfiledir_ = FilePath(modelfiledir); } -void mjXReader::SetAssetDir(std::string assetdir) { - assetdir_ = assetdir; +void mjXReader::SetAssetDir(const std::string& assetdir) { + assetdir_ = FilePath(assetdir); } -void mjXReader::SetMeshDir(std::string meshdir) { - meshdir_ = meshdir; +void mjXReader::SetMeshDir(const std::string& meshdir) { + meshdir_ = FilePath(meshdir); } -void mjXReader::SetTextureDir(std::string texturedir) { - texturedir_ = texturedir; +void mjXReader::SetTextureDir(const std::string& texturedir) { + texturedir_ = FilePath(texturedir); } -std::string mjXReader::AssetDir() const { - return mjuu_combinePaths(modelfiledir_, assetdir_); +FilePath mjXReader::AssetDir() const { + return modelfiledir_ + assetdir_; } -std::string mjXReader::MeshDir() const { +FilePath mjXReader::MeshDir() const { if (meshdir_.empty()) { return AssetDir(); } - return mjuu_combinePaths(modelfiledir_, meshdir_); + return modelfiledir_ + meshdir_; } - -std::string mjXReader::TextureDir() const { +FilePath mjXReader::TextureDir() const { if (texturedir_.empty()) { return AssetDir(); } - return mjuu_combinePaths(modelfiledir_, texturedir_); + return modelfiledir_ + texturedir_; } diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index 8e4e8cf4..cfa87961 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -22,6 +22,7 @@ #include #include +#include "user/user_util.h" #include "xml/xml_base.h" #include "xml/xml_util.h" @@ -33,13 +34,13 @@ class mjXReader : public mjXBase { void Parse(tinyxml2::XMLElement* root, const mjVFS* vfs = nullptr); // parse XML document void PrintSchema(std::stringstream& str, bool html, bool pad); // print text or HTML schema - void SetModelFileDir(std::string modelfiledir); - const std::string& ModelFileDir() const { return modelfiledir_; } + void SetModelFileDir(const std::string& modelfiledir); + const mujoco::user::FilePath& ModelFileDir() const { return modelfiledir_; } // setters for directory defaults - void SetAssetDir(std::string assetdir); - void SetMeshDir(std::string meshdir); - void SetTextureDir(std::string texturedir); + void SetAssetDir(const std::string& assetdir); + void SetMeshDir(const std::string& meshdir); + void SetTextureDir(const std::string& texturedir); // XML sections embedded in all formats static void Compiler(tinyxml2::XMLElement* section, mjSpec* spec); // compiler section @@ -48,16 +49,17 @@ class mjXReader : public mjXBase { private: // XML section specific to MJCF - void Default(tinyxml2::XMLElement* section, const mjsDefault* def); // default section + void Default(tinyxml2::XMLElement* section, const mjsDefault* def, + const mjVFS* vfs); // default section void Extension(tinyxml2::XMLElement* section); // extension section void Custom(tinyxml2::XMLElement* section); // custom section void Visual(tinyxml2::XMLElement* section); // visual section void Statistic(tinyxml2::XMLElement* section); // statistic section void Asset(tinyxml2::XMLElement* section, const mjVFS* vfs); // asset section void Body(tinyxml2::XMLElement* section, mjsBody* pbody, - mjsFrame* pframe); // body/world section + mjsFrame* pframe, const mjVFS* vfs); // body/world section void Contact(tinyxml2::XMLElement* section); // contact section - void Deformable(tinyxml2::XMLElement* section); // deformable section + void Deformable(tinyxml2::XMLElement* section, const mjVFS* vfs); // deformable section void Equality(tinyxml2::XMLElement* section); // equality section void Tendon(tinyxml2::XMLElement* section); // tendon section void Actuator(tinyxml2::XMLElement* section); // actuator section @@ -66,8 +68,8 @@ class mjXReader : public mjXBase { // single element parsers, used in defaults and main body void OneFlex(tinyxml2::XMLElement* elem, mjsFlex* pflex); - void OneMesh(tinyxml2::XMLElement* elem, mjsMesh* pmesh); - void OneSkin(tinyxml2::XMLElement* elem, mjsSkin* pskin); + void OneMesh(tinyxml2::XMLElement* elem, mjsMesh* pmesh, const mjVFS* vfs); + void OneSkin(tinyxml2::XMLElement* elem, mjsSkin* pskin, const mjVFS* vfs); void OneMaterial(tinyxml2::XMLElement* elem, mjsMaterial* pmaterial); void OneJoint(tinyxml2::XMLElement* elem, mjsJoint* pjoint); void OneGeom(tinyxml2::XMLElement* elem, mjsGeom* pgeom); @@ -79,23 +81,23 @@ class mjXReader : public mjXBase { void OneTendon(tinyxml2::XMLElement* elem, mjsTendon* ptendon); void OneActuator(tinyxml2::XMLElement* elem, mjsActuator* pactuator); void OneComposite(tinyxml2::XMLElement* elem, mjsBody* pbody, mjsDefault* def); - void OneFlexcomp(tinyxml2::XMLElement* elem, mjsBody* pbody); + void OneFlexcomp(tinyxml2::XMLElement* elem, mjsBody* pbody, const mjVFS* vfs); void OnePlugin(tinyxml2::XMLElement* elem, mjsPlugin* plugin); - mjXSchema schema; // schema used for validation - mjsDefault* GetClass(tinyxml2::XMLElement* section); // get default class name + mjXSchema schema; // schema used for validation + mjsDefault* GetClass(tinyxml2::XMLElement* section); // get default class name bool readingdefaults; // true while reading defaults // accessors for directory defaults - std::string AssetDir() const; - std::string MeshDir() const; - std::string TextureDir() const; + mujoco::user::FilePath AssetDir() const; + mujoco::user::FilePath MeshDir() const; + mujoco::user::FilePath TextureDir() const; - std::string modelfiledir_; - std::string assetdir_; - std::string meshdir_; - std::string texturedir_; + mujoco::user::FilePath modelfiledir_; + mujoco::user::FilePath assetdir_; + mujoco::user::FilePath meshdir_; + mujoco::user::FilePath texturedir_; }; // MJCF schema diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 5798ef7e..ceb5d836 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1001,6 +1001,7 @@ void mjXWriter::Option(XMLElement* root) { WRITEDSBL("sensor", mjDSBL_SENSOR) WRITEDSBL("midphase", mjDSBL_MIDPHASE) WRITEDSBL("eulerdamp", mjDSBL_EULERDAMP) + WRITEDSBL("autoreset", mjDSBL_AUTORESET) #undef WRITEDSBL #define WRITEENBL(NAME, MASK) \ diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index a0740cf0..cd2fe3ed 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -45,6 +45,7 @@ namespace { using tinyxml2::XMLAttribute; using tinyxml2::XMLElement; +using mujoco::user::FilePath; namespace mju = ::mujoco::util; @@ -74,6 +75,36 @@ static std::optional ParseInfOrNan(const std::string& s) { return std::nullopt; } +FilePath ResolveFilePath(XMLElement* e, const FilePath& filename, + const FilePath& dir, const mjVFS* vfs) { + std::string path = ""; + if (filename.IsAbs()) { + return filename; + } + + // TODO(kylebayes): We first look in the base model directory for files to + // remain backwards compatible. + FilePath fullname = dir + filename; + mjResource *resource = mju_openResource("", fullname.c_str(), vfs, + nullptr, 0); + if (resource != nullptr) { + mju_closeResource(resource); + return filename; + } + + XMLElement* parent = e->Parent()->ToElement(); + for (; parent; parent = parent->Parent()->ToElement()) { + if (!std::strcmp(parent->Value(), "include")) { + auto file_attr = mjXUtil::ReadAttrStr(parent, "dir", false); + if (file_attr.has_value()) { + path = file_attr.value(); + } + break; + } + } + return FilePath(path) + filename; +} + } // namespace @@ -155,36 +186,6 @@ XMLElement* NextSiblingElement(XMLElement* e, const char* name) { return nullptr; } -static std::string ResolveFilePath(XMLElement* e, std::string filename, - const std::string& dir) { - std::string path = ""; - if (mjuu_isabspath(filename)) { - return filename; - } - - // TODO(kylebayes): We first look in the base model directory for files to - // remain backwards compatible. - std::string full_filename = mjuu_combinePaths(dir, filename); - mjResource *resource = mju_openResource(full_filename.c_str(), nullptr, - nullptr, 0); - if (resource != nullptr) { - mju_closeResource(resource); - return filename; - } - - XMLElement* parent = e->Parent()->ToElement(); - for (; parent; parent = parent->Parent()->ToElement()) { - if (!std::strcmp(parent->Value(), "include")) { - auto file_attr = mjXUtil::ReadAttrStr(parent, "dir", false); - if (file_attr.has_value()) { - path = file_attr.value(); - } - break; - } - } - return mjuu_combinePaths(path, filename); -} - // constructor mjXSchema::mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow) { // set name and type @@ -621,14 +622,15 @@ mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, bool required) { } // if attribute is present, return attribute as a filename -std::optional -mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr, - const std::string& dir, bool required) { +std::optional +mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr, const mjVFS* vfs, + const FilePath& dir, bool required) { auto maybe_str = ReadAttrStr(elem, attr, required); if (!maybe_str.has_value()) { return std::nullopt; } - return ResolveFilePath(elem, maybe_str.value(), dir); + FilePath filename(maybe_str.value()); + return ResolveFilePath(elem, filename, dir, vfs); } // if attribute is present, return numerical value of attribute diff --git a/src/xml/xml_util.h b/src/xml/xml_util.h index f1799cb4..86d61948 100644 --- a/src/xml/xml_util.h +++ b/src/xml/xml_util.h @@ -26,6 +26,8 @@ #include "tinyxml2.h" +#include +#include "user/user_util.h" // error string copy void mjCopyError(char* dst, const char* src, int maxlen); @@ -101,26 +103,33 @@ class mjXUtil { // if attribute is present, return vector of numerical data template - static std::optional> ReadAttrVec(tinyxml2::XMLElement* elem, const char* attr, + static std::optional> ReadAttrVec(tinyxml2::XMLElement* elem, + const char* attr, bool required = false); // if attribute is present, return attribute as a string - static std::optional ReadAttrStr(tinyxml2::XMLElement* elem, const char* attr, + static std::optional ReadAttrStr(tinyxml2::XMLElement* elem, + const char* attr, bool required = false); // if attribute is present, return attribute as a filename - static std::optional ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr, - const std::string& dir = "", bool required = false); + static std::optional + ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr, + const mjVFS* vfs, + const mujoco::user::FilePath& dir = mujoco::user::FilePath(), + bool required = false); // if attribute is present, return numerical value of attribute template - static std::optional ReadAttrNum(tinyxml2::XMLElement* elem, const char* attr, + static std::optional ReadAttrNum(tinyxml2::XMLElement* elem, + const char* attr, bool required = false); // if attribute is present, return array of numerical data // N should be small as data is allocated on the stack template - static std::optional> ReadAttrArr(tinyxml2::XMLElement* elem, const char* attr, + static std::optional> ReadAttrArr(tinyxml2::XMLElement* elem, + const char* attr, bool required = false) { std::array arr; int n = 0; diff --git a/test/engine/engine_derivative_test.cc b/test/engine/engine_derivative_test.cc index 85ecf60a..9a7a776e 100644 --- a/test/engine/engine_derivative_test.cc +++ b/test/engine/engine_derivative_test.cc @@ -159,6 +159,70 @@ TEST_F(DerivativeTest, SmoothDvel) { } } +// disabled actuators do not contribute to d_qfrc_actuator/d_qvel +TEST_F(DerivativeTest, DisabledActuators) { + // model with only a position actuator + static constexpr char xml1[] = R"( + + + )"; + + mjModel* m1 = LoadModelFromString(xml1); + mjData* d1 = mj_makeData(m1); + + d1->ctrl[0] = 6; + while (d1->time < 1) + mj_step(m1, d1); + + // model with a position actuator and an intvelocity actuator + static constexpr char xml2[] = R"( + + + )"; + + mjModel* m2 = LoadModelFromString(xml2); + mjData* d2 = mj_makeData(m2); + + d2->ctrl[0] = 6; + d2->ctrl[1] = 6; + + while (d2->time < 1) + mj_step(m2, d2); + + // expect same qvel in both models + EXPECT_EQ(d1->qvel[0], d2->qvel[0]); + + mj_deleteData(d2); + mj_deleteModel(m2); + mj_deleteData(d1); + mj_deleteModel(m1); +} + // compare analytic and fin-diff d_qfrc_passive/d_qvel TEST_F(DerivativeTest, PassiveDvel) { for (const char* local_path : {kTumblingThinObjectPath, diff --git a/test/engine/testdata/core_smooth/tendon_wrap_cylinder.xml b/test/engine/testdata/core_smooth/tendon_wrap_cylinder.xml new file mode 100644 index 00000000..fa70af44 --- /dev/null +++ b/test/engine/testdata/core_smooth/tendon_wrap_cylinder.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/tendon_wrap_sphere.xml b/test/engine/testdata/core_smooth/tendon_wrap_sphere.xml new file mode 100644 index 00000000..2ddba3bb --- /dev/null +++ b/test/engine/testdata/core_smooth/tendon_wrap_sphere.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/user/user_cache_test.cc b/test/user/user_cache_test.cc index 5b353eac..f226827b 100644 --- a/test/user/user_cache_test.cc +++ b/test/user/user_cache_test.cc @@ -47,7 +47,7 @@ void CacheText(mjCCache& cache, const std::string& model, mjVFS vfs; mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, name.c_str(), text.data(), text.size()); - mjResource* resource = mju_openResource(name.c_str(), &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", name.c_str(), &vfs, nullptr, 0); std::shared_ptr data(&text, +[](const void* data) {}); cache.Insert(model, resource, data, text.size()); mju_closeResource(resource); @@ -61,7 +61,7 @@ GetCachedText(mjCCache& cache, const std::string& model, mjVFS vfs; mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, name.c_str(), text.data(), std::strlen(text.c_str())); - mjResource* resource = mju_openResource(name.c_str(), &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", name.c_str(), &vfs, nullptr, 0); bool inserted = cache.PopulateData(resource, [&cached_text](const void* data) { cached_text = *(static_cast(data)); diff --git a/test/user/user_resource_test.cc b/test/user/user_resource_test.cc index 3f243641..357e75e4 100644 --- a/test/user/user_resource_test.cc +++ b/test/user/user_resource_test.cc @@ -233,7 +233,7 @@ TEST_F(ResourceTest, GeneralTest) { EXPECT_GT(i, 0); // open resource - mjResource* resource = mju_openResource("str:file", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "str:file", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); const char* buffer = NULL; @@ -256,7 +256,7 @@ TEST_F(ResourceTest, GeneralFailureTest) { static std::array error; // open resource - mjResource* resource = mju_openResource("str:notfound", nullptr, + mjResource* resource = mju_openResource("", "str:notfound", nullptr, error.data(), error.size()); ASSERT_THAT(resource, IsNull()); @@ -281,7 +281,7 @@ TEST_F(ResourceTest, NameWithValidPrefix) { }; // open resource - mjResource* resource = mju_openResource("nop:found", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "nop:found", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); mju_closeResource(resource); } @@ -304,7 +304,7 @@ TEST_F(ResourceTest, NameWithUpperCasePrefix) { }; // open resource - mjResource* resource = mju_openResource("NOP:found", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "NOP:found", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); mju_closeResource(resource); } @@ -327,7 +327,7 @@ TEST_F(ResourceTest, NameWithInvalidPrefix) { }; // open resource - mjResource* resource = mju_openResource("nopfound", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "nopfound", nullptr, nullptr, 0); ASSERT_THAT(resource, IsNull()); } @@ -338,7 +338,7 @@ TEST_F(ResourceTest, OSFilesystemTimestamps) { const char* const file = "engine/testdata/collision_box/boxbox_deep.xml"; const std::string xml_path = GetTestDataFilePath(file); - mjResource* resource = mju_openResource(xml_path.c_str(), nullptr, + mjResource* resource = mju_openResource("", xml_path.c_str(), nullptr, nullptr, 0); mju_decodeBase64((uint8_t*) &t, resource->timestamp); diff --git a/test/user/user_util_test.cc b/test/user/user_util_test.cc index 02b30c08..9873c74b 100644 --- a/test/user/user_util_test.cc +++ b/test/user/user_util_test.cc @@ -46,6 +46,11 @@ TEST_F(UserUtilTest, PathReduce2) { EXPECT_EQ(path.Str(), "../hello/world/"); } +TEST_F(UserUtilTest, PathReduce3) { + FilePath path = FilePath("../../hello/world.txt"); + EXPECT_EQ(path.Str(), "../../hello/world.txt"); +} + TEST_F(UserUtilTest, PathReduceWin) { FilePath path = FilePath("C:\\hello\\..\\world"); EXPECT_EQ(path.Str(), "C:\\world"); diff --git a/test/user/user_vfs_test.cc b/test/user/user_vfs_test.cc index 36378130..2210c8d6 100644 --- a/test/user/user_vfs_test.cc +++ b/test/user/user_vfs_test.cc @@ -30,7 +30,7 @@ using ::testing::NotNull; using UserVfsTest = MujocoTest; static bool HasFile(const mjVFS* vfs, const std::string& filename) { - mjResource* resource = mju_openResource(filename.c_str(), vfs, nullptr, 0); + mjResource* resource = mju_openResource("", filename.c_str(), vfs, nullptr, 0); bool result = resource != nullptr; mju_closeResource(resource); return result; @@ -196,14 +196,13 @@ TEST_F(UserVfsTest, AddBufferRepeat) { mj_deleteVFS(&vfs); } -TEST_F(UserVfsTest, BufferStripPath) { +TEST_F(UserVfsTest, BufferPath) { mjVFS vfs; mj_defaultVFS(&vfs); std::string buffer = ""; const void* ptr = static_cast(buffer.c_str()); mj_addBufferVFS(&vfs, "dir/model", ptr, buffer.size()); - EXPECT_TRUE(HasFile(&vfs, "MODEL")); - EXPECT_TRUE(HasFile(&vfs, "dir\\model")); + EXPECT_TRUE(HasFile(&vfs, "files/../dir/model")); mj_deleteVFS(&vfs); } @@ -222,7 +221,7 @@ TEST_F(UserVfsTest, Timestamps) { mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, "cube.obj", cube, sizeof(cube)); - mjResource* resource = mju_openResource("cube.obj", &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", "cube.obj", &vfs, nullptr, 0); // same timestamps EXPECT_EQ(mju_isModifiedResource(resource, resource->timestamp), 0); diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 817ab79b..674f3578 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -15,7 +15,6 @@ // Tests for xml/xml_native_reader.cc. #include -#include #include #include #include @@ -613,6 +612,18 @@ static constexpr unsigned char kTinyPng[] = { 0x82 }; +// mesh OBJ file of a cube +static constexpr char kTinyObj[] = 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)"; + + TEST_F(XMLReaderTest, IncludeTest) { static constexpr char xml[] = R"( @@ -649,7 +660,7 @@ TEST_F(XMLReaderTest, IncludeTest) { std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size(), vfs.get()); - ASSERT_THAT(model, NotNull()); + ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); mj_deleteModel(model); @@ -726,22 +737,22 @@ TEST_F(XMLReaderTest, IncludePathTest) { )"; - MockFilesystem fs("IncludePathTest"); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); + mj_addBufferVFS(&vfs, "submodels/model1.xml", xml1, sizeof(xml1)); + mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2)); + mj_addBufferVFS(&vfs, "submodels/subsubmodels/model3.xml", xml3, + sizeof(xml3)); - fs.ChangeDirectory("submodels/"); - fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); - fs.AddFile("model2.xml", (const unsigned char*) xml2, sizeof(xml2)); - fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, sizeof(xml3)); - fs.ChangeDirectory("/"); - - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, - nullptr, 0); - ASSERT_THAT(model, NotNull()); + std::array error; + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), + error.size()); + ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, FallbackIncludePathTest) { @@ -770,22 +781,21 @@ TEST_F(XMLReaderTest, FallbackIncludePathTest) { )"; - MockFilesystem fs("FallbackIncludePathTest"); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); - - fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); - fs.AddFile("submodels/model2.xml", (const unsigned char*) xml2, sizeof(xml2)); - fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, - sizeof(xml3)); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); + mj_addBufferVFS(&vfs, "model1.xml", xml1, sizeof(xml1)); + mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2)); + mj_addBufferVFS(&vfs, "subsubmodels/model3.xml", xml3, sizeof(xml3)); std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, MaterialTextureTest) { @@ -807,14 +817,14 @@ TEST_F(XMLReaderTest, MaterialTextureTest) { )"; - MockFilesystem fs("MaterialTextureTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); char error[1024]; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error, 1024); + mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024); EXPECT_THAT(model, NotNull()) << error; EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1); @@ -823,6 +833,7 @@ TEST_F(XMLReaderTest, MaterialTextureTest) { EXPECT_EQ(model->mat_texid[mjTEXROLE_OCCLUSION], 0); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, LegacyMaterialTextureTest) { @@ -839,19 +850,20 @@ TEST_F(XMLReaderTest, LegacyMaterialTextureTest) { )"; - MockFilesystem fs("LegacyMaterialTextureTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); char error[1024]; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error, 1024); + mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024); EXPECT_THAT(model, NotNull()) << error; EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, MaterialTextureFailTest) { @@ -871,12 +883,6 @@ TEST_F(XMLReaderTest, MaterialTextureFailTest) { )"; - MockFilesystem fs("MaterialTextureFailTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); - std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, IsNull()); @@ -905,26 +911,32 @@ TEST_F(XMLReaderTest, IncludeAssetsTest) { static constexpr char subassets[] = R"( + )"; - MockFilesystem fs("IncludeAssetsTest"); - fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/subassets/subtiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/assets.xml", (const unsigned char*) assets, - sizeof(assets)); - fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, - sizeof(subassets)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "assets/subassets/subtiny.png", kTinyPng, + sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "assets/subassets/cube.obj", kTinyObj, + sizeof(kTinyObj)); + mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets)); + mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets, + sizeof(subassets)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); // loading the file should be successful - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, nullptr, 0); + std::array error; + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), + error.size()); - EXPECT_THAT(model, NotNull()); + ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) { @@ -952,26 +964,26 @@ TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) { )"; - MockFilesystem fs("FallbackIncludeAssetsTest"); - fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng)); // need to fallback for backwards compatibility - fs.AddFile("subtiny.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "subtiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/assets.xml", (const unsigned char*) assets, - sizeof(assets)); - fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, - sizeof(subassets)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets)); + mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets, + sizeof(subassets)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); // loading the file should be successful std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); - EXPECT_THAT(model, NotNull()); + ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, IncludeAbsoluteTest) { @@ -1013,7 +1025,7 @@ TEST_F(XMLReaderTest, IncludeAbsoluteTest) { // loading the file should be successful mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error.data(), error.size()); - EXPECT_THAT(model, NotNull()); + ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); } @@ -1054,7 +1066,7 @@ TEST_F(XMLReaderTest, IncludeAbsoluteMeshDirTest) { // loading the file should be successful mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error.data(), error.size()); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 46f89ab6..b5563b2e 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -157,7 +157,7 @@ public enum mjtDisableBit : int{ mjDSBL_SENSOR = 4096, mjDSBL_MIDPHASE = 8192, mjDSBL_EULERDAMP = 16384, - mjDSBL_AUTORESETNAN = 32768, + mjDSBL_AUTORESET = 32768, mjNDISABLE = 16, } public enum mjtEnableBit : int{