From 8051f408176163652cc48430ddd6bbf821e1e33a Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Thu, 6 Nov 2025 07:05:11 -0800 Subject: [PATCH] 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 --- src/user/user_api.cc | 31 ++++++++++++++++++++++++++++- test/plugin/decoder/decoder_test.cc | 31 ++++++++++++----------------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 8393e101..8a327def 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -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); diff --git a/test/plugin/decoder/decoder_test.cc b/test/plugin/decoder/decoder_test.cc index f7076dd8..581b0db2 100644 --- a/test/plugin/decoder/decoder_test.cc +++ b/test/plugin/decoder/decoder_test.cc @@ -78,49 +78,44 @@ TEST_F(DecoderPluginTest, CanDecode) { )"; - 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