diff --git a/src/engine/engine_plugin.cc b/src/engine/engine_plugin.cc index 3e0995ea..d6375856 100644 --- a/src/engine/engine_plugin.cc +++ b/src/engine/engine_plugin.cc @@ -588,7 +588,7 @@ int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) { break; } - if (!vfs_provider) { + if (!vfs_provider && existing.prefix != nullptr) { int n = std::strlen(provider->prefix); int m = std::strlen(existing.prefix); diff --git a/src/engine/engine_resource.c b/src/engine/engine_resource.c index e8e915bc..c81f5aea 100644 --- a/src/engine/engine_resource.c +++ b/src/engine/engine_resource.c @@ -19,6 +19,8 @@ #include #include +#include +#include #include "engine/engine_plugin.h" #include "engine/engine_util_errmem.h" @@ -59,7 +61,9 @@ mjResource* mju_openResource(const char* name, int default_provider) { 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); return NULL; @@ -69,7 +73,8 @@ mjResource* mju_openResource(const char* name, int default_provider) { if (default_provider > 0) { provider = mjp_getResourceProviderAtSlot(default_provider); 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); return NULL; @@ -81,7 +86,9 @@ mjResource* mju_openResource(const char* name, int default_provider) { 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); return NULL; diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 19029344..614c65ed 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -13,15 +13,12 @@ // limitations under the License. #include -#include #include #include #include #include #include #include -#include -#include #include #include @@ -189,15 +186,28 @@ void mjCMesh::Compile(int default_provider) { // load STL, OBJ or MSH string ext = mjuu_getext(file); - if (!strcasecmp(ext.c_str(), ".stl")) { - LoadSTL(default_provider); - } else if (!strcasecmp(ext.c_str(), ".obj")) { - LoadOBJ(default_provider); - } else if (!strcasecmp(ext.c_str(), ".msh")) { - LoadMSH(default_provider); - } else { + if (strcasecmp(ext.c_str(), ".stl") && + strcasecmp(ext.c_str(), ".obj") && + strcasecmp(ext.c_str(), ".msh")) { 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 @@ -583,36 +593,20 @@ void mjCMesh::RemoveRepeated() { // load OBJ mesh -void mjCMesh::LoadOBJ(int default_provider) { - - // 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()); - } - } - +void mjCMesh::LoadOBJ(mjResource* resource) { tinyobj::ObjReader objReader; const void* bytes = nullptr; - int buffer_sz = mju_readResource(r, &bytes); + int buffer_sz = mju_readResource(resource, &bytes); if (buffer_sz < 0) { - mju_closeResource(r); - throw mjCError(this, "could not read OBJ file '%s'", filename.c_str()); + throw mjCError(this, "could not read OBJ file '%s'", resource->name); } // TODO(etom): support .mtl files? const char* buffer = (const char*) bytes; objReader.ParseFromString(std::string(buffer, buffer_sz), std::string()); - mju_closeResource(r); 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(); @@ -631,7 +625,7 @@ void mjCMesh::LoadOBJ(int default_provider) { if (nfacevert < 3 || nfacevert > 4) { throw mjCError( this, "only tri or quad meshes are supported for OBJ (file '%s')", - filename.c_str()); + resource->name); } face_indices.push_back(mesh.indices[idx]); @@ -686,53 +680,38 @@ void mjCMesh::LoadOBJ(int default_provider) { // load STL binary mesh -void mjCMesh::LoadSTL(int default_provider) { +void mjCMesh::LoadSTL(mjResource* resource) { 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 char* buffer = 0; - int buffer_sz = mju_readResource(r, (const void**) &buffer); + int buffer_sz = mju_readResource(resource, (const void**) &buffer); // still not found if (buffer_sz < 0) { - mju_closeResource(r); - throw mjCError(this, "could not read STL file '%s'", filename.c_str()); + throw mjCError(this, "could not read STL file '%s'", resource->name); } else if (!buffer_sz) { - mju_closeResource(r); - throw mjCError(this, "STL file '%s' is empty", filename.c_str()); + throw mjCError(this, "STL file '%s' is empty", resource->name); } // make sure there is enough data for header if (buffer_sz<84) { - mju_closeResource(r); - throw mjCError(this, "invalid header in STL file '%s'", filename.c_str()); + throw mjCError(this, "invalid header in STL file '%s'", resource->name); } // get number of triangles, check bounds nface = *(unsigned int*)(buffer+80); if (nface<1 || nface>200000) { - mju_closeResource(r); throw mjCError(this, "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 if (nface*50 != buffer_sz-84) { - mju_closeResource(r); throw mjCError(this, "STL file '%s' has wrong size; perhaps this is an ASCII file?", - filename.c_str()); + resource->name); } // assign stl data pointer @@ -749,16 +728,14 @@ void mjCMesh::LoadSTL(int default_provider) { float* v = (float*)(stl+50*i+12*(j+1)); for (int k=0; k < 3; k++) { if (std::isnan(v[k]) || std::isinf(v[k])) { - mju_closeResource(r); 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 if (fabs(v[k])>pow(2, 30)) { - mju_closeResource(r); throw mjCError(this, "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(); } // load MSH binary mesh -void mjCMesh::LoadMSH(int default_provider) { +void mjCMesh::LoadMSH(mjResource* resource) { 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 char* buffer = 0; - int buffer_sz = mju_readResource(r, (const void**) &buffer); + int buffer_sz = mju_readResource(resource, (const void**) &buffer); // still not found if (buffer_sz < 0) { - mju_closeResource(r); - throw mjCError(this, "could not read MSH file '%s'", filename.c_str()); + throw mjCError(this, "could not read MSH file '%s'", resource->name); } else if (!buffer_sz) { - mju_closeResource(r); - throw mjCError(this, "MSH file '%s' is empty", filename.c_str()); + throw mjCError(this, "MSH file '%s' is empty", resource->name); } // make sure header is present if (buffer_sz<4*sizeof(int)) { - mju_closeResource(r); - throw mjCError(this, "missing header in MSH file '%s'", filename.c_str()); + throw mjCError(this, "missing header in MSH file '%s'", resource->name); } // get sizes from header @@ -825,15 +787,13 @@ void mjCMesh::LoadMSH(int default_provider) { if (nvert<4 || nface<0 || nnormal<0 || ntexcoord<0 || (nnormal>0 && nnormal!=nvert) || (ntexcoord>0 && ntexcoord!=nvert)) { - mju_closeResource(r); - throw mjCError(this, "invalid sizes in MSH file '%s'", filename.c_str()); + throw mjCError(this, "invalid sizes in MSH file '%s'", resource->name); } // check file size if (buffer_sz != 4*sizeof(int) + 3*nvert*sizeof(float) + 3*nnormal*sizeof(float) + 2*ntexcoord*sizeof(float) + 3*nface*sizeof(int)) { - mju_closeResource(r); - throw mjCError(this, "unexpected file size in MSH file '%s'", filename.c_str()); + throw mjCError(this, "unexpected file size in MSH file '%s'", resource->name); } // allocate and copy @@ -872,8 +832,6 @@ void mjCMesh::LoadMSH(int default_provider) { face[3*i+2] = tmp; } } - - mju_closeResource(r); } @@ -1601,11 +1559,20 @@ void mjCSkin::Compile(int default_provider) { // load SKN string ext = mjuu_getext(file); - if (!strcasecmp(ext.c_str(), ".skn")) { - LoadSKN(default_provider); - } else { + if (strcasecmp(ext.c_str(), ".skn")) { 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 @@ -1724,33 +1691,19 @@ void mjCSkin::Compile(int default_provider) { // load skin in SKN BIN format -void mjCSkin::LoadSKN(int default_provider) { - // 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()); - } - } - +void mjCSkin::LoadSKN(mjResource* resource) { 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) { - mju_closeResource(r); - throw mjCError(this, "could not read SKN file '%s'", filename.c_str()); + throw mjCError(this, "could not read SKN file '%s'", resource->name); } else if (!buffer_sz) { - mju_closeResource(r); - throw mjCError(this, "SKN file '%s' is empty", filename.c_str()); + throw mjCError(this, "SKN file '%s' is empty", resource->name); } // make sure header is present if (buffer_sz<16) { - mju_closeResource(r); - throw mjCError(this, "missing header in SKN file '%s'", filename.c_str()); + throw mjCError(this, "missing header in SKN file '%s'", resource->name); } // get sizes from header @@ -1761,14 +1714,12 @@ void mjCSkin::LoadSKN(int default_provider) { // negative sizes not allowed if (nvert<0 || ntexcoord<0 || nface<0 || nbone<0) { - mju_closeResource(r); - throw mjCError(this, "negative size in header of SKN file '%s'", filename.c_str()); + throw mjCError(this, "negative size in header of SKN file '%s'", resource->name); } // make sure we have data for vert, texcoord, face if (buffer_sz < 16 + 12*nvert + 8*ntexcoord + 12*nface) { - mju_closeResource(r); - throw mjCError(this, "insufficient data in SKN file '%s'", filename.c_str()); + throw mjCError(this, "insufficient data in SKN file '%s'", resource->name); } // data pointer and counter @@ -1807,8 +1758,7 @@ void mjCSkin::LoadSKN(int default_provider) { for (int i=0; iname, i); } // read name @@ -1832,16 +1782,14 @@ void mjCSkin::LoadSKN(int default_provider) { // check for negative if (vcount<1) { - mju_closeResource(r); throw mjCError(this, "vertex count must be positive in SKN file '%s', bone %d", - filename.c_str(), i); + resource->name, i); } // check size if (buffer_sz/4-4-cnt < 2*vcount) { - mju_closeResource(r); throw mjCError(this, "insufficient vertex data in SKN file '%s', bone %d", - filename.c_str(), i); + resource->name, i); } // read vertid @@ -1855,10 +1803,8 @@ void mjCSkin::LoadSKN(int default_provider) { cnt += vcount; } - mju_closeResource(r); - // check final size 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); } } diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 64f7dd5d..d79f6ee9 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -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 -------------------------------------------------- // constructor @@ -1978,28 +1997,20 @@ mjCHField::~mjCHField() { // load elevation data from custom format -void mjCHField::LoadCustom(string filename, int default_provider) { +void mjCHField::LoadCustom(mjResource* resource) { // get file data in buffer 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); - // still not found - if (!buffer || buffer_sz < 1) { - throw mjCError(this, "could not read hfield file '%s'", filename.c_str()); + if (buffer_sz < 1) { + throw mjCError(this, "could not read hfield file '%s'", resource->name); + } else if (!buffer_sz) { + throw mjCError(this, "empty hfield file '%s'", resource->name); } + if (buffer_sz < 2*sizeof(int)) { - mju_closeResource(resource); - throw mjCError(this, "hfield missing header '%s'", filename.c_str()); + throw mjCError(this, "hfield missing header '%s'", resource->name); } // read dimensions @@ -2009,51 +2020,38 @@ void mjCHField::LoadCustom(string filename, int default_provider) { // check dimensions if (nrow<1 || ncol<1) { - mju_closeResource(resource); - throw mjCError(this, "non-positive hfield dimensions in file '%s'", filename.c_str()); + throw mjCError(this, "non-positive hfield dimensions in file '%s'", resource->name); } // check buffer size if (buffer_sz != nrow*ncol*sizeof(float)+8) { - mju_closeResource(resource); - throw mjCError(this, "unexpected file size in file '%s'", filename.c_str()); + throw mjCError(this, "unexpected file size in file '%s'", resource->name); } // allocate data = (float*) mju_malloc(nrow*ncol*sizeof(float)); if (!data) { - mju_closeResource(resource); throw mjCError(this, "could not allocate buffers in hfield"); } // copy data memcpy(data, (void*)(pint+2), nrow*ncol*sizeof(float)); - - // close file - mju_closeResource(resource); } // load elevation data from PNG format -void mjCHField::LoadPNG(string filename, int default_provider) { +void mjCHField::LoadPNG(mjResource* resource) { // determine data source 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); - // still not found - if (!inbuffer || inbuffer_sz < 1) { - mju_closeResource(resource); - throw mjCError(this, "could not read PNG file '%s'", filename.c_str()); + if (inbuffer_sz < 1) { + throw mjCError(this, "could not read hfield PNG file '%s'", resource->name); + } + + if (!inbuffer_sz) { + throw mjCError(this, "empty hfield PNG file '%s'", resource->name); } // load PNG from file or memory @@ -2063,18 +2061,15 @@ void mjCHField::LoadPNG(string filename, int default_provider) { // check if (err) { - mju_closeResource(resource); throw mjCError(this, "PNG load error '%s' in hfield id = %d", lodepng_error_text(err), id); } if (!w || !h) { - mju_closeResource(resource); - throw mjCError(this, "Zero dimension in PNG hfield '%s' (id = %d)", name.c_str(), id); + throw mjCError(this, "Zero dimension in PNG hfield '%s' (id = %d)", resource->name, id); } // allocate data = (float*) mju_malloc(w*h*sizeof(float)); if (!data) { - mju_closeResource(resource); 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]; } image.clear(); - mju_closeResource(resource); } @@ -2114,13 +2108,21 @@ void mjCHField::Compile(int default_provider) { // make filename string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); + mjResource* resource = LoadResource(filename, default_provider); // load depending on format string ext = mjuu_getext(filename); - if (!strcasecmp(ext.c_str(), ".png")) { - LoadPNG(filename, default_provider); - } else { - LoadCustom(filename, default_provider); + + try { + if (!strcasecmp(ext.c_str(), ".png")) { + 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 -void mjCTexture::LoadPNG(string filename, int default_provider, +void mjCTexture::LoadPNG(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h) { 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); // still not found - if (!inbuffer || inbuffer_sz < 1) { - mju_closeResource(resource); - throw mjCError(this, "could not read PNG file '%s'", filename.c_str()); + if (inbuffer_sz < 1) { + throw mjCError(this, "could not read PNG texture file '%s'", resource->name); + } else if (!inbuffer_sz) { + throw mjCError(this, "PNG texture file '%s' is empty", resource->name); } + // load PNG from file or memory unsigned int err = lodepng::decode(image, w, h, (const unsigned char*) inbuffer, inbuffer_sz, LCT_RGB, 8); - mju_closeResource(resource); // check 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); } 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 -void mjCTexture::LoadCustom(string filename, int default_provider, +void mjCTexture::LoadCustom(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h) { 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); // still not found - if (!buffer || buffer_sz < 0) { - mju_closeResource(resource); - throw mjCError(this, "could not read texture file '%s'", filename.c_str()); + if (buffer_sz < 0) { + throw mjCError(this, "could not read texture file '%s'", resource->name); + } else if (!buffer_sz) { + throw mjCError(this, "texture file is empty: '%s'", resource->name); } + // read dimensions int* pint = (int*)buffer; w = pint[0]; @@ -2497,23 +2484,19 @@ void mjCTexture::LoadCustom(string filename, int default_provider, // check dimensions if (w<1 || h<1) { - mju_closeResource(resource); 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 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" - "unexpected file size in file '%s'", filename.c_str()); + "unexpected file size in file '%s'", resource->name); } // allocate and copy image.resize(w*h*3); 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) { // dispatch to PNG or Custom loaded string ext = mjuu_getext(filename); - if (!strcasecmp(ext.c_str(), ".png")) { - LoadPNG(filename, default_provider, image, w, h); - } else { - LoadCustom(filename, default_provider, image, w, h); + mjResource* resource = LoadResource(filename, default_provider); + + try { + 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 diff --git a/src/user/user_objects.h b/src/user/user_objects.h index db8595f0..c63d150b 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -144,6 +144,9 @@ class mjCBase { friend class mjCDef; public: + // load resource if found (fallback to OS filesystem) + mjResource* LoadResource(std::string filename, int provider); + std::string name; // object name std::string classname; // defaults class name int id; // object id @@ -513,9 +516,9 @@ class mjCMesh: public mjCBase { mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor ~mjCMesh(); // destructor void Compile(int default_provider); // compiler - void LoadOBJ(int default_provider); // load mesh in wavefront OBJ format - void LoadSTL(int default_provider); // load mesh in STL BIN format - void LoadMSH(int default_provider); // load mesh in MSH BIN format + void LoadOBJ(mjResource* resource); // load mesh in wavefront OBJ format + void LoadSTL(mjResource* resource); // load mesh in STL BIN format + void LoadMSH(mjResource* resource); // load mesh in MSH BIN format void MakeGraph(void); // make graph of convex hull void CopyGraph(void); // copy graph into face data void MakeNormal(void); // compute vertex normals @@ -595,7 +598,7 @@ class mjCSkin: public mjCBase { mjCSkin(mjCModel* = 0); // constructor ~mjCSkin(); // destructor 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 std::vector bodyid; // body ids @@ -622,8 +625,8 @@ class mjCHField : public mjCBase { ~mjCHField(); // destructor void Compile(int default_provider); // compiler - void LoadCustom(std::string filename, int default_provider); // load from custom format - void LoadPNG(std::string filename, int default_provider); // load from PNG format + void LoadCustom(mjResource* resource); // load from custom format + void LoadPNG(mjResource* resource); // load from PNG format }; @@ -676,10 +679,10 @@ class mjCTexture : public mjCBase { std::vector& image, unsigned int& w, unsigned int& h); - void LoadPNG(std::string filename, int default_provider, + void LoadPNG(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h); - void LoadCustom(std::string filename, int default_provider, + void LoadCustom(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h); @@ -708,8 +711,8 @@ class mjCMaterial : public mjCBase { float rgba[4]; // rgba private: - mjCMaterial(mjCModel* = 0, mjCDef* = 0);// constructor - void Compile(void); // compiler + mjCMaterial(mjCModel* = 0, mjCDef* = 0); // constructor + void Compile(void); // compiler int texid; // id of material }; @@ -742,8 +745,8 @@ class mjCPair : public mjCBase { } private: - mjCPair(mjCModel* = 0, mjCDef* = 0);// constructor - void Compile(void); // compiler + mjCPair(mjCModel* = 0, mjCDef* = 0); // constructor + void Compile(void); // compiler int geom1; // id of geom1 int geom2; // id of geom2 @@ -828,22 +831,22 @@ class mjCTendon : public mjCBase { mjCWrap* GetWrap(int); // pointer to wrap // variables set by user - int group; // group for visualization - std::string material; // name of material for rendering - int limited; // does tendon have limits: 0 false, 1 true, 2 auto - double width; // width for rendering - mjtNum solref_limit[mjNREF]; // solver reference: tendon limits - mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits - mjtNum solref_friction[mjNREF]; // solver reference: tendon friction - mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction - double range[2]; // length limits - double margin; // margin value for tendon limit detection - double stiffness; // stiffness coefficient - double damping; // damping coefficient - double frictionloss; // friction loss - double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring - std::vector userdata; // user data - float rgba[4]; // rgba when material is omitted + int group; // group for visualization + std::string material; // name of material for rendering + int limited; // does tendon have limits: 0 false, 1 true, 2 auto + double width; // width for rendering + mjtNum solref_limit[mjNREF]; // solver reference: tendon limits + mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits + mjtNum solref_friction[mjNREF]; // solver reference: tendon friction + mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction + double range[2]; // length limits + double margin; // margin value for tendon limit detection + double stiffness; // stiffness coefficient + double damping; // damping coefficient + double frictionloss; // friction loss + double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring + std::vector userdata; // user data + float rgba[4]; // rgba when material is omitted private: mjCTendon(mjCModel* = 0, mjCDef* = 0); // constructor @@ -934,8 +937,8 @@ class mjCActuator : public mjCBase { std::string refsite; // reference site, for site transmission only private: - mjCActuator(mjCModel* = 0, mjCDef* = 0);// constructor - void Compile(void); // compiler + mjCActuator(mjCModel* = 0, mjCDef* = 0); // constructor + void Compile(void); // compiler int trnid[2]; // id of transmission target }; @@ -1046,18 +1049,18 @@ class mjCKey : public mjCBase { friend class mjXWriter; public: - double time; // time - std::vector qpos; // qpos - std::vector qvel; // qvel - std::vector act; // act - std::vector mpos; // mocap pos - std::vector mquat; // mocap quat - std::vector ctrl; // ctrl + double time; // time + std::vector qpos; // qpos + std::vector qvel; // qvel + std::vector act; // act + std::vector mpos; // mocap pos + std::vector mquat; // mocap quat + std::vector ctrl; // ctrl private: - mjCKey(mjCModel*); // constructor - ~mjCKey(); // destructor - void Compile(const mjModel* m); // compiler + mjCKey(mjCModel*); // constructor + ~mjCKey(); // destructor + void Compile(const mjModel* m); // compiler }; diff --git a/test/engine/engine_sensor_test.cc b/test/engine/engine_sensor_test.cc index d34b852f..ae66fc0e 100644 --- a/test/engine/engine_sensor_test.cc +++ b/test/engine/engine_sensor_test.cc @@ -55,7 +55,7 @@ TEST_F(SensorTest, DisableSensors) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); // before calling anything, check that sensors are initialised to 0 @@ -105,7 +105,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMat) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); mj_forward(model, data); @@ -143,7 +143,7 @@ TEST_F(RelativeFrameSensorTest, ReferenceQuatMat) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); // call mj_forward and convert orientation matrix to quaternion @@ -190,7 +190,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMatQuat) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); constexpr int nsensordata = 32; ASSERT_EQ(model->nsensordata, nsensordata); mjData* data = mj_makeData(model); @@ -240,7 +240,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelLinearFixed) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); data->qvel[0] = mju_sqrt(2); data->qvel[1] = 1; @@ -272,7 +272,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngFixed) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); // set joint velocities and call forward dynamics @@ -307,7 +307,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngOpposing) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); // set joint velocities and call forward dynamics @@ -350,7 +350,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelGeneral) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); mjtNum dt = 1e-6; // timestep used for finite differencing @@ -411,7 +411,7 @@ TEST_F(SensorTest, Clock) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); mjData* data = mj_makeData(model); // call step 4 times, checking that clock works as expected diff --git a/test/fixture.cc b/test/fixture.cc index d95a5289..2715f197 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -68,18 +68,49 @@ const std::string GetModelPath(std::string_view 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, - int error_size) { - static constexpr char file[] = "filename.xml"; - // mjVFS structs need to be allocated on the heap, because it's ~2MB - auto vfs = std::make_unique(); - mj_defaultVFS(vfs.get()); - mj_makeEmptyFileVFS(vfs.get(), file, xml.size()); - int file_idx = mj_findFileVFS(vfs.get(), file); - memcpy(vfs->filedata[file_idx], xml.data(), xml.size()); - mjModel* m = mj_loadXML(file, vfs.get(), error, error_size); - mj_deleteFileVFS(vfs.get(), file); - return m; + int error_size, mjVFS* vfs) { + + // register string resource provider if not registered before + if (mjp_getResourceProvider("S(") == nullptr) { + mjpResourceProvider resourceProvider; + resourceProvider.prefix = "S("; + resourceProvider.open = str_open_callback; + resourceProvider.read = str_read_callback; + resourceProvider.close = str_close_callback; + mjp_registerResourceProvider(&resourceProvider); + } + 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, diff --git a/test/fixture.h b/test/fixture.h index 755c3baf..d658bae4 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -52,7 +52,7 @@ const std::string GetModelPath(absl::string_view path); // Returns a newly-allocated mjModel, loaded from the contents of xml. // On failure returns nullptr and populates the error array if present. 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. // On failure it asserts that model is null. diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index a29c9c46..e1221edb 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -16,8 +16,8 @@ #include #include -#include #include +#include #include #include @@ -25,6 +25,7 @@ #include #include #include +#include "src/cc/array_safety.h" #include "test/fixture.h" namespace mujoco { @@ -58,6 +59,7 @@ static const char* const kMalformedFaceOBJPath = "user/testdata/malformed_face.xml"; using ::testing::HasSubstr; +using ::testing::IsNull; // ------------- test invalid filenames ---------------------------------------- @@ -91,6 +93,83 @@ TEST_F(MjCMeshTest, UnknownMeshFormat) { } } +// -------------------- test OS filesystem fallback ---------------------------- + +TEST_F(MjCMeshTest, LoadMSHWithVFS) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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_F(MjCMeshTest, DeDuplicateSTLVertices) { @@ -102,7 +181,8 @@ TEST_F(MjCMeshTest, DeDuplicateSTLVertices) { mj_deleteModel(model); } -// ------------- test Mesh loading (MSH) ------------------------------ +// -------------------- test Mesh loading (MSH) -------------------------------- + TEST_F(MjCMeshTest, LoadMSH) { const std::string xml_path = GetTestDataFilePath(kMeshPath); char error[1024]; @@ -184,7 +264,7 @@ TEST_F(MjCMeshTest, TinyMeshLoads) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, testing::NotNull()); mj_deleteModel(model); } @@ -204,7 +284,7 @@ TEST_F(MjCMeshTest, FaceNormalAutogenerated) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, testing::NotNull()); mj_deleteModel(model); } @@ -226,7 +306,7 @@ TEST_F(MjCMeshTest, SmallInertiaLoads) { )"; - mjModel* model = LoadModelFromString(xml, 0, 0); + mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, testing::NotNull()); 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[3] - 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+1] - 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; 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); - 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); } mj_deleteModel(model); diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 2109eb6d..a2d7d6d0 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -14,15 +14,18 @@ // Tests for user/user_objects.cc. +#include #include #include #include +#include #include #include #include #include #include +#include "src/cc/array_safety.h" #include "test/fixture.h" namespace mujoco { @@ -37,6 +40,116 @@ using ::testing::HasSubstr; using ::testing::IsNull; using ::testing::NotNull; +// -------------------- test OS filesystem fallback ---------------------------- + +using VfsTest = MujocoTest; + +TEST_F(VfsTest, HFieldPngWithVFS) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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"( + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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"( + + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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"( + + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + 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 ------------------------------------- using KeyframeTest = MujocoTest; @@ -289,6 +402,7 @@ TEST_F(MjCGeomTest, CapsuleInertiaX) { } // ------------- test inertiagrouprange ---------------------------------------- + TEST_F(MjCGeomTest, IgnoreGeomOutsideInertiagrouprange) { static constexpr char xml[] = R"(