Move mjz_decoder out of experimental to xml/mjz.

Since the mjz format is just an archive for MJCF assets this seems like a sensible place. The upcoming mjz encoder will also need to make use of the full precision XML utility that is local to src/xml.

PiperOrigin-RevId: 912044746
Change-Id: I6a9fef24b1c3fec5b5edc3a8d4c8e22584273264
This commit is contained in:
Sam Haves
2026-05-07 10:51:28 -07:00
committed by Copybara-Service
parent 5f36cb9e64
commit eddfbcfc50
5 changed files with 24 additions and 21 deletions
+21
View File
@@ -0,0 +1,21 @@
# Copyright 2026 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
#
# https://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.
include(third_party_deps/miniz)
set(MUJOCO_MJZ_SRCS
mjz_decoder.cc
)
target_sources(mujoco PRIVATE ${MUJOCO_MJZ_SRCS})
target_link_libraries(mujoco miniz)
+219
View File
@@ -0,0 +1,219 @@
// Copyright 2026 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.
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <string>
#include <string_view>
#include <span>
#include <unordered_map>
#include <utility>
#include <vector>
#include <miniz_zip.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include "user/user_resource.h"
static void mjPRINTFLIKE(3, 4)
SetError(char* error, int error_sz, const char* format, ...) {
if (error) {
va_list args;
va_start(args, format);
std::vsnprintf(error, error_sz, format, args);
va_end(args);
}
}
// A mjpResourceProvider that reads files from a zip archive.
//
// The zip archive itself is provided as a byte buffer in the constructor. This
// class can then be used to lazily read the contents of the individual files
// from within the archive as needed.
class ZipArchiveProvider : public mjpResourceProvider {
public:
ZipArchiveProvider(std::string name, const void* buffer, int nbuffer,
char* error, int error_sz)
: name_(std::move(name)), buffer_((char*)buffer, (char*)buffer + nbuffer) {
mjp_defaultResourceProvider(this);
std::memset(&archive_, 0, sizeof(archive_));
if (!mz_zip_reader_init_mem(&archive_, buffer_.data(), nbuffer, 0)) {
SetError(error, error_sz, "Zip error: invalid zip archive");
return;
}
// Create an index of the files in the archive.
const int num_files = mz_zip_reader_get_num_files(&archive_);
for (int i = 0; i < num_files; ++i) {
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&archive_, i, &stat)) {
SetError(error, error_sz, "Zip error: failed to stat item %d.", i);
files_.clear();
return;
}
const int size = static_cast<int>(stat.m_uncomp_size);
if (size == 0) {
continue;
}
files_[stat.m_filename] = FileInfo{i, size, {}};
}
// Look for the root XML model in the archive. First look for an XML file
// with the same name as the archive itself. Failing that, look for an XML
// file within a subdirectory with the same name as the archive.
const std::filesystem::path path(name_);
root_model_ = (path / path.stem()).string() + ".xml";
if (!Contains(root_model_)) {
root_model_ = (path / path.stem() / path.stem()).string() + ".xml";
if (!Contains(root_model_)) {
SetError(error, error_sz, "Zip error: no root XML file found.");
return;
}
}
// Setup mjpResourceProvider callbacks.
mount = [](mjResource* resource) {
return 0;
};
unmount = [](mjResource* resource) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
delete self;
return 0;
};
open = [](mjResource* resource) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
const bool found = self->Contains(resource->name);
return found ? 1 : 0;
};
read = [](mjResource* resource, const void** buffer) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
std::span<char> bytes = self->Read(resource->name);
*buffer = bytes.data();
return static_cast<int>(bytes.size());
};
close = [](mjResource* resource) {
// no-op
};
}
~ZipArchiveProvider() {
mz_zip_reader_end(&archive_);
}
ZipArchiveProvider(const ZipArchiveProvider&) = delete;
ZipArchiveProvider& operator=(const ZipArchiveProvider&) = delete;
// Returns the path to the root XML model in the archive.
std::string GetRootModelPath() const {
return root_model_;
}
// Returns true if the archive contains a file with the given name/path.
bool Contains(std::string_view name) const {
const std::string_view filename = name.substr(name_.size() + 1);
return files_.find(filename.data()) != files_.end();
}
// Reads the contents of the file with the given name/path. The contents are
// cached internally so that subsequent reads for the same file do not need to
// re-read the file from the archive.
std::span<char> Read(const std::string& name) {
const std::string filename = name.substr(name_.size() + 1);
auto it = files_.find(filename);
if (it == files_.end()) {
return {};
}
FileInfo& info = it->second;
// Lazily read and store the file contents from the archive.
// The mutex is needed because mz_zip_archive is not thread-safe, and
// multiple threads may call Read concurrently during parallel compilation.
std::lock_guard<std::mutex> lock(mutex_);
if (info.contents.empty()) {
info.contents.resize(info.size);
if (!mz_zip_reader_extract_to_mem(&archive_, info.index,
info.contents.data(), info.size, 0)) {
return {};
}
}
return info.contents;
}
private:
struct FileInfo {
// Index of the file in the archive.
int index = 0;
// Size (in bytes) of the uncompressed file
int size = 0;
// Contents of the uncompressed file.
std::vector<char> contents;
};
std::string name_;
std::string root_model_;
mz_zip_archive archive_;
std::vector<char> buffer_;
std::unordered_map<std::string, FileInfo> files_;
mutable std::mutex mutex_;
};
static mjSpec* ParseZipBuffer(const void* buffer, int nbuffer, const char* name,
mjVFS* vfs, char* error, int error_sz) {
if (error) {
error[0] = 0;
}
ZipArchiveProvider* provider =
new ZipArchiveProvider(name, buffer, nbuffer, error, error_sz);
if (error && error[0]) {
return nullptr;
}
const int status = mj_mountVFS(vfs, name, provider);
if (status != 0) {
SetError(error, error_sz, "Failed to mount zip archive: %s", name);
return nullptr;
}
const std::string root = provider->GetRootModelPath();
return mj_parseXML(root.c_str(), vfs, error, error_sz);
}
mjPLUGIN_LIB_INIT(mjz_decoder) {
mjpDecoder decoder;
decoder.content_type = "application/zip";
decoder.extension = ".mjz|.zip";
decoder.can_decode = +[](const mjResource* resource) {
const char* ext = strrchr(resource->name, '.');
return ext ? (!strcmp(ext, ".mjz") || !strcmp(ext, ".zip")) : 0;
};
decoder.decode = +[](mjResource* resource, const mjVFS* vfs) -> mjSpec* {
const char* buffer = nullptr;
const int size = mju_readResource(resource, (const void**)&buffer);
if (size <= 0) {
return nullptr;
}
char error[1024];
return ParseZipBuffer(buffer, size, resource->name, const_cast<mjVFS*>(vfs),
error, sizeof(error));
};
mjp_registerDecoder(&decoder);
}