Skip to content

Commit 2fec922

Browse files
havesscopybara-github
authored andcommitted
Introduce archive resource providers.
This change introduces a new registration function mjp_registerArchiveResourceProvider for mjpResourceProvider plugins. These special resource providers are meant to address asset fetching from within an "archive" such as an mjz, but it's easy to imagine other extensions such as reference buffers within a binary format, or a tar file. Assets can be addressed via regular path syntax anywhere filepaths are used in MuJoCo: ``` <mesh name="chair_leg" file="a/b/c.mjz/meshes/chair.obj"/> ``` By consequence, this allows us to remove the shared VFS requirement between model parsing and compilation for MJZ. ``` # BEFORE with mujoco.MjVfs() as vfs: spec = mujoco.MjSpec.from_file("some_asset.mjz", vfs=vfs) model = spec.compile(vfs=vfs) # AFTER spec = mujoco.MjSpec.from_file("some_asset.mjz") model = spec.compile() ``` A VFS can still be used if you need tight control over rsource management in tight loops compiling a lot of models, but we hope to address that specific usecase in a follow up soon. PiperOrigin-RevId: 983512715 Change-Id: I639bd80ca7edbec379ccf7aba667f742ea311323
1 parent 6297987 commit 2fec922

19 files changed

Lines changed: 999 additions & 251 deletions

File tree

‎doc/APIreference/functions.rst‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4108,6 +4108,36 @@ Return the encoder that matches against the content type or filename extension.
41084108

41094109
If no match, return NULL.
41104110

4111+
.. _mjp_registerArchiveResourceProvider:
4112+
4113+
`mjp_registerArchiveResourceProvider <#mjp_registerArchiveResourceProvider>`__
4114+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4115+
4116+
.. mujoco-include:: mjp_registerArchiveResourceProvider
4117+
4118+
Globally register an archive resource provider. This function is thread-safe.
4119+
provider->prefix specifies the filename extension(s) (e.g. .mjz|.zip).
4120+
4121+
.. _mjp_findArchiveResourceProvider:
4122+
4123+
`mjp_findArchiveResourceProvider <#mjp_findArchiveResourceProvider>`__
4124+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4125+
4126+
.. mujoco-include:: mjp_findArchiveResourceProvider
4127+
4128+
Return the archive resource provider that matches against the resource name.
4129+
4130+
If no match, return NULL.
4131+
4132+
.. _mjp_archiveResourceProviderCount:
4133+
4134+
`mjp_archiveResourceProviderCount <#mjp_archiveResourceProviderCount>`__
4135+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4136+
4137+
.. mujoco-include:: mjp_archiveResourceProviderCount
4138+
4139+
Return the number of globally registered archive resource providers.
4140+
41114141
.. _Thread:
41124142

41134143
Threads

‎doc/changelog.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ General
1616
while ``elastic2d`` elasticity is integrated implicitly through the effective metric. The contacts the mode resolves
1717
are frictionless. The mode keeps contact multipliers across steps that no state specification covers, so
1818
``mj_getState``/``mj_setState`` do not capture its full state and exact replay is not supported.
19+
- Introduced :ref:`archive resource providers<mjp_registerArchiveResourceProvider>`
20+
(``mjp_registerArchiveResourceProvider``). Archive providers use the
21+
:ref:`mjpResourceProvider` interface to mount and read archive containers (such as
22+
``.mjz``/``.zip``), decoupling container handling from format decoders and enabling
23+
on-demand asset extraction without requiring a pre-allocated :ref:`VFS<Virtualfilesystem>`.
24+
1925

2026
Engine
2127
^^^^^^

‎doc/includes/references.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4122,6 +4122,9 @@ const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* conten
41224122
void mjp_registerEncoder(const mjpEncoder* encoder);
41234123
void mjp_defaultEncoder(mjpEncoder* encoder);
41244124
const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
4125+
void mjp_registerArchiveResourceProvider(const mjpResourceProvider* provider);
4126+
const mjpResourceProvider* mjp_findArchiveResourceProvider(const char* resource_name);
4127+
int mjp_archiveResourceProviderCount(void);
41254128
mjResource* mju_openResource(const char* dir, const char* name,
41264129
const mjVFS* vfs, char* error, size_t nerror);
41274130
void mju_closeResource(mjResource* resource);

‎doc/programming/modeledit.rst‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,6 @@ When decoding an ``.mjz`` archive, MuJoCo searches for the root model XML file i
122122
Options 2-4 exist for backwards compatibility with legacy archives and common zip packaging layouts, but are
123123
susceptible to failure if the archive file is renamed.
124124

125-
VFS Requirement
126-
^^^^^^^^^^^^^^^
127-
128-
Parsing and compilation of an ``.mjz`` archive (and all of its contained asset files) require using the **exact same
129-
VFS instance**.
130-
131125
.. _meCustomFormats:
132126

