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
|
||||
|
||||
.. _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:
|
||||
|
||||
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,
|
||||
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:
|
||||
|
||||
|
||||
@@ -59,6 +59,19 @@ Parse spec from a file.
|
||||
|
||||
*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>`__
|
||||
@@ -3255,6 +3268,39 @@ Return the resource provider with the prefix that matches against the resource n
|
||||
|
||||
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:
|
||||
|
||||
Threads
|
||||
|
||||
@@ -25,6 +25,10 @@ General
|
||||
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.
|
||||
|
||||
- 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
|
||||
:class: attention
|
||||
|
||||
|
||||
@@ -1686,6 +1686,13 @@ struct mjpDecoder {
|
||||
// for cleaning it up
|
||||
};
|
||||
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_ {
|
||||
mjPLUGIN_ACTUATOR = 1<<0, // actuator forces
|
||||
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_parse(const char* filename, const char* content_type,
|
||||
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);
|
||||
int mj_copyBack(mjSpec* s, const mjModel* m);
|
||||
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_defaultDecoder(mjpDecoder* decoder);
|
||||
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,
|
||||
const mjVFS* vfs, char* error, size_t nerror);
|
||||
void mju_closeResource(mjResource* resource);
|
||||
|
||||
@@ -89,6 +89,19 @@ struct 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 -------------------------------------------------------
|
||||
|
||||
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,
|
||||
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.
|
||||
// Nullable: 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.
|
||||
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 -----------------------------------------------------
|
||||
|
||||
|
||||
@@ -209,6 +209,7 @@ _OPAQUE_STRUCTS = [
|
||||
'mjTask',
|
||||
'mjThreadPool',
|
||||
'mjpDecoder',
|
||||
'mjpEncoder',
|
||||
'mjpResourceProvider',
|
||||
'mjsElement',
|
||||
'mjString',
|
||||
|
||||
@@ -41,6 +41,8 @@ _ANONYMOUS_KEY_PATTERN = re.compile(r'\d+:\d+(?=\))')
|
||||
_EXCLUDED = (
|
||||
'mjpDecoder',
|
||||
'mjpDecoder_',
|
||||
'mjpEncoder',
|
||||
'mjpEncoder_',
|
||||
'mjpPlugin',
|
||||
'mjpPlugin_',
|
||||
'mjpResourceProvider',
|
||||
|
||||
@@ -387,6 +387,57 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
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',
|
||||
FunctionDecl(
|
||||
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
|
||||
)),
|
||||
('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',
|
||||
FunctionDecl(
|
||||
name='mju_openResource',
|
||||
|
||||
@@ -357,6 +357,85 @@ bool GlobalTable<mjpDecoder>::CopyObject(mjpDecoder& dst, const mjpDecoder& src,
|
||||
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
|
||||
int mjp_registerPlugin(const mjpPlugin* plugin) {
|
||||
if (!plugin->name) {
|
||||
@@ -539,6 +618,75 @@ const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* conten
|
||||
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
|
||||
void mj_loadPluginLibrary(const char* path) {
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
|
||||
@@ -71,6 +71,15 @@ 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);
|
||||
|
||||
// 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.
|
||||
// "Unsafe" suffix indicates that improper use of these functions may result in data races.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
@@ -136,6 +138,57 @@ mjSpec* mj_parse(const char* filename, const char* content_type,
|
||||
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
|
||||
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) {
|
||||
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)]
|
||||
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)]
|
||||
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_gradient",
|
||||
"mjp_defaultDecoder",
|
||||
"mjp_defaultEncoder",
|
||||
"mjp_defaultPlugin",
|
||||
"mjp_defaultResourceProvider",
|
||||
"mjp_findDecoder",
|
||||
"mjp_findEncoder",
|
||||
"mjp_getPlugin",
|
||||
"mjp_getPluginAtSlot",
|
||||
"mjp_getResourceProvider",
|
||||
"mjp_getResourceProviderAtSlot",
|
||||
"mjp_pluginCount",
|
||||
"mjp_registerDecoder",
|
||||
"mjp_registerEncoder",
|
||||
"mjp_registerPlugin",
|
||||
"mjp_registerResourceProvider",
|
||||
"mjp_resourceProviderCount",
|
||||
@@ -74,6 +77,8 @@ _SKIPPED_CLASS_METHODS: tuple[str, ...] = (
|
||||
"mj_deleteModel",
|
||||
"mj_deleteSpec",
|
||||
"mj_deleteVFS",
|
||||
"mj_encode",
|
||||
"mj_parse", # TODO(manevi): Bind this function.
|
||||
"mj_loadModel",
|
||||
"mj_loadXML",
|
||||
"mj_makeData",
|
||||
|
||||
Reference in New Issue
Block a user