Change compile sample to use mj_parse and mj_encode as input/output.

There is a single remaining exception for mjb as it's a special case by bypassing mjSpec.

PiperOrigin-RevId: 943556676
Change-Id: Iaf480ea6d2d42a6007b35c23435b25c9eb24ead0
This commit is contained in:
Sam Haves
2026-07-06 16:34:39 -07:00
committed by Copybara-Service
parent 19e175c063
commit ae58855c15
2 changed files with 112 additions and 111 deletions
+81 -111
View File
@@ -12,42 +12,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <chrono>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string_view>
#include <mujoco/mujoco.h>
// help
static constexpr char helpstring[] =
"\n Usage: compile infile [outfile]\n"
" infile can be in mjcf, urdf, mjb format\n"
" outfile can be in mjcf, mjb, txt format\n\n"
" if infile is mjcf or urdf and outfile is omitted, a detailed\n"
" timing breakdown is printed for two compilations (cold and warm cache)\n\n"
" Example: compile model.xml [model.mjb]\n";
// timer (seconds)
double gettm(void) {
using Clock = std::chrono::steady_clock;
using Seconds = std::chrono::duration<double>;
static const Clock::time_point tm_start = Clock::now();
return Seconds(Clock::now() - tm_start).count();
}
static constexpr char kHelp[] =
"\n Usage: compile infile [outfile]\n"
" infile can be in any format with a registered decoder (e.g. mjcf, "
"urdf) or mjb\n"
" outfile can be in mjcf, mjb, txt format, or empty\n\n"
" if outfile is empty, compilation will be "
"timed twice to measure the impact of caching\n\n"
" Example: compile model.xml [model.mjb]\n";
// deallocate and print message
int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0) {
int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0,
mjVFS* vfs = 0) {
// deallocated everything
if (m) {
mj_deleteModel(m);
}
if (vfs) {
mj_deleteVFS(vfs);
}
// print message
if (msg) {
@@ -57,52 +52,20 @@ int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0) {
return exitcode;
}
// check if filename has extension (case-insensitive)
bool HasExtension(std::string_view filename, std::string_view ext) {
if (filename.length() < ext.length()) return false;
// possible file types
enum {
typeUNKNOWN = 0,
typeXML,
typeMJB,
typeTXT,
typeNONE
};
// determine file type
int filetype(const char* filename) {
// convert to lower case for string comparison
char lower[1000];
std::size_t i=0;
while (i<std::strlen(filename) && i<999) {
lower[i] = (char)tolower(filename[i]);
i++;
}
lower[i] = 0;
// find last dot
int dot = (int)std::strlen(lower);
while (dot>=0 && lower[dot]!='.') {
dot--;
}
// no dot found
if (dot<0) {
return typeUNKNOWN;
}
// check extension
if (!std::strcmp(lower+dot, ".xml") || !std::strcmp(lower+dot, ".urdf")) {
return typeXML;
} else if (!std::strcmp(lower+dot, ".mjb")) {
return typeMJB;
} else if (!std::strcmp(lower+dot, ".txt")) {
return typeTXT;
} else {
return typeUNKNOWN;
std::string_view file_ext = filename.substr(filename.length() - ext.length());
for (std::size_t i = 0; i < ext.length(); ++i) {
if (std::tolower(static_cast<unsigned char>(file_ext[i])) !=
std::tolower(static_cast<unsigned char>(ext[i]))) {
return false;
}
}
return true;
}
// main function
int main(int argc, char** argv) {
@@ -111,34 +74,33 @@ int main(int argc, char** argv) {
char error[1000];
// print help if arguments are missing
if (argc!=3 && argc!=2) {
return finish(helpstring, EXIT_FAILURE);
if (argc != 3 && argc != 2) {
return finish(kHelp, EXIT_FAILURE);
}
// determine file types
int type1 = filetype(argv[1]);
int type2 = argc==2 ? typeNONE : filetype(argv[2]);
const bool is_mjb = HasExtension(argv[1], ".mjb");
// check types
if (type1 == typeUNKNOWN || type1 == typeTXT ||
type2 == typeUNKNOWN || (type1 == typeMJB && type2 == typeXML)) {
return finish("Illegal combination of file formats", EXIT_FAILURE);
if (is_mjb && argc == 3 && HasExtension(argv[2], ".xml")) {
return finish("Illegal combination: cannot save binary model to XML",
EXIT_FAILURE);
}
// check if output file exists
std::FILE* fp = std::fopen(argv[2], "r");
if (fp) {
std::cout << "Output file already exists, overwrite? (Y/n) ";
char c;
std::cin >> c;
if (c!='y' && c!='Y') {
if (argc == 3) {
std::FILE* fp = std::fopen(argv[2], "r");
if (fp) {
std::fclose(fp);
return finish();
std::cout << "Output file already exists, overwrite? (Y/n) ";
char c;
std::cin >> c;
if (c != 'y' && c != 'Y') {
return finish();
}
}
}
// enable compile timing diagnostics
if (type2 == typeNONE) {
if (argc == 2) {
mjLogConfig config = mju_getLogConfig();
config.logfile[0] = '\0';
config.topics |= (1 << (mjTOPIC_TIME_CMP - 1));
@@ -146,50 +108,58 @@ int main(int argc, char** argv) {
}
// load model
mjSpec* s = nullptr;
if (type1==typeXML) {
s = mj_parseXML(argv[1], 0, error, 1000);
if (!s) {
return finish(error, EXIT_FAILURE);
}
mjVFS vfs;
mj_defaultVFS(&vfs);
mjSpec* spec = nullptr;
if (type2 == typeNONE) {
std::cout << "Compile 1 (cold cache)\n";
}
m = mj_compile(s, 0);
if (is_mjb) {
m = mj_loadModel(argv[1], &vfs);
if (!m) {
mj_deleteSpec(s);
return finish("Could not compile model", EXIT_FAILURE);
}
if (type2 == typeNONE) {
mj_deleteModel(m);
std::cout << "Compile 2 (warm cache)\n";
m = mj_compile(s, 0);
return finish("Could not load binary model", EXIT_FAILURE, nullptr, &vfs);
}
} else {
m = mj_loadModel(argv[1], 0);
spec = mj_parse(argv[1], nullptr, &vfs, error, 1000);
if (!spec) {
return finish(error, EXIT_FAILURE, nullptr, &vfs);
}
if (argc == 2) {
std::cout << "Compile 1 (cold cache)\n";
}
m = mj_compile(spec, &vfs);
if (!m) {
auto err_msg = mjs_getError(spec);
mj_deleteSpec(spec);
return finish(err_msg, EXIT_FAILURE, nullptr, &vfs);
}
if (argc == 2) {
mj_deleteModel(m);
std::cout << "Compile 2 (warm cache)\n";
m = mj_compile(spec, &vfs);
}
}
// check error
if (!m) {
if (s) mj_deleteSpec(s);
return finish("Could not load model", EXIT_FAILURE);
if (spec) {
auto err_msg = mjs_getError(spec);
mj_deleteSpec(spec);
return finish(err_msg, EXIT_FAILURE, nullptr, &vfs);
} else {
return finish("Could not load model", EXIT_FAILURE, nullptr, &vfs);
}
}
// save model
if (type2 == typeXML) {
if (!mj_saveLastXML(argv[2], m, error, 1000)) {
if (s) mj_deleteSpec(s);
return finish(error, EXIT_FAILURE, m);
// encode output
if (argc == 3) {
if (mj_encode(spec, m, argv[2], nullptr, &vfs, error, 1000) < 0) {
if (spec) mj_deleteSpec(spec);
return finish(error, EXIT_FAILURE, m, &vfs);
}
} else if (type2 == typeMJB) {
mj_saveModel(m, argv[2], 0, 0);
} else if (type2 == typeTXT) {
mj_printModel(m, argv[2]);
}
// finalize
if (s) mj_deleteSpec(s);
return finish("Done.", EXIT_SUCCESS, m);
if (spec) mj_deleteSpec(spec);
return finish("Done.", EXIT_SUCCESS, m, &vfs);
}
+31
View File
@@ -35,4 +35,35 @@ if [ ! -s "$OUTPUT_FILE" ]; then
die "Output file empty or missing (${OUTPUT_FILE})."
fi
# Test invalid arguments (expect failure)
"$TARGET_BINARY" > /dev/null 2>&1
if [ $? -eq 0 ]; then
die "Expected failure with no arguments"
fi
# Test illegal combination (MJB -> XML, expect failure)
"$TARGET_BINARY" "$OUTPUT_FILE" "${TEST_TMPDIR}/failed.xml" > /dev/null 2>&1
if [ $? -eq 0 ]; then
die "Expected failure for MJB -> XML"
fi
# Test compile with 1 argument (expect success)
"$TARGET_BINARY" "$MODEL" > /dev/null || die "compile with 1 argument failed"
# Test XML -> MJZ (expect success)
OUTPUT_MJZ="${TEST_TMPDIR}/compiled.mjz"
"$TARGET_BINARY" "$MODEL" "$OUTPUT_MJZ" || die "compile to MJZ failed"
if [ ! -s "$OUTPUT_MJZ" ]; then
die "MJZ output file empty or missing (${OUTPUT_MJZ})."
fi
# Test MJZ -> MJB (expect success)
OUTPUT_MJB_FROM_MJZ="${TEST_TMPDIR}/compiled_from_mjz.mjb"
"$TARGET_BINARY" "$OUTPUT_MJZ" "$OUTPUT_MJB_FROM_MJZ" || die "compile from MJZ to MJB failed"
if [ ! -s "$OUTPUT_MJB_FROM_MJZ" ]; then
die "MJB output from MJZ empty or missing (${OUTPUT_MJB_FROM_MJZ})."
fi
echo "PASS"