From 55e7ad7fa77eea33ccdf75cb1ff43554b9c4fdad Mon Sep 17 00:00:00 2001 From: Roberto Meroni Date: Tue, 11 Aug 2026 12:04:03 +0200 Subject: [PATCH] Do not set geom_dataid for primitives fitted to a mesh. A geom whose type is a primitive but which carries a mesh= attribute is fitted to that mesh, and XMLreference states that the compiled mjModel holds "no reference to the mesh used for fitting". mjCGeom::Compile clears meshname_ to that end, but IndexAssets runs afterwards and re-resolves geom->mesh from spec_meshname_, which is untouched, so CopyTree stored the mesh id anyway. The stale id defeats the geom_dataid == -1 early-out in mjc_PlaneConvex, which then walks the fitting mesh and can emit up to 3 contacts into a buffer that mj_maxContact sized at 1 for a plane-ellipsoid pair: SIGSEGV, or the driver's own contact-count check firing. Saving and reloading the same model produced a correct geom_dataid of -1, so the two paths disagreed. Gate the assignment on the geom type, matching the guard used elsewhere in the compiler (user_model.cc:5034). Fitted primitives now also render as primitives: texture repeat is no longer divided by geom size, and mjVIS_MESHBVH no longer draws the fitting mesh's bounding volumes on them. --- src/user/user_model.cc | 3 +- test/engine/engine_collision_convex_test.cc | 27 ++++++++++++++++ test/user/user_objects_test.cc | 34 +++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 9ff5362adce..fbfd57712e5 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2791,7 +2791,8 @@ void mjCModel::CopyTree(mjModel* m) { m->geom_conaffinity[gid] = pg->conaffinity; m->geom_condim[gid] = pg->condim; m->geom_bodyid[gid] = pg->body->id; - if (pg->mesh) { + // a primitive fitted to a mesh keeps pg->mesh, but must not reference it in mjModel + if (pg->mesh && (pg->type == mjGEOM_MESH || pg->type == mjGEOM_SDF)) { m->geom_dataid[gid] = pg->mesh->id; } else if (pg->hfield) { m->geom_dataid[gid] = pg->hfield->id; diff --git a/test/engine/engine_collision_convex_test.cc b/test/engine/engine_collision_convex_test.cc index 7d4346e5e35..3ee6615770c 100644 --- a/test/engine/engine_collision_convex_test.cc +++ b/test/engine/engine_collision_convex_test.cc @@ -76,5 +76,32 @@ TEST_F(MjcConvexTest, CylinderBox) { mj_deleteModel(model); } +TEST_F(MjcConvexTest, PlaneFittedEllipsoid) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + )"; + char error[1024]; + MjModelPtr model = LoadModelFromString(xml, error, sizeof(error)); + ASSERT_THAT(model.get(), NotNull()) << error; + MjDataPtr data = MakeData(model); + + // plane vs ellipsoid is a single contact, whether or not it was mesh-fitted + mj_forward(model.get(), data.get()); + EXPECT_EQ(data->ncon, 1); +} + } // namespace } // namespace mujoco diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 857847240a3..1c6e19620a0 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -1153,6 +1153,40 @@ TEST_F(MjCGeomTest, BadMeshZeroMassDensityDoesntError) { EXPECT_EQ(model->body_mass[2], 0); } +TEST_F(MjCGeomTest, FittedPrimitiveHasNoMeshDataid) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + )"; + std::array error; + MjModelPtr model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model.get(), NotNull()) << error.data(); + ASSERT_EQ(model->ngeom, 6); + + // fitted primitives must not reference the mesh they were fitted to + for (int i = 0; i < 5; i++) { + EXPECT_NE(model->geom_type[i], mjGEOM_MESH); + EXPECT_EQ(model->geom_dataid[i], -1) << "geom " << i; + } + + // an actual mesh geom still references its mesh + EXPECT_EQ(model->geom_type[5], mjGEOM_MESH); + EXPECT_EQ(model->geom_dataid[5], 0); +} + // ------------- test joints -------------------------------------------------- using MjCJointTest = MujocoTest;