Refactor user codebase to use LoadResource helper method.

* Fix small bug when registering resource providers.
* Add test cases for OS filesystem fallback with VFS.
* Rewrite LoadModelFromString using a resource provider (now can pass custom VFSs).

PiperOrigin-RevId: 526959705
Change-Id: Iee1773e5584535d868aab6b206d1bf362e600c8a
This commit is contained in:
Kyle Bayes
2023-04-25 07:23:53 -07:00
committed by Copybara-Service
parent c2115a1f51
commit 23961e24b7
10 changed files with 446 additions and 274 deletions
+1 -1
View File
@@ -588,7 +588,7 @@ int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) {
break; break;
} }
if (!vfs_provider) { if (!vfs_provider && existing.prefix != nullptr) {
int n = std::strlen(provider->prefix); int n = std::strlen(provider->prefix);
int m = std::strlen(existing.prefix); int m = std::strlen(existing.prefix);
+10 -3
View File
@@ -19,6 +19,8 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjplugin.h>
#include "engine/engine_plugin.h" #include "engine/engine_plugin.h"
#include "engine/engine_util_errmem.h" #include "engine/engine_util_errmem.h"
@@ -59,7 +61,9 @@ mjResource* mju_openResource(const char* name, int default_provider) {
return resource; return resource;
} }
mju_warning("mju_openResource: could not open resource '%s", name); mju_warning("mju_openResource: could not open resource '%s' "
"using a resource provider matching prefix '%s'",
name, provider->prefix);
mju_free(resource->name); mju_free(resource->name);
mju_free(resource); mju_free(resource);
return NULL; return NULL;
@@ -69,7 +73,8 @@ mjResource* mju_openResource(const char* name, int default_provider) {
if (default_provider > 0) { if (default_provider > 0) {
provider = mjp_getResourceProviderAtSlot(default_provider); provider = mjp_getResourceProviderAtSlot(default_provider);
if (provider == NULL) { if (provider == NULL) {
mju_warning("mju_openResource: unknown resource provider"); mju_warning("mju_openResource: unknown resource provider at slot %d",
default_provider);
mju_free(resource->name); mju_free(resource->name);
mju_free(resource); mju_free(resource);
return NULL; return NULL;
@@ -81,7 +86,9 @@ mjResource* mju_openResource(const char* name, int default_provider) {
return resource; return resource;
} }
mju_warning("mju_openResource: could not open resource '%s", name); mju_warning("mju_openResource: could not open resource '%s' "
"with default provider at slot %d",
name, default_provider);
mju_free(resource->name); mju_free(resource->name);
mju_free(resource); mju_free(resource);
return NULL; return NULL;
+64 -118
View File
@@ -13,15 +13,12 @@
// limitations under the License. // limitations under the License.
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <csetjmp> #include <csetjmp>
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <ostream>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -189,15 +186,28 @@ void mjCMesh::Compile(int default_provider) {
// load STL, OBJ or MSH // load STL, OBJ or MSH
string ext = mjuu_getext(file); string ext = mjuu_getext(file);
if (!strcasecmp(ext.c_str(), ".stl")) { if (strcasecmp(ext.c_str(), ".stl") &&
LoadSTL(default_provider); strcasecmp(ext.c_str(), ".obj") &&
} else if (!strcasecmp(ext.c_str(), ".obj")) { strcasecmp(ext.c_str(), ".msh")) {
LoadOBJ(default_provider);
} else if (!strcasecmp(ext.c_str(), ".msh")) {
LoadMSH(default_provider);
} else {
throw mjCError(this, "Unknown mesh file type: %s", file.c_str()); throw mjCError(this, "Unknown mesh file type: %s", file.c_str());
} }
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
mjResource* resource = LoadResource(filename, default_provider);
try {
if (!strcasecmp(ext.c_str(), ".stl")) {
LoadSTL(resource);
} else if (!strcasecmp(ext.c_str(), ".obj")) {
LoadOBJ(resource);
} else {
LoadMSH(resource);
}
mju_closeResource(resource);
} catch (mjCError err) {
mju_closeResource(resource);
throw err;
}
} }
// copy user vertex // copy user vertex
@@ -583,36 +593,20 @@ void mjCMesh::RemoveRepeated() {
// load OBJ mesh // load OBJ mesh
void mjCMesh::LoadOBJ(int default_provider) { void mjCMesh::LoadOBJ(mjResource* resource) {
// make filename
string filename = mjuu_makefullname(
model->modelfiledir, model->meshdir, file);
mjResource* r = nullptr;
// try reading from default provider
if ((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// try reading from filesystem
if (!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open OBJ file '%s'", filename.c_str());
}
}
tinyobj::ObjReader objReader; tinyobj::ObjReader objReader;
const void* bytes = nullptr; const void* bytes = nullptr;
int buffer_sz = mju_readResource(r, &bytes); int buffer_sz = mju_readResource(resource, &bytes);
if (buffer_sz < 0) { if (buffer_sz < 0) {
mju_closeResource(r); throw mjCError(this, "could not read OBJ file '%s'", resource->name);
throw mjCError(this, "could not read OBJ file '%s'", filename.c_str());
} }
// TODO(etom): support .mtl files? // TODO(etom): support .mtl files?
const char* buffer = (const char*) bytes; const char* buffer = (const char*) bytes;
objReader.ParseFromString(std::string(buffer, buffer_sz), std::string()); objReader.ParseFromString(std::string(buffer, buffer_sz), std::string());
mju_closeResource(r);
if (!objReader.Valid()) { if (!objReader.Valid()) {
throw mjCError(this, "could not parse OBJ file '%s'", filename.c_str()); throw mjCError(this, "could not parse OBJ file '%s'", resource->name);
} }
const auto& attrib = objReader.GetAttrib(); const auto& attrib = objReader.GetAttrib();
@@ -631,7 +625,7 @@ void mjCMesh::LoadOBJ(int default_provider) {
if (nfacevert < 3 || nfacevert > 4) { if (nfacevert < 3 || nfacevert > 4) {
throw mjCError( throw mjCError(
this, "only tri or quad meshes are supported for OBJ (file '%s')", this, "only tri or quad meshes are supported for OBJ (file '%s')",
filename.c_str()); resource->name);
} }
face_indices.push_back(mesh.indices[idx]); face_indices.push_back(mesh.indices[idx]);
@@ -686,53 +680,38 @@ void mjCMesh::LoadOBJ(int default_provider) {
// load STL binary mesh // load STL binary mesh
void mjCMesh::LoadSTL(int default_provider) { void mjCMesh::LoadSTL(mjResource* resource) {
bool righthand = (scale[0]*scale[1]*scale[2]>0); bool righthand = (scale[0]*scale[1]*scale[2]>0);
// make filename
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
mjResource* r = nullptr;
if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open STL file '%s'", filename.c_str());
}
}
// get file data in buffer // get file data in buffer
char* buffer = 0; char* buffer = 0;
int buffer_sz = mju_readResource(r, (const void**) &buffer); int buffer_sz = mju_readResource(resource, (const void**) &buffer);
// still not found // still not found
if (buffer_sz < 0) { if (buffer_sz < 0) {
mju_closeResource(r); throw mjCError(this, "could not read STL file '%s'", resource->name);
throw mjCError(this, "could not read STL file '%s'", filename.c_str());
} else if (!buffer_sz) { } else if (!buffer_sz) {
mju_closeResource(r); throw mjCError(this, "STL file '%s' is empty", resource->name);
throw mjCError(this, "STL file '%s' is empty", filename.c_str());
} }
// make sure there is enough data for header // make sure there is enough data for header
if (buffer_sz<84) { if (buffer_sz<84) {
mju_closeResource(r); throw mjCError(this, "invalid header in STL file '%s'", resource->name);
throw mjCError(this, "invalid header in STL file '%s'", filename.c_str());
} }
// get number of triangles, check bounds // get number of triangles, check bounds
nface = *(unsigned int*)(buffer+80); nface = *(unsigned int*)(buffer+80);
if (nface<1 || nface>200000) { if (nface<1 || nface>200000) {
mju_closeResource(r);
throw mjCError(this, throw mjCError(this,
"number of faces should be between 1 and 200000 in STL file '%s';" "number of faces should be between 1 and 200000 in STL file '%s';"
" perhaps this is an ASCII file?", filename.c_str()); " perhaps this is an ASCII file?", resource->name);
} }
// check remaining buffer size // check remaining buffer size
if (nface*50 != buffer_sz-84) { if (nface*50 != buffer_sz-84) {
mju_closeResource(r);
throw mjCError(this, throw mjCError(this,
"STL file '%s' has wrong size; perhaps this is an ASCII file?", "STL file '%s' has wrong size; perhaps this is an ASCII file?",
filename.c_str()); resource->name);
} }
// assign stl data pointer // assign stl data pointer
@@ -749,16 +728,14 @@ void mjCMesh::LoadSTL(int default_provider) {
float* v = (float*)(stl+50*i+12*(j+1)); float* v = (float*)(stl+50*i+12*(j+1));
for (int k=0; k < 3; k++) { for (int k=0; k < 3; k++) {
if (std::isnan(v[k]) || std::isinf(v[k])) { if (std::isnan(v[k]) || std::isinf(v[k])) {
mju_closeResource(r);
throw mjCError(this, "STL file '%s' contains invalid vertices.", throw mjCError(this, "STL file '%s' contains invalid vertices.",
filename.c_str()); resource->name);
} }
// check if vertex coordinates can be cast to an int safely // check if vertex coordinates can be cast to an int safely
if (fabs(v[k])>pow(2, 30)) { if (fabs(v[k])>pow(2, 30)) {
mju_closeResource(r);
throw mjCError(this, throw mjCError(this,
"vertex coordinates in STL file '%s' exceed maximum bounds", "vertex coordinates in STL file '%s' exceed maximum bounds",
filename.c_str()); resource->name);
} }
} }
@@ -775,44 +752,29 @@ void mjCMesh::LoadSTL(int default_provider) {
} }
} }
mju_closeResource(r);
RemoveRepeated(); RemoveRepeated();
} }
// load MSH binary mesh // load MSH binary mesh
void mjCMesh::LoadMSH(int default_provider) { void mjCMesh::LoadMSH(mjResource* resource) {
bool righthand = (scale[0]*scale[1]*scale[2]>0); bool righthand = (scale[0]*scale[1]*scale[2]>0);
// make filename
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
mjResource* r = nullptr;
if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// fall back to OS filesystem
if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open MSH file '%s'", filename.c_str());
}
}
// get file data in buffer // get file data in buffer
char* buffer = 0; char* buffer = 0;
int buffer_sz = mju_readResource(r, (const void**) &buffer); int buffer_sz = mju_readResource(resource, (const void**) &buffer);
// still not found // still not found
if (buffer_sz < 0) { if (buffer_sz < 0) {
mju_closeResource(r); throw mjCError(this, "could not read MSH file '%s'", resource->name);
throw mjCError(this, "could not read MSH file '%s'", filename.c_str());
} else if (!buffer_sz) { } else if (!buffer_sz) {
mju_closeResource(r); throw mjCError(this, "MSH file '%s' is empty", resource->name);
throw mjCError(this, "MSH file '%s' is empty", filename.c_str());
} }
// make sure header is present // make sure header is present
if (buffer_sz<4*sizeof(int)) { if (buffer_sz<4*sizeof(int)) {
mju_closeResource(r); throw mjCError(this, "missing header in MSH file '%s'", resource->name);
throw mjCError(this, "missing header in MSH file '%s'", filename.c_str());
} }
// get sizes from header // get sizes from header
@@ -825,15 +787,13 @@ void mjCMesh::LoadMSH(int default_provider) {
if (nvert<4 || nface<0 || nnormal<0 || ntexcoord<0 || if (nvert<4 || nface<0 || nnormal<0 || ntexcoord<0 ||
(nnormal>0 && nnormal!=nvert) || (nnormal>0 && nnormal!=nvert) ||
(ntexcoord>0 && ntexcoord!=nvert)) { (ntexcoord>0 && ntexcoord!=nvert)) {
mju_closeResource(r); throw mjCError(this, "invalid sizes in MSH file '%s'", resource->name);
throw mjCError(this, "invalid sizes in MSH file '%s'", filename.c_str());
} }
// check file size // check file size
if (buffer_sz != 4*sizeof(int) + 3*nvert*sizeof(float) + 3*nnormal*sizeof(float) + if (buffer_sz != 4*sizeof(int) + 3*nvert*sizeof(float) + 3*nnormal*sizeof(float) +
2*ntexcoord*sizeof(float) + 3*nface*sizeof(int)) { 2*ntexcoord*sizeof(float) + 3*nface*sizeof(int)) {
mju_closeResource(r); throw mjCError(this, "unexpected file size in MSH file '%s'", resource->name);
throw mjCError(this, "unexpected file size in MSH file '%s'", filename.c_str());
} }
// allocate and copy // allocate and copy
@@ -872,8 +832,6 @@ void mjCMesh::LoadMSH(int default_provider) {
face[3*i+2] = tmp; face[3*i+2] = tmp;
} }
} }
mju_closeResource(r);
} }
@@ -1601,11 +1559,20 @@ void mjCSkin::Compile(int default_provider) {
// load SKN // load SKN
string ext = mjuu_getext(file); string ext = mjuu_getext(file);
if (!strcasecmp(ext.c_str(), ".skn")) { if (strcasecmp(ext.c_str(), ".skn")) {
LoadSKN(default_provider);
} else {
throw mjCError(this, "Unknown skin file type: %s", file.c_str()); throw mjCError(this, "Unknown skin file type: %s", file.c_str());
} }
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
mjResource* resource = LoadResource(filename, default_provider);
try {
LoadSKN(resource);
mju_closeResource(resource);
} catch(mjCError err) {
mju_closeResource(resource);
throw err;
}
} }
// make sure all data is present // make sure all data is present
@@ -1724,33 +1691,19 @@ void mjCSkin::Compile(int default_provider) {
// load skin in SKN BIN format // load skin in SKN BIN format
void mjCSkin::LoadSKN(int default_provider) { void mjCSkin::LoadSKN(mjResource* resource) {
// make filename
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
// get file data in buffer
mjResource* r = nullptr;
if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open SKN file '%s'", filename.c_str());
}
}
char* buffer = 0; char* buffer = 0;
int buffer_sz = mju_readResource(r, (const void**) &buffer); int buffer_sz = mju_readResource(resource, (const void**) &buffer);
if (buffer_sz < 0) { if (buffer_sz < 0) {
mju_closeResource(r); throw mjCError(this, "could not read SKN file '%s'", resource->name);
throw mjCError(this, "could not read SKN file '%s'", filename.c_str());
} else if (!buffer_sz) { } else if (!buffer_sz) {
mju_closeResource(r); throw mjCError(this, "SKN file '%s' is empty", resource->name);
throw mjCError(this, "SKN file '%s' is empty", filename.c_str());
} }
// make sure header is present // make sure header is present
if (buffer_sz<16) { if (buffer_sz<16) {
mju_closeResource(r); throw mjCError(this, "missing header in SKN file '%s'", resource->name);
throw mjCError(this, "missing header in SKN file '%s'", filename.c_str());
} }
// get sizes from header // get sizes from header
@@ -1761,14 +1714,12 @@ void mjCSkin::LoadSKN(int default_provider) {
// negative sizes not allowed // negative sizes not allowed
if (nvert<0 || ntexcoord<0 || nface<0 || nbone<0) { if (nvert<0 || ntexcoord<0 || nface<0 || nbone<0) {
mju_closeResource(r); throw mjCError(this, "negative size in header of SKN file '%s'", resource->name);
throw mjCError(this, "negative size in header of SKN file '%s'", filename.c_str());
} }
// make sure we have data for vert, texcoord, face // make sure we have data for vert, texcoord, face
if (buffer_sz < 16 + 12*nvert + 8*ntexcoord + 12*nface) { if (buffer_sz < 16 + 12*nvert + 8*ntexcoord + 12*nface) {
mju_closeResource(r); throw mjCError(this, "insufficient data in SKN file '%s'", resource->name);
throw mjCError(this, "insufficient data in SKN file '%s'", filename.c_str());
} }
// data pointer and counter // data pointer and counter
@@ -1807,8 +1758,7 @@ void mjCSkin::LoadSKN(int default_provider) {
for (int i=0; i<nbone; i++) { for (int i=0; i<nbone; i++) {
// check size // check size
if (buffer_sz/4-4-cnt < 18) { if (buffer_sz/4-4-cnt < 18) {
mju_closeResource(r); throw mjCError(this, "insufficient data in SKN file '%s', bone %d", resource->name, i);
throw mjCError(this, "insufficient data in SKN file '%s', bone %d", filename.c_str(), i);
} }
// read name // read name
@@ -1832,16 +1782,14 @@ void mjCSkin::LoadSKN(int default_provider) {
// check for negative // check for negative
if (vcount<1) { if (vcount<1) {
mju_closeResource(r);
throw mjCError(this, "vertex count must be positive in SKN file '%s', bone %d", throw mjCError(this, "vertex count must be positive in SKN file '%s', bone %d",
filename.c_str(), i); resource->name, i);
} }
// check size // check size
if (buffer_sz/4-4-cnt < 2*vcount) { if (buffer_sz/4-4-cnt < 2*vcount) {
mju_closeResource(r);
throw mjCError(this, "insufficient vertex data in SKN file '%s', bone %d", throw mjCError(this, "insufficient vertex data in SKN file '%s', bone %d",
filename.c_str(), i); resource->name, i);
} }
// read vertid // read vertid
@@ -1855,10 +1803,8 @@ void mjCSkin::LoadSKN(int default_provider) {
cnt += vcount; cnt += vcount;
} }
mju_closeResource(r);
// check final size // check final size
if (buffer_sz != 16+4*cnt) { if (buffer_sz != 16+4*cnt) {
throw mjCError(this, "unexpected buffer size in SKN file '%s'", filename.c_str()); throw mjCError(this, "unexpected buffer size in SKN file '%s'", resource->name);
} }
} }
+75 -84
View File
@@ -466,6 +466,25 @@ mjCBase::mjCBase() {
// load resource if found (fallback to OS filesystem)
mjResource* mjCBase::LoadResource(string filename, int provider) {
mjResource* r = nullptr;
const char* cname = filename.c_str();
// try reading from given provider
if ((r = mju_openResource(cname, provider)) == nullptr) {
if (!provider) {
throw mjCError(0, "file not found: '%s'", cname);
}
// if provider wasn't the OS filesystem try to fallback to OS filesystem
if ((r = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "resource not found via provider or OS filesystem: '%s'", cname);
}
}
return r;
}
//------------------ class mjCBody implementation -------------------------------------------------- //------------------ class mjCBody implementation --------------------------------------------------
// constructor // constructor
@@ -1978,28 +1997,20 @@ mjCHField::~mjCHField() {
// load elevation data from custom format // load elevation data from custom format
void mjCHField::LoadCustom(string filename, int default_provider) { void mjCHField::LoadCustom(mjResource* resource) {
// get file data in buffer // get file data in buffer
const void* buffer = 0; const void* buffer = 0;
mjResource* resource = nullptr;
if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// default to OS filesystem
if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open hfield file '%s'", filename.c_str());
}
}
int buffer_sz = mju_readResource(resource, &buffer); int buffer_sz = mju_readResource(resource, &buffer);
// still not found if (buffer_sz < 1) {
if (!buffer || buffer_sz < 1) { throw mjCError(this, "could not read hfield file '%s'", resource->name);
throw mjCError(this, "could not read hfield file '%s'", filename.c_str()); } else if (!buffer_sz) {
throw mjCError(this, "empty hfield file '%s'", resource->name);
} }
if (buffer_sz < 2*sizeof(int)) { if (buffer_sz < 2*sizeof(int)) {
mju_closeResource(resource); throw mjCError(this, "hfield missing header '%s'", resource->name);
throw mjCError(this, "hfield missing header '%s'", filename.c_str());
} }
// read dimensions // read dimensions
@@ -2009,51 +2020,38 @@ void mjCHField::LoadCustom(string filename, int default_provider) {
// check dimensions // check dimensions
if (nrow<1 || ncol<1) { if (nrow<1 || ncol<1) {
mju_closeResource(resource); throw mjCError(this, "non-positive hfield dimensions in file '%s'", resource->name);
throw mjCError(this, "non-positive hfield dimensions in file '%s'", filename.c_str());
} }
// check buffer size // check buffer size
if (buffer_sz != nrow*ncol*sizeof(float)+8) { if (buffer_sz != nrow*ncol*sizeof(float)+8) {
mju_closeResource(resource); throw mjCError(this, "unexpected file size in file '%s'", resource->name);
throw mjCError(this, "unexpected file size in file '%s'", filename.c_str());
} }
// allocate // allocate
data = (float*) mju_malloc(nrow*ncol*sizeof(float)); data = (float*) mju_malloc(nrow*ncol*sizeof(float));
if (!data) { if (!data) {
mju_closeResource(resource);
throw mjCError(this, "could not allocate buffers in hfield"); throw mjCError(this, "could not allocate buffers in hfield");
} }
// copy data // copy data
memcpy(data, (void*)(pint+2), nrow*ncol*sizeof(float)); memcpy(data, (void*)(pint+2), nrow*ncol*sizeof(float));
// close file
mju_closeResource(resource);
} }
// load elevation data from PNG format // load elevation data from PNG format
void mjCHField::LoadPNG(string filename, int default_provider) { void mjCHField::LoadPNG(mjResource* resource) {
// determine data source // determine data source
const void* inbuffer = 0; const void* inbuffer = 0;
mjResource* resource = nullptr;
if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// try reading from filesystem
if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open PNG file '%s' %d", filename.c_str(), default_provider);
}
}
int inbuffer_sz = mju_readResource(resource, &inbuffer); int inbuffer_sz = mju_readResource(resource, &inbuffer);
// still not found if (inbuffer_sz < 1) {
if (!inbuffer || inbuffer_sz < 1) { throw mjCError(this, "could not read hfield PNG file '%s'", resource->name);
mju_closeResource(resource); }
throw mjCError(this, "could not read PNG file '%s'", filename.c_str());
if (!inbuffer_sz) {
throw mjCError(this, "empty hfield PNG file '%s'", resource->name);
} }
// load PNG from file or memory // load PNG from file or memory
@@ -2063,18 +2061,15 @@ void mjCHField::LoadPNG(string filename, int default_provider) {
// check // check
if (err) { if (err) {
mju_closeResource(resource);
throw mjCError(this, "PNG load error '%s' in hfield id = %d", lodepng_error_text(err), id); throw mjCError(this, "PNG load error '%s' in hfield id = %d", lodepng_error_text(err), id);
} }
if (!w || !h) { if (!w || !h) {
mju_closeResource(resource); throw mjCError(this, "Zero dimension in PNG hfield '%s' (id = %d)", resource->name, id);
throw mjCError(this, "Zero dimension in PNG hfield '%s' (id = %d)", name.c_str(), id);
} }
// allocate // allocate
data = (float*) mju_malloc(w*h*sizeof(float)); data = (float*) mju_malloc(w*h*sizeof(float));
if (!data) { if (!data) {
mju_closeResource(resource);
throw mjCError(this, "could not allocate buffers in hfield"); throw mjCError(this, "could not allocate buffers in hfield");
} }
@@ -2086,7 +2081,6 @@ void mjCHField::LoadPNG(string filename, int default_provider) {
data[c+(nrow-1-r)*ncol] = (float)image[c+r*ncol]; data[c+(nrow-1-r)*ncol] = (float)image[c+r*ncol];
} }
image.clear(); image.clear();
mju_closeResource(resource);
} }
@@ -2114,13 +2108,21 @@ void mjCHField::Compile(int default_provider) {
// make filename // make filename
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
mjResource* resource = LoadResource(filename, default_provider);
// load depending on format // load depending on format
string ext = mjuu_getext(filename); string ext = mjuu_getext(filename);
if (!strcasecmp(ext.c_str(), ".png")) {
LoadPNG(filename, default_provider); try {
} else { if (!strcasecmp(ext.c_str(), ".png")) {
LoadCustom(filename, default_provider); LoadPNG(resource);
} else {
LoadCustom(resource);
}
mju_closeResource(resource);
} catch(mjCError err) {
mju_closeResource(resource);
throw err;
} }
} }
@@ -2431,30 +2433,22 @@ void mjCTexture::BuiltinCube(void) {
// load PNG file // load PNG file
void mjCTexture::LoadPNG(string filename, int default_provider, void mjCTexture::LoadPNG(mjResource* resource,
std::vector<unsigned char>& image, std::vector<unsigned char>& image,
unsigned int& w, unsigned int& h) { unsigned int& w, unsigned int& h) {
const void* inbuffer = 0; const void* inbuffer = 0;
mjResource* resource = nullptr;
if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// try reading from filesystem
if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open PNG file '%s' %d", filename.c_str(), default_provider);
}
}
int inbuffer_sz = mju_readResource(resource, &inbuffer); int inbuffer_sz = mju_readResource(resource, &inbuffer);
// still not found // still not found
if (!inbuffer || inbuffer_sz < 1) { if (inbuffer_sz < 1) {
mju_closeResource(resource); throw mjCError(this, "could not read PNG texture file '%s'", resource->name);
throw mjCError(this, "could not read PNG file '%s'", filename.c_str()); } else if (!inbuffer_sz) {
throw mjCError(this, "PNG texture file '%s' is empty", resource->name);
} }
// load PNG from file or memory // load PNG from file or memory
unsigned int err = lodepng::decode(image, w, h, (const unsigned char*) inbuffer, inbuffer_sz, LCT_RGB, 8); unsigned int err = lodepng::decode(image, w, h, (const unsigned char*) inbuffer, inbuffer_sz, LCT_RGB, 8);
mju_closeResource(resource);
// check // check
if (err) { if (err) {
@@ -2462,34 +2456,27 @@ void mjCTexture::LoadPNG(string filename, int default_provider,
"PNG file load error '%s' in texture id = %d", lodepng_error_text(err), id); "PNG file load error '%s' in texture id = %d", lodepng_error_text(err), id);
} }
if (w<1 || h<1) { if (w<1 || h<1) {
throw mjCError(this, "Empty PNG file in texture '%s' (id %d)", (const char*)file.c_str(), id); throw mjCError(this, "Empty PNG file in texture '%s' (id %d)", resource->name, id);
} }
} }
// load custom file // load custom file
void mjCTexture::LoadCustom(string filename, int default_provider, void mjCTexture::LoadCustom(mjResource* resource,
std::vector<unsigned char>& image, std::vector<unsigned char>& image,
unsigned int& w, unsigned int& h) { unsigned int& w, unsigned int& h) {
const void* buffer = 0; const void* buffer = 0;
mjResource* resource = nullptr;
if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) {
// default to OS filesystem
if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) {
throw mjCError(this, "could not open texture file '%s'", filename.c_str());
}
}
int buffer_sz = mju_readResource(resource, &buffer); int buffer_sz = mju_readResource(resource, &buffer);
// still not found // still not found
if (!buffer || buffer_sz < 0) { if (buffer_sz < 0) {
mju_closeResource(resource); throw mjCError(this, "could not read texture file '%s'", resource->name);
throw mjCError(this, "could not read texture file '%s'", filename.c_str()); } else if (!buffer_sz) {
throw mjCError(this, "texture file is empty: '%s'", resource->name);
} }
// read dimensions // read dimensions
int* pint = (int*)buffer; int* pint = (int*)buffer;
w = pint[0]; w = pint[0];
@@ -2497,23 +2484,19 @@ void mjCTexture::LoadCustom(string filename, int default_provider,
// check dimensions // check dimensions
if (w<1 || h<1) { if (w<1 || h<1) {
mju_closeResource(resource);
throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n"
"non-positive texture dimensions in file '%s'", filename.c_str()); "non-positive texture dimensions in file '%s'", resource->name);
} }
// check buffer size // check buffer size
if (buffer_sz != 2*sizeof(int) + w*h*3*sizeof(char)) { if (buffer_sz != 2*sizeof(int) + w*h*3*sizeof(char)) {
mju_closeResource(resource);
throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n"
"unexpected file size in file '%s'", filename.c_str()); "unexpected file size in file '%s'", resource->name);
} }
// allocate and copy // allocate and copy
image.resize(w*h*3); image.resize(w*h*3);
memcpy(image.data(), (void*)(pint+2), w*h*3*sizeof(char)); memcpy(image.data(), (void*)(pint+2), w*h*3*sizeof(char));
mju_closeResource(resource);
} }
@@ -2524,10 +2507,18 @@ void mjCTexture::LoadFlip(string filename, int default_provider,
unsigned int& w, unsigned int& h) { unsigned int& w, unsigned int& h) {
// dispatch to PNG or Custom loaded // dispatch to PNG or Custom loaded
string ext = mjuu_getext(filename); string ext = mjuu_getext(filename);
if (!strcasecmp(ext.c_str(), ".png")) { mjResource* resource = LoadResource(filename, default_provider);
LoadPNG(filename, default_provider, image, w, h);
} else { try {
LoadCustom(filename, default_provider, image, w, h); if (!strcasecmp(ext.c_str(), ".png")) {
LoadPNG(resource, image, w, h);
} else {
LoadCustom(resource, image, w, h);
}
mju_closeResource(resource);
} catch(mjCError err) {
mju_closeResource(resource);
throw err;
} }
// horizontal flip // horizontal flip
+43 -40
View File
@@ -144,6 +144,9 @@ class mjCBase {
friend class mjCDef; friend class mjCDef;
public: public:
// load resource if found (fallback to OS filesystem)
mjResource* LoadResource(std::string filename, int provider);
std::string name; // object name std::string name; // object name
std::string classname; // defaults class name std::string classname; // defaults class name
int id; // object id int id; // object id
@@ -513,9 +516,9 @@ class mjCMesh: public mjCBase {
mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor
~mjCMesh(); // destructor ~mjCMesh(); // destructor
void Compile(int default_provider); // compiler void Compile(int default_provider); // compiler
void LoadOBJ(int default_provider); // load mesh in wavefront OBJ format void LoadOBJ(mjResource* resource); // load mesh in wavefront OBJ format
void LoadSTL(int default_provider); // load mesh in STL BIN format void LoadSTL(mjResource* resource); // load mesh in STL BIN format
void LoadMSH(int default_provider); // load mesh in MSH BIN format void LoadMSH(mjResource* resource); // load mesh in MSH BIN format
void MakeGraph(void); // make graph of convex hull void MakeGraph(void); // make graph of convex hull
void CopyGraph(void); // copy graph into face data void CopyGraph(void); // copy graph into face data
void MakeNormal(void); // compute vertex normals void MakeNormal(void); // compute vertex normals
@@ -595,7 +598,7 @@ class mjCSkin: public mjCBase {
mjCSkin(mjCModel* = 0); // constructor mjCSkin(mjCModel* = 0); // constructor
~mjCSkin(); // destructor ~mjCSkin(); // destructor
void Compile(int default_provider); // compiler void Compile(int default_provider); // compiler
void LoadSKN(int default_provider); // load skin in SKN BIN format void LoadSKN(mjResource* resource); // load skin in SKN BIN format
int matid; // material id int matid; // material id
std::vector<int> bodyid; // body ids std::vector<int> bodyid; // body ids
@@ -622,8 +625,8 @@ class mjCHField : public mjCBase {
~mjCHField(); // destructor ~mjCHField(); // destructor
void Compile(int default_provider); // compiler void Compile(int default_provider); // compiler
void LoadCustom(std::string filename, int default_provider); // load from custom format void LoadCustom(mjResource* resource); // load from custom format
void LoadPNG(std::string filename, int default_provider); // load from PNG format void LoadPNG(mjResource* resource); // load from PNG format
}; };
@@ -676,10 +679,10 @@ class mjCTexture : public mjCBase {
std::vector<unsigned char>& image, std::vector<unsigned char>& image,
unsigned int& w, unsigned int& h); unsigned int& w, unsigned int& h);
void LoadPNG(std::string filename, int default_provider, void LoadPNG(mjResource* resource,
std::vector<unsigned char>& image, std::vector<unsigned char>& image,
unsigned int& w, unsigned int& h); unsigned int& w, unsigned int& h);
void LoadCustom(std::string filename, int default_provider, void LoadCustom(mjResource* resource,
std::vector<unsigned char>& image, std::vector<unsigned char>& image,
unsigned int& w, unsigned int& h); unsigned int& w, unsigned int& h);
@@ -708,8 +711,8 @@ class mjCMaterial : public mjCBase {
float rgba[4]; // rgba float rgba[4]; // rgba
private: private:
mjCMaterial(mjCModel* = 0, mjCDef* = 0);// constructor mjCMaterial(mjCModel* = 0, mjCDef* = 0); // constructor
void Compile(void); // compiler void Compile(void); // compiler
int texid; // id of material int texid; // id of material
}; };
@@ -742,8 +745,8 @@ class mjCPair : public mjCBase {
} }
private: private:
mjCPair(mjCModel* = 0, mjCDef* = 0);// constructor mjCPair(mjCModel* = 0, mjCDef* = 0); // constructor
void Compile(void); // compiler void Compile(void); // compiler
int geom1; // id of geom1 int geom1; // id of geom1
int geom2; // id of geom2 int geom2; // id of geom2
@@ -828,22 +831,22 @@ class mjCTendon : public mjCBase {
mjCWrap* GetWrap(int); // pointer to wrap mjCWrap* GetWrap(int); // pointer to wrap
// variables set by user // variables set by user
int group; // group for visualization int group; // group for visualization
std::string material; // name of material for rendering std::string material; // name of material for rendering
int limited; // does tendon have limits: 0 false, 1 true, 2 auto int limited; // does tendon have limits: 0 false, 1 true, 2 auto
double width; // width for rendering double width; // width for rendering
mjtNum solref_limit[mjNREF]; // solver reference: tendon limits mjtNum solref_limit[mjNREF]; // solver reference: tendon limits
mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits
mjtNum solref_friction[mjNREF]; // solver reference: tendon friction mjtNum solref_friction[mjNREF]; // solver reference: tendon friction
mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction
double range[2]; // length limits double range[2]; // length limits
double margin; // margin value for tendon limit detection double margin; // margin value for tendon limit detection
double stiffness; // stiffness coefficient double stiffness; // stiffness coefficient
double damping; // damping coefficient double damping; // damping coefficient
double frictionloss; // friction loss double frictionloss; // friction loss
double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring
std::vector<double> userdata; // user data std::vector<double> userdata; // user data
float rgba[4]; // rgba when material is omitted float rgba[4]; // rgba when material is omitted
private: private:
mjCTendon(mjCModel* = 0, mjCDef* = 0); // constructor mjCTendon(mjCModel* = 0, mjCDef* = 0); // constructor
@@ -934,8 +937,8 @@ class mjCActuator : public mjCBase {
std::string refsite; // reference site, for site transmission only std::string refsite; // reference site, for site transmission only
private: private:
mjCActuator(mjCModel* = 0, mjCDef* = 0);// constructor mjCActuator(mjCModel* = 0, mjCDef* = 0); // constructor
void Compile(void); // compiler void Compile(void); // compiler
int trnid[2]; // id of transmission target int trnid[2]; // id of transmission target
}; };
@@ -1046,18 +1049,18 @@ class mjCKey : public mjCBase {
friend class mjXWriter; friend class mjXWriter;
public: public:
double time; // time double time; // time
std::vector<double> qpos; // qpos std::vector<double> qpos; // qpos
std::vector<double> qvel; // qvel std::vector<double> qvel; // qvel
std::vector<double> act; // act std::vector<double> act; // act
std::vector<double> mpos; // mocap pos std::vector<double> mpos; // mocap pos
std::vector<double> mquat; // mocap quat std::vector<double> mquat; // mocap quat
std::vector<double> ctrl; // ctrl std::vector<double> ctrl; // ctrl
private: private:
mjCKey(mjCModel*); // constructor mjCKey(mjCModel*); // constructor
~mjCKey(); // destructor ~mjCKey(); // destructor
void Compile(const mjModel* m); // compiler void Compile(const mjModel* m); // compiler
}; };
+9 -9
View File
@@ -55,7 +55,7 @@ TEST_F(SensorTest, DisableSensors) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
// before calling anything, check that sensors are initialised to 0 // before calling anything, check that sensors are initialised to 0
@@ -105,7 +105,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMat) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
mj_forward(model, data); mj_forward(model, data);
@@ -143,7 +143,7 @@ TEST_F(RelativeFrameSensorTest, ReferenceQuatMat) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
// call mj_forward and convert orientation matrix to quaternion // call mj_forward and convert orientation matrix to quaternion
@@ -190,7 +190,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMatQuat) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
constexpr int nsensordata = 32; constexpr int nsensordata = 32;
ASSERT_EQ(model->nsensordata, nsensordata); ASSERT_EQ(model->nsensordata, nsensordata);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
@@ -240,7 +240,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelLinearFixed) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
data->qvel[0] = mju_sqrt(2); data->qvel[0] = mju_sqrt(2);
data->qvel[1] = 1; data->qvel[1] = 1;
@@ -272,7 +272,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngFixed) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
// set joint velocities and call forward dynamics // set joint velocities and call forward dynamics
@@ -307,7 +307,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngOpposing) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
// set joint velocities and call forward dynamics // set joint velocities and call forward dynamics
@@ -350,7 +350,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelGeneral) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
mjtNum dt = 1e-6; // timestep used for finite differencing mjtNum dt = 1e-6; // timestep used for finite differencing
@@ -411,7 +411,7 @@ TEST_F(SensorTest, Clock) {
</sensor> </sensor>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
mjData* data = mj_makeData(model); mjData* data = mj_makeData(model);
// call step 4 times, checking that clock works as expected // call step 4 times, checking that clock works as expected
+42 -11
View File
@@ -68,18 +68,49 @@ const std::string GetModelPath(std::string_view path) {
return absl::StrCat("../model/", path); return absl::StrCat("../model/", path);
} }
int datastr_length(mjResource* resource) {
int len = strlen(resource->name)-1;
// string should have the format "S(data).ext"
for (; len > 2; len--) {
if (resource->name[len] == ')') {
return len - 2;
}
}
return -1;
}
int str_open_callback(mjResource* resource) {
if (datastr_length(resource) < 0) {
return 0;
}
resource->data = resource->name+2;
return 1;
}
int str_read_callback(mjResource* resource, const void** buffer) {
*buffer = resource->data;
return datastr_length(resource);
}
void str_close_callback(mjResource* resource) {
}
mjModel* LoadModelFromString(std::string_view xml, char* error, mjModel* LoadModelFromString(std::string_view xml, char* error,
int error_size) { int error_size, mjVFS* vfs) {
static constexpr char file[] = "filename.xml";
// mjVFS structs need to be allocated on the heap, because it's ~2MB // register string resource provider if not registered before
auto vfs = std::make_unique<mjVFS>(); if (mjp_getResourceProvider("S(") == nullptr) {
mj_defaultVFS(vfs.get()); mjpResourceProvider resourceProvider;
mj_makeEmptyFileVFS(vfs.get(), file, xml.size()); resourceProvider.prefix = "S(";
int file_idx = mj_findFileVFS(vfs.get(), file); resourceProvider.open = str_open_callback;
memcpy(vfs->filedata[file_idx], xml.data(), xml.size()); resourceProvider.read = str_read_callback;
mjModel* m = mj_loadXML(file, vfs.get(), error, error_size); resourceProvider.close = str_close_callback;
mj_deleteFileVFS(vfs.get(), file); mjp_registerResourceProvider(&resourceProvider);
return m; }
std::string xml2 = {xml.begin(), xml.end()};
std::string str = "S(" + xml2 + ")";
return mj_loadXML(str.c_str(), vfs, error, error_size);
} }
static void AssertModelNotNull(mjModel* model, static void AssertModelNotNull(mjModel* model,
+1 -1
View File
@@ -52,7 +52,7 @@ const std::string GetModelPath(absl::string_view path);
// Returns a newly-allocated mjModel, loaded from the contents of xml. // Returns a newly-allocated mjModel, loaded from the contents of xml.
// On failure returns nullptr and populates the error array if present. // On failure returns nullptr and populates the error array if present.
mjModel* LoadModelFromString(absl::string_view xml, char* error = nullptr, mjModel* LoadModelFromString(absl::string_view xml, char* error = nullptr,
int error_size = 0); int error_size = 0, mjVFS* vfs = nullptr);
// Returns a newly-allocated mjModel, loaded from the contents in model_path. // Returns a newly-allocated mjModel, loaded from the contents in model_path.
// On failure it asserts that model is null. // On failure it asserts that model is null.
+87 -7
View File
@@ -16,8 +16,8 @@
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <ostream>
#include <string> #include <string>
#include <vector>
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -25,6 +25,7 @@
#include <mujoco/mjmodel.h> #include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h> #include <mujoco/mjtnum.h>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "src/cc/array_safety.h"
#include "test/fixture.h" #include "test/fixture.h"
namespace mujoco { namespace mujoco {
@@ -58,6 +59,7 @@ static const char* const kMalformedFaceOBJPath =
"user/testdata/malformed_face.xml"; "user/testdata/malformed_face.xml";
using ::testing::HasSubstr; using ::testing::HasSubstr;
using ::testing::IsNull;
// ------------- test invalid filenames ---------------------------------------- // ------------- test invalid filenames ----------------------------------------
@@ -91,6 +93,83 @@ TEST_F(MjCMeshTest, UnknownMeshFormat) {
} }
} }
// -------------------- test OS filesystem fallback ----------------------------
TEST_F(MjCMeshTest, LoadMSHWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="mesh1" file="unknown_file.msh"/>
</asset>
<worldbody>
<geom type="mesh" mesh="mesh1"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
TEST_F(MjCMeshTest, LoadOBJWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="mesh1" file="unknown_file.obj"/>
</asset>
<worldbody>
<geom type="mesh" mesh="mesh1"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
TEST_F(MjCMeshTest, LoadSTLWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="mesh1" file="unknown_file.stl"/>
</asset>
<worldbody>
<geom type="mesh" mesh="mesh1"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
// ------------- test vertex de-duplication (STL) ------------------------------ // ------------- test vertex de-duplication (STL) ------------------------------
TEST_F(MjCMeshTest, DeDuplicateSTLVertices) { TEST_F(MjCMeshTest, DeDuplicateSTLVertices) {
@@ -102,7 +181,8 @@ TEST_F(MjCMeshTest, DeDuplicateSTLVertices) {
mj_deleteModel(model); mj_deleteModel(model);
} }
// ------------- test Mesh loading (MSH) ------------------------------ // -------------------- test Mesh loading (MSH) --------------------------------
TEST_F(MjCMeshTest, LoadMSH) { TEST_F(MjCMeshTest, LoadMSH) {
const std::string xml_path = GetTestDataFilePath(kMeshPath); const std::string xml_path = GetTestDataFilePath(kMeshPath);
char error[1024]; char error[1024];
@@ -184,7 +264,7 @@ TEST_F(MjCMeshTest, TinyMeshLoads) {
</worldbody> </worldbody>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, testing::NotNull()); ASSERT_THAT(model, testing::NotNull());
mj_deleteModel(model); mj_deleteModel(model);
} }
@@ -204,7 +284,7 @@ TEST_F(MjCMeshTest, FaceNormalAutogenerated) {
</worldbody> </worldbody>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, testing::NotNull()); ASSERT_THAT(model, testing::NotNull());
mj_deleteModel(model); mj_deleteModel(model);
} }
@@ -226,7 +306,7 @@ TEST_F(MjCMeshTest, SmallInertiaLoads) {
</worldbody> </worldbody>
</mujoco> </mujoco>
)"; )";
mjModel* model = LoadModelFromString(xml, 0, 0); mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, testing::NotNull()); ASSERT_THAT(model, testing::NotNull());
mj_deleteModel(model); mj_deleteModel(model);
} }
@@ -533,7 +613,7 @@ TEST_F(MjCMeshTest, ExactConcaveInertia) {
EXPECT_LE(fabs(model->body_mass[2] - m_concave_cube), max_abs_err); EXPECT_LE(fabs(model->body_mass[2] - m_concave_cube), max_abs_err);
EXPECT_LE(fabs(model->body_mass[3] - m_concave_cube), max_abs_err); EXPECT_LE(fabs(model->body_mass[3] - m_concave_cube), max_abs_err);
EXPECT_LE(fabs(model->body_mass[4] - m_concave_cube), max_abs_err); EXPECT_LE(fabs(model->body_mass[4] - m_concave_cube), max_abs_err);
for (int i=3; i<15; i+=3) { for (int i = 3; i < 15; i += 3) {
EXPECT_LE(fabs(model->body_inertia[i] - I1), max_abs_err); EXPECT_LE(fabs(model->body_inertia[i] - I1), max_abs_err);
EXPECT_LE(fabs(model->body_inertia[i+1] - I2), max_abs_err); EXPECT_LE(fabs(model->body_inertia[i+1] - I2), max_abs_err);
EXPECT_LE(fabs(model->body_inertia[i+2] - I2), max_abs_err); EXPECT_LE(fabs(model->body_inertia[i+2] - I2), max_abs_err);
@@ -550,7 +630,7 @@ TEST_F(MjCMeshTest, ExactConvexInertia) {
mjtNum I_solid_cube = 1./6. * m_solid_cube; mjtNum I_solid_cube = 1./6. * m_solid_cube;
EXPECT_LE(fabs(model->body_mass[1] - m_solid_cube), max_abs_err); EXPECT_LE(fabs(model->body_mass[1] - m_solid_cube), max_abs_err);
EXPECT_LE(fabs(model->body_mass[2] - m_solid_cube), max_abs_err); EXPECT_LE(fabs(model->body_mass[2] - m_solid_cube), max_abs_err);
for (int i=3; i<9; i++) { for (int i = 3; i < 9; i++) {
EXPECT_LE(fabs(model->body_inertia[i] - I_solid_cube), max_abs_err); EXPECT_LE(fabs(model->body_inertia[i] - I_solid_cube), max_abs_err);
} }
mj_deleteModel(model); mj_deleteModel(model);
+114
View File
@@ -14,15 +14,18 @@
// Tests for user/user_objects.cc. // Tests for user/user_objects.cc.
#include <algorithm>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <string> #include <string>
#include <vector>
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <mujoco/mjmodel.h> #include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h> #include <mujoco/mjtnum.h>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "src/cc/array_safety.h"
#include "test/fixture.h" #include "test/fixture.h"
namespace mujoco { namespace mujoco {
@@ -37,6 +40,116 @@ using ::testing::HasSubstr;
using ::testing::IsNull; using ::testing::IsNull;
using ::testing::NotNull; using ::testing::NotNull;
// -------------------- test OS filesystem fallback ----------------------------
using VfsTest = MujocoTest;
TEST_F(VfsTest, HFieldPngWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<hfield name="hfield" file="unknown_file.png" size="0.5 0.5 1 0.1"/>
</asset>
<worldbody>
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
TEST_F(VfsTest, HFieldCustomWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<hfield name="hfield" file="unknown_file" size="0.5 0.5 1 0.1"/>
</asset>
<worldbody>
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
TEST_F(VfsTest, TexturePngWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture name="texture" file="unknown_file.png" type="2d"/>
<material name="material" texture="texture"/>
</asset>
<worldbody>
<geom type="plane" material="material" size="4 4 4"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
TEST_F(VfsTest, TextureCustomWithVFS) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture name="texture" file="unknown_file" type="2d"/>
<material name="material" texture="texture"/>
</asset>
<worldbody>
<geom type="plane" material="material" size="4 4 4"/>
</worldbody>
</mujoco>
)";
char error[1024];
size_t error_sz = 1024;
// load VFS on the heap
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
// should fallback to OS filesystem
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
}
// ------------------------ test keyframes ------------------------------------- // ------------------------ test keyframes -------------------------------------
using KeyframeTest = MujocoTest; using KeyframeTest = MujocoTest;
@@ -289,6 +402,7 @@ TEST_F(MjCGeomTest, CapsuleInertiaX) {
} }
// ------------- test inertiagrouprange ---------------------------------------- // ------------- test inertiagrouprange ----------------------------------------
TEST_F(MjCGeomTest, IgnoreGeomOutsideInertiagrouprange) { TEST_F(MjCGeomTest, IgnoreGeomOutsideInertiagrouprange) {
static constexpr char xml[] = R"( static constexpr char xml[] = R"(
<mujoco> <mujoco>