133127
Custom formats

‎doc/python.rst‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,7 @@ Assets
522522
^^^^^^
523523

524524
MuJoCo optionally uses a :ref:`Virtual File System <Virtualfilesystem>` (VFS) to load assets (like meshes and textures)
525-
from memory. Some :ref:`decoders<exDecoder>` may also choose to leverage the VFS as a way to load assets on
526-
demand, such as when addressing files in an archive format. This requires the same VFS to be used when parsing and
527-
compiling a spec (and all attached specs) into a model.
525+
from memory.
528526

529527
The Python bindings provide the ``mujoco.MjVfs`` as a wrapper around the :ref:`mjVFS` C struct.
530528

‎include/mujoco/mujoco.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,16 @@ MJAPI void mjp_defaultEncoder(mjpEncoder* encoder);
15901590
// If no match, return NULL.
15911591
MJAPI const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
15921592

1593+
// Globally register an archive resource provider. This function is thread-safe.
1594+
// provider->prefix specifies the filename extension(s) (e.g. .mjz|.zip).
1595+
MJAPI void mjp_registerArchiveResourceProvider(const mjpResourceProvider* provider);
1596+
1597+
// Return the archive resource provider that matches against the resource name.
1598+
// If no match, return NULL.
1599+
MJAPI const mjpResourceProvider* mjp_findArchiveResourceProvider(const char* resource_name);
1600+
1601+
// Return the number of globally registered archive resource providers.
1602+
MJAPI int mjp_archiveResourceProviderCount(void);
15931603

15941604

15951605
//---------------------------------- Resources -----------------------------------------------------

‎python/mujoco/introspect/functions.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9975,6 +9975,43 @@
99759975
),
99769976
doc='Return the encoder that matches against the content type or filename extension. If no match, return NULL.', # pylint: disable=line-too-long
99779977
)),
9978+
('mjp_registerArchiveResourceProvider',
9979+
FunctionDecl(
9980+
name='mjp_registerArchiveResourceProvider',
9981+
return_type=ValueType(name='void'),
9982+
parameters=(
9983+
FunctionParameterDecl(
9984+
name='provider',
9985+
type=PointerType(
9986+
inner_type=ValueType(name='mjpResourceProvider', is_const=True), # pylint: disable=line-too-long
9987+
),
9988+
),
9989+
),
9990+
doc='Globally register an archive resource provider. This function is thread-safe. provider->prefix specifies the filename extension(s) (e.g. .mjz|.zip).', # pylint: disable=line-too-long
9991+
)),
9992+
('mjp_findArchiveResourceProvider',
9993+
FunctionDecl(
9994+
name='mjp_findArchiveResourceProvider',
9995+
return_type=PointerType(
9996+
inner_type=ValueType(name='mjpResourceProvider', is_const=True),
9997+
),
9998+
parameters=(
9999+
FunctionParameterDecl(
10000+
name='resource_name',
10001+
type=PointerType(
10002+
inner_type=ValueType(name='char', is_const=True),
10003+
),
10004+
),
10005+
),
10006+
doc='Return the archive resource provider that matches against the resource name. If no match, return NULL.', # pylint: disable=line-too-long
10007+
)),
10008+
('mjp_archiveResourceProviderCount',
10009+
FunctionDecl(
10010+
name='mjp_archiveResourceProviderCount',
10011+
return_type=ValueType(name='int'),
10012+
parameters=(),
10013+
doc='Return the number of globally registered archive resource providers.', # pylint: disable=line-too-long
10014+
)),
997810015
('mju_openResource',
997910016
FunctionDecl(
998010017
name='mju_openResource',

‎src/engine/engine_plugin.cc‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ void ForEachPipeSeparated(std::string_view str, Fn&& fn) {
141141
}
142142
}
143143

144+
struct ArchiveTag {};
145+
144146
// resource provider helpers
145147
std::string_view ResourceProviderKey(const mjpResourceProvider& provider) {
146148
return std::string_view(provider.prefix, strklen(provider.prefix));
@@ -151,6 +153,8 @@ bool ResourceProviderEqual(const mjpResourceProvider& p1, const mjpResourceProvi
151153
p1.open == p2.open &&
152154
p1.read == p2.read &&
153155
p1.close == p2.close &&
156+
p1.mount == p2.mount &&
157+
p1.unmount == p2.unmount &&
154158
p1.modified == p2.modified &&
155159
p1.write == p2.write &&
156160
p1.data == p2.data);
@@ -283,6 +287,9 @@ bool GlobalTable<mjpPlugin>::CopyObject(mjpPlugin& dst, const mjpPlugin& src, Er
283287
return true;
284288
}
285289

290+
291+
292+
// Standard URI resource provider table specialization
286293
template <>
287294
const char* GlobalTable<mjpResourceProvider>::HumanReadableTypeName() {
288295
return "resource provider";
@@ -304,6 +311,27 @@ bool GlobalTable<mjpResourceProvider>::CopyObject(mjpResourceProvider& dst, cons
304311
return ResourceProviderCopy(dst, src, err);
305312
}
306313

314+
// Archive resource provider table specialization
315+
template <>
316+
const char* GlobalTable<mjpResourceProvider, ArchiveTag>::HumanReadableTypeName() {
317+
return "archive resource provider";
318+
}
319+
320+
template <>
321+
std::string_view GlobalTable<mjpResourceProvider, ArchiveTag>::ObjectKey(const mjpResourceProvider& plugin) {
322+
return ResourceProviderKey(plugin);
323+
}
324+
325+
template <>
326+
bool GlobalTable<mjpResourceProvider, ArchiveTag>::ObjectEqual(const mjpResourceProvider& p1, const mjpResourceProvider& p2) {
327+
return ResourceProviderEqual(p1, p2);
328+
}
329+
330+
template <>
331+
bool GlobalTable<mjpResourceProvider, ArchiveTag>::CopyObject(mjpResourceProvider& dst, const mjpResourceProvider& src, ErrorMessage& err) {
332+
return ResourceProviderCopy(dst, src, err);
333+
}
334+
307335
template <>
308336
const char* GlobalTable<mjpDecoder>::HumanReadableTypeName() {
309337
return "resource decoder";
@@ -465,6 +493,8 @@ bool GlobalTable<mjpEncoder>::CopyObject(mjpEncoder& dst, const mjpEncoder& src,
465493
return true;
466494
}
467495

496+
497+
468498
// globally register a plugin (thread-safe), return new slot id
469499
int mjp_registerPlugin(const mjpPlugin* plugin) {
470500
if (!plugin->name) {
@@ -706,6 +736,47 @@ const mjpEncoder* mjp_findEncoder(const char* filename,
706736
return nullptr;
707737
}
708738

739+
// registers an archive resource provider
740+
void mjp_registerArchiveResourceProvider(const mjpResourceProvider* provider) {
741+
if (!provider || !provider->open || !provider->read || !provider->close) {
742+
mju_error("archive provider must provide open, read, and close callbacks.");
743+
return;
744+
}
745+
746+
if (!provider->prefix) {
747+
mju_error("archive provider must provide prefix (file extensions).");
748+
return;
749+
}
750+
751+
char ext_buf[kMaxNameLength + 1];
752+
ForEachPipeSeparated(provider->prefix, [&](std::string_view ext) {
753+
if (ext.size() <= kMaxNameLength) {
754+
std::memcpy(ext_buf, ext.data(), ext.size());
755+
ext_buf[ext.size()] = '\0';
756+
mjpResourceProvider provider_copy = *provider;
757+
provider_copy.prefix = ext_buf;
758+
GlobalTable<mjpResourceProvider, ArchiveTag>::GetSingleton().AppendIfUnique(provider_copy);
759+
}
760+
});
761+
}
762+
763+
// find an archive resource provider that matches a given resource or content type
764+
const mjpResourceProvider* mjp_findArchiveResourceProvider(const char* resource_name) {
765+
std::string extension = resource_name ? getext(resource_name) : "";
766+
if (extension.empty()) {
767+
return nullptr;
768+
}
769+
770+
return GlobalTable<mjpResourceProvider, ArchiveTag>::GetSingleton().GetByKey(
771+
extension.c_str(), nullptr);
772+
}
773+
774+
// return the number of globally registered archive resource providers
775+
int mjp_archiveResourceProviderCount() {
776+
return GlobalTable<mjpResourceProvider, ArchiveTag>::GetSingleton().count();
777+
}
778+
779+
709780
// load plugins from a dynamic library
710781
void mj_loadPluginLibrary(const char* path) {
711782
#if defined(_WIN32) || defined(__CYGWIN__)

‎src/engine/engine_plugin.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ MJAPI void mjp_defaultEncoder(mjpEncoder* encoder);
8080
// find an encoder that can write a given format
8181
MJAPI const mjpEncoder* mjp_findEncoder(const char* filename, const char* content_type);
8282

83+
// registers an archive resource provider
84+
MJAPI void mjp_registerArchiveResourceProvider(const mjpResourceProvider* provider);
85+
86+
// find an archive resource provider that matches a given name
87+
MJAPI const mjpResourceProvider* mjp_findArchiveResourceProvider(const char* resource_name);
88+
89+
// return the number of globally registered archive resource providers
90+
MJAPI int mjp_archiveResourceProviderCount(void);
91+
92+
8393
// =================================================================================================
8494
// MuJoCo-internal functions beyond this point.
8595
// "Unsafe" suffix indicates that improper use of these functions may result in data races.

0 commit comments

Comments
 (0)
⌂ 换网址