Re-implement VFS internals in C++, removing the constraints of the previous implementation.

PiperOrigin-RevId: 649433087
Change-Id: Icaa3e6b7f2ef14f56b04fef2f4360b46d8b5f022
This commit is contained in:
Kyle Bayes
2024-07-04 09:21:18 -07:00
committed by Copybara-Service
parent 118b6810b3
commit 57e6760ec9
19 changed files with 513 additions and 717 deletions
-6
View File
@@ -414,12 +414,6 @@ shown in the table below. Their names are in the format ``mjKEY_XXX``. They corr
- 50
- The maximum depth of each body and mesh bounding volume hierarchy. If this large limit is exceeded, a warning
is raised and ray casting may not be possible. For a balanced hierarchy, this implies 1E15 bounding volumes.
* - ``mjMAXVFS``
- 200
- The maximal number of characters in the name of each file in the virtual file system.
* - ``mjMAXVFSNAME``
- 100
- The maximal number of characters in the name of each file in the virtual file system.
* - ``mjNEQDATA``
- 11
- The maximal number of real-valued parameters used to define each equality constraint. Determines the size of
+22 -19
View File
@@ -10,11 +10,12 @@ General
.. admonition:: Breaking API changes
:class: attention
1. Removed deprecated ``mj_makeEmptyFileVFS`` and ``mj_findFileVFS`` functions.
1. Removed deprecated ``mj_makeEmptyFileVFS`` and ``mj_findFileVFS`` functions. The constants ``mjMAXVFS`` and
``mjMAXVFSNAME`` are also removed as they are no longer needed.
**Migration:** Use :ref:`mj_addBufferVFS` to copy a buffer into a VFS file directly.
2. Calls to:ref:`mj_defaultVFS` may allocate memory inside VFS, and the corresponding
2. Calls to :ref:`mj_defaultVFS` may allocate memory inside VFS, and the corresponding
:ref:`mj_deleteVFS` must be called to deallocate any internal allocated memory.
3. Deprecated :ref:`mju_rotVecMat` and :ref:`mju_rotVecMatT` in favor of :ref:`mju_mulMatVec3` and
@@ -22,7 +23,9 @@ General
The older functions have been removed from the Python bindings and will be removed from the C API in the next
release.
4. Added a new API for :doc:`procedural model manipulation<programming/modeledit>`. Fixes :github:issue:`364`.
4. The :ref:`VFS<Virtualfilesystem>` implementation has been rewritten in C++ and is now considerably more efficient in
speed and memory footprint.
5. Added a new API for :doc:`procedural model manipulation<programming/modeledit>`. Fixes :github:issue:`364`.
Still missing:
- Detailed documentation.
@@ -31,27 +34,27 @@ General
:align: right
:width: 240px
5. Added support for orthographic cameras. This is available for both fixed cameras and the free camera, using the
6. Added support for orthographic cameras. This is available for both fixed cameras and the free camera, using the
:ref:`camera/orthographic<body-camera-orthographic>` and :ref:`global/orthographic<visual-global-orthographic>`
attributes, respectively.
6. Added :ref:`maxhullvert<asset-mesh-maxhullvert>`, the maximum number of vertices in a mesh's convex hull.
7. Added :ref:`mj_setKeyframe` for saving the current state into a model keyframe.
8. Added support for ``ball`` joints in the URDF parser ("spherical" in URDF).
9. Replaced ``mjUSEDOUBLE`` which was previously hard-coded in
`mjtnum.h <https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjtnum.h>`__
with the build-time flag ``mjUSESINGLE``. If this symbol is not defined, MuJoCo will use double-precision floating
point, as usual. If ``mjUSESINGLE`` is defined, MuJoCo will use single-precision floating point. See :ref:`mjtNum`.
7. Added :ref:`maxhullvert<asset-mesh-maxhullvert>`, the maximum number of vertices in a mesh's convex hull.
8. Added :ref:`mj_setKeyframe` for saving the current state into a model keyframe.
9. Added support for ``ball`` joints in the URDF parser ("spherical" in URDF).
10. Replaced ``mjUSEDOUBLE`` which was previously hard-coded in
`mjtnum.h <https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjtnum.h>`__
with the build-time flag ``mjUSESINGLE``. If this symbol is not defined, MuJoCo will use double-precision floating
point, as usual. If ``mjUSESINGLE`` is defined, MuJoCo will use single-precision floating point. See :ref:`mjtNum`.
Relatedly, fixed various type errors that prevented building with single-precision.
10. Quaternions in ``mjData->qpos`` and ``mjData->mocap_quat`` are no longer normalized in-place by
Relatedly, fixed various type errors that prevented building with single-precision.
11. Quaternions in ``mjData->qpos`` and ``mjData->mocap_quat`` are no longer normalized in-place by
:ref:`mj_kinematics`. Instead they are normalized when they are used. After the first step, quaternions in
``mjData->qpos`` will be normalized.
MJX
~~~
10. Added support for :ref:`elliptic friction cones<option-cone>`.
11. Fixed a bug that resulted in less-optimal linesearch solutions for some difficult constraint settings.
12. Fixed a bug in the Newton solver that sometimes resulted in less-optimal gradients.
12. Added support for :ref:`elliptic friction cones<option-cone>`.
13. Fixed a bug that resulted in less-optimal linesearch solutions for some difficult constraint settings.
14. Fixed a bug in the Newton solver that sometimes resulted in less-optimal gradients.
.. youtube:: P83tKA1iz2Y
@@ -60,14 +63,14 @@ MJX
Simulate
^^^^^^^^
13. Added improved tutorial video.
14. Improved the Brownian noise generator.
15. Added improved tutorial video.
16. Improved the Brownian noise generator.
|br| |br| |br| |br|
Python bindings
^^^^^^^^^^^^^^^
15. Fixed a memory leak when using ``copy.deepcopy()`` on a ``mujoco.MjData`` instance (:github:issue:`1572`).
17. Fixed a memory leak when using ``copy.deepcopy()`` on a ``mujoco.MjData`` instance (:github:issue:`1572`).
Version 3.1.6 (Jun 3, 2024)
---------------------------
+1 -5
View File
@@ -699,11 +699,7 @@ struct mjLROpt_ { // options for mj_setLengthRange()
};
typedef struct mjLROpt_ mjLROpt;
struct mjVFS_ { // virtual file system for loading from memory
int nfile; // number of files present
char filename[mjMAXVFS][mjMAXVFSNAME]; // file name without path
size_t filesize[mjMAXVFS]; // file size in bytes
void* filedata[mjMAXVFS]; // buffer with file data
uint64_t filestamp[mjMAXVFS]; // checksum of the file data
void* impl_; // internal pointer to VFS memory
};
typedef struct mjVFS_ mjVFS;
struct mjOption_ { // physics options
+1 -7
View File
@@ -29,8 +29,6 @@
#define mjMAXIMP 0.9999 // maximum constraint impedance
#define mjMAXCONPAIR 50 // maximum number of contacts per geom pair
#define mjMAXTREEDEPTH 50 // maximum bounding volume hierarchy depth
#define mjMAXVFS 2000 // maximum number of files in virtual file system
#define mjMAXVFSNAME 1000 // maximum filename size in virtual file system
//---------------------------------- sizes ---------------------------------------------------------
@@ -399,11 +397,7 @@ typedef struct mjLROpt_ mjLROpt;
//---------------------------------- mjVFS ---------------------------------------------------------
struct mjVFS_ { // virtual file system for loading from memory
int nfile; // number of files present
char filename[mjMAXVFS][mjMAXVFSNAME]; // file name without path
size_t filesize[mjMAXVFS]; // file size in bytes
void* filedata[mjMAXVFS]; // buffer with file data
uint64_t filestamp[mjMAXVFS]; // checksum of the file data
void* impl_; // internal pointer to VFS memory
};
typedef struct mjVFS_ mjVFS;
+4 -36
View File
@@ -91,43 +91,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([
declname='struct mjVFS_',
fields=(
StructFieldDecl(
name='nfile',
type=ValueType(name='int'),
doc='number of files present',
),
StructFieldDecl(
name='filename',
type=ArrayType(
inner_type=ValueType(name='char'),
extents=(2000, 1000),
name='impl_',
type=PointerType(
inner_type=ValueType(name='void'),
),
doc='file name without path',
),
StructFieldDecl(
name='filesize',
type=ArrayType(
inner_type=ValueType(name='size_t'),
extents=(2000,),
),
doc='file size in bytes',
),
StructFieldDecl(
name='filedata',
type=ArrayType(
inner_type=PointerType(
inner_type=ValueType(name='void'),
),
extents=(2000,),
),
doc='buffer with file data',
),
StructFieldDecl(
name='filestamp',
type=ArrayType(
inner_type=ValueType(name='uint64_t'),
extents=(2000,),
),
doc='checksum of the file data',
doc='internal pointer to VFS memory',
),
),
)),
-16
View File
@@ -409,19 +409,6 @@ class MuJoCoBindingsTest(parameterized.TestCase):
np.testing.assert_array_equal(self.model.geom_size[1], [0.5, 0.5, 0.5])
np.testing.assert_array_equal(model_copy.geom_size[1], [0.1, 0.1, 0.1])
def test_assets_array_filename_too_long(self):
# Longest allowed filename (excluding null byte)
limit = mujoco.mjMAXVFSNAME - 1
contents = b'<mujoco/>'
valid_filename = 'a' * limit
mujoco.MjModel.from_xml_path(valid_filename, {valid_filename: contents})
invalid_filename = 'a' * (limit + 1)
expected_message = (
f'Filename length 1000 exceeds 999 character limit: {invalid_filename}')
with self.assertRaisesWithLiteralMatch(ValueError, expected_message):
mujoco.MjModel.from_xml_path(invalid_filename,
{invalid_filename: contents})
def test_mjdata_can_copy(self):
self.data.qpos = [0, 0, 0.1*np.sqrt(2) - 0.001,
np.cos(np.pi/8), np.sin(np.pi/8), 0, 0, 0,
@@ -856,9 +843,6 @@ Return the current version of MuJoCo as a null-terminated string.
Euler integrator, semi-implicit in velocity.
""")
def test_int_constant(self):
self.assertEqual(mujoco.mjMAXVFSNAME, 1000)
def test_float_constant(self):
self.assertEqual(mujoco.mjMAXVAL, 1e10)
self.assertEqual(mujoco.mjMINVAL, 1e-15)
-2
View File
@@ -57,8 +57,6 @@ PYBIND11_MODULE(_constants, pymodule) {
X(mjMINIMP);
X(mjMAXIMP);
X(mjMAXCONPAIR);
X(mjMAXVFS);
X(mjMAXVFSNAME);
X(mjNEQDATA);
X(mjNDYN);
X(mjNGAIN);
+7 -15
View File
@@ -317,24 +317,22 @@ static raw::MjModel* LoadModelFileImpl(
const std::string& filename,
const std::vector<VfsAsset>& assets,
LoadFunc&& loadfunc) {
std::unique_ptr<mjVFS, void(*)(mjVFS*)> vfs(nullptr, [](mjVFS*){});
mjVFS vfs;
mjVFS* vfs_ptr = nullptr;
if (!assets.empty()) {
// mjVFS should be allocated on the heap, because it's ~2MB
vfs = decltype(vfs)(new mjVFS, [](mjVFS* vfs) {
mj_deleteVFS(vfs);
delete vfs;
});
mj_defaultVFS(vfs.get());
mj_defaultVFS(&vfs);
vfs_ptr = &vfs;
for (const auto& asset : assets) {
const int vfs_error = InterceptMjErrors(mj_addBufferVFS)(
vfs.get(), asset.name, asset.content, asset.content_size);
vfs_ptr, asset.name, asset.content, asset.content_size);
if (vfs_error) {
throw py::value_error("assets dict is too big");
}
}
}
raw::MjModel* model = loadfunc(filename.c_str(), vfs.get());
raw::MjModel* model = loadfunc(filename.c_str(), vfs_ptr);
mj_deleteVFS(vfs_ptr);
if (model && !model->buffer) {
mj_deleteModel(model);
model = nullptr;
@@ -351,12 +349,6 @@ ConvertAssetsDict(
std::vector<VfsAsset> out;
if (assets.has_value()) {
for (const auto& [name, content] : *assets) {
if (name.length() >= mjMAXVFSNAME) {
std::ostringstream error;
error << "Filename length " << name.length() << " exceeds "
<< mjMAXVFSNAME - 1 << " character limit: " << name;
throw py::value_error(error.str());
}
out.emplace_back(name.c_str(), PYBIND11_BYTES_AS_STRING(content.ptr()),
py::len(content));
}
-45
View File
@@ -1407,51 +1407,6 @@ char* mju_strncpy(char *dst, const char *src, int n) {
// assemble full filename from directory and filename, return 0 on success
int mju_makefullname(char* full, size_t nfull, const char* dir, const char* file) {
int dirlen = (!dir) ? 0 : strlen(dir);
int filelen = (!file) ? 0 : strlen(file);
char* filepos = full + dirlen;
// missing filename
if (!filelen) {
return -1;
}
// no directory then just copy filename over
if (!dirlen) {
// make sure full has space
if (filelen >= nfull) {
return -1;
}
strcpy(full, file);
return 0;
}
// make sure full has space
if (dirlen + filelen >= nfull) {
return -1;
}
// dir doesn't end with a slash
if (dir[dirlen - 1] != '\\' && dir[dirlen - 1] != '/') {
// need extra space for forward slash
if ((dirlen + filelen + 1) >= nfull) {
return -1;
}
// add forward slash
*filepos++ = '/';
}
// copy directory and file over
memcpy(full, dir, sizeof(char) * dirlen);
strcpy(filepos, file);
return 0;
}
// sigmoid function over 0<=x<=1 using quintic polynomial
mjtNum mju_sigmoid(mjtNum x) {
// fast return
-4
View File
@@ -164,10 +164,6 @@ MJAPI mjtNum mju_Halton(int index, int base);
// call strncpy, then set dst[n-1] = 0
MJAPI char* mju_strncpy(char *dst, const char *src, int n);
// assemble full filename from directory and filename, return 0 on success
MJAPI int mju_makefullname(char* full, size_t nfull,
const char* dir, const char* file);
// sigmoid function over 0<=x<=1 using quintic polynomial
MJAPI mjtNum mju_sigmoid(mjtNum x);
+1 -1
View File
@@ -29,7 +29,7 @@ set(MUJOCO_USER_SRCS
user_objects.h
user_util.cc
user_util.h
user_vfs.c
user_vfs.cc
user_vfs.h
)
-420
View File
@@ -1,420 +0,0 @@
// Copyright 2021 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 "user/user_vfs.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include "engine/engine_array_safety.h"
#include "engine/engine_resource.h"
#include "engine/engine_util_errmem.h"
#include "engine/engine_util_misc.h"
// strip path prefix from filename
static void vfs_strippath(char* newname, const char* oldname) {
int sz = strlen(oldname);
// find last delimiter
int i;
for (i=sz-1; i >= 0; i--) {
if (oldname[i] == '\\' || oldname[i] == '/') {
break;
}
}
// check resulting length
if (sz-(i+1) >= mjMAXVFSNAME) {
mjERROR("filename too long");
}
if (sz-(i+1) <= 0) {
mjERROR("empty filename");
}
// copy
mju_strncpy(newname, oldname+i+1, mjMAXVFSNAME);
// make lowercase
for (int j=strlen(newname)-1; j >= 0; j--) {
if (newname[j] >= 'A' && newname[j] <= 'Z') {
newname[j] = (char)(((int)newname[j]) +'a' - 'A');
}
}
}
// copies data into a buffer and produces a hash of the data
static uint64_t vfs_memcpy(void* dest, const void* restrict src, size_t n) {
uint64_t hash = 0xcbf29ce484222325; // magic number
uint64_t prime = 0x100000001b3; // magic prime
const uint8_t* bytes = (uint8_t*) src;
uint8_t* buffer = (uint8_t*) dest;
for (size_t i = 0; i < n; i++) {
buffer[i] = bytes[i];
// do FNV-1 hash
hash |= bytes[i];
hash *= prime;
}
return hash;
}
// VFS hash function implemented using the FNV-1 hash
static uint64_t vfs_hash(const void* restrict buffer, size_t n) {
uint64_t hash = 0xcbf29ce484222325; // magic number
uint64_t prime = 0x100000001b3; // magic prime
const uint8_t* bytes = (uint8_t*) buffer;
for (size_t i = 0; i < n; i++) {
hash |= bytes[i];
hash *= prime;
}
return hash;
}
// initialize to empty (no deallocation)
void mj_defaultVFS(mjVFS* vfs) {
memset(vfs, 0, sizeof(mjVFS));
}
// add file to VFS, return 0: success, 1: full, 2: repeated name, -1: failed to load
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename) {
// check vfs size
if (vfs->nfile >= mjMAXVFS-1) {
return 1;
}
// make full name
char fullname[1000];
if (mju_makefullname(fullname, sizeof(fullname), directory, filename)) {
return -1;
}
// strip path
char newname[mjMAXVFSNAME];
vfs_strippath(newname, filename);
// check for repeated name
for (int i=0; i < vfs->nfile; i++) {
if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME) == 0) {
return 2;
}
}
// assign name
mjSTRNCPY(vfs->filename[vfs->nfile], newname);
// allocate and read
size_t filesize = 0;
vfs->filedata[vfs->nfile] = mju_fileToMemory(fullname, &filesize);
if (!vfs->filedata[vfs->nfile]) {
return -1;
}
// assign size, count, and checksum
vfs->filestamp[vfs->nfile] = vfs_hash(vfs->filedata[vfs->nfile], filesize);
vfs->filesize[vfs->nfile] = filesize;
vfs->nfile++;
return 0;
}
// make empty file in VFS, return 0: success, 1: full, 2: repeated name
int mj_makeEmptyFileVFS(mjVFS* vfs, const char* filename, int filesize) {
// check vfs size
if (vfs->nfile >= mjMAXVFS-1) {
return 1;
}
// check filesize
if (filesize <= 0) {
mjERROR("expects positive filesize");
}
// strip path
char newname[mjMAXVFSNAME];
vfs_strippath(newname, filename);
// check for repeated name
for (int i=0; i < vfs->nfile; i++) {
if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME) == 0) {
return 2;
}
}
// assign name
mjSTRNCPY(vfs->filename[vfs->nfile], newname);
// allocate and clear
vfs->filedata[vfs->nfile] = mju_malloc(filesize);
if (!vfs->filedata[vfs->nfile]) {
mjERROR("could not allocate memory");
}
memset(vfs->filedata[vfs->nfile], 0, filesize);
vfs->filestamp[vfs->nfile] = 0;
// assign size and count
vfs->filesize[vfs->nfile] = filesize;
vfs->nfile++;
return 0;
}
// add file from buffer into VFS
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, int nbuffer) {
if (!vfs || !buffer || !name) {
mjERROR("null pointer");
}
if (vfs->nfile >= mjMAXVFS-1) {
return 1;
}
// check buffer size
if (nbuffer <= 0) {
mjERROR("expects positive buffer size");
}
// strip path
char newname[mjMAXVFSNAME];
vfs_strippath(newname, name);
// check for repeated name
for (int i=0; i < vfs->nfile; i++) {
if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME) == 0) {
return 2;
}
}
// assign name
mjSTRNCPY(vfs->filename[vfs->nfile], newname);
// allocate and clear
vfs->filedata[vfs->nfile] = mju_malloc(nbuffer);
if (!vfs->filedata[vfs->nfile]) {
mjERROR("could not allocate memory");
}
vfs->filestamp[vfs->nfile] = vfs_memcpy(vfs->filedata[vfs->nfile], buffer, nbuffer);
// assign size and count
vfs->filesize[vfs->nfile] = nbuffer;
vfs->nfile++;
return 0;
}
// return file index in VFS, or -1 if not found in VFS
int mj_findFileVFS(const mjVFS* vfs, const char* filename) {
// strip path
char newname[mjMAXVFSNAME];
vfs_strippath(newname, filename);
// find specific file
for (int i=0; i < vfs->nfile; i++) {
if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME) == 0) {
return i;
}
}
return -1;
}
// delete file from VFS, return 0: success, -1: not found in VFS
int mj_deleteFileVFS(mjVFS* vfs, const char* filename) {
// strip path
char newname[mjMAXVFSNAME];
vfs_strippath(newname, filename);
// find specified file
for (int i=0; i < vfs->nfile; i++) {
if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME) == 0) {
// free buffer
mju_free(vfs->filedata[i]);
// scroll remaining files forward
for (int j=i; j < vfs->nfile-1; j++) {
mjSTRNCPY(vfs->filename[j], vfs->filename[j+1]);
vfs->filesize[j] = vfs->filesize[j+1];
vfs->filedata[j] = vfs->filedata[j+1];
}
// set last to 0, for style
vfs->filename[vfs->nfile-1][0] = 0;
vfs->filesize[vfs->nfile-1] = 0;
vfs->filedata[vfs->nfile-1] = NULL;
// decrease counter
vfs->nfile--;
return 0;
}
}
return -1;
}
// delete all files from VFS
void mj_deleteVFS(mjVFS* vfs) {
for (int i=0; i < vfs->nfile; i++) {
mju_free(vfs->filedata[i]);
}
memset(vfs, 0, sizeof(mjVFS));
}
// open callback for the VFS resource provider
static int vfs_open_callback(mjResource* resource) {
if (!resource || !resource->name || !resource->data) {
return 0;
}
const mjVFS* vfs = (const mjVFS*) resource->data;
int i = mj_findFileVFS(vfs, resource->name);
resource->timestamp[0] = '\0';
if (i >= 0 && vfs->filestamp[i]) {
mju_encodeBase64(resource->timestamp, (uint8_t*) &vfs->filestamp[i],
sizeof(uint64_t));
}
return i >= 0;
}
// read callback for the VFS resource provider
static int vfs_read_callback(mjResource* resource, const void** buffer) {
if (!resource || !resource->name || !resource->data) {
*buffer = NULL;
return -1;
}
const mjVFS* vfs = (const mjVFS*) resource->data;
int i = mj_findFileVFS(vfs, resource->name);
if (i < 0) {
*buffer = NULL;
return -1;
}
*buffer = vfs->filedata[i];
return vfs->filesize[i];
}
// close callback for the VFS resource provider
static void vfs_close_callback(mjResource* resource) {
}
// getdir callback for the VFS resource provider
static void vfs_getdir_callback(mjResource* resource, const char** dir, int* ndir) {
if (resource) {
*dir = resource->name;
*ndir = mju_dirnamelen(resource->name);
} else {
*dir = NULL;
*ndir = 0;
}
}
// modified callback for the VFS resource provider
// return > 0 if modified and 0 if unmodified
static int vfs_modified_callback(const mjResource* resource, const char* timestamp) {
uint64_t filestamp;
if (mju_isValidBase64(timestamp) > sizeof(uint64_t)) {
return 2; // error (assume modified)
}
mju_decodeBase64((uint8_t*) &filestamp, timestamp);
if (!filestamp) return 3; // no hash (assume modified)
if (resource) {
const mjVFS* vfs = (const mjVFS*) resource->data;
int i = mj_findFileVFS(vfs, resource->name);
if (i < 0) return 4; // missing file (assume modified)
if (!vfs->filestamp[i]) return 5; // missing filestamp (assume modified)
if (vfs->filestamp[i] == filestamp) {
return 0; // unmodified
}
}
return 1; // modified
}
// open VFS resource
mjResource* mju_openVfsResource(const char* name, const mjVFS* vfs) {
if (vfs == NULL) {
return NULL;
}
// VFS provider
static struct mjpResourceProvider provider = {
.prefix = NULL,
.data = NULL,
.open = &vfs_open_callback,
.read = &vfs_read_callback,
.close = &vfs_close_callback,
.getdir = &vfs_getdir_callback,
.modified = &vfs_modified_callback,
};
// create resource
mjResource* resource = (mjResource*) mju_malloc(sizeof(mjResource));
if (resource == NULL) {
mjERROR("could not allocate memory");
return NULL;
}
// clear out resource
memset(resource, 0, sizeof(mjResource));
// copy name
resource->name = mju_malloc(sizeof(char) * (strlen(name) + 1));
if (resource->name == NULL) {
mju_closeResource(resource);
mjERROR("could not allocate memory");
return NULL;
}
memcpy(resource->name, name, sizeof(char) * (strlen(name) + 1));
resource->data = (void*) vfs;
// open resource
resource->provider = &provider;
if (provider.open(resource)) {
return resource;
}
// not found in VFS
mju_closeResource(resource);
return NULL;
}
+322
View File
@@ -0,0 +1,322 @@
// Copyright 2021 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 "user/user_vfs.h"
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstring>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include "engine/engine_resource.h"
#include "engine/engine_util_errmem.h"
#include "engine/engine_util_misc.h"
#include "user/user_util.h"
namespace {
// internal struct for VFS files
struct VFSFile {
std::string filename;
std::unique_ptr<void, std::function<void(void*)>> filedata;
std::size_t filesize;
uint64_t filestamp;
};
// internal container class for VFS
class VFS {
public:
// returns true if the file exists in the VFS
bool HasFile(const std::string& filename) const;
// returns inserted mjuuVFSFile if the file was added successfully. This class
// assumes ownership of the buffer and will free it when the VFS is deleted.
VFSFile* AddFile(const std::string& filename, void* buffer,
std::size_t nbuffer, uint64_t filestamp);
// returns the internal file struct for the given filename
const VFSFile* GetFile(const std::string& filename) const;
// deletes file from VFS, return 0: success, -1: not found
int DeleteFile(const std::string& filename);
private:
std::unordered_map<std::string, VFSFile> files_;
};
// returns the internal VFS class pointer from the VFS C struct
inline VFS* GetVFSImpl(const mjVFS* vfs) {
return vfs->impl_ ? static_cast<VFS*>(vfs->impl_) : nullptr;
}
// strip path prefix from filename and make lowercase
std::string StripPath(const char* name) {
std::string newname = mjuu_strippath(name);
// make lowercase
std::transform(newname.begin(), newname.end(), newname.begin(),
[](unsigned char c) { return std::tolower(c); });
return newname;
}
// copies data into a buffer and produces a hash of the data
uint64_t vfs_memcpy(void* dest, const void* src, size_t n) {
uint64_t hash = 0xcbf29ce484222325; // magic number
uint64_t prime = 0x100000001b3; // magic prime
const uint8_t* bytes = (uint8_t*) src;
uint8_t* buffer = (uint8_t*) dest;
for (size_t i = 0; i < n; i++) {
buffer[i] = bytes[i];
// do FNV-1 hash
hash |= bytes[i];
hash *= prime;
}
return hash;
}
// VFS hash function implemented using the FNV-1 hash
uint64_t vfs_hash(const void* buffer, size_t n) {
uint64_t hash = 0xcbf29ce484222325; // magic number
uint64_t prime = 0x100000001b3; // magic prime
const uint8_t* bytes = (uint8_t*) buffer;
for (size_t i = 0; i < n; i++) {
hash |= bytes[i];
hash *= prime;
}
return hash;
}
bool VFS::HasFile(const std::string& filename) const {
return files_.find(filename) != files_.end();
}
VFSFile* VFS::AddFile(const std::string& filename, void* buffer,
std::size_t nbuffer, uint64_t filestamp) {
auto [it, inserted] = files_.insert({filename, VFSFile()});
if (!inserted) {
return nullptr; // repeated name
}
it->second.filename = filename;
it->second.filedata = std::unique_ptr<void, void(*)(void*)>(
buffer, [](void* b) { mju_free(b); }); // corresponding to mju_malloc
it->second.filesize = nbuffer;
it->second.filestamp = filestamp;
return &(it->second);
}
const VFSFile* VFS::GetFile(const std::string& filename) const {
auto it = files_.find(filename);
if (it == files_.end()) {
return nullptr;
}
return &it->second;
}
int VFS::DeleteFile(const std::string& filename) {
auto it = files_.find(filename);
if (it == files_.end()) {
return -1;
}
files_.erase(it);
return 0;
}
// open callback for the VFS resource provider
int Open(mjResource* resource) {
if (!resource || !resource->name || !resource->data) {
return 0;
}
const mjVFS* vfs = (const mjVFS*) resource->data;
const VFS* cvfs = GetVFSImpl(vfs);
const VFSFile* file = cvfs->GetFile(StripPath(resource->name));
if (file == nullptr) {
return 0;
}
resource->timestamp[0] = '\0';
if (file->filestamp) {
mju_encodeBase64(resource->timestamp, (uint8_t*) &file->filestamp,
sizeof(uint64_t));
}
return 1;
}
// read callback for the VFS resource provider
int Read(mjResource* resource, const void** buffer) {
if (!resource || !resource->name || !resource->data) {
*buffer = nullptr;
return -1;
}
const VFS* vfs = GetVFSImpl(static_cast<const mjVFS*>(resource->data));
const VFSFile* file = vfs->GetFile(StripPath(resource->name));
if (file == nullptr) {
*buffer = nullptr;
return -1;
}
*buffer = file->filedata.get();
return file->filesize;
}
// close callback for the VFS resource provider
void Close(mjResource* resource) {
}
// getdir callback for the VFS resource provider
void GetDir(mjResource* resource, const char** dir, int* ndir) {
*dir = (resource) ? resource->name : nullptr;
*ndir = (resource) ? mju_dirnamelen(resource->name) : 0;
}
// modified callback for the VFS resource provider
// return > 0 if modified and 0 if unmodified
int Modified(const mjResource* resource, const char* timestamp) {
uint64_t filestamp;
if (mju_isValidBase64(timestamp) > sizeof(uint64_t)) {
return 2; // error (assume modified)
}
mju_decodeBase64((uint8_t*) &filestamp, timestamp);
if (!filestamp) return 3; // no hash (assume modified)
if (resource) {
const VFS* cvfs = GetVFSImpl(static_cast<const mjVFS*>(resource->data));
const VFSFile* file = cvfs->GetFile(StripPath(resource->name));
if (file == nullptr) return 4; // missing file (assume modified)
if (!file->filestamp) return 5; // missing filestamp (assume modified)
if (file->filestamp == filestamp) {
return 0; // unmodified
}
}
return 1; // modified
}
} // namespace
// initialize to empty (no deallocation)
void mj_defaultVFS(mjVFS* vfs) {
vfs->impl_ = new VFS();
}
// add file to VFS, return 0: success, 2: repeated name, -1: failed to load
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename) {
VFS* cvfs = GetVFSImpl(vfs);
// make full name
std::string fullname = mjuu_combinePaths(directory, filename);
// strip path
std::string newname = StripPath(filename);
// check beforehand for repeated name, to avoid reading file into memory
if (cvfs->HasFile(newname)) {
return 2;
}
// allocate and read
size_t nbuffer = 0;
void* buffer = mju_fileToMemory(fullname.c_str(), &nbuffer);
if (buffer == nullptr) {
return -1;
}
if (!cvfs->AddFile(newname, buffer, nbuffer, vfs_hash(buffer, nbuffer))) {
mju_free(buffer);
return 2; // AddFile failed, SHOULD NOT OCCUR
}
return 0;
}
// add file from buffer into VFS
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer,
int nbuffer) {
VFS* cvfs = GetVFSImpl(vfs);
// allocate and clear
void* inbuffer = mju_malloc(nbuffer);
if (buffer == nullptr) {
mjERROR("could not allocate memory");
}
VFSFile* file;
if (!(file = cvfs->AddFile(StripPath(name), inbuffer, nbuffer, 0))) {
mju_free(inbuffer);
return 2; // AddFile failed, repeated name
}
file->filestamp = vfs_memcpy(inbuffer, buffer, nbuffer);
return 0;
}
// delete file from VFS, return 0: success, -1: not found in VFS
int mj_deleteFileVFS(mjVFS* vfs, const char* filename) {
VFS* cvfs = GetVFSImpl(vfs);
return cvfs->DeleteFile(StripPath(filename));
}
// delete all files from VFS
void mj_deleteVFS(mjVFS* vfs) {
if (vfs) {
delete GetVFSImpl(vfs);
}
}
// open VFS resource
mjResource* mju_openVfsResource(const char* name, const mjVFS* vfs) {
if (vfs == nullptr) {
return nullptr;
}
// VFS provider
static struct mjpResourceProvider provider = { nullptr, &Open, &Read, &Close,
&GetDir, &Modified, nullptr };
// create resource
mjResource* resource = (mjResource*) mju_malloc(sizeof(mjResource));
if (resource == nullptr) {
mjERROR("could not allocate memory");
return nullptr;
}
// clear out resource
memset(resource, 0, sizeof(mjResource));
// copy name
std::size_t n = std::strlen(name);
resource->name = (char*) mju_malloc(sizeof(char) * (n + 1));
if (resource->name == nullptr) {
mju_closeResource(resource);
mjERROR("could not allocate memory");
return nullptr;
}
std::memcpy(resource->name, name, sizeof(char) * (n + 1));
resource->data = (void*) vfs;
// open resource
resource->provider = &provider;
if (provider.open(resource)) {
return resource;
}
// not found in VFS
mju_closeResource(resource);
return nullptr;
}
+2 -2
View File
@@ -30,13 +30,13 @@ extern "C" {
// Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS
MJAPI void mj_defaultVFS(mjVFS* vfs);
// add file to VFS, return 0: success, 1: full, 2: repeated name, -1: not found on disk
// add file to VFS, return 0: success, 2: repeated name, -1: not found on disk
MJAPI int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
// deprecated: use mj_addBufferVFS
MJAPI int mj_makeEmptyFileVFS(mjVFS* vfs, const char* filename, int filesize);
// add file from buffer into VFS, return 0: success, 1: full, 2: repeated name, -1: failed to load
// add file from buffer into VFS, return 0: success, 2: repeated name, -1: failed to load
MJAPI int mj_addBufferVFS(mjVFS* vfs, const char* filename, const void* buffer, int nbuffer);
// return file index in VFS, or -1 if not found in VFS
-86
View File
@@ -148,92 +148,6 @@ TEST_F(MujocoTest, MuscleGainLength) {
EXPECT_EQ(mju_muscleGainLength(2.0, lmin, lmax), 0);
}
TEST_F(MujocoTest, mju_makefullname) {
char buffer[1000];
constexpr char path[] = "engine/testdata/";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
ASSERT_THAT(buffer, StrEq("engine/testdata/file"));
EXPECT_THAT(n, 0);
}
TEST_F(MujocoTest, mju_makefullname2) {
char buffer[1000];
constexpr char path[] = "engine\\testdata\\";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
ASSERT_THAT(buffer, StrEq("engine\\testdata\\file"));
EXPECT_THAT(n, 0);
}
TEST_F(MujocoTest, mju_makefullname_missingSlash) {
char buffer[1000];
constexpr char path[] = "engine/testdata";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
ASSERT_THAT(buffer, StrEq("engine/testdata/file"));
EXPECT_THAT(n, 0);
}
TEST_F(MujocoTest, mju_makefullname_withoutDir) {
char buffer[1000];
constexpr char *path = NULL;
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
ASSERT_THAT(buffer, StrEq("file"));
EXPECT_THAT(n, 0);
}
TEST_F(MujocoTest, mju_makefullname_withoutDir2) {
char buffer[1000];
constexpr char path[] = "";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
ASSERT_THAT(buffer, StrEq("file"));
EXPECT_THAT(n, 0);
}
TEST_F(MujocoTest, mju_makefullname_error) {
char buffer[1000];
constexpr char path[] = "engine/testdata";
constexpr char *file = NULL;
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
EXPECT_THAT(n, Ne(0));
}
TEST_F(MujocoTest, mju_makefullname_error2) {
char buffer[1000];
constexpr char path[] = "engine/testdata";
constexpr char file[] = "";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
EXPECT_THAT(n, Ne(0));
}
TEST_F(MujocoTest, mju_makefullname_error3) {
char buffer[20];
constexpr char path[] = "engine/testdata/";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
EXPECT_THAT(n, Ne(0));
}
TEST_F(MujocoTest, mju_makefullname_error4) {
char buffer[20];
constexpr char path[] = "engine/testdata";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
EXPECT_THAT(n, Ne(0));
}
TEST_F(MujocoTest, mju_makefullname_error5) {
char buffer[4];
constexpr char path[] = "";
constexpr char file[] = "file";
int n = mju_makefullname(buffer, sizeof(buffer), path, file);
EXPECT_THAT(n, Ne(0));
}
// --------------------------------- Base64 ------------------------------------
using Base64Test = MujocoTest;
+151 -40
View File
@@ -14,7 +14,6 @@
#include <array>
#include <cstdio>
#include <memory>
#include <string>
#include <gmock/gmock.h>
@@ -28,9 +27,16 @@ namespace mujoco {
namespace {
using ::testing::NotNull;
using EngineVfsTest = MujocoTest;
using UserVfsTest = MujocoTest;
TEST_F(EngineVfsTest, AddFile) {
static bool HasFile(const mjVFS* vfs, const std::string& filename) {
mjResource* resource = mju_openVfsResource(filename.c_str(), vfs);
bool result = resource != nullptr;
mju_closeResource(resource);
return result;
}
TEST_F(UserVfsTest, AddFile) {
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file1 = "activation.xml";
@@ -49,54 +55,159 @@ TEST_F(EngineVfsTest, AddFile) {
ASSERT_THAT(fp3, NotNull()) << "Input file3 missing.";
std::fclose(fp3);
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mjVFS vfs;
mj_defaultVFS(&vfs);
EXPECT_FALSE(HasFile(&vfs, file1));
EXPECT_THAT(mj_addFileVFS(&vfs, dir.c_str(), file1.c_str()), 0);
EXPECT_TRUE(HasFile(&vfs, file1));
EXPECT_THAT(vfs->nfile, 0);
EXPECT_THAT(mj_addFileVFS(vfs.get(), dir.c_str(), file1.c_str()), 0);
EXPECT_THAT(vfs->nfile, 1);
EXPECT_THAT(vfs->filename[0], file1);
EXPECT_THAT(mj_addFileVFS(&vfs, dir.c_str(), file2.c_str()), 0);
EXPECT_TRUE(HasFile(&vfs, file1));
EXPECT_TRUE(HasFile(&vfs, file2));
EXPECT_THAT(mj_addFileVFS(vfs.get(), dir.c_str(), file2.c_str()), 0);
EXPECT_THAT(vfs->nfile, 2);
EXPECT_THAT(vfs->filename[0], file1);
EXPECT_THAT(vfs->filename[1], file2);
EXPECT_THAT(mj_addFileVFS(&vfs, dir.c_str(), file3.c_str()), 0);
EXPECT_TRUE(HasFile(&vfs, file1.c_str()));
EXPECT_TRUE(HasFile(&vfs, file2.c_str()));
EXPECT_TRUE(HasFile(&vfs, file3.c_str()));
EXPECT_THAT(mj_addFileVFS(vfs.get(), dir.c_str(), file3.c_str()), 0);
EXPECT_THAT(vfs->nfile, 3);
EXPECT_THAT(vfs->filename[0], file1);
EXPECT_THAT(vfs->filename[1], file2);
EXPECT_THAT(vfs->filename[2], file3);
mj_deleteFileVFS(&vfs, file1.c_str());
EXPECT_FALSE(HasFile(&vfs, file1.c_str()));
EXPECT_TRUE(HasFile(&vfs, file2.c_str()));
EXPECT_TRUE(HasFile(&vfs, file3.c_str()));
mj_deleteFileVFS(vfs.get(), file1.c_str());
EXPECT_THAT(vfs->nfile, 2);
EXPECT_THAT(vfs->filename[0], file2);
EXPECT_THAT(vfs->filename[1], file3);
mj_deleteFileVFS(vfs.get(), file3.c_str());
EXPECT_THAT(vfs->nfile, 1);
EXPECT_THAT(vfs->filename[0], file2);
mj_deleteFileVFS(&vfs, file3.c_str());
EXPECT_FALSE(HasFile(&vfs, file1.c_str()));
EXPECT_TRUE(HasFile(&vfs, file2.c_str()));
EXPECT_FALSE(HasFile(&vfs, file3.c_str()));
mj_deleteFileVFS(vfs.get(), file2.c_str());
EXPECT_THAT(vfs->nfile, 0);
mj_deleteFileVFS(&vfs, file2.c_str());
EXPECT_FALSE(HasFile(&vfs, file1.c_str()));
EXPECT_FALSE(HasFile(&vfs, file2.c_str()));
EXPECT_FALSE(HasFile(&vfs, file3.c_str()));
mj_deleteVFS(vfs.get());
mj_deleteVFS(&vfs);
}
TEST_F(EngineVfsTest, AddBuffer) {
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
TEST_F(UserVfsTest, AddFileStripPath) {
mjVFS vfs;
mj_defaultVFS(&vfs);
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file1 = "activation.xml";
mj_addFileVFS(&vfs, dir.c_str(), file1.c_str());
EXPECT_TRUE(HasFile(&vfs, file1));
EXPECT_TRUE(HasFile(&vfs, dir + file1));
EXPECT_TRUE(HasFile(&vfs, "some/dir/" + file1));
EXPECT_TRUE(HasFile(&vfs, "some/dir\\" + file1));
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, AddFileRepeat) {
mjVFS vfs;
mj_defaultVFS(&vfs);
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file1 = "activation.xml";
mj_addFileVFS(&vfs, dir.c_str(), file1.c_str());
EXPECT_TRUE(HasFile(&vfs, file1));
EXPECT_THAT(mj_addFileVFS(&vfs, "dir/", file1.c_str()), 2);
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, DeleteFile) {
mjVFS vfs;
mj_defaultVFS(&vfs);
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file1 = "activation.xml";
mj_addFileVFS(&vfs, dir.c_str(), file1.c_str());
EXPECT_TRUE(HasFile(&vfs, file1));
EXPECT_THAT(mj_deleteFileVFS(&vfs, file1.c_str()), 0);
EXPECT_FALSE(HasFile(&vfs, file1));
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, DeleteFileStripPath) {
mjVFS vfs;
mj_defaultVFS(&vfs);
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file = "activation.xml";
std::string fileUpper = "Activation.xml";
mj_addFileVFS(&vfs, dir.c_str(), file.c_str());
EXPECT_TRUE(HasFile(&vfs, file));
EXPECT_THAT(mj_deleteFileVFS(&vfs, ("dir\\" + fileUpper).c_str()), 0);
EXPECT_FALSE(HasFile(&vfs, file));
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, DeleteFileRepeat) {
mjVFS vfs;
mj_defaultVFS(&vfs);
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
std::string file = "activation.xml";
mj_addFileVFS(&vfs, dir.c_str(), file.c_str());
EXPECT_TRUE(HasFile(&vfs, file));
EXPECT_THAT(mj_deleteFileVFS(&vfs, file.c_str()), 0);
EXPECT_FALSE(HasFile(&vfs, file));
EXPECT_THAT(mj_deleteFileVFS(&vfs, file.c_str()), -1);
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, AddBuffer) {
mjVFS vfs;
mj_defaultVFS(&vfs);
std::string buffer = "<mujoco/>";
mj_addBufferVFS(vfs.get(), "model", static_cast<const void*>(buffer.c_str()),
mj_addBufferVFS(&vfs, "model", static_cast<const void*>(buffer.c_str()),
buffer.size());
std::array<char, 1024> error;
mjModel* model = mj_loadXML("model", vfs.get(), error.data(), error.size());
mjModel* model = mj_loadXML("model", &vfs, error.data(), error.size());
EXPECT_THAT(model, NotNull());
mj_deleteModel(model);
mj_deleteVFS(vfs.get());
mj_deleteVFS(&vfs);
}
TEST_F(EngineVfsTest, Timestamps) {
TEST_F(UserVfsTest, AddBufferRepeat) {
mjVFS vfs;
mj_defaultVFS(&vfs);
std::string buffer = "<mujoco/>";
const void* ptr = static_cast<const void*>(buffer.c_str());
mj_addBufferVFS(&vfs, "model", ptr, buffer.size());
int result = mj_addBufferVFS(&vfs, "model", ptr, buffer.size());
EXPECT_EQ(result, 2);
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, BufferStripPath) {
mjVFS vfs;
mj_defaultVFS(&vfs);
std::string buffer = "<mujoco/>";
const void* ptr = static_cast<const void*>(buffer.c_str());
mj_addBufferVFS(&vfs, "dir/model", ptr, buffer.size());
EXPECT_TRUE(HasFile(&vfs, "MODEL"));
EXPECT_TRUE(HasFile(&vfs, "dir\\model"));
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, Timestamps) {
static constexpr char cube[] = R"(
v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
@@ -107,11 +218,11 @@ TEST_F(EngineVfsTest, Timestamps) {
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "cube.obj", cube, sizeof(cube));
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "cube.obj", cube, sizeof(cube));
mjResource* resource = mju_openVfsResource("cube.obj", vfs.get());
mjResource* resource = mju_openVfsResource("cube.obj", &vfs);
// same timestamps
EXPECT_EQ(mju_isModifiedResource(resource, resource->timestamp), 0);
@@ -120,7 +231,7 @@ TEST_F(EngineVfsTest, Timestamps) {
EXPECT_EQ(mju_isModifiedResource(resource, "QQ=="), 1);
mju_closeResource(resource);
mj_deleteVFS(vfs.get());
mj_deleteVFS(&vfs);
}
} // namespace
+1 -7
View File
@@ -41,8 +41,6 @@ public const double mjMINIMP = 0.0001;
public const double mjMAXIMP = 0.9999;
public const int mjMAXCONPAIR = 50;
public const int mjMAXTREEDEPTH = 50;
public const int mjMAXVFS = 2000;
public const int mjMAXVFSNAME = 1000;
public const int mjNEQDATA = 11;
public const int mjNDYN = 10;
public const int mjNGAIN = 10;
@@ -4978,11 +4976,7 @@ public unsafe struct mjLROpt_ {
[StructLayout(LayoutKind.Sequential)]
public unsafe struct _mjVFS
{
public int nfile;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2000 * 1000)] public char[] filename;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2000)] public UIntPtr[] filesize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2000)] public IntPtr[] filedata;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2000)] public UInt64[] filestamp;
public void* impl_;
}
[StructLayout(LayoutKind.Sequential)]
-5
View File
@@ -40,11 +40,6 @@ public sealed class MjVfs : IDisposable {
}
}
// Number of files added to the filesystem.
public int FilesCount {
get { return Data.nfile; }
}
// Adds a new file to the virtual filesystem.
public unsafe void AddFile(string filename, string contents) {
var contents_bytes = Encoding.UTF8.GetBytes(contents);
+1 -1
View File
@@ -40,7 +40,7 @@ namespace Mujoco {
var filename = "filename";
var contents = "contents";
_vfs.AddFile(filename, contents);
Assert.That(_vfs.FilesCount, Is.EqualTo(1));
Assert.That(() => { _vfs.AddFile(filename, contents); }, Throws.Exception); // duplicate file
}
}
}