During mj_parse, if mju_openResource fails pass a resource with only the filename to decoders.

This enables decoders to leverage their own file resolution systems. For example: USD can compose a stage (scene) from memory, via a file on disk, or via custom asset resolver.

Passing it through to the decoder reduces the need for custom resource providers that wrap these other resolution systems.

This will also enable a fun, but likely niche usecase where the decoder does procedural generation and does not have to be backed by a resource. See the tests for an example of this.

PiperOrigin-RevId: 828939899
Change-Id: Ic319a685332d91198a4ae926867c834bf7f7f724
This commit is contained in:
Sam Haves
2025-11-06 07:05:11 -08:00
committed by Copybara-Service
parent 89fec8f7b2
commit 8051f40817
2 changed files with 43 additions and 19 deletions
+13 -18
View File
@@ -78,49 +78,44 @@ TEST_F(DecoderPluginTest, CanDecode) {
</worldbody>
</mujoco>
)";
char error[1024];
// create VFS with the XML model and a dummy mesh
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "model.xml", xml, strlen(xml));
mj_addBufferVFS(&vfs, "dummy.fakeformat", "0 1 2", strlen("0 1 2"));
mj_addBufferVFS(&vfs, "dummy.alsoFakeFormat", "0 1 2", strlen("0 1 2"));
// Check referencing a resource via XML invokes the decoder.
mjModel* model = mj_loadXML("model.xml", &vfs, error, sizeof(error));
char error[1024];
mjSpec* spec =
mj_parseXMLString(xml, nullptr, error, sizeof(error));
mjModel* model = mj_compile(spec, nullptr);
ASSERT_THAT(model, testing::NotNull()) << error;
EXPECT_EQ(model->nbody, 2); // world + included body
EXPECT_EQ(model->ngeom, 1);
mj_deleteModel(model);
mj_deleteSpec(spec);
// Check mj_parse with extension .fakeformat
mjSpec* spec =
mj_parse("dummy.fakeformat", nullptr, &vfs, error, sizeof(error));
model = mj_compile(spec, &vfs);
spec =
mj_parse("dummy.fakeformat", nullptr, nullptr, error, sizeof(error));
model = mj_compile(spec, nullptr);
EXPECT_EQ(model->nbody, 2); // world + included body
EXPECT_EQ(model->ngeom, 1);
mj_deleteModel(model);
mj_deleteSpec(spec);
// Check mj_parse with extension .alsoFakeFormat
spec = mj_parse("dummy.alsoFakeFormat", nullptr, &vfs, error, sizeof(error));
model = mj_compile(spec, &vfs);
spec =
mj_parse("dummy.alsoFakeFormat", nullptr, nullptr, error, sizeof(error));
model = mj_compile(spec, nullptr);
EXPECT_EQ(model->nbody, 2); // world + included body
EXPECT_EQ(model->ngeom, 1);
mj_deleteModel(model);
mj_deleteSpec(spec);
// Check mj_parse with content_type
spec = mj_parse("dummy.fakeformat", "model/fakeformat", &vfs, error,
spec = mj_parse("dummy.fakeformat", "model/fakeformat", nullptr, error,
sizeof(error));
model = mj_compile(spec, &vfs);
model = mj_compile(spec, nullptr);
EXPECT_EQ(model->nbody, 2); // world + included body
EXPECT_EQ(model->ngeom, 1);
mj_deleteModel(model);
mj_deleteSpec(spec);
mj_deleteVFS(&vfs);
}
} // namespace