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
+30 -1
View File
@@ -74,8 +74,37 @@ mjSpec* mj_parse(const char* filename, const char* content_type,
}
mjResource* resource = mju_openResource("", filename, vfs, error, error_sz);
// If we are unable to open the resource, we will create our own resource with
// just the filename. This allows decoders that rely on other systems to fetch
// their content to function without a custom resource provider.
// For example, USD may use identifiers to assets that are strictly in memory
// or that are fetched on a need-be basis via URI.
if (!resource) {
mju_error("Could not load resource %s", filename);
resource = (mjResource*) mju_malloc(sizeof(mjResource));
if (resource == nullptr) {
if (error) {
strncpy(error, "could not allocate memory", error_sz);
error[error_sz - 1] = '\0';
}
return nullptr;
}
// clear out resource
memset(resource, 0, sizeof(mjResource));
// make space for filename
std::string fullname = filename;
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", error_sz);
error[error_sz - 1] = '\0';
}
mju_closeResource(resource);
return nullptr;
}
memcpy(resource->name, fullname.c_str(), sizeof(char) * (n + 1));
}
mjSpec* spec = mju_decodeResource(resource, content_type);
+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