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:
committed by
Copybara-Service
parent
f2badc05ac
commit
57f7145806
@@ -48,6 +48,17 @@ Parse spec from XML string.
|
||||
|
||||
*Nullable:* ``vfs``, ``error``
|
||||
|
||||
.. _mj_parse:
|
||||
|
||||
`mj_parse <#mj_parse>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mj_parse
|
||||
|
||||
Parse spec from a file.
|
||||
|
||||
*Nullable:* ``vfs``, ``error``
|
||||
|
||||
.. _mj_compile:
|
||||
|
||||
`mj_compile <#mj_compile>`__
|
||||
@@ -2926,6 +2937,39 @@ Look up a resource provider by slot number returned by mjp_registerResourceProvi
|
||||
|
||||
If invalid slot number, return NULL.
|
||||
|
||||
.. _mjp_registerDecoder:
|
||||
|
||||
`mjp_registerDecoder <#mjp_registerDecoder>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mjp_registerDecoder
|
||||
|
||||
Globally register a decoder. This function is thread-safe.
|
||||
|
||||
If an identical mjpDecoder is already registered, this function does nothing.
|
||||
|
||||
If a non-identical mjpDecoder with the same name is already registered, an mju_error is raised.
|
||||
|
||||
.. _mjp_defaultDecoder:
|
||||
|
||||
`mjp_defaultDecoder <#mjp_defaultDecoder>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mjp_defaultDecoder
|
||||
|
||||
Set default resource decoder definition.
|
||||
|
||||
.. _mjp_findDecoder:
|
||||
|
||||
`mjp_findDecoder <#mjp_findDecoder>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mjp_findDecoder
|
||||
|
||||
Return the resource provider with the prefix that matches against the resource name.
|
||||
|
||||
If no match, return NULL.
|
||||
|
||||
.. _Thread:
|
||||
|
||||
Threads
|
||||
|
||||
@@ -1563,6 +1563,16 @@ struct mjpResourceProvider {
|
||||
void* data; // opaque data pointer (resource invariant)
|
||||
};
|
||||
typedef struct mjpResourceProvider mjpResourceProvider;
|
||||
struct mjpDecoder {
|
||||
const char* content_type;
|
||||
const char* extension;
|
||||
// user-facing functions
|
||||
mjfCanDecode can_decode; // quickly check if this decoder can handle the resource
|
||||
mjfDecode decode; // main decoding function
|
||||
// the caller takes ownership of the spec returned by decode and is responsible
|
||||
// for cleaning it up
|
||||
};
|
||||
typedef struct mjpDecoder mjpDecoder;
|
||||
typedef enum mjtPluginCapabilityBit_ {
|
||||
mjPLUGIN_ACTUATOR = 1<<0, // actuator forces
|
||||
mjPLUGIN_SENSOR = 1<<1, // sensor measurements
|
||||
@@ -3014,6 +3024,8 @@ void mj_clearCache(mjCache* cache);
|
||||
mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, char* error, int error_sz);
|
||||
mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz);
|
||||
mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int error_sz);
|
||||
mjSpec* mj_parse(const char* filename, const char* content_type,
|
||||
const mjVFS* vfs, char* error, int error_sz);
|
||||
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
||||
int mj_copyBack(mjSpec* s, const mjModel* m);
|
||||
int mj_recompile(mjSpec* s, const mjVFS* vfs, mjModel* m, mjData* d);
|
||||
@@ -3431,6 +3443,9 @@ int mjp_registerResourceProvider(const mjpResourceProvider* provider);
|
||||
int mjp_resourceProviderCount(void);
|
||||
const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name);
|
||||
const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot);
|
||||
void mjp_registerDecoder(const mjpDecoder* decoder);
|
||||
void mjp_defaultDecoder(mjpDecoder* decoder);
|
||||
const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
||||
mjThreadPool* mju_threadPoolCreate(size_t number_of_threads);
|
||||
void mju_bindThreadPool(mjData* d, void* thread_pool);
|
||||
void mju_threadPoolEnqueue(mjThreadPool* thread_pool, mjTask* task);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <mujoco/mjdata.h>
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mjspec.h>
|
||||
#include <mujoco/mjtnum.h>
|
||||
#include <mujoco/mjvisualize.h>
|
||||
|
||||
@@ -64,6 +65,25 @@ struct mjpResourceProvider {
|
||||
};
|
||||
typedef struct mjpResourceProvider mjpResourceProvider;
|
||||
|
||||
//---------------------------------- Decoder -------------------------------------------------------
|
||||
|
||||
// function pointer types
|
||||
// return an mjSpec representing the decoded resource.
|
||||
typedef mjSpec* (*mjfDecode)(const mjResource* resource);
|
||||
// return true if the given resource can be decoded.
|
||||
typedef int (*mjfCanDecode)(const mjResource* resource);
|
||||
|
||||
// the struct defining the decoder plugin's interface
|
||||
struct mjpDecoder {
|
||||
const char* content_type;
|
||||
const char* extension;
|
||||
// user-facing functions
|
||||
mjfCanDecode can_decode; // quickly check if this decoder can handle the resource
|
||||
mjfDecode decode; // main decoding function
|
||||
// the caller takes ownership of the spec returned by decode and is responsible
|
||||
// for cleaning it up
|
||||
};
|
||||
typedef struct mjpDecoder mjpDecoder;
|
||||
|
||||
//---------------------------------- Plugins -------------------------------------------------------
|
||||
|
||||
|
||||
@@ -123,6 +123,11 @@ MJAPI mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, i
|
||||
// Nullable: vfs, error
|
||||
MJAPI mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int error_sz);
|
||||
|
||||
// Parse spec from a file.
|
||||
// Nullable: vfs, error
|
||||
MJAPI mjSpec* mj_parse(const char* filename, const char* content_type,
|
||||
const mjVFS* vfs, char* error, int error_sz);
|
||||
|
||||
// Compile spec to model.
|
||||
// Nullable: vfs
|
||||
MJAPI mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
||||
@@ -1449,6 +1454,17 @@ MJAPI const mjpResourceProvider* mjp_getResourceProvider(const char* resource_na
|
||||
// If invalid slot number, return NULL.
|
||||
MJAPI const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot);
|
||||
|
||||
// Globally register a decoder. This function is thread-safe.
|
||||
// If an identical mjpDecoder is already registered, this function does nothing.
|
||||
// If a non-identical mjpDecoder with the same name is already registered, an mju_error is raised.
|
||||
MJAPI void mjp_registerDecoder(const mjpDecoder* decoder);
|
||||
|
||||
// Set default resource decoder definition.
|
||||
MJAPI void mjp_defaultDecoder(mjpDecoder* decoder);
|
||||
|
||||
// Return the resource provider with the prefix that matches against the resource name.
|
||||
// If no match, return NULL.
|
||||
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
||||
|
||||
//---------------------------------- Threads -------------------------------------------------------
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ ClangJsonNode = Mapping[str, Any]
|
||||
|
||||
_ANONYMOUS_KEY_PATTERN = re.compile(r'\d+:\d+(?=\))')
|
||||
_EXCLUDED = (
|
||||
'mjpDecoder',
|
||||
'mjpDecoder_',
|
||||
'mjpPlugin',
|
||||
'mjpPlugin_',
|
||||
'mjpResourceProvider',
|
||||
|
||||
@@ -553,6 +553,105 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjNTIMER', 15),
|
||||
]),
|
||||
)),
|
||||
('mjtGeomInertia',
|
||||
EnumDecl(
|
||||
name='mjtGeomInertia',
|
||||
declname='enum mjtGeomInertia_',
|
||||
values=dict([
|
||||
('mjINERTIA_VOLUME', 0),
|
||||
('mjINERTIA_SHELL', 1),
|
||||
]),
|
||||
)),
|
||||
('mjtMeshInertia',
|
||||
EnumDecl(
|
||||
name='mjtMeshInertia',
|
||||
declname='enum mjtMeshInertia_',
|
||||
values=dict([
|
||||
('mjMESH_INERTIA_CONVEX', 0),
|
||||
('mjMESH_INERTIA_EXACT', 1),
|
||||
('mjMESH_INERTIA_LEGACY', 2),
|
||||
('mjMESH_INERTIA_SHELL', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtMeshBuiltin',
|
||||
EnumDecl(
|
||||
name='mjtMeshBuiltin',
|
||||
declname='enum mjtMeshBuiltin_',
|
||||
values=dict([
|
||||
('mjMESH_BUILTIN_NONE', 0),
|
||||
('mjMESH_BUILTIN_SPHERE', 1),
|
||||
('mjMESH_BUILTIN_HEMISPHERE', 2),
|
||||
('mjMESH_BUILTIN_CONE', 3),
|
||||
('mjMESH_BUILTIN_SUPERSPHERE', 4),
|
||||
('mjMESH_BUILTIN_SUPERTORUS', 5),
|
||||
('mjMESH_BUILTIN_WEDGE', 6),
|
||||
('mjMESH_BUILTIN_PLATE', 7),
|
||||
]),
|
||||
)),
|
||||
('mjtBuiltin',
|
||||
EnumDecl(
|
||||
name='mjtBuiltin',
|
||||
declname='enum mjtBuiltin_',
|
||||
values=dict([
|
||||
('mjBUILTIN_NONE', 0),
|
||||
('mjBUILTIN_GRADIENT', 1),
|
||||
('mjBUILTIN_CHECKER', 2),
|
||||
('mjBUILTIN_FLAT', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtMark',
|
||||
EnumDecl(
|
||||
name='mjtMark',
|
||||
declname='enum mjtMark_',
|
||||
values=dict([
|
||||
('mjMARK_NONE', 0),
|
||||
('mjMARK_EDGE', 1),
|
||||
('mjMARK_CROSS', 2),
|
||||
('mjMARK_RANDOM', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtLimited',
|
||||
EnumDecl(
|
||||
name='mjtLimited',
|
||||
declname='enum mjtLimited_',
|
||||
values=dict([
|
||||
('mjLIMITED_FALSE', 0),
|
||||
('mjLIMITED_TRUE', 1),
|
||||
('mjLIMITED_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtAlignFree',
|
||||
EnumDecl(
|
||||
name='mjtAlignFree',
|
||||
declname='enum mjtAlignFree_',
|
||||
values=dict([
|
||||
('mjALIGNFREE_FALSE', 0),
|
||||
('mjALIGNFREE_TRUE', 1),
|
||||
('mjALIGNFREE_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtInertiaFromGeom',
|
||||
EnumDecl(
|
||||
name='mjtInertiaFromGeom',
|
||||
declname='enum mjtInertiaFromGeom_',
|
||||
values=dict([
|
||||
('mjINERTIAFROMGEOM_FALSE', 0),
|
||||
('mjINERTIAFROMGEOM_TRUE', 1),
|
||||
('mjINERTIAFROMGEOM_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtOrientation',
|
||||
EnumDecl(
|
||||
name='mjtOrientation',
|
||||
declname='enum mjtOrientation_',
|
||||
values=dict([
|
||||
('mjORIENTATION_QUAT', 0),
|
||||
('mjORIENTATION_AXISANGLE', 1),
|
||||
('mjORIENTATION_XYAXES', 2),
|
||||
('mjORIENTATION_ZAXIS', 3),
|
||||
('mjORIENTATION_EULER', 4),
|
||||
]),
|
||||
)),
|
||||
('mjtCatBit',
|
||||
EnumDecl(
|
||||
name='mjtCatBit',
|
||||
@@ -774,105 +873,6 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjFONT_BIG', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtGeomInertia',
|
||||
EnumDecl(
|
||||
name='mjtGeomInertia',
|
||||
declname='enum mjtGeomInertia_',
|
||||
values=dict([
|
||||
('mjINERTIA_VOLUME', 0),
|
||||
('mjINERTIA_SHELL', 1),
|
||||
]),
|
||||
)),
|
||||
('mjtMeshInertia',
|
||||
EnumDecl(
|
||||
name='mjtMeshInertia',
|
||||
declname='enum mjtMeshInertia_',
|
||||
values=dict([
|
||||
('mjMESH_INERTIA_CONVEX', 0),
|
||||
('mjMESH_INERTIA_EXACT', 1),
|
||||
('mjMESH_INERTIA_LEGACY', 2),
|
||||
('mjMESH_INERTIA_SHELL', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtMeshBuiltin',
|
||||
EnumDecl(
|
||||
name='mjtMeshBuiltin',
|
||||
declname='enum mjtMeshBuiltin_',
|
||||
values=dict([
|
||||
('mjMESH_BUILTIN_NONE', 0),
|
||||
('mjMESH_BUILTIN_SPHERE', 1),
|
||||
('mjMESH_BUILTIN_HEMISPHERE', 2),
|
||||
('mjMESH_BUILTIN_CONE', 3),
|
||||
('mjMESH_BUILTIN_SUPERSPHERE', 4),
|
||||
('mjMESH_BUILTIN_SUPERTORUS', 5),
|
||||
('mjMESH_BUILTIN_WEDGE', 6),
|
||||
('mjMESH_BUILTIN_PLATE', 7),
|
||||
]),
|
||||
)),
|
||||
('mjtBuiltin',
|
||||
EnumDecl(
|
||||
name='mjtBuiltin',
|
||||
declname='enum mjtBuiltin_',
|
||||
values=dict([
|
||||
('mjBUILTIN_NONE', 0),
|
||||
('mjBUILTIN_GRADIENT', 1),
|
||||
('mjBUILTIN_CHECKER', 2),
|
||||
('mjBUILTIN_FLAT', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtMark',
|
||||
EnumDecl(
|
||||
name='mjtMark',
|
||||
declname='enum mjtMark_',
|
||||
values=dict([
|
||||
('mjMARK_NONE', 0),
|
||||
('mjMARK_EDGE', 1),
|
||||
('mjMARK_CROSS', 2),
|
||||
('mjMARK_RANDOM', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtLimited',
|
||||
EnumDecl(
|
||||
name='mjtLimited',
|
||||
declname='enum mjtLimited_',
|
||||
values=dict([
|
||||
('mjLIMITED_FALSE', 0),
|
||||
('mjLIMITED_TRUE', 1),
|
||||
('mjLIMITED_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtAlignFree',
|
||||
EnumDecl(
|
||||
name='mjtAlignFree',
|
||||
declname='enum mjtAlignFree_',
|
||||
values=dict([
|
||||
('mjALIGNFREE_FALSE', 0),
|
||||
('mjALIGNFREE_TRUE', 1),
|
||||
('mjALIGNFREE_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtInertiaFromGeom',
|
||||
EnumDecl(
|
||||
name='mjtInertiaFromGeom',
|
||||
declname='enum mjtInertiaFromGeom_',
|
||||
values=dict([
|
||||
('mjINERTIAFROMGEOM_FALSE', 0),
|
||||
('mjINERTIAFROMGEOM_TRUE', 1),
|
||||
('mjINERTIAFROMGEOM_AUTO', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtOrientation',
|
||||
EnumDecl(
|
||||
name='mjtOrientation',
|
||||
declname='enum mjtOrientation_',
|
||||
values=dict([
|
||||
('mjORIENTATION_QUAT', 0),
|
||||
('mjORIENTATION_AXISANGLE', 1),
|
||||
('mjORIENTATION_XYAXES', 2),
|
||||
('mjORIENTATION_ZAXIS', 3),
|
||||
('mjORIENTATION_EULER', 4),
|
||||
]),
|
||||
)),
|
||||
('mjtButton',
|
||||
EnumDecl(
|
||||
name='mjtButton',
|
||||
|
||||
@@ -301,6 +301,46 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Parse spec from XML string.',
|
||||
)),
|
||||
('mj_parse',
|
||||
FunctionDecl(
|
||||
name='mj_parse',
|
||||
return_type=PointerType(
|
||||
inner_type=ValueType(name='mjSpec'),
|
||||
),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='filename',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='content_type',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='vfs',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjVFS', is_const=True),
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='error',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char'),
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='error_sz',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
),
|
||||
doc='Parse spec from a file.',
|
||||
)),
|
||||
('mj_compile',
|
||||
FunctionDecl(
|
||||
name='mj_compile',
|
||||
@@ -9096,6 +9136,56 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Look up a resource provider by slot number returned by mjp_registerResourceProvider. If invalid slot number, return NULL.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mjp_registerDecoder',
|
||||
FunctionDecl(
|
||||
name='mjp_registerDecoder',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='decoder',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjpDecoder', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Globally register a decoder. This function is thread-safe. If an identical mjpDecoder is already registered, this function does nothing. If a non-identical mjpDecoder with the same name is already registered, an mju_error is raised.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mjp_defaultDecoder',
|
||||
FunctionDecl(
|
||||
name='mjp_defaultDecoder',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='decoder',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjpDecoder'),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Set default resource decoder definition.',
|
||||
)),
|
||||
('mjp_findDecoder',
|
||||
FunctionDecl(
|
||||
name='mjp_findDecoder',
|
||||
return_type=PointerType(
|
||||
inner_type=ValueType(name='mjpDecoder', is_const=True),
|
||||
),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='resource',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjResource', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='content_type',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Return the resource provider with the prefix that matches against the resource name. If no match, return NULL.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mju_threadPoolCreate',
|
||||
FunctionDecl(
|
||||
name='mju_threadPoolCreate',
|
||||
|
||||
+1445
-1445
File diff suppressed because it is too large
Load Diff
@@ -281,8 +281,8 @@ PYBIND11_MODULE(_specs, m) {
|
||||
spec = LoadSpecFileImpl(
|
||||
filename, files,
|
||||
[&error](const char* filename, const mjVFS* vfs) {
|
||||
return InterceptMjErrors(mj_parseXML)(
|
||||
filename, vfs, error, sizeof(error));
|
||||
return InterceptMjErrors(mj_parse)(
|
||||
filename, nullptr, vfs, error, sizeof(error));
|
||||
});
|
||||
if (!spec) {
|
||||
throw py::value_error(error);
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2025 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Tests for decoder plugins.
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mjplugin.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "test/fixture.h"
|
||||
|
||||
namespace mujoco {
|
||||
namespace {
|
||||
|
||||
// A simple mjSpec with one body and one geom.
|
||||
static mjSpec* MakeSimpleSpec() {
|
||||
mjSpec* s = mj_makeSpec();
|
||||
mjsBody* world = mjs_findBody(s, "world");
|
||||
mjsBody* body = mjs_addBody(world, nullptr);
|
||||
mjsGeom* geom = mjs_addGeom(body, nullptr);
|
||||
geom->size[0] = 1.0;
|
||||
geom->size[1] = 1.0;
|
||||
geom->size[2] = 1.0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Always returns a simple mjSpec, ignoring the resource.
|
||||
mjSpec* FakeDecode(const mjResource* resource) { return MakeSimpleSpec(); }
|
||||
|
||||
// Can decode any resource that has a .fakeformat extension.
|
||||
int FakeCanDecode(const mjResource* resource) {
|
||||
const char* ext = strrchr(resource->name, '.');
|
||||
if (ext) {
|
||||
return strcmp(ext, ".fakeformat") == 0 ||
|
||||
strcmp(ext, ".alsoFakeFormat") == 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
mjpDecoder FakeDecoder() {
|
||||
mjpDecoder decoder;
|
||||
mjp_defaultDecoder(&decoder);
|
||||
decoder.content_type = "model/fakeformat";
|
||||
decoder.extension = ".fakeformat|.alsoFakeFormat";
|
||||
decoder.can_decode = FakeCanDecode;
|
||||
decoder.decode = FakeDecode;
|
||||
return decoder;
|
||||
}
|
||||
|
||||
using DecoderPluginTest = MujocoTest;
|
||||
|
||||
TEST_F(DecoderPluginTest, CanDecode) {
|
||||
mjpDecoder decoder = FakeDecoder();
|
||||
mjp_registerDecoder(&decoder);
|
||||
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<model name="fakeformat" file="dummy.fakeformat"/>
|
||||
<model name="also_fakeformat" file="dummy.alsoFakeFormat"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<attach model="fakeformat" prefix="test"/>
|
||||
</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));
|
||||
ASSERT_THAT(model, testing::NotNull()) << error;
|
||||
EXPECT_EQ(model->nbody, 2); // world + included body
|
||||
EXPECT_EQ(model->ngeom, 1);
|
||||
mj_deleteModel(model);
|
||||
|
||||
// Check mj_parse with extension .fakeformat
|
||||
mjSpec* spec =
|
||||
mj_parse("dummy.fakeformat", nullptr, &vfs, error, sizeof(error));
|
||||
model = mj_compile(spec, &vfs);
|
||||
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);
|
||||
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,
|
||||
sizeof(error));
|
||||
model = mj_compile(spec, &vfs);
|
||||
EXPECT_EQ(model->nbody, 2); // world + included body
|
||||
EXPECT_EQ(model->ngeom, 1);
|
||||
mj_deleteModel(model);
|
||||
mj_deleteSpec(spec);
|
||||
|
||||
mj_deleteVFS(&vfs);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
Reference in New Issue
Block a user