Introduce mjpEncoder plugin architecture
Add a new mjpEncoder plugin type mirroring the existing mjpDecoder pattern. Encoders serialize an mjSpec + mjModel to an mjResource for a given format. New API functions: - mjp_registerEncoder: globally register an encoder - mjp_defaultEncoder: zero-initialize an encoder struct - mjp_findEncoder: look up an encoder by filename extension or content type The mjfEncode callback takes (mjSpec*, mjModel*, mjVFS*, mjResource*) and returns 0 on success. Writing to mjResource keeps symmetry with the decoder reading from mjResource and leaves the door open for writable resource providers. PiperOrigin-RevId: 889187898 Change-Id: I180771b2255b91dea188ac5e2cdc3a8f0fb85364
This commit is contained in:
committed by
Copybara-Service
parent
2d33b50243
commit
f5d3ce3451
@@ -1580,6 +1580,29 @@ used for opening and reading resources.
|
|||||||
|
|
||||||
.. mujoco-include:: mjpResourceProvider
|
.. mujoco-include:: mjpResourceProvider
|
||||||
|
|
||||||
|
.. _mjpDecoder:
|
||||||
|
|
||||||
|
mjpDecoder
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This data structure defines a decoder. It contains a set of callbacks used for decoding :ref:`mjResource`
|
||||||
|
into :ref:`mjSpec`.
|
||||||
|
|
||||||
|
.. mujoco-include:: mjpDecoder
|
||||||
|
|
||||||
|
.. _mjpEncoder:
|
||||||
|
|
||||||
|
mjpEncoder
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This data structure defines an encoder. It contains a set of callbacks used for encoding of :ref:`mjSpec` and
|
||||||
|
:ref:`mjModel` into :ref:`mjResource`.
|
||||||
|
|
||||||
|
.. mujoco-include:: mjpEncoder
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _tyFunction:
|
.. _tyFunction:
|
||||||
|
|
||||||
Function types
|
Function types
|
||||||
@@ -1757,6 +1780,48 @@ This callback is for checking if a resource was modified since it was last read.
|
|||||||
Returns positive value if the resource was modified since last open, 0 if resource was not modified,
|
Returns positive value if the resource was modified since last open, 0 if resource was not modified,
|
||||||
and negative value if inconclusive.
|
and negative value if inconclusive.
|
||||||
|
|
||||||
|
.. _mjfDecode:
|
||||||
|
|
||||||
|
mjfDecode
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: C
|
||||||
|
|
||||||
|
typedef mjSpec* (*mjfDecode)(mjResource* resource, const mjVFS* vfs);
|
||||||
|
|
||||||
|
|
||||||
|
This callback is given an opened resource, and is responsible for decoding it into a :ref:`mjSpec`.
|
||||||
|
Ownership of the resource and the returned spec is responsibility of the caller.
|
||||||
|
When decoding fails, the callback should return NULL.
|
||||||
|
|
||||||
|
.. _mjfCanDecode:
|
||||||
|
|
||||||
|
mjfCanDecode
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: C
|
||||||
|
|
||||||
|
typedef int (*mjfCanDecode)(const mjResource* resource);
|
||||||
|
|
||||||
|
|
||||||
|
This callback is given an opened resource, and is responsible for returning true if the resource can
|
||||||
|
be decoded by the :ref:`mjpDecoder<mjpDecoder>`.
|
||||||
|
|
||||||
|
.. _mjfEncode:
|
||||||
|
|
||||||
|
mjfEncode
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: C
|
||||||
|
|
||||||
|
typedef int (*mjfEncode)(const mjSpec* s, const mjModel* m, const mjVFS* vfs,
|
||||||
|
mjResource* resource);
|
||||||
|
|
||||||
|
|
||||||
|
This callback populates the :ref:`mjResource<mjResource>` `data` member with bytes representing the
|
||||||
|
given spec in the format associated with the owning plugin. This may be called with the associated
|
||||||
|
compiled :ref:`mjModel`.
|
||||||
|
|
||||||
|
|
||||||
.. _tyNotes:
|
.. _tyNotes:
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ Parse spec from a file.
|
|||||||
|
|
||||||
*Nullable:* ``vfs``, ``error``
|
*Nullable:* ``vfs``, ``error``
|
||||||
|
|
||||||
|
.. _mj_encode:
|
||||||
|
|
||||||
|
`mj_encode <#mj_encode>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mj_encode
|
||||||
|
|
||||||
|
Encode spec/model to a file using a registered encoder.
|
||||||
|
|
||||||
|
Returns the number of bytes written on success, -1 on failure.
|
||||||
|
|
||||||
|
*Nullable:* ``m``, ``vfs``, ``error``
|
||||||
|
|
||||||
.. _mj_compile:
|
.. _mj_compile:
|
||||||
|
|
||||||
`mj_compile <#mj_compile>`__
|
`mj_compile <#mj_compile>`__
|
||||||
@@ -3255,6 +3268,39 @@ Return the resource provider with the prefix that matches against the resource n
|
|||||||
|
|
||||||
If no match, return NULL.
|
If no match, return NULL.
|
||||||
|
|
||||||
|
.. _mjp_registerEncoder:
|
||||||
|
|
||||||
|
`mjp_registerEncoder <#mjp_registerEncoder>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mjp_registerEncoder
|
||||||
|
|
||||||
|
Globally register an encoder. This function is thread-safe.
|
||||||
|
|
||||||
|
If an identical mjpEncoder is already registered, this function does nothing.
|
||||||
|
|
||||||
|
If a non-identical mjpEncoder with the same name is already registered, an mju_error is raised.
|
||||||
|
|
||||||
|
.. _mjp_defaultEncoder:
|
||||||
|
|
||||||
|
`mjp_defaultEncoder <#mjp_defaultEncoder>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mjp_defaultEncoder
|
||||||
|
|
||||||
|
Set default resource encoder definition.
|
||||||
|
|
||||||
|
.. _mjp_findEncoder:
|
||||||
|
|
||||||
|
`mjp_findEncoder <#mjp_findEncoder>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mjp_findEncoder
|
||||||
|
|
||||||
|
Return the encoder that matches against the content type or filename extension.
|
||||||
|
|
||||||
|
If no match, return NULL.
|
||||||
|
|
||||||
.. _Thread:
|
.. _Thread:
|
||||||
|
|
||||||
Threads
|
Threads
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ General
|
|||||||
The polynomial order is defined by the new constant :ref:`mjNPOLY<glNumericSizes>`. A future breaking C-API change
|
The polynomial order is defined by the new constant :ref:`mjNPOLY<glNumericSizes>`. A future breaking C-API change
|
||||||
may unify the linear and higher-order coefficients into a single array.
|
may unify the linear and higher-order coefficients into a single array.
|
||||||
|
|
||||||
|
- Introduced :ref:`mjpEncoder`, the counterpart to :ref:`mjpDecoder` for encoding of :ref:`mjSpec` and :ref:`mjModel` into :ref:`mjResource`.
|
||||||
|
|
||||||
|
- Added :ref:`mj_encode`, :ref:`mjp_registerEncoder`, :ref:`mjp_defaultEncoder`, and :ref:`mjp_findEncoder`.
|
||||||
|
|
||||||
.. admonition:: Breaking API changes
|
.. admonition:: Breaking API changes
|
||||||
:class: attention
|
:class: attention
|
||||||
|
|
||||||
|
|||||||
@@ -1686,6 +1686,13 @@ struct mjpDecoder {
|
|||||||
// for cleaning it up
|
// for cleaning it up
|
||||||
};
|
};
|
||||||
typedef struct mjpDecoder mjpDecoder;
|
typedef struct mjpDecoder mjpDecoder;
|
||||||
|
struct mjpEncoder {
|
||||||
|
const char* content_type;
|
||||||
|
const char* extension;
|
||||||
|
mjfEncode encode;
|
||||||
|
mjfCloseResource close_resource;
|
||||||
|
};
|
||||||
|
typedef struct mjpEncoder mjpEncoder;
|
||||||
typedef enum mjtPluginCapabilityBit_ {
|
typedef enum mjtPluginCapabilityBit_ {
|
||||||
mjPLUGIN_ACTUATOR = 1<<0, // actuator forces
|
mjPLUGIN_ACTUATOR = 1<<0, // actuator forces
|
||||||
mjPLUGIN_SENSOR = 1<<1, // sensor measurements
|
mjPLUGIN_SENSOR = 1<<1, // sensor measurements
|
||||||
@@ -3155,6 +3162,9 @@ mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, int err
|
|||||||
mjSpec* mj_parseXMLString(const char* xml, 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,
|
mjSpec* mj_parse(const char* filename, const char* content_type,
|
||||||
const mjVFS* vfs, char* error, int error_sz);
|
const mjVFS* vfs, char* error, int error_sz);
|
||||||
|
int mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
||||||
|
const char* content_type, const mjVFS* vfs, char* error,
|
||||||
|
int error_sz);
|
||||||
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
||||||
int mj_copyBack(mjSpec* s, const mjModel* m);
|
int mj_copyBack(mjSpec* s, const mjModel* m);
|
||||||
int mj_recompile(mjSpec* s, const mjVFS* vfs, mjModel* m, mjData* d);
|
int mj_recompile(mjSpec* s, const mjVFS* vfs, mjModel* m, mjData* d);
|
||||||
@@ -3591,6 +3601,9 @@ const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot);
|
|||||||
void mjp_registerDecoder(const mjpDecoder* decoder);
|
void mjp_registerDecoder(const mjpDecoder* decoder);
|
||||||
void mjp_defaultDecoder(mjpDecoder* decoder);
|
void mjp_defaultDecoder(mjpDecoder* decoder);
|
||||||
const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
||||||
|
void mjp_registerEncoder(const mjpEncoder* encoder);
|
||||||
|
void mjp_defaultEncoder(mjpEncoder* encoder);
|
||||||
|
const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
|
||||||
mjResource* mju_openResource(const char* dir, const char* name,
|
mjResource* mju_openResource(const char* dir, const char* name,
|
||||||
const mjVFS* vfs, char* error, size_t nerror);
|
const mjVFS* vfs, char* error, size_t nerror);
|
||||||
void mju_closeResource(mjResource* resource);
|
void mju_closeResource(mjResource* resource);
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ struct mjpDecoder {
|
|||||||
};
|
};
|
||||||
typedef struct mjpDecoder mjpDecoder;
|
typedef struct mjpDecoder mjpDecoder;
|
||||||
|
|
||||||
|
//---------------------------------- Encoder -------------------------------------------------------
|
||||||
|
|
||||||
|
typedef int (*mjfEncode)(const mjSpec* s, const mjModel* m, const mjVFS* vfs,
|
||||||
|
mjResource* resource);
|
||||||
|
|
||||||
|
struct mjpEncoder {
|
||||||
|
const char* content_type;
|
||||||
|
const char* extension;
|
||||||
|
mjfEncode encode;
|
||||||
|
mjfCloseResource close_resource;
|
||||||
|
};
|
||||||
|
typedef struct mjpEncoder mjpEncoder;
|
||||||
|
|
||||||
//---------------------------------- Plugins -------------------------------------------------------
|
//---------------------------------- Plugins -------------------------------------------------------
|
||||||
|
|
||||||
typedef enum mjtPluginCapabilityBit_ {
|
typedef enum mjtPluginCapabilityBit_ {
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ MJAPI mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error,
|
|||||||
MJAPI mjSpec* mj_parse(const char* filename, const char* content_type,
|
MJAPI mjSpec* mj_parse(const char* filename, const char* content_type,
|
||||||
const mjVFS* vfs, char* error, int error_sz);
|
const mjVFS* vfs, char* error, int error_sz);
|
||||||
|
|
||||||
|
// Encode spec/model to a file using a registered encoder.
|
||||||
|
// Returns the number of bytes written on success, -1 on failure.
|
||||||
|
// Nullable: m, vfs, error
|
||||||
|
MJAPI int mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
||||||
|
const char* content_type, const mjVFS* vfs, char* error,
|
||||||
|
int error_sz);
|
||||||
|
|
||||||
// Compile spec to model.
|
// Compile spec to model.
|
||||||
// Nullable: vfs
|
// Nullable: vfs
|
||||||
MJAPI mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
MJAPI mjModel* mj_compile(mjSpec* s, const mjVFS* vfs);
|
||||||
@@ -1522,6 +1529,19 @@ MJAPI void mjp_defaultDecoder(mjpDecoder* decoder);
|
|||||||
// If no match, return NULL.
|
// If no match, return NULL.
|
||||||
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
||||||
|
|
||||||
|
// Globally register an encoder. This function is thread-safe.
|
||||||
|
// If an identical mjpEncoder is already registered, this function does nothing.
|
||||||
|
// If a non-identical mjpEncoder with the same name is already registered, an mju_error is raised.
|
||||||
|
MJAPI void mjp_registerEncoder(const mjpEncoder* encoder);
|
||||||
|
|
||||||
|
// Set default resource encoder definition.
|
||||||
|
MJAPI void mjp_defaultEncoder(mjpEncoder* encoder);
|
||||||
|
|
||||||
|
// Return the encoder that matches against the content type or filename extension.
|
||||||
|
// If no match, return NULL.
|
||||||
|
MJAPI const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---------------------------------- Resources -----------------------------------------------------
|
//---------------------------------- Resources -----------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ _OPAQUE_STRUCTS = [
|
|||||||
'mjTask',
|
'mjTask',
|
||||||
'mjThreadPool',
|
'mjThreadPool',
|
||||||
'mjpDecoder',
|
'mjpDecoder',
|
||||||
|
'mjpEncoder',
|
||||||
'mjpResourceProvider',
|
'mjpResourceProvider',
|
||||||
'mjsElement',
|
'mjsElement',
|
||||||
'mjString',
|
'mjString',
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ _ANONYMOUS_KEY_PATTERN = re.compile(r'\d+:\d+(?=\))')
|
|||||||
_EXCLUDED = (
|
_EXCLUDED = (
|
||||||
'mjpDecoder',
|
'mjpDecoder',
|
||||||
'mjpDecoder_',
|
'mjpDecoder_',
|
||||||
|
'mjpEncoder',
|
||||||
|
'mjpEncoder_',
|
||||||
'mjpPlugin',
|
'mjpPlugin',
|
||||||
'mjpPlugin_',
|
'mjpPlugin_',
|
||||||
'mjpResourceProvider',
|
'mjpResourceProvider',
|
||||||
|
|||||||
@@ -387,6 +387,57 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
|||||||
),
|
),
|
||||||
doc='Parse spec from a file.',
|
doc='Parse spec from a file.',
|
||||||
)),
|
)),
|
||||||
|
('mj_encode',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mj_encode',
|
||||||
|
return_type=ValueType(name='int'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='s',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjSpec', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='m',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjModel', is_const=True),
|
||||||
|
),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
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='Encode spec/model to a file using a registered encoder. Returns the number of bytes written on success, -1 on failure.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
('mj_compile',
|
('mj_compile',
|
||||||
FunctionDecl(
|
FunctionDecl(
|
||||||
name='mj_compile',
|
name='mj_compile',
|
||||||
@@ -9600,6 +9651,56 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
|||||||
),
|
),
|
||||||
doc='Return the resource provider with the prefix that matches against the resource name. If no match, return NULL.', # pylint: disable=line-too-long
|
doc='Return the resource provider with the prefix that matches against the resource name. If no match, return NULL.', # pylint: disable=line-too-long
|
||||||
)),
|
)),
|
||||||
|
('mjp_registerEncoder',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mjp_registerEncoder',
|
||||||
|
return_type=ValueType(name='void'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='encoder',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjpEncoder', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Globally register an encoder. This function is thread-safe. If an identical mjpEncoder is already registered, this function does nothing. If a non-identical mjpEncoder with the same name is already registered, an mju_error is raised.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
|
('mjp_defaultEncoder',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mjp_defaultEncoder',
|
||||||
|
return_type=ValueType(name='void'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='encoder',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjpEncoder'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Set default resource encoder definition.',
|
||||||
|
)),
|
||||||
|
('mjp_findEncoder',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mjp_findEncoder',
|
||||||
|
return_type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjpEncoder', is_const=True),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Return the encoder that matches against the content type or filename extension. If no match, return NULL.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
('mju_openResource',
|
('mju_openResource',
|
||||||
FunctionDecl(
|
FunctionDecl(
|
||||||
name='mju_openResource',
|
name='mju_openResource',
|
||||||
|
|||||||
@@ -357,6 +357,85 @@ bool GlobalTable<mjpDecoder>::CopyObject(mjpDecoder& dst, const mjpDecoder& src,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
const char* GlobalTable<mjpEncoder>::HumanReadableTypeName() {
|
||||||
|
return "resource encoder";
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::string_view GlobalTable<mjpEncoder>::ObjectKey(const mjpEncoder& encoder) {
|
||||||
|
if (encoder.content_type) {
|
||||||
|
if (int len = strklen(encoder.content_type); len != -1) {
|
||||||
|
return std::string_view(encoder.content_type, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::string_view(encoder.extension, strklen(encoder.extension));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
bool GlobalTable<mjpEncoder>::ObjectEqual(const mjpEncoder& e1,
|
||||||
|
const mjpEncoder& e2) {
|
||||||
|
bool content_type_match = false;
|
||||||
|
if (e1.content_type && e2.content_type) {
|
||||||
|
content_type_match = CaseInsensitiveEqual(e1.content_type, e2.content_type);
|
||||||
|
} else {
|
||||||
|
content_type_match = (e1.content_type == e2.content_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool extension_match = false;
|
||||||
|
if (e1.extension && e2.extension) {
|
||||||
|
extension_match = CaseInsensitiveEqual(e1.extension, e2.extension);
|
||||||
|
} else {
|
||||||
|
extension_match = (e1.extension == e2.extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content_type_match && extension_match
|
||||||
|
&& e1.encode == e2.encode && e1.close_resource == e2.close_resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
bool GlobalTable<mjpEncoder>::CopyObject(mjpEncoder& dst, const mjpEncoder& src, ErrorMessage& err) {
|
||||||
|
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),
|
||||||
|
"encoder->content_type length exceeds the maximum limit of %d",
|
||||||
|
kMaxNameLength);
|
||||||
|
} else {
|
||||||
|
std::snprintf(err, sizeof(err), "failed to allocate memory for encoder 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),
|
||||||
|
"encoder->extension length exceeds the maximum limit of %d",
|
||||||
|
kMaxNameLength);
|
||||||
|
} else {
|
||||||
|
std::snprintf(err, sizeof(err), "failed to allocate memory for encoder extension");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.extension = extension.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// globally register a plugin (thread-safe), return new slot id
|
// globally register a plugin (thread-safe), return new slot id
|
||||||
int mjp_registerPlugin(const mjpPlugin* plugin) {
|
int mjp_registerPlugin(const mjpPlugin* plugin) {
|
||||||
if (!plugin->name) {
|
if (!plugin->name) {
|
||||||
@@ -539,6 +618,75 @@ const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* conten
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void mjp_registerEncoder(const mjpEncoder* encoder) {
|
||||||
|
if (!encoder->encode) {
|
||||||
|
mju_warning("encoder must provide an encode callback.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encoder->close_resource) {
|
||||||
|
mju_warning("encoder must provide a close_resource callback.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encoder->content_type && !encoder->extension) {
|
||||||
|
mju_warning("encoder must provide content_type and/or extensions.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mjpEncoder encoder_copy = *encoder;
|
||||||
|
|
||||||
|
if (encoder->content_type) {
|
||||||
|
encoder_copy.extension = nullptr;
|
||||||
|
GlobalTable<mjpEncoder>::GetSingleton().AppendIfUnique(encoder_copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoder->extension) {
|
||||||
|
encoder_copy.content_type = nullptr;
|
||||||
|
std::string extensions_str(encoder->extension);
|
||||||
|
std::stringstream ss(extensions_str);
|
||||||
|
std::string extension;
|
||||||
|
while (std::getline(ss, extension, '|')) {
|
||||||
|
if (!extension.empty()) {
|
||||||
|
encoder_copy.extension = extension.c_str();
|
||||||
|
GlobalTable<mjpEncoder>::GetSingleton().AppendIfUnique(encoder_copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mjp_defaultEncoder(mjpEncoder* encoder) {
|
||||||
|
std::memset(encoder, 0, sizeof(*encoder));
|
||||||
|
}
|
||||||
|
|
||||||
|
const mjpEncoder* mjp_findEncoder(const char* filename,
|
||||||
|
const char* content_type) {
|
||||||
|
auto extension = getext(filename ? filename : "");
|
||||||
|
bool has_content_type = content_type && strklen(content_type) > 0;
|
||||||
|
if (!has_content_type && extension.empty()) {
|
||||||
|
mju_warning("Must provide extension or content_type to mjp_findEncoder.");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_content_type) {
|
||||||
|
auto* encoder =
|
||||||
|
GlobalTable<mjpEncoder>::GetSingleton().GetByKey(content_type, nullptr);
|
||||||
|
if (encoder) {
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extension.empty()) {
|
||||||
|
auto* encoder = GlobalTable<mjpEncoder>::GetSingleton().GetByKey(
|
||||||
|
extension.c_str(), nullptr);
|
||||||
|
if (encoder) {
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// load plugins from a dynamic library
|
// load plugins from a dynamic library
|
||||||
void mj_loadPluginLibrary(const char* path) {
|
void mj_loadPluginLibrary(const char* path) {
|
||||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ MJAPI void mjp_defaultDecoder(mjpDecoder* decoder);
|
|||||||
// find a decoder that can process a given resource
|
// find a decoder that can process a given resource
|
||||||
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
MJAPI const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type);
|
||||||
|
|
||||||
|
// registers a resource encoder
|
||||||
|
MJAPI void mjp_registerEncoder(const mjpEncoder* encoder);
|
||||||
|
|
||||||
|
// set default encoder definition
|
||||||
|
MJAPI void mjp_defaultEncoder(mjpEncoder* encoder);
|
||||||
|
|
||||||
|
// find an encoder that can write a given format
|
||||||
|
MJAPI const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
|
||||||
|
|
||||||
// =================================================================================================
|
// =================================================================================================
|
||||||
// MuJoCo-internal functions beyond this point.
|
// MuJoCo-internal functions beyond this point.
|
||||||
// "Unsafe" suffix indicates that improper use of these functions may result in data races.
|
// "Unsafe" suffix indicates that improper use of these functions may result in data races.
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
@@ -136,6 +138,57 @@ mjSpec* mj_parse(const char* filename, const char* content_type,
|
|||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encode spec/model to file
|
||||||
|
int mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
||||||
|
const char* content_type, const mjVFS* vfs, char* error,
|
||||||
|
int error_sz) {
|
||||||
|
const mjpEncoder* encoder = mjp_findEncoder(filename, content_type);
|
||||||
|
if (!encoder) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "no encoder found", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mjResource resource;
|
||||||
|
memset(&resource, 0, sizeof(resource));
|
||||||
|
resource.name = const_cast<char*>(filename);
|
||||||
|
|
||||||
|
const int nbytes = encoder->encode(s, m, vfs, &resource);
|
||||||
|
if (nbytes < 0 || !resource.data) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "encoder failed", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* fp = fopen(filename, "wb");
|
||||||
|
if (!fp) {
|
||||||
|
std::free(resource.data);
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "could not open file for writing", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t written = fwrite(resource.data, 1, nbytes, fp);
|
||||||
|
fclose(fp);
|
||||||
|
encoder->close_resource(&resource);
|
||||||
|
|
||||||
|
if (static_cast<int>(written) != nbytes) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "failed to write all bytes to file", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nbytes;
|
||||||
|
}
|
||||||
|
|
||||||
// compile model
|
// compile model
|
||||||
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) {
|
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) {
|
||||||
mjCModel* modelC = static_cast<mjCModel*>(s->element);
|
mjCModel* modelC = static_cast<mjCModel*>(s->element);
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// 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 encoder plugins.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
struct FakeEncoderOutput {
|
||||||
|
int nbody;
|
||||||
|
int ngeom;
|
||||||
|
int njnt;
|
||||||
|
char resource_name[512];
|
||||||
|
};
|
||||||
|
|
||||||
|
int FakeEncode(const mjSpec* s, const mjModel* m, const mjVFS* vfs,
|
||||||
|
mjResource* resource) {
|
||||||
|
auto* output = new FakeEncoderOutput;
|
||||||
|
output->nbody = m->nbody;
|
||||||
|
output->ngeom = m->ngeom;
|
||||||
|
output->njnt = m->njnt;
|
||||||
|
std::snprintf(output->resource_name, sizeof(output->resource_name), "%s",
|
||||||
|
resource->name);
|
||||||
|
resource->data = output;
|
||||||
|
return sizeof(FakeEncoderOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseResource(mjResource* resource) {
|
||||||
|
delete static_cast<FakeEncoderOutput*>(resource->data);
|
||||||
|
delete resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
mjpEncoder FakeEncoder() {
|
||||||
|
mjpEncoder encoder;
|
||||||
|
mjp_defaultEncoder(&encoder);
|
||||||
|
encoder.content_type = "model/fakeformat";
|
||||||
|
encoder.extension = ".fakeformat|.alsoFakeFormat";
|
||||||
|
encoder.encode = FakeEncode;
|
||||||
|
encoder.close_resource = CloseResource;
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
using EncoderPluginTest = MujocoTest;
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, RegisterAndFindByExtension) {
|
||||||
|
mjpEncoder encoder = FakeEncoder();
|
||||||
|
mjp_registerEncoder(&encoder);
|
||||||
|
|
||||||
|
const mjpEncoder* found = mjp_findEncoder("output.fakeformat", nullptr);
|
||||||
|
ASSERT_THAT(found, testing::NotNull());
|
||||||
|
EXPECT_EQ(found->encode, FakeEncode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, FindByAlternateExtension) {
|
||||||
|
const mjpEncoder* found = mjp_findEncoder("output.alsoFakeFormat", nullptr);
|
||||||
|
ASSERT_THAT(found, testing::NotNull());
|
||||||
|
EXPECT_EQ(found->encode, FakeEncode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, FindByContentType) {
|
||||||
|
const mjpEncoder* found = mjp_findEncoder(nullptr, "model/fakeformat");
|
||||||
|
ASSERT_THAT(found, testing::NotNull());
|
||||||
|
EXPECT_EQ(found->encode, FakeEncode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, FindUnknownExtensionReturnsNull) {
|
||||||
|
const mjpEncoder* found = mjp_findEncoder("output.unknown", nullptr);
|
||||||
|
EXPECT_THAT(found, testing::IsNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, DefaultEncoderIsZeroed) {
|
||||||
|
mjpEncoder encoder;
|
||||||
|
mjp_defaultEncoder(&encoder);
|
||||||
|
EXPECT_EQ(encoder.content_type, nullptr);
|
||||||
|
EXPECT_EQ(encoder.extension, nullptr);
|
||||||
|
EXPECT_EQ(encoder.encode, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EncoderPluginTest, EncodeModel) {
|
||||||
|
mjSpec* spec = mj_makeSpec();
|
||||||
|
mjsBody* world = mjs_findBody(spec, "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;
|
||||||
|
|
||||||
|
mjModel* model = mj_compile(spec, nullptr);
|
||||||
|
ASSERT_THAT(model, testing::NotNull());
|
||||||
|
|
||||||
|
const mjpEncoder* found = mjp_findEncoder("output.fakeformat", nullptr);
|
||||||
|
ASSERT_THAT(found, testing::NotNull());
|
||||||
|
|
||||||
|
mjResource resource = {};
|
||||||
|
resource.name = const_cast<char*>("output.fakeformat");
|
||||||
|
|
||||||
|
int result = found->encode(spec, model, nullptr, &resource);
|
||||||
|
EXPECT_GT(result, 0);
|
||||||
|
|
||||||
|
auto* output = static_cast<FakeEncoderOutput*>(resource.data);
|
||||||
|
ASSERT_THAT(output, testing::NotNull());
|
||||||
|
EXPECT_EQ(output->nbody, 2);
|
||||||
|
EXPECT_EQ(output->ngeom, 1);
|
||||||
|
EXPECT_EQ(output->njnt, 0);
|
||||||
|
EXPECT_STREQ(output->resource_name, "output.fakeformat");
|
||||||
|
|
||||||
|
delete output;
|
||||||
|
mj_deleteModel(model);
|
||||||
|
mj_deleteSpec(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace mujoco
|
||||||
@@ -6700,6 +6700,9 @@ public static unsafe extern void mj_clearCache(mjCache_* cache);
|
|||||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static unsafe extern mjModel_* mj_loadXML([MarshalAs(UnmanagedType.LPStr)]string filename, void* vfs, StringBuilder error, int error_sz);
|
public static unsafe extern mjModel_* mj_loadXML([MarshalAs(UnmanagedType.LPStr)]string filename, void* vfs, StringBuilder error, int error_sz);
|
||||||
|
|
||||||
|
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static unsafe extern int mj_encode(void* s, mjModel_* m, [MarshalAs(UnmanagedType.LPStr)]string filename, [MarshalAs(UnmanagedType.LPStr)]string content_type, void* vfs, StringBuilder error, int error_sz);
|
||||||
|
|
||||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static unsafe extern int mj_saveLastXML([MarshalAs(UnmanagedType.LPStr)]string filename, mjModel_* m, StringBuilder error, int error_sz);
|
public static unsafe extern int mj_saveLastXML([MarshalAs(UnmanagedType.LPStr)]string filename, mjModel_* m, StringBuilder error, int error_sz);
|
||||||
|
|
||||||
|
|||||||
@@ -46,15 +46,18 @@ _SKIPPED_PLUGIN_FUNCTIONS: tuple[str, ...] = (
|
|||||||
"mjc_getSDF",
|
"mjc_getSDF",
|
||||||
"mjc_gradient",
|
"mjc_gradient",
|
||||||
"mjp_defaultDecoder",
|
"mjp_defaultDecoder",
|
||||||
|
"mjp_defaultEncoder",
|
||||||
"mjp_defaultPlugin",
|
"mjp_defaultPlugin",
|
||||||
"mjp_defaultResourceProvider",
|
"mjp_defaultResourceProvider",
|
||||||
"mjp_findDecoder",
|
"mjp_findDecoder",
|
||||||
|
"mjp_findEncoder",
|
||||||
"mjp_getPlugin",
|
"mjp_getPlugin",
|
||||||
"mjp_getPluginAtSlot",
|
"mjp_getPluginAtSlot",
|
||||||
"mjp_getResourceProvider",
|
"mjp_getResourceProvider",
|
||||||
"mjp_getResourceProviderAtSlot",
|
"mjp_getResourceProviderAtSlot",
|
||||||
"mjp_pluginCount",
|
"mjp_pluginCount",
|
||||||
"mjp_registerDecoder",
|
"mjp_registerDecoder",
|
||||||
|
"mjp_registerEncoder",
|
||||||
"mjp_registerPlugin",
|
"mjp_registerPlugin",
|
||||||
"mjp_registerResourceProvider",
|
"mjp_registerResourceProvider",
|
||||||
"mjp_resourceProviderCount",
|
"mjp_resourceProviderCount",
|
||||||
@@ -74,6 +77,8 @@ _SKIPPED_CLASS_METHODS: tuple[str, ...] = (
|
|||||||
"mj_deleteModel",
|
"mj_deleteModel",
|
||||||
"mj_deleteSpec",
|
"mj_deleteSpec",
|
||||||
"mj_deleteVFS",
|
"mj_deleteVFS",
|
||||||
|
"mj_encode",
|
||||||
|
"mj_parse", # TODO(manevi): Bind this function.
|
||||||
"mj_loadModel",
|
"mj_loadModel",
|
||||||
"mj_loadXML",
|
"mj_loadXML",
|
||||||
"mj_makeData",
|
"mj_makeData",
|
||||||
|
|||||||
Reference in New Issue
Block a user