Create mjpDecoder, mj_parse, and mju_decodeResource

- plugin system similar to mjpResourceProvider, but instead of loading a resource it converts an existing mjResource into an mjSpec.
  - The returned spec is then composed into the referencing spec.
  - This enables different file types to generate arbitrary specs, and allows us to separate format parsing from compilation code.

Follow up CLs will move some of the logic in src/engine for PNG, USD, KTX, OBJ loading into decoders.

The mj_parse function MjSpec from a given file, it's a more generic version of mj_parseXML.

In it's implementation, mj_parse as opposed to mj_parseXML will look for any registered decoder and
not assume we are striclty dealing with MJCF.

PiperOrigin-RevId: 826149497
Change-Id: I0ece26904280cb94bd5ded6dd5a565c539d60254
This commit is contained in:
Sam Haves
2025-10-30 12:53:40 -07:00
committed by Copybara-Service
parent f2badc05ac
commit 57f7145806
16 changed files with 2063 additions and 1555 deletions
+147
View File
@@ -25,6 +25,7 @@
#include <cstring>
#include <memory>
#include <new>
#include <sstream>
#include <string>
#include <string_view>
@@ -62,6 +63,16 @@ int strklen(const char* s) {
return -1;
}
// return filename extension
std::string getext(std::string_view filename) {
size_t dot = filename.find_last_of('.');
if (dot == std::string::npos) {
return "";
}
return std::string(filename.substr(dot, filename.size() - dot));
}
// copy a null-terminated string into a new heap-allocated char array managed by a unique_ptr
std::unique_ptr<char[]> CopyName(const char* s) {
int len = strklen(s);
@@ -265,6 +276,77 @@ bool GlobalTable<mjpResourceProvider>::CopyObject(mjpResourceProvider& dst, cons
return true;
}
template <>
const char* GlobalTable<mjpDecoder>::HumanReadableTypeName() {
return "resource decoder";
}
template <>
std::string_view GlobalTable<mjpDecoder>::ObjectKey(const mjpDecoder& decoder) {
// When registering decoders, if the user provides both a content type and an extension we add two
// entries to the table. One with content_type set and extension unset, and one with the opposite.
// This means that within a vector, we will only ever have either content_type or extension.
if (decoder.content_type) {
if (int len = strklen(decoder.content_type); len != -1) {
return std::string_view(decoder.content_type, len);
}
}
return std::string_view(decoder.extension, strklen(decoder.extension));
}
// return true if two resource providers are identical
template <>
bool GlobalTable<mjpDecoder>::ObjectEqual(const mjpDecoder& d1, const mjpDecoder& d2) {
// check if two resource providers are identical
if (!(CaseInsensitiveEqual(d1.content_type, d2.content_type) &&
CaseInsensitiveEqual(d1.extension, d2.extension) &&
d1.decode == d2.decode &&
d1.can_decode == d2.can_decode)) {
return false;
}
return true;
}
template <>
bool GlobalTable<mjpDecoder>::CopyObject(mjpDecoder& dst, const mjpDecoder& src, ErrorMessage& err) {
// Just a list of pointers so copy directly.
dst = src;
dst.content_type = nullptr;
dst.extension = nullptr;
if (src.content_type) {
std::unique_ptr<char[]> content_type = CopyName(src.content_type);
if (!content_type) {
if (strklen(src.content_type) == -1) {
std::snprintf(err, sizeof(err),
"decoder->content_type length exceeds the maximum limit of %d", kMaxNameLength);
} else {
std::snprintf(err, sizeof(err), "failed to allocate memory for decoder content_type");
}
return false;
}
dst.content_type = content_type.release();
}
if (src.extension) {
std::unique_ptr<char[]> extension = CopyName(src.extension);
if (!extension) {
if (strklen(src.extension) == -1) {
std::snprintf(err, sizeof(err),
"decoder->extension length exceeds the maximum limit of %d", kMaxNameLength);
} else {
std::snprintf(err, sizeof(err), "failed to allocate memory for decoder extension");
}
return false;
}
dst.extension = extension.release();
}
return true;
}
// globally register a plugin (thread-safe), return new slot id
int mjp_registerPlugin(const mjpPlugin* plugin) {
if (!plugin->name) {
@@ -382,6 +464,71 @@ const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot) {
return GlobalTable<mjpResourceProvider>::GetSingleton().GetAtSlot(slot - 1);
}
// register a resource decoder
void mjp_registerDecoder(const mjpDecoder* decoder) {
if (!decoder->decode || !decoder->can_decode) {
mju_warning("decoder must provide decode and can_decode callbacks.");
return;
}
if (!decoder->content_type && !decoder->extension) {
mju_warning("decoder must provide content_type and/or extensions.");
return;
}
mjpDecoder decoder_copy = *decoder;
// Register with content_type
if (decoder->content_type) {
decoder_copy.extension = nullptr;
GlobalTable<mjpDecoder>::GetSingleton().AppendIfUnique(decoder_copy);
}
// Register with extensions
if (decoder->extension) {
decoder_copy.content_type = nullptr;
std::string extensions_str(decoder->extension);
std::stringstream ss(extensions_str);
std::string extension;
while (std::getline(ss, extension, '|')) {
if (!extension.empty()) {
decoder_copy.extension = extension.c_str();
GlobalTable<mjpDecoder>::GetSingleton().AppendIfUnique(decoder_copy);
}
}
}
}
// set default resource decoder definition
void mjp_defaultDecoder(mjpDecoder* decoder) {
std::memset(decoder, 0, sizeof(*decoder));
}
// find a decoder that can process a given resource and content_type
const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type) {
auto extension = getext(resource->name);
if (strklen(content_type) == -1 && extension.empty()) {
mju_warning("Must provide extension or content_type to mjp_findDecoder.");
return nullptr;
}
if (strklen(content_type) > 0) {
auto* decoder = GlobalTable<mjpDecoder>::GetSingleton().GetByKey(content_type, nullptr);
if (decoder && decoder->can_decode(resource)) {
return decoder;
}
}
if (!extension.empty()) {
auto* decoder = GlobalTable<mjpDecoder>::GetSingleton().GetByKey(extension.c_str(), nullptr);
if (decoder && decoder->can_decode(resource)) {
return decoder;
}
}
return nullptr;
}
// load plugins from a dynamic library
void mj_loadPluginLibrary(const char* path) {
#if defined(_WIN32) || defined(__CYGWIN__)
+9
View File
@@ -62,6 +62,15 @@ MJAPI void mj_loadPluginLibrary(const char* path);
// scan a directory and load all dynamic libraries
MJAPI void mj_loadAllPluginLibraries(const char* directory, mjfPluginLibraryLoadCallback callback);
// registers a resource decoder
MJAPI void mjp_registerDecoder(const mjpDecoder* decoder);
// set default decoder definition
MJAPI void mjp_defaultDecoder(mjpDecoder* decoder);
// find a decoder that can process a given resource
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
// =================================================================================================
// MuJoCo-internal functions beyond this point.
// "Unsafe" suffix indicates that improper use of these functions may result in data races.
+18
View File
@@ -31,6 +31,7 @@
#include "user/user_cache.h"
#include "user/user_model.h"
#include "user/user_objects.h"
#include "user/user_resource.h"
#include "user/user_util.h"
namespace {
@@ -63,7 +64,24 @@ mjSpec* mj_copySpec(const mjSpec* s) {
return &modelC->spec;
}
// parse file into spec
mjSpec* mj_parse(const char* filename, const char* content_type,
const mjVFS* vfs, char* error, int error_sz) {
// early exit for existing XML workflow
auto filepath = mujoco::user::FilePath(filename);
if (filepath.Ext() == ".xml" || (content_type && std::strcmp(content_type, "text/xml") == 0)) {
return mj_parseXML(filename, vfs, error, error_sz);
}
mjResource* resource = mju_openResource("", filename, vfs, error, error_sz);
if (!resource) {
mju_error("Could not load resource %s", filename);
}
mjSpec* spec = mju_decodeResource(resource, content_type);
mju_closeResource(resource);
return spec;
}
// compile model
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) {
+17
View File
@@ -24,7 +24,9 @@
#include <cstring>
#include <ctime>
#include <string>
#include <string_view>
#include <vector>
#include <mujoco/mujoco.h>
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
#include <unistd.h>
@@ -262,3 +264,18 @@ int mju_isModifiedResource(const mjResource* resource, const char* timestamp) {
// fallback to OS filesystem
return FileModified(resource, timestamp);
}
mjSpec* mju_decodeResource(mjResource* resource, const char* content_type) {
const mjpDecoder* decoder = nullptr;
if (content_type) {
decoder = mjp_findDecoder(resource, content_type);
} else {
decoder = mjp_findDecoder(resource, mjuu_extToContentType(resource->name).c_str());
}
if (!decoder) {
mju_error("Could not find decoder for resource '%s'", resource->name);
}
return decoder->decode(resource);
}
+4
View File
@@ -46,6 +46,10 @@ MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir)
// return < 0 if the resource is older than the given timestamp
MJAPI int mju_isModifiedResource(const mjResource* resource, const char* timestamp);
// given a resource, find its decoder and return the decoded spec
// the caller takes ownership of the spec and is responsible for cleaning it up
MJAPI mjSpec* mju_decodeResource(mjResource* resource, const char* content_type);
#ifdef __cplusplus
}
#endif
+8 -9
View File
@@ -3404,25 +3404,24 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) {
// model sub-element
else if (name == "model") {
string content_type;
if (!ReadAttrTxt(elem, "content_type", content_type)) {
content_type = "text/xml";
}
std::string content_type;
ReadAttrTxt(elem, "content_type", content_type);
// parse the child
mjSpec* child = nullptr;
std::array<char, 1024> error;
auto filename = modelfiledir_ + ReadAttrFile(elem, "file", vfs).value();
if (content_type == "text/xml") {
child = mj_parseXML(filename.c_str(), vfs, error.data(), error.size());
#ifdef mjUSEUSD
} else if (content_type == "text/usd") {
if (content_type == "text/usd") {
child = mj_parseUSD(filename.c_str(), vfs, error.data(), error.size());
#endif // mjUSEUSD
} else {
throw mjXError(elem, "unsupported content_type: %s", content_type.c_str());
#endif // mjUSEUSD
child = mj_parse(filename.c_str(), content_type.c_str(), vfs,
error.data(), error.size());
#ifdef mjUSEUSD
}
#endif // mjUSEUSD
if (!child) {
throw mjXError(elem, "could not parse model file with error: %s", error.data());