diff --git a/CMakeLists.txt b/CMakeLists.txt index 91a54ad2..f5aee6b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,10 +39,11 @@ enable_language(CXX) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON) +option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON) option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON) option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON) -if(APPLE AND MUJOCO_BUILD_EXAMPLES) +if(APPLE AND (MUJOCO_BUILD_EXAMPLES OR MUJOCO_BUILD_SIMULATE)) enable_language(OBJC) enable_language(OBJCXX) endif() @@ -152,6 +153,13 @@ add_library(mujoco::mujoco ALIAS mujoco) add_subdirectory(model) +# `simulate` defines a macro, embed_in_bundle, that's used by `sample`, so that +# subdirectory needs to be added first. +# TODO: Remove this order dependency. +if(MUJOCO_BUILD_SIMULATE) + add_subdirectory(simulate) +endif() + if(MUJOCO_BUILD_EXAMPLES) add_subdirectory(sample) endif() diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 7e20cacb..1756dae2 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -70,14 +70,6 @@ if(MUJOCO_HARDEN) endif() endif() -# Utility library -add_library(uitools STATIC) -target_sources(uitools PRIVATE uitools.h uitools.c) -target_include_directories(uitools PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_compile_options(uitools PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) -target_link_libraries(uitools PUBLIC glfw mujoco::mujoco) -target_link_options(uitools PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) - # Build sample binaries add_executable(compile compile.cc) target_compile_options(compile PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) @@ -126,85 +118,13 @@ target_link_libraries( ) target_link_options(record PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) -if(APPLE) - set(SIMULATE_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../dist/mujoco.icns) -elseif(WIN32) - set(SIMULATE_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../dist/appicon.rc) -else() - set(SIMULATE_RESOURCE_FILES "") -endif() - -add_executable(simulate simulate.cc array_safety.h ${SIMULATE_RESOURCE_FILES}) -target_compile_options(simulate PUBLIC ${MUJOCO_SAMPLE_COMPILE_OPTIONS}) -if(WIN32) - add_custom_command( - TARGET simulate - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../dist/mujoco.ico - ${CMAKE_CURRENT_SOURCE_DIR} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E rm ${CMAKE_CURRENT_SOURCE_DIR}/mujoco.ico - ) -endif() - -target_link_libraries( - simulate - mujoco::mujoco - uitools - glfw - Threads::Threads -) -target_link_options(simulate PRIVATE ${MUJOCO_SAMPLE_LINK_OPTIONS}) - -if(APPLE) - target_sources(simulate PRIVATE macos_save.mm) - target_link_libraries(simulate "-framework Cocoa") -endif() - if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) - set_target_properties( - simulate - PROPERTIES INSTALL_RPATH @executable_path/../Frameworks - BUILD_WITH_INSTALL_RPATH TRUE - RESOURCE ${SIMULATE_RESOURCE_FILES} - MACOSX_BUNDLE TRUE - MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../dist/Info.plist.simulate.in - MACOSX_BUNDLE_BUNDLE_NAME "MuJoCo" - MACOSX_BUNDLE_GUI_IDENTIFIER "org.mujoco.app" - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_INFO_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_ICON_FILE "mujoco.icns" - MACOSX_BUNDLE_COPYRIGHT "Copyright 2021 DeepMind Technologies Limited." - ) - - macro(embed_in_bundle target) - add_dependencies(${target} simulate) - set_target_properties( - ${target} - PROPERTIES INSTALL_RPATH @executable_path/../Frameworks - BUILD_WITH_INSTALL_RPATH TRUE - RUNTIME_OUTPUT_DIRECTORY $ - ) - endmacro() - embed_in_bundle(basic simulate) embed_in_bundle(compile simulate) embed_in_bundle(derivative simulate) embed_in_bundle(record simulate) embed_in_bundle(testspeed simulate) embed_in_bundle(testxml simulate) - - # Embed mujoco.framework inside the App bundle. - add_custom_command( - TARGET simulate - POST_BUILD - COMMAND mkdir -p $/../Frameworks - COMMAND rm -rf $/../Frameworks/mujoco.framework - COMMAND cp -a $/../../../mujoco.framework - $/../Frameworks/ - ) endif() # Do not install if macOS Bundles are created as RPATH is managed manually there. @@ -225,7 +145,6 @@ if(_INSTALL_SAMPLES) record testspeed testxml - simulate INSTALL_DIRECTORY "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}" LIB_DIRS @@ -241,7 +160,6 @@ if(_INSTALL_SAMPLES) record testspeed testxml - simulate EXPORT ${PROJECT_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT samples LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT samples diff --git a/sample/Makefile b/sample/Makefile index 8d3c2d5b..c6ca7568 100644 --- a/sample/Makefile +++ b/sample/Makefile @@ -11,6 +11,3 @@ all: $(CXX) $(COMMON) derivative.cc -lmujoco -fopenmp -o ../bin/derivative $(CXX) $(COMMON) basic.cc -lmujoco -lglfw -o ../bin/basic $(CXX) $(COMMON) record.cc -lmujoco -lglfw -o ../bin/record - $(CC) -c -O2 -I../include uitools.c - $(CXX) $(COMMON) uitools.o simulate.cc -lmujoco -lglfw -o ../bin/simulate - rm *.o diff --git a/sample/Makefile.macos b/sample/Makefile.macos index 2e02f6d6..ba5b6093 100644 --- a/sample/Makefile.macos +++ b/sample/Makefile.macos @@ -17,7 +17,3 @@ all: clang++ $(ALLFLAGS) derivative.cc -framework mujoco -o derivative clang++ $(ALLFLAGS) basic.cc -framework mujoco -lglfw -o basic clang++ $(ALLFLAGS) record.cc -framework mujoco -lglfw -o record - clang -c $(CFLAGS) uitools.c - clang++ -c $(CXXFLAGS) macos_save.mm - clang++ $(ALLFLAGS) simulate.cc uitools.o macos_save.o -framework mujoco -framework Cocoa -lglfw -o simulate - rm *.o diff --git a/sample/Makefile.windows b/sample/Makefile.windows index e8ac9831..6bbc028a 100644 --- a/sample/Makefile.windows +++ b/sample/Makefile.windows @@ -15,5 +15,4 @@ all: cl $(COMMON) derivative.cc /openmp ../lib/mujoco.lib cl $(COMMON) basic.cc ../lib/glfw3dll.lib ../lib/mujoco.lib cl $(COMMON) record.cc ../lib/glfw3dll.lib ../lib/mujoco.lib - cl $(COMMON) simulate.cc uitools.c ../lib/glfw3dll.lib ../lib/mujoco.lib del *.obj diff --git a/sample/cmake/SampleDependencies.cmake b/sample/cmake/SampleDependencies.cmake index 14cc9430..3534929f 100644 --- a/sample/cmake/SampleDependencies.cmake +++ b/sample/cmake/SampleDependencies.cmake @@ -58,8 +58,10 @@ findorfetch( EXCLUDE_FROM_ALL ) -option(MUJOCO_SAMPLES_STATIC_GLFW "Link MuJoCo sample apps against GLFW statically." ON) -if(MUJOCO_SAMPLES_STATIC_GLFW) +option(MUJOCO_EXTRAS_STATIC_GLFW + "Link MuJoCo sample apps and simulate libraries against GLFW statically." ON +) +if(MUJOCO_EXTRAS_STATIC_GLFW) set(BUILD_SHARED_LIBS_OLD ${BUILD_SHARED_LIBS}) set(BUILD_SHARED_LIBS OFF @@ -88,7 +90,7 @@ findorfetch( EXCLUDE_FROM_ALL ) -if(MUJOCO_SAMPLES_STATIC_GLFW) +if(MUJOCO_EXTRAS_STATIC_GLFW) set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_OLD} CACHE BOOL "Build SHARED libraries" FORCE diff --git a/sample/simulate.cc b/sample/simulate.cc deleted file mode 100644 index ac2e308b..00000000 --- a/sample/simulate.cc +++ /dev/null @@ -1,2185 +0,0 @@ -// Copyright 2021 DeepMind Technologies Limited -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include -#include -#include -#include - -#include -#include "uitools.h" - -#include "array_safety.h" -namespace mju = ::mujoco::sample_util; - -//-------------------------------- global ----------------------------------------------- - -static constexpr int kBufSize = 1000; - -// constants -const int maxgeom = 5000; // preallocated geom array in mjvScene -const double syncmisalign = 0.1; // maximum time mis-alignment before re-sync -const double refreshfactor = 0.7; // fraction of refresh available for simulation -const int max_slow_down = 128; // maximum slow-down quotient -const double zoom_increment = 0.02; // ratio of single click-wheel zoom increment to vertical extent - -// model and data -mjModel* m = NULL; -mjData* d = NULL; - - -// strings -char filename[kBufSize] = ""; -char previous_filename[kBufSize] = ""; -char loadError[kBufSize] = ""; - - -// control noise variables -mjtNum* ctrlnoise = nullptr; - - -// abstract visualization -mjvScene scn; -mjvCamera cam; -mjvOption vopt; -mjvPerturb pert; -mjvFigure figconstraint; -mjvFigure figcost; -mjvFigure figtimer; -mjvFigure figsize; -mjvFigure figsensor; - - -// OpenGL rendering and UI -GLFWvidmode vmode; -int windowpos[2]; -int windowsize[2]; -mjrContext con; -GLFWwindow* window = NULL; -mjuiState uistate; -mjUI ui0, ui1; - - -// UI settings not contained in MuJoCo structures -struct { - // file - int exitrequest = 0; - - // option - int spacing = 0; - int color = 0; - int font = 0; - int ui0 = 1; - int ui1 = 1; - int help = 0; - int info = 0; - int profiler = 0; - int sensor = 0; - int fullscreen = 0; - int vsync = 1; - int busywait = 0; - - // simulation - int run = 1; - int key = 0; - int loadrequest = 0; - int slow_down = 1; - bool speed_changed = true; - double ctrlnoisestd = 0.0; - double ctrlnoiserate = 0.0; - - // watch - char field[mjMAXUITEXT] = "qpos"; - int index = 0; - - // physics: need sync - int disable[mjNDISABLE]; - int enable[mjNENABLE]; - - // rendering: need sync - int camera = 0; -} settings; - - -// section ids -enum { - // left ui - SECT_FILE = 0, - SECT_OPTION, - SECT_SIMULATION, - SECT_WATCH, - SECT_PHYSICS, - SECT_RENDERING, - SECT_GROUP, - NSECT0, - - // right ui - SECT_JOINT = 0, - SECT_CONTROL, - NSECT1 -}; - - -// file section of UI -const mjuiDef defFile[] = { - {mjITEM_SECTION, "File", 1, NULL, "AF"}, - {mjITEM_BUTTON, "Save xml", 2, NULL, ""}, - {mjITEM_BUTTON, "Save mjb", 2, NULL, ""}, - {mjITEM_BUTTON, "Print model", 2, NULL, "CM"}, - {mjITEM_BUTTON, "Print data", 2, NULL, "CD"}, - {mjITEM_BUTTON, "Quit", 1, NULL, "CQ"}, - {mjITEM_END} -}; - - -// option section of UI -const mjuiDef defOption[] = { - {mjITEM_SECTION, "Option", 1, NULL, "AO"}, - {mjITEM_SELECT, "Spacing", 1, &settings.spacing, "Tight\nWide"}, - {mjITEM_SELECT, "Color", 1, &settings.color, "Default\nOrange\nWhite\nBlack"}, - {mjITEM_SELECT, "Font", 1, &settings.font, "50 %\n100 %\n150 %\n200 %\n250 %\n300 %"}, - {mjITEM_CHECKINT, "Left UI (Tab)", 1, &settings.ui0, " #258"}, - {mjITEM_CHECKINT, "Right UI", 1, &settings.ui1, "S#258"}, - {mjITEM_CHECKINT, "Help", 2, &settings.help, " #290"}, - {mjITEM_CHECKINT, "Info", 2, &settings.info, " #291"}, - {mjITEM_CHECKINT, "Profiler", 2, &settings.profiler, " #292"}, - {mjITEM_CHECKINT, "Sensor", 2, &settings.sensor, " #293"}, -#ifdef __APPLE__ - {mjITEM_CHECKINT, "Fullscreen", 0, &settings.fullscreen, " #294"}, -#else - {mjITEM_CHECKINT, "Fullscreen", 1, &settings.fullscreen, " #294"}, -#endif - {mjITEM_CHECKINT, "Vertical Sync", 1, &settings.vsync, ""}, - {mjITEM_CHECKINT, "Busy Wait", 1, &settings.busywait, ""}, - {mjITEM_END} -}; - - -// simulation section of UI -const mjuiDef defSimulation[] = { - {mjITEM_SECTION, "Simulation", 1, NULL, "AS"}, - {mjITEM_RADIO, "", 2, &settings.run, "Pause\nRun"}, - {mjITEM_BUTTON, "Reset", 2, NULL, " #259"}, - {mjITEM_BUTTON, "Reload", 2, NULL, "CL"}, - {mjITEM_BUTTON, "Align", 2, NULL, "CA"}, - {mjITEM_BUTTON, "Copy pose", 2, NULL, "CC"}, - {mjITEM_SLIDERINT, "Key", 3, &settings.key, "0 0"}, - {mjITEM_BUTTON, "Load key", 3}, - {mjITEM_BUTTON, "Save key", 3}, - {mjITEM_SLIDERNUM, "Noise scale", 2, &settings.ctrlnoisestd, "0 2"}, - {mjITEM_SLIDERNUM, "Noise rate", 2, &settings.ctrlnoiserate, "0 2"}, - {mjITEM_END} -}; - - -// watch section of UI -const mjuiDef defWatch[] = { - {mjITEM_SECTION, "Watch", 0, NULL, "AW"}, - {mjITEM_EDITTXT, "Field", 2, settings.field, "qpos"}, - {mjITEM_EDITINT, "Index", 2, &settings.index, "1"}, - {mjITEM_STATIC, "Value", 2, NULL, " "}, - {mjITEM_END} -}; - - -// help strings -const char help_content[] = - "Space\n" - "+ -\n" - "Right arrow\n" - "[ ]\n" - "Esc\n" - "Double-click\n" - "Page Up\n" - "Right double-click\n" - "Ctrl Right double-click\n" - "Scroll, middle drag\n" - "Left drag\n" - "[Shift] right drag\n" - "Ctrl [Shift] drag\n" - "Ctrl [Shift] right drag\n" - "F1\n" - "F2\n" - "F3\n" - "F4\n" - "F5\n" - "UI right hold\n" - "UI title double-click"; - -const char help_title[] = - "Play / Pause\n" - "Speed up / down\n" - "Step\n" - "Cycle cameras\n" - "Free camera\n" - "Select\n" - "Select parent\n" - "Center\n" - "Tracking camera\n" - "Zoom\n" - "View rotate\n" - "View translate\n" - "Object rotate\n" - "Object translate\n" - "Help\n" - "Info\n" - "Profiler\n" - "Sensors\n" - "Full screen\n" - "Show UI shortcuts\n" - "Expand/collapse all"; - - -// info strings -char info_title[kBufSize]; -char info_content[kBufSize]; - - - -//-------------------------------- profiler, sensor, info, watch ----------------------------------- - -// init profiler figures -void profilerinit(void) { - int i, n; - - // set figures to default - mjv_defaultFigure(&figconstraint); - mjv_defaultFigure(&figcost); - mjv_defaultFigure(&figtimer); - mjv_defaultFigure(&figsize); - - // titles - mju::strcpy_arr(figconstraint.title, "Counts"); - mju::strcpy_arr(figcost.title, "Convergence (log 10)"); - mju::strcpy_arr(figsize.title, "Dimensions"); - mju::strcpy_arr(figtimer.title, "CPU time (msec)"); - - // x-labels - mju::strcpy_arr(figconstraint.xlabel, "Solver iteration"); - mju::strcpy_arr(figcost.xlabel, "Solver iteration"); - mju::strcpy_arr(figsize.xlabel, "Video frame"); - mju::strcpy_arr(figtimer.xlabel, "Video frame"); - - // y-tick nubmer formats - mju::strcpy_arr(figconstraint.yformat, "%.0f"); - mju::strcpy_arr(figcost.yformat, "%.1f"); - mju::strcpy_arr(figsize.yformat, "%.0f"); - mju::strcpy_arr(figtimer.yformat, "%.2f"); - - // colors - figconstraint.figurergba[0] = 0.1f; - figcost.figurergba[2] = 0.2f; - figsize.figurergba[0] = 0.1f; - figtimer.figurergba[2] = 0.2f; - figconstraint.figurergba[3] = 0.5f; - figcost.figurergba[3] = 0.5f; - figsize.figurergba[3] = 0.5f; - figtimer.figurergba[3] = 0.5f; - - // legends - mju::strcpy_arr(figconstraint.linename[0], "total"); - mju::strcpy_arr(figconstraint.linename[1], "active"); - mju::strcpy_arr(figconstraint.linename[2], "changed"); - mju::strcpy_arr(figconstraint.linename[3], "evals"); - mju::strcpy_arr(figconstraint.linename[4], "updates"); - mju::strcpy_arr(figcost.linename[0], "improvement"); - mju::strcpy_arr(figcost.linename[1], "gradient"); - mju::strcpy_arr(figcost.linename[2], "lineslope"); - mju::strcpy_arr(figsize.linename[0], "dof"); - mju::strcpy_arr(figsize.linename[1], "body"); - mju::strcpy_arr(figsize.linename[2], "constraint"); - mju::strcpy_arr(figsize.linename[3], "sqrt(nnz)"); - mju::strcpy_arr(figsize.linename[4], "contact"); - mju::strcpy_arr(figsize.linename[5], "iteration"); - mju::strcpy_arr(figtimer.linename[0], "total"); - mju::strcpy_arr(figtimer.linename[1], "collision"); - mju::strcpy_arr(figtimer.linename[2], "prepare"); - mju::strcpy_arr(figtimer.linename[3], "solve"); - mju::strcpy_arr(figtimer.linename[4], "other"); - - // grid sizes - figconstraint.gridsize[0] = 5; - figconstraint.gridsize[1] = 5; - figcost.gridsize[0] = 5; - figcost.gridsize[1] = 5; - figsize.gridsize[0] = 3; - figsize.gridsize[1] = 5; - figtimer.gridsize[0] = 3; - figtimer.gridsize[1] = 5; - - // minimum ranges - figconstraint.range[0][0] = 0; - figconstraint.range[0][1] = 20; - figconstraint.range[1][0] = 0; - figconstraint.range[1][1] = 80; - figcost.range[0][0] = 0; - figcost.range[0][1] = 20; - figcost.range[1][0] = -15; - figcost.range[1][1] = 5; - figsize.range[0][0] = -200; - figsize.range[0][1] = 0; - figsize.range[1][0] = 0; - figsize.range[1][1] = 100; - figtimer.range[0][0] = -200; - figtimer.range[0][1] = 0; - figtimer.range[1][0] = 0; - figtimer.range[1][1] = 0.4f; - - // init x axis on history figures (do not show yet) - for (n=0; n<6; n++) - for (i=0; isolver_iter, mjNSOLVER), mjMAXLINEPNT); - for (i=1; i<5; i++) { - figconstraint.linepnt[i] = figconstraint.linepnt[0]; - } - if (m->opt.solver==mjSOL_PGS) { - figconstraint.linepnt[3] = 0; - figconstraint.linepnt[4] = 0; - } - if (m->opt.solver==mjSOL_CG) { - figconstraint.linepnt[4] = 0; - } - for (i=0; inefc; - figconstraint.linedata[1][2*i+1] = (float)d->solver[i].nactive; - figconstraint.linedata[2][2*i+1] = (float)d->solver[i].nchange; - figconstraint.linedata[3][2*i+1] = (float)d->solver[i].neval; - figconstraint.linedata[4][2*i+1] = (float)d->solver[i].nupdate; - } - - // update cost figure - figcost.linepnt[0] = mjMIN(mjMIN(d->solver_iter, mjNSOLVER), mjMAXLINEPNT); - for (i=1; i<3; i++) { - figcost.linepnt[i] = figcost.linepnt[0]; - } - if (m->opt.solver==mjSOL_PGS) { - figcost.linepnt[1] = 0; - figcost.linepnt[2] = 0; - } - - for (i=0; isolver[i].improvement)); - figcost.linedata[1][2*i+1] = (float)mju_log10(mju_max(mjMINVAL, d->solver[i].gradient)); - figcost.linedata[2][2*i+1] = (float)mju_log10(mju_max(mjMINVAL, d->solver[i].lineslope)); - } - - // get timers: total, collision, prepare, solve, other - mjtNum total = d->timer[mjTIMER_STEP].duration; - int number = d->timer[mjTIMER_STEP].number; - if (!number) { - total = d->timer[mjTIMER_FORWARD].duration; - number = d->timer[mjTIMER_FORWARD].number; - } - number = mjMAX(1, number); - float tdata[5] = { - (float)(total/number), - (float)(d->timer[mjTIMER_POS_COLLISION].duration/number), - (float)(d->timer[mjTIMER_POS_MAKE].duration/number) + - (float)(d->timer[mjTIMER_POS_PROJECT].duration/number), - (float)(d->timer[mjTIMER_CONSTRAINT].duration/number), - 0 - }; - tdata[4] = tdata[0] - tdata[1] - tdata[2] - tdata[3]; - - // update figtimer - int pnt = mjMIN(201, figtimer.linepnt[0]+1); - for (n=0; n<5; n++) { - // shift data - for (i=pnt-1; i>0; i--) { - figtimer.linedata[n][2*i+1] = figtimer.linedata[n][2*i-1]; - } - - // assign new - figtimer.linepnt[n] = pnt; - figtimer.linedata[n][1] = tdata[n]; - } - - // get sizes: nv, nbody, nefc, sqrt(nnz), ncont, iter - float sdata[6] = { - (float)m->nv, - (float)m->nbody, - (float)d->nefc, - (float)mju_sqrt((mjtNum)d->solver_nnz), - (float)d->ncon, - (float)d->solver_iter - }; - - // update figsize - pnt = mjMIN(201, figsize.linepnt[0]+1); - for (n=0; n<6; n++) { - // shift data - for (i=pnt-1; i>0; i--) { - figsize.linedata[n][2*i+1] = figsize.linedata[n][2*i-1]; - } - - // assign new - figsize.linepnt[n] = pnt; - figsize.linedata[n][1] = sdata[n]; - } -} - - - -// show profiler figures -void profilershow(mjrRect rect) { - mjrRect viewport = { - rect.left + rect.width - rect.width/4, - rect.bottom, - rect.width/4, - rect.height/4 - }; - mjr_figure(viewport, &figtimer, &con); - viewport.bottom += rect.height/4; - mjr_figure(viewport, &figsize, &con); - viewport.bottom += rect.height/4; - mjr_figure(viewport, &figcost, &con); - viewport.bottom += rect.height/4; - mjr_figure(viewport, &figconstraint, &con); -} - - - -// init sensor figure -void sensorinit(void) { - // set figure to default - mjv_defaultFigure(&figsensor); - figsensor.figurergba[3] = 0.5f; - - // set flags - figsensor.flg_extend = 1; - figsensor.flg_barplot = 1; - figsensor.flg_symmetric = 1; - - // title - mju::strcpy_arr(figsensor.title, "Sensor data"); - - // y-tick nubmer format - mju::strcpy_arr(figsensor.yformat, "%.0f"); - - // grid size - figsensor.gridsize[0] = 2; - figsensor.gridsize[1] = 3; - - // minimum range - figsensor.range[0][0] = 0; - figsensor.range[0][1] = 0; - figsensor.range[1][0] = -1; - figsensor.range[1][1] = 1; -} - - - -// update sensor figure -void sensorupdate(void) { - static const int maxline = 10; - - // clear linepnt - for (int i=0; insensor; n++) { - // go to next line if type is different - if (n>0 && m->sensor_type[n]!=m->sensor_type[n-1]) { - lineid = mjMIN(lineid+1, maxline-1); - } - - // get info about this sensor - mjtNum cutoff = (m->sensor_cutoff[n]>0 ? m->sensor_cutoff[n] : 1); - int adr = m->sensor_adr[n]; - int dim = m->sensor_dim[n]; - - // data pointer in line - int p = figsensor.linepnt[lineid]; - - // fill in data for this sensor - for (int i=0; i=mjMAXLINEPNT/2) { - break; - } - - // x - figsensor.linedata[lineid][2*p+4*i] = (float)(adr+i); - figsensor.linedata[lineid][2*p+4*i+2] = (float)(adr+i); - - // y - figsensor.linedata[lineid][2*p+4*i+1] = 0; - figsensor.linedata[lineid][2*p+4*i+3] = (float)(d->sensordata[adr+i]/cutoff); - } - - // update linepnt - figsensor.linepnt[lineid] = mjMIN(mjMAXLINEPNT-1, - figsensor.linepnt[lineid]+2*dim); - } -} - - - -// show sensor figure -void sensorshow(mjrRect rect) { - // constant width with and without profiler - int width = settings.profiler ? rect.width/3 : rect.width/4; - - // render figure on the right - mjrRect viewport = { - rect.left + rect.width - width, - rect.bottom, - width, - rect.height/3 - }; - mjr_figure(viewport, &figsensor, &con); -} - - - -// prepare info text -void infotext(char (&title)[kBufSize], char (&content)[kBufSize], double interval) { - char tmp[20]; - - // compute solver error - mjtNum solerr = 0; - if (d->solver_iter) { - int ind = mjMIN(d->solver_iter-1, mjNSOLVER-1); - solerr = mju_min(d->solver[ind].improvement, d->solver[ind].gradient); - if (solerr==0) { - solerr = mju_max(d->solver[ind].improvement, d->solver[ind].gradient); - } - } - solerr = mju_log10(mju_max(mjMINVAL, solerr)); - - // prepare info text - const std::string realtime_nominator = settings.slow_down == 1 ? "" : "1/"; - mju::strcpy_arr(title, "Time\nSize\nCPU\nSolver \nFPS\nstack\nconbuf\nefcbuf"); - mju::sprintf_arr(content, - "%-9.3f %s%d x\n%d (%d con)\n%.3f\n%.1f (%d it)\n%.0f\n%.3f\n%.3f\n%.3f", - d->time, realtime_nominator.c_str(), settings.slow_down, - d->nefc, d->ncon, - settings.run ? - d->timer[mjTIMER_STEP].duration / mjMAX(1, d->timer[mjTIMER_STEP].number) : - d->timer[mjTIMER_FORWARD].duration / mjMAX(1, d->timer[mjTIMER_FORWARD].number), - solerr, d->solver_iter, - 1/interval, - d->maxuse_stack/(double)d->nstack, - d->maxuse_con/(double)m->nconmax, - d->maxuse_efc/(double)m->njmax); - - // add Energy if enabled - if (mjENABLED(mjENBL_ENERGY)) { - mju::sprintf_arr(tmp, "\n%.3f", d->energy[0]+d->energy[1]); - mju::strcat_arr(content, tmp); - mju::strcat_arr(title, "\nEnergy"); - } - - // add FwdInv if enabled - if (mjENABLED(mjENBL_FWDINV)) { - mju::sprintf_arr(tmp, "\n%.1f %.1f", - mju_log10(mju_max(mjMINVAL, d->solver_fwdinv[0])), - mju_log10(mju_max(mjMINVAL, d->solver_fwdinv[1]))); - mju::strcat_arr(content, tmp); - mju::strcat_arr(title, "\nFwdInv"); - } -} - - - -// sprintf forwarding, to avoid compiler warning in x-macro -void printfield(char (&str)[mjMAXUINAME], void* ptr) { - mju::sprintf_arr(str, "%g", *(mjtNum*)ptr); -} - - - -// update watch -void watch(void) { - // clear - ui0.sect[SECT_WATCH].item[2].multi.nelem = 1; - mju::strcpy_arr(ui0.sect[SECT_WATCH].item[2].multi.name[0], "invalid field"); - - // prepare symbols needed by xmacro - MJDATA_POINTERS_PREAMBLE(m); - - // find specified field in mjData arrays, update value - #define X(TYPE, NAME, NR, NC) \ - if (!mju::strcmp_arr(#NAME, settings.field) && \ - !mju::strcmp_arr(#TYPE, "mjtNum")) { \ - if (settings.index>=0 && settings.indexNR*NC) { \ - printfield(ui0.sect[SECT_WATCH].item[2].multi.name[0], d->NAME + settings.index); \ - } else { \ - mju::strcpy_arr(ui0.sect[SECT_WATCH].item[2].multi.name[0], "invalid index"); \ - } \ - return; \ - } - - MJDATA_POINTERS - #undef X -} - - - -//---------------------------------- UI construction ----------------------------------------------- - -// make physics section of UI -void makephysics(int oldstate) { - int i; - - mjuiDef defPhysics[] = { - {mjITEM_SECTION, "Physics", oldstate, NULL, "AP"}, - {mjITEM_SELECT, "Integrator", 2, &(m->opt.integrator), "Euler\nRK4\nimplicit"}, - {mjITEM_SELECT, "Collision", 2, &(m->opt.collision), "All\nPair\nDynamic"}, - {mjITEM_SELECT, "Cone", 2, &(m->opt.cone), "Pyramidal\nElliptic"}, - {mjITEM_SELECT, "Jacobian", 2, &(m->opt.jacobian), "Dense\nSparse\nAuto"}, - {mjITEM_SELECT, "Solver", 2, &(m->opt.solver), "PGS\nCG\nNewton"}, - {mjITEM_SEPARATOR, "Algorithmic Parameters", 1}, - {mjITEM_EDITNUM, "Timestep", 2, &(m->opt.timestep), "1 0 1"}, - {mjITEM_EDITINT, "Iterations", 2, &(m->opt.iterations), "1 0 1000"}, - {mjITEM_EDITNUM, "Tolerance", 2, &(m->opt.tolerance), "1 0 1"}, - {mjITEM_EDITINT, "Noslip Iter", 2, &(m->opt.noslip_iterations), "1 0 1000"}, - {mjITEM_EDITNUM, "Noslip Tol", 2, &(m->opt.noslip_tolerance), "1 0 1"}, - {mjITEM_EDITINT, "MRR Iter", 2, &(m->opt.mpr_iterations), "1 0 1000"}, - {mjITEM_EDITNUM, "MPR Tol", 2, &(m->opt.mpr_tolerance), "1 0 1"}, - {mjITEM_EDITNUM, "API Rate", 2, &(m->opt.apirate), "1 0 1000"}, - {mjITEM_SEPARATOR, "Physical Parameters", 1}, - {mjITEM_EDITNUM, "Gravity", 2, m->opt.gravity, "3"}, - {mjITEM_EDITNUM, "Wind", 2, m->opt.wind, "3"}, - {mjITEM_EDITNUM, "Magnetic", 2, m->opt.magnetic, "3"}, - {mjITEM_EDITNUM, "Density", 2, &(m->opt.density), "1"}, - {mjITEM_EDITNUM, "Viscosity", 2, &(m->opt.viscosity), "1"}, - {mjITEM_EDITNUM, "Imp Ratio", 2, &(m->opt.impratio), "1"}, - {mjITEM_SEPARATOR, "Disable Flags", 1}, - {mjITEM_END} - }; - mjuiDef defEnableFlags[] = { - {mjITEM_SEPARATOR, "Enable Flags", 1}, - {mjITEM_END} - }; - mjuiDef defOverride[] = { - {mjITEM_SEPARATOR, "Contact Override", 1}, - {mjITEM_EDITNUM, "Margin", 2, &(m->opt.o_margin), "1"}, - {mjITEM_EDITNUM, "Sol Imp", 2, &(m->opt.o_solimp), "5"}, - {mjITEM_EDITNUM, "Sol Ref", 2, &(m->opt.o_solref), "2"}, - {mjITEM_END} - }; - - // add physics - mjui_add(&ui0, defPhysics); - - // add flags programmatically - mjuiDef defFlag[] = { - {mjITEM_CHECKINT, "", 2, NULL, ""}, - {mjITEM_END} - }; - for (i=0; incam, mjMAXUIMULTI-2); i++) { - // prepare name - char camname[mjMAXUITEXT] = "\n"; - if (m->names[m->name_camadr[i]]) { - mju::strcat_arr(camname, m->names+m->name_camadr[i]); - } else { - mju::sprintf_arr(camname, "\nCamera %d", i); - } - - // check string length - if (mju::strlen_arr(camname) + mju::strlen_arr(defRendering[1].other)>=mjMAXUITEXT-1) { - break; - } - - // add camera - mju::strcat_arr(defRendering[1].other, camname); - } - - // add rendering standard - mjui_add(&ui0, defRendering); - - // add flags programmatically - mjuiDef defFlag[] = { - {mjITEM_CHECKBYTE, "", 2, NULL, ""}, - {mjITEM_END} - }; - for (i=0; injnt && itemcntjnt_type[i]==mjJNT_HINGE || m->jnt_type[i]==mjJNT_SLIDE)) { - // skip if joint group is disabled - if (!vopt.jointgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->jnt_group[i]))]) { - continue; - } - - // set data and name - defSlider[0].pdata = d->qpos + m->jnt_qposadr[i]; - if (m->names[m->name_jntadr[i]]) { - mju::strcpy_arr(defSlider[0].name, m->names+m->name_jntadr[i]); - } else { - mju::sprintf_arr(defSlider[0].name, "joint %d", i); - } - - // set range - if (m->jnt_limited[i]) - mju::sprintf_arr(defSlider[0].other, "%.4g %.4g", - m->jnt_range[2*i], m->jnt_range[2*i+1]); - else if (m->jnt_type[i]==mjJNT_SLIDE) { - mju::strcpy_arr(defSlider[0].other, "-1 1"); - } else { - mju::strcpy_arr(defSlider[0].other, "-3.1416 3.1416"); - } - - // add and count - mjui_add(&ui1, defSlider); - itemcnt++; - } -} - - - -// make control section of UI -void makecontrol(int oldstate) { - int i; - - mjuiDef defControl[] = { - {mjITEM_SECTION, "Control", oldstate, NULL, "AC"}, - {mjITEM_BUTTON, "Clear all", 2}, - {mjITEM_END} - }; - mjuiDef defSlider[] = { - {mjITEM_SLIDERNUM, "", 2, NULL, "0 1"}, - {mjITEM_END} - }; - - // add section - mjui_add(&ui1, defControl); - defSlider[0].state = 2; - - // add controls, exit if UI limit reached (Clear button already added) - int itemcnt = 1; - for (i=0; inu && itemcntactuator_group[i]))]) { - continue; - } - - // set data and name - defSlider[0].pdata = d->ctrl + i; - if (m->names[m->name_actuatoradr[i]]) { - mju::strcpy_arr(defSlider[0].name, m->names+m->name_actuatoradr[i]); - } else { - mju::sprintf_arr(defSlider[0].name, "control %d", i); - } - - // set range - if (m->actuator_ctrllimited[i]) - mju::sprintf_arr(defSlider[0].other, "%.4g %.4g", - m->actuator_ctrlrange[2*i], m->actuator_ctrlrange[2*i+1]); - else { - mju::strcpy_arr(defSlider[0].other, "-1 1"); - } - - // add and count - mjui_add(&ui1, defSlider); - itemcnt++; - } -} - - - -// make model-dependent UI sections -void makesections(void) { - int i; - - // get section open-close state, UI 0 - int oldstate0[NSECT0]; - for (i=0; ii) { - oldstate0[i] = ui0.sect[i].state; - } - } - - // get section open-close state, UI 1 - int oldstate1[NSECT1]; - for (i=0; ii) { - oldstate1[i] = ui1.sect[i].state; - } - } - - // clear model-dependent sections of UI - ui0.nsect = SECT_PHYSICS; - ui1.nsect = 0; - - // make - makephysics(oldstate0[SECT_PHYSICS]); - makerendering(oldstate0[SECT_RENDERING]); - makegroup(oldstate0[SECT_GROUP]); - makejoint(oldstate1[SECT_JOINT]); - makecontrol(oldstate1[SECT_CONTROL]); -} - - - -//---------------------------------- utility functions --------------------------------------------- - -// align and scale view -void alignscale(void) { - // autoscale - cam.lookat[0] = m->stat.center[0]; - cam.lookat[1] = m->stat.center[1]; - cam.lookat[2] = m->stat.center[2]; - cam.distance = 1.5 * m->stat.extent; - - // set to free camera - cam.type = mjCAMERA_FREE; -} - - - -// copy qpos to clipboard as key -void copykey(void) { - char clipboard[5000] = ""); - - // copy to clipboard - glfwSetClipboardString(window, clipboard); -} - - - -// millisecond timer, for MuJoCo built-in profiler -mjtNum timer(void) { - return (mjtNum)(1000*glfwGetTime()); -} - - - -// clear all times -void cleartimers(void) { - for (int i=0; itimer[i].duration = 0; - d->timer[i].number = 0; - } -} - - - -// copy current camera to clipboard as MJCF specification -void copycamera(mjvGLCamera* camera) { - char clipboard[500]; - mjtNum cam_right[3]; - mjtNum cam_forward[3]; - mjtNum cam_up[3]; - - // get camera spec from the GLCamera - mju_f2n(cam_forward, camera[0].forward, 3); - mju_f2n(cam_up, camera[0].up, 3); - mju_cross(cam_right, cam_forward, cam_up); - - // make MJCF camera spec - mju::sprintf_arr(clipboard, - "\n", - (camera[0].pos[0] + camera[1].pos[0]) / 2, - (camera[0].pos[1] + camera[1].pos[1]) / 2, - (camera[0].pos[2] + camera[1].pos[2]) / 2, - cam_right[0], cam_right[1], cam_right[2], - camera[0].up[0], camera[0].up[1], camera[0].up[2]); - - // copy spec into clipboard - glfwSetClipboardString(window, clipboard); -} - - - -// update UI 0 when MuJoCo structures change (except for joint sliders) -void updatesettings(void) { - int i; - - // physics flags - for (i=0; iopt.disableflags & (1<opt.enableflags & (1<0) { - mju::strcpy_arr(filename, paths[0]); - settings.loadrequest = 1; - } -} - - - -// load mjb or xml model -void loadmodel(void) { - // clear request - settings.loadrequest = 0; - - // make sure filename is not empty - if (!filename[0]) { - return; - } - - // load and compile - loadError[0] = '\0'; - mjModel* mnew = 0; - if (mju::strlen_arr(filename)>4 && - !std::strncmp(filename+mju::strlen_arr(filename)-4, ".mjb", - mju::sizeof_arr(filename)-mju::strlen_arr(filename)+4)) { - mnew = mj_loadModel(filename, NULL); - if (!mnew) { - mju::strcpy_arr(loadError, "could not load binary model"); - } - } else { - mnew = mj_loadXML(filename, NULL, loadError, kBufSize); - // remove trailing newline character from loadError - if (loadError[0]) { - int error_length = mju::strlen_arr(loadError); - if (loadError[error_length-1] == '\n') { - loadError[error_length-1] = '\0'; - } - } - } - if (!mnew) { - std::printf("%s\n", loadError); - return; - } - - // compiler warning: print and pause - if (loadError[0]) { - // mj_forward() below will print the warning message - std::printf("Model compiled, but simulation warning (paused):\n %s\n", loadError); - settings.run = 0; - } - - // delete old model, assign new - mj_deleteData(d); - mj_deleteModel(m); - m = mnew; - d = mj_makeData(m); - mj_forward(m, d); - - // allocate ctrlnoise - free(ctrlnoise); - ctrlnoise = (mjtNum*) malloc(sizeof(mjtNum)*m->nu); - mju_zero(ctrlnoise, m->nu); - - // re-create scene and context - mjv_makeScene(m, &scn, maxgeom); - mjr_makeContext(m, &con, 50*(settings.font+1)); - - // clear perturbation state - pert.active = 0; - pert.select = 0; - pert.skinselect = -1; - - // align and scale view unless reloading the same file - if (mju::strcmp_arr(filename, previous_filename)) { - alignscale(); - mju::strcpy_arr(previous_filename, filename); - } - - // update scene - mjv_updateScene(m, d, &vopt, &pert, &cam, mjCAT_ALL, &scn); - - // set window title to model name - if (window && m->names) { - char title[200] = "Simulate : "; - mju::strcat_arr(title, m->names); - glfwSetWindowTitle(window, title); - } - - // set keyframe range and divisions - ui0.sect[SECT_SIMULATION].item[5].slider.range[0] = 0; - ui0.sect[SECT_SIMULATION].item[5].slider.range[1] = mjMAX(0, m->nkey - 1); - ui0.sect[SECT_SIMULATION].item[5].slider.divisions = mjMAX(1, m->nkey - 1); - - // rebuild UI sections - makesections(); - - // full ui update - uiModify(window, &ui0, &uistate, &con); - uiModify(window, &ui1, &uistate, &con); - updatesettings(); -} - - - -//---------------------------------- UI hooks (for uitools.c) -------------------------------------- - -// determine enable/disable item state given category -int uiPredicate(int category, void* userdata) { - switch (category) { - case 2: // require model - return (m!=NULL); - - case 3: // require model and nkey - return (m && m->nkey); - - case 4: // require model and paused - return (m && !settings.run); - - default: - return 1; - } -} - - - -// set window layout -void uiLayout(mjuiState* state) { - mjrRect* rect = state->rect; - - // set number of rectangles - state->nrect = 4; - - // rect 0: entire framebuffer - rect[0].left = 0; - rect[0].bottom = 0; - glfwGetFramebufferSize(window, &rect[0].width, &rect[0].height); - - // rect 1: UI 0 - rect[1].left = 0; - rect[1].width = settings.ui0 ? ui0.width : 0; - rect[1].bottom = 0; - rect[1].height = rect[0].height; - - // rect 2: UI 1 - rect[2].width = settings.ui1 ? ui1.width : 0; - rect[2].left = mjMAX(0, rect[0].width - rect[2].width); - rect[2].bottom = 0; - rect[2].height = rect[0].height; - - // rect 3: 3D plot (everything else is an overlay) - rect[3].left = rect[1].width; - rect[3].width = mjMAX(0, rect[0].width - rect[1].width - rect[2].width); - rect[3].bottom = 0; - rect[3].height = rect[0].height; -} - - - -// When launched via an App Bundle on macOS, the working directory is the path to the App Bundle's -// resource directory. This causes files to be saved into the bundle, which is not the desired -// behavior. Instead, we open a save dialog box to ask the user where to put the file. -// Since the dialog box logic needs to be written in Objective-C, we separate it into a different -// source file. -#ifdef __APPLE__ -std::string getSavePath(const char* filename); -#else -static std::string getSavePath(const char* filename) { - return filename; -} -#endif - - - -// handle UI event -void uiEvent(mjuiState* state) { - int i; - char err[200]; - - // call UI 0 if event is directed to it - if ((state->dragrect==ui0.rectid) || - (state->dragrect==0 && state->mouserect==ui0.rectid) || - state->type==mjEVENT_KEY) { - // process UI event - mjuiItem* it = mjui_event(&ui0, state, &con); - - // file section - if (it && it->sectionid==SECT_FILE) { - switch (it->itemid) { - case 0: // Save xml - { - const std::string path = getSavePath("mjmodel.xml"); - if (!path.empty() && !mj_saveLastXML(path.c_str(), m, err, 200)) { - std::printf("Save XML error: %s", err); - } - } - break; - - case 1: // Save mjb - { - const std::string path = getSavePath("mjmodel.mjb"); - if (!path.empty()) { - mj_saveModel(m, path.c_str(), NULL, 0); - } - } - break; - - case 2: // Print model - mj_printModel(m, "MJMODEL.TXT"); - break; - - case 3: // Print data - mj_printData(m, d, "MJDATA.TXT"); - break; - - case 4: // Quit - settings.exitrequest = 1; - break; - } - } - - // option section - else if (it && it->sectionid==SECT_OPTION) { - switch (it->itemid) { - case 0: // Spacing - ui0.spacing = mjui_themeSpacing(settings.spacing); - ui1.spacing = mjui_themeSpacing(settings.spacing); - break; - - case 1: // Color - ui0.color = mjui_themeColor(settings.color); - ui1.color = mjui_themeColor(settings.color); - break; - - case 2: // Font - mjr_changeFont(50*(settings.font+1), &con); - break; - - case 9: // Full screen - if (glfwGetWindowMonitor(window)) { - // restore window from saved data - glfwSetWindowMonitor(window, NULL, windowpos[0], windowpos[1], - windowsize[0], windowsize[1], 0); - } - - // currently windowed: switch to full screen - else { - // save window data - glfwGetWindowPos(window, windowpos, windowpos+1); - glfwGetWindowSize(window, windowsize, windowsize+1); - - // switch - glfwSetWindowMonitor(window, glfwGetPrimaryMonitor(), 0, 0, - vmode.width, vmode.height, vmode.refreshRate); - } - - // reinstante vsync, just in case - glfwSwapInterval(settings.vsync); - break; - - case 10: // Vertical sync - glfwSwapInterval(settings.vsync); - break; - } - - // modify UI - uiModify(window, &ui0, state, &con); - uiModify(window, &ui1, state, &con); - } - - // simulation section - else if (it && it->sectionid==SECT_SIMULATION) { - switch (it->itemid) { - case 1: // Reset - if (m) { - mj_resetData(m, d); - mj_forward(m, d); - profilerupdate(); - sensorupdate(); - updatesettings(); - } - break; - - case 2: // Reload - settings.loadrequest = 1; - break; - - case 3: // Align - alignscale(); - updatesettings(); - break; - - case 4: // Copy pose - copykey(); - break; - - case 5: // Adjust key - case 6: // Load key - i = settings.key; - d->time = m->key_time[i]; - mju_copy(d->qpos, m->key_qpos+i*m->nq, m->nq); - mju_copy(d->qvel, m->key_qvel+i*m->nv, m->nv); - mju_copy(d->act, m->key_act+i*m->na, m->na); - mju_copy(d->mocap_pos, m->key_mpos+i*3*m->nmocap, 3*m->nmocap); - mju_copy(d->mocap_quat, m->key_mquat+i*4*m->nmocap, 4*m->nmocap); - mju_copy(d->ctrl, m->key_ctrl+i*m->nu, m->nu); - mj_forward(m, d); - profilerupdate(); - sensorupdate(); - updatesettings(); - break; - - case 7: // Save key - i = settings.key; - m->key_time[i] = d->time; - mju_copy(m->key_qpos+i*m->nq, d->qpos, m->nq); - mju_copy(m->key_qvel+i*m->nv, d->qvel, m->nv); - mju_copy(m->key_act+i*m->na, d->act, m->na); - mju_copy(m->key_mpos+i*3*m->nmocap, d->mocap_pos, 3*m->nmocap); - mju_copy(m->key_mquat+i*4*m->nmocap, d->mocap_quat, 4*m->nmocap); - mju_copy(m->key_ctrl+i*m->nu, d->ctrl, m->nu); - break; - } - } - - // physics section - else if (it && it->sectionid==SECT_PHYSICS) { - // update disable flags in mjOption - m->opt.disableflags = 0; - for (i=0; iopt.disableflags |= (1<opt.enableflags = 0; - for (i=0; iopt.enableflags |= (1<sectionid==SECT_RENDERING) { - // set camera in mjvCamera - if (settings.camera==0) { - cam.type = mjCAMERA_FREE; - } else if (settings.camera==1) { - if (pert.select>0) { - cam.type = mjCAMERA_TRACKING; - cam.trackbodyid = pert.select; - cam.fixedcamid = -1; - } else { - cam.type = mjCAMERA_FREE; - settings.camera = 0; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - } else { - cam.type = mjCAMERA_FIXED; - cam.fixedcamid = settings.camera - 2; - } - // copy camera spec to clipboard (as MJCF element) - if (it->itemid == 3) { - copycamera(scn.camera); - } - } - - // group section - else if (it && it->sectionid==SECT_GROUP) { - // remake joint section if joint group changed - if (it->name[0]=='J' && it->name[1]=='o') { - ui1.nsect = SECT_JOINT; - makejoint(ui1.sect[SECT_JOINT].state); - ui1.nsect = NSECT1; - uiModify(window, &ui1, state, &con); - } - - // remake control section if actuator group changed - if (it->name[0]=='A' && it->name[1]=='c') { - ui1.nsect = SECT_CONTROL; - makecontrol(ui1.sect[SECT_CONTROL].state); - ui1.nsect = NSECT1; - uiModify(window, &ui1, state, &con); - } - } - - // stop if UI processed event - if (it!=NULL || (state->type==mjEVENT_KEY && state->key==0)) { - return; - } - } - - // call UI 1 if event is directed to it - if ((state->dragrect==ui1.rectid) || - (state->dragrect==0 && state->mouserect==ui1.rectid) || - state->type==mjEVENT_KEY) { - // process UI event - mjuiItem* it = mjui_event(&ui1, state, &con); - - // control section - if (it && it->sectionid==SECT_CONTROL) { - // clear controls - if (it->itemid==0) { - mju_zero(d->ctrl, m->nu); - mjui_update(SECT_CONTROL, -1, &ui1, &uistate, &con); - } - } - - // stop if UI processed event - if (it!=NULL || (state->type==mjEVENT_KEY && state->key==0)) { - return; - } - } - - // shortcut not handled by UI - if (state->type==mjEVENT_KEY && state->key!=0) { - switch (state->key) { - case ' ': // Mode - if (m) { - settings.run = 1 - settings.run; - pert.active = 0; - mjui_update(-1, -1, &ui0, state, &con); - } - break; - - case mjKEY_RIGHT: // step forward - if (m && !settings.run) { - cleartimers(); - mj_step(m, d); - profilerupdate(); - sensorupdate(); - updatesettings(); - } - break; - - case mjKEY_PAGE_UP: // select parent body - if (m && pert.select>0) { - pert.select = m->body_parentid[pert.select]; - pert.skinselect = -1; - - // stop perturbation if world reached - if (pert.select<=0) { - pert.active = 0; - } - } - - break; - - case ']': // cycle up fixed cameras - if (m && m->ncam) { - cam.type = mjCAMERA_FIXED; - // settings.camera = {0 or 1} are reserved for the free and tracking cameras - if (settings.camera < 2 || settings.camera == 2 + m->ncam-1) { - settings.camera = 2; - } else { - settings.camera += 1; - } - cam.fixedcamid = settings.camera - 2; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - break; - - case '[': // cycle down fixed cameras - if (m && m->ncam) { - cam.type = mjCAMERA_FIXED; - // settings.camera = {0 or 1} are reserved for the free and tracking cameras - if (settings.camera <= 2) { - settings.camera = 2 + m->ncam-1; - } else { - settings.camera -= 1; - } - cam.fixedcamid = settings.camera - 2; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - break; - - case mjKEY_F6: // cycle frame visualisation - if (m) { - vopt.frame = (vopt.frame + 1) % mjNFRAME; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - break; - - case mjKEY_F7: // cycle label visualisation - if (m) { - vopt.label = (vopt.label + 1) % mjNLABEL; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - break; - - case mjKEY_ESCAPE: // free camera - cam.type = mjCAMERA_FREE; - settings.camera = 0; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - break; - - case '-': // slow down - if (settings.slow_down < max_slow_down && !state->shift) { - settings.slow_down *= 2; - settings.speed_changed = true; - } - break; - - case '=': // speed up - if (settings.slow_down > 1 && !state->shift) { - settings.slow_down /= 2; - settings.speed_changed = true; - } - break; - } - - return; - } - - // 3D scroll - if (state->type==mjEVENT_SCROLL && state->mouserect==3 && m) { - // emulate vertical mouse motion = 2% of window height - mjv_moveCamera(m, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &scn, &cam); - - return; - } - - // 3D press - if (state->type==mjEVENT_PRESS && state->mouserect==3 && m) { - // set perturbation - int newperturb = 0; - if (state->control && pert.select>0) { - // right: translate; left: rotate - if (state->right) { - newperturb = mjPERT_TRANSLATE; - } else if (state->left) { - newperturb = mjPERT_ROTATE; - } - - // perturbation onset: reset reference - if (newperturb && !pert.active) { - mjv_initPerturb(m, d, &scn, &pert); - } - } - pert.active = newperturb; - - // handle double-click - if (state->doubleclick) { - // determine selection mode - int selmode; - if (state->button==mjBUTTON_LEFT) { - selmode = 1; - } else if (state->control) { - selmode = 3; - } else { - selmode = 2; - } - - // find geom and 3D click point, get corresponding body - mjrRect r = state->rect[3]; - mjtNum selpnt[3]; - int selgeom, selskin; - int selbody = mjv_select(m, d, &vopt, - (mjtNum)r.width/(mjtNum)r.height, - (mjtNum)(state->x-r.left)/(mjtNum)r.width, - (mjtNum)(state->y-r.bottom)/(mjtNum)r.height, - &scn, selpnt, &selgeom, &selskin); - - // set lookat point, start tracking is requested - if (selmode==2 || selmode==3) { - // copy selpnt if anything clicked - if (selbody>=0) { - mju_copy3(cam.lookat, selpnt); - } - - // switch to tracking camera if dynamic body clicked - if (selmode==3 && selbody>0) { - // mujoco camera - cam.type = mjCAMERA_TRACKING; - cam.trackbodyid = selbody; - cam.fixedcamid = -1; - - // UI camera - settings.camera = 1; - mjui_update(SECT_RENDERING, -1, &ui0, &uistate, &con); - } - } - - // set body selection - else { - if (selbody>=0) { - // record selection - pert.select = selbody; - pert.skinselect = selskin; - - // compute localpos - mjtNum tmp[3]; - mju_sub3(tmp, selpnt, d->xpos+3*pert.select); - mju_mulMatTVec(pert.localpos, d->xmat+9*pert.select, tmp, 3, 3); - } else { - pert.select = 0; - pert.skinselect = -1; - } - } - - // stop perturbation on select - pert.active = 0; - } - - return; - } - - // 3D release - if (state->type==mjEVENT_RELEASE && state->dragrect==3 && m) { - // stop perturbation - pert.active = 0; - - return; - } - - // 3D move - if (state->type==mjEVENT_MOVE && state->dragrect==3 && m) { - // determine action based on mouse button - mjtMouse action; - if (state->right) { - action = state->shift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V; - } else if (state->left) { - action = state->shift ? mjMOUSE_ROTATE_H : mjMOUSE_ROTATE_V; - } else { - action = mjMOUSE_ZOOM; - } - - // move perturb or camera - mjrRect r = state->rect[3]; - if (pert.active) - mjv_movePerturb(m, d, action, state->dx/r.height, -state->dy/r.height, - &scn, &pert); - else - mjv_moveCamera(m, action, state->dx/r.height, -state->dy/r.height, - &scn, &cam); - - return; - } -} - - - -//---------------------------------- rendering and simulation -------------------------------------- - -// sim thread synchronization -std::mutex mtx; - - -// prepare to render -void prepare(void) { - // data for FPS calculation - static double lastupdatetm = 0; - - // update interval, save update time - double tmnow = glfwGetTime(); - double interval = tmnow - lastupdatetm; - interval = mjMIN(1, mjMAX(0.0001, interval)); - lastupdatetm = tmnow; - - // no model: nothing to do - if (!m) { - return; - } - - // update scene - mjv_updateScene(m, d, &vopt, &pert, &cam, mjCAT_ALL, &scn); - - // update watch - if (settings.ui0 && ui0.sect[SECT_WATCH].state) { - watch(); - mjui_update(SECT_WATCH, -1, &ui0, &uistate, &con); - } - - // update joint - if (settings.ui1 && ui1.sect[SECT_JOINT].state) { - mjui_update(SECT_JOINT, -1, &ui1, &uistate, &con); - } - - // update info text - if (settings.info) { - infotext(info_title, info_content, interval); - } - - // update control - if( settings.ui1 && ui1.sect[SECT_CONTROL].state ) { - mjui_update(SECT_CONTROL, -1, &ui1, &uistate, &con); - } - - // update profiler - if (settings.profiler && settings.run) { - profilerupdate(); - } - - // update sensor - if (settings.sensor && settings.run) { - sensorupdate(); - } - - // clear timers once profiler info has been copied - cleartimers(); -} - - - -// render im main thread (while simulating in background thread) -void render(GLFWwindow* window) { - // get 3D rectangle and reduced for profiler - mjrRect rect = uistate.rect[3]; - mjrRect smallrect = rect; - if (settings.profiler) { - smallrect.width = rect.width - rect.width/4; - } - - // no model - if (!m) { - // blank screen - mjr_rectangle(rect, 0.2f, 0.3f, 0.4f, 1); - - // label - if (settings.loadrequest) { - mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, "loading", NULL, &con); - } else { - char intro_message[kBufSize]; - mju::sprintf_arr(intro_message, - "MuJoCo version %s\nDrag-and-drop model file here", mj_versionString()); - mjr_overlay(mjFONT_NORMAL, mjGRID_TOPLEFT, rect, intro_message, 0, &con); - } - - // show last loading error - if (loadError[0]) { - mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, loadError, 0, &con); - } - - // render uis - if (settings.ui0) { - mjui_render(&ui0, &uistate, &con); - } - if (settings.ui1) { - mjui_render(&ui1, &uistate, &con); - } - - // finalize - glfwSwapBuffers(window); - - return; - } - - // render scene - mjr_render(rect, &scn, &con); - - // show last loading error - if (loadError[0]) { - mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, loadError, 0, &con); - } - - // show pause/loading label - if (!settings.run || settings.loadrequest) { - mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, - settings.loadrequest ? "loading" : "pause", NULL, &con); - } - - // show realtime label - if (settings.run && settings.slow_down != 1) { - std::string realtime_label = "1/" + std::to_string(settings.slow_down) + " x"; - mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, realtime_label.c_str(), NULL, &con); - } - - // show ui 0 - if (settings.ui0) { - mjui_render(&ui0, &uistate, &con); - } - - // show ui 1 - if (settings.ui1) { - mjui_render(&ui1, &uistate, &con); - } - - // show help - if (settings.help) { - mjr_overlay(mjFONT_NORMAL, mjGRID_TOPLEFT, rect, help_title, help_content, &con); - } - - // show info - if (settings.info) { - mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, info_title, info_content, &con); - } - - // show profiler - if (settings.profiler) { - profilershow(rect); - } - - // show sensor - if (settings.sensor) { - sensorshow(smallrect); - } - - // finalize - glfwSwapBuffers(window); -} - - - -// simulate in background thread (while rendering in main thread) -void simulate(void) { - // cpu-sim syncronization point - double cpusync = 0; - mjtNum simsync = 0; - - // run until asked to exit - while (!settings.exitrequest) { - // sleep for 1 ms or yield, to let main thread run - // yield results in busy wait - which has better timing but kills battery life - if (settings.run && settings.busywait) { - std::this_thread::yield(); - } else { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - // start exclusive access - mtx.lock(); - - // run only if model is present - if (m) { - // running - if (settings.run) { - // record cpu time at start of iteration - double tmstart = glfwGetTime(); - - // inject noise - if (settings.ctrlnoisestd) { - // convert rate and scale to discrete time given current timestep - mjtNum rate = mju_exp(-m->opt.timestep / settings.ctrlnoiserate); - mjtNum scale = settings.ctrlnoisestd * mju_sqrt(1-rate*rate); - - for (int i=0; inu; i++) { - // update noise - ctrlnoise[i] = rate * ctrlnoise[i] + scale * mju_standardNormal(nullptr); - // apply noise - d->ctrl[i] = ctrlnoise[i]; - } - } - - // out-of-sync (for any reason) - mjtNum offset = mju_abs((d->time*settings.slow_down-simsync)-(tmstart-cpusync)); - if( d->time*settings.slow_down syncmisalign*settings.slow_down || settings.speed_changed) { - // re-sync - cpusync = tmstart; - simsync = d->time*settings.slow_down; - settings.speed_changed = false; - - // clear old perturbations, apply new - mju_zero(d->xfrc_applied, 6*m->nbody); - mjv_applyPerturbPose(m, d, &pert, 0); // move mocap bodies only - mjv_applyPerturbForce(m, d, &pert); - - // run single step, let next iteration deal with timing - mj_step(m, d); - } - - // in-sync - else { - // step while simtime lags behind cputime, and within safefactor - while ((d->time*settings.slow_down-simsync) < (glfwGetTime()-cpusync) && - (glfwGetTime()-tmstart) < refreshfactor/vmode.refreshRate) { - // clear old perturbations, apply new - mju_zero(d->xfrc_applied, 6*m->nbody); - mjv_applyPerturbPose(m, d, &pert, 0); // move mocap bodies only - mjv_applyPerturbForce(m, d, &pert); - - // run mj_step - mjtNum prevtm = d->time*settings.slow_down; - mj_step(m, d); - - // break on reset - if (d->time*settings.slow_down1) { - mju::strcpy_arr(filename, argv[1]); - settings.loadrequest = 2; - } - - // start simulation thread - std::thread simthread(simulate); - - // event loop - while (!glfwWindowShouldClose(window) && !settings.exitrequest) { - // start exclusive access (block simulation thread) - mtx.lock(); - - // load model (not on first pass, to show "loading" label) - if (settings.loadrequest==1) { - loadmodel(); - } else if (settings.loadrequest>1) { - settings.loadrequest = 1; - } - - // handle events (calls all callbacks) - glfwPollEvents(); - - // prepare to render - prepare(); - - // end exclusive access (allow simulation thread to run) - mtx.unlock(); - - // render while simulation is running - render(window); - } - - // stop simulation thread - settings.exitrequest = 1; - simthread.join(); - - // delete everything we allocated - uiClearCallback(window); - free(ctrlnoise); - mj_deleteData(d); - mj_deleteModel(m); - mjv_freeScene(&scn); - mjr_freeContext(&con); - - // terminate GLFW (crashes with Linux NVidia drivers) -#if defined(__APPLE__) || defined(_WIN32) - glfwTerminate(); -#endif - - return 0; -} diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt new file mode 100644 index 00000000..7c47984b --- /dev/null +++ b/simulate/CMakeLists.txt @@ -0,0 +1,216 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.16) + +# INTERPROCEDURAL_OPTIMIZATION is enforced when enabled. +set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) +# Default to GLVND if available. +set(CMAKE_POLICY_DEFAULT_CMP0072 NEW) + +# This line has to appear before 'PROJECT' in order to be able to disable incremental linking +set(MSVC_INCREMENTAL_DEFAULT ON) + +project( + mujoco_simulate + VERSION 2.2.1 + DESCRIPTION "MuJoCo simulate binaries" + HOMEPAGE_URL "https://mujoco.org" +) + +enable_language(C) +enable_language(CXX) +if(APPLE) + enable_language(OBJC) + enable_language(OBJCXX) +endif() + +# Check if we are building as standalone project. +set(SIMULATE_STANDALONE OFF) +set(_INSTALL_SIMULATE ON) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(SIMULATE_STANDALONE ON) + # If standalone, do not install the samples. + set(_INSTALL_SIMULATE OFF) +endif() + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../cmake") + +if(SIMULATE_STANDALONE) + include(SimulateOptions) +else() + enforce_mujoco_macosx_min_version() +endif() +include(SimulateDependencies) + +set(MUJOCO_SIMULATE_COMPILE_OPTIONS "${AVX_COMPILE_OPTIONS}" "${EXTRA_COMPILE_OPTIONS}") +set(MUJOCO_SIMULATE_LINK_OPTIONS "${EXTRA_LINK_OPTIONS}") + +if(MUJOCO_HARDEN) + if(WIN32) + set(MUJOCO_SIMULATE_LINK_OPTIONS "${MUJOCO_SIMULATE_LINK_OPTIONS}" -Wl,/DYNAMICBASE) + else() + set(MUJOCO_SIMULATE_COMPILE_OPTIONS "${MUJOCO_SIMULATE_COMPILE_OPTIONS}" -fPIE -fPIC) + if(APPLE) + set(MUJOCO_SIMULATE_LINK_OPTIONS "${MUJOCO_SIMULATE_LINK_OPTIONS}" -Wl,-pie) + else() + set(MUJOCO_SIMULATE_LINK_OPTIONS "${MUJOCO_SIMULATE_LINK_OPTIONS}" -pie) + endif() + endif() +endif() + +# mjsimulate library +add_library(mjsimulate STATIC) +target_sources( + mjsimulate + PUBLIC simulate.h + array_safety.h + simulate.cc + uitools.h + uitools.c +) +target_include_directories(mjsimulate PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(mjsimulate PUBLIC MJSIMULATE_STATIC) +target_compile_options(mjsimulate PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) +target_link_libraries(mjsimulate PUBLIC glfw mujoco::mujoco) +target_link_options(mjsimulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) + +if(APPLE) + target_sources(mjsimulate PRIVATE macos_save.mm) + target_link_libraries(mjsimulate PUBLIC "-framework Cocoa") +endif() + +# Build simulate executable +if(APPLE) + set(SIMULATE_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../dist/mujoco.icns) +elseif(WIN32) + set(SIMULATE_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../dist/appicon.rc) +else() + set(SIMULATE_RESOURCE_FILES "") +endif() + +add_executable(simulate main.cc array_safety.h ${SIMULATE_RESOURCE_FILES}) +target_compile_options(simulate PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) +if(WIN32) + add_custom_command( + TARGET simulate + PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../dist/mujoco.ico + ${CMAKE_CURRENT_SOURCE_DIR} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rm ${CMAKE_CURRENT_SOURCE_DIR}/mujoco.ico + ) +endif() + +target_link_libraries( + simulate + mjsimulate + mujoco::mujoco + glfw + Threads::Threads +) + +target_link_options(simulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) + +if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) + set_target_properties( + simulate + PROPERTIES INSTALL_RPATH @executable_path/../Frameworks + BUILD_WITH_INSTALL_RPATH TRUE + RESOURCE ${SIMULATE_RESOURCE_FILES} + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../dist/Info.plist.simulate.in + MACOSX_BUNDLE_BUNDLE_NAME "MuJoCo" + MACOSX_BUNDLE_GUI_IDENTIFIER "org.mujoco.mujoco" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_INFO_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_ICON_FILE "mujoco.icns" + MACOSX_BUNDLE_COPYRIGHT "Copyright 2021 DeepMind Technologies Limited." + ) + + macro(embed_in_bundle target) + add_dependencies(${target} simulate) + set_target_properties( + ${target} + PROPERTIES INSTALL_RPATH @executable_path/../Frameworks + BUILD_WITH_INSTALL_RPATH TRUE + RUNTIME_OUTPUT_DIRECTORY $ + ) + endmacro() + + # Embed mujoco.framework inside the App bundle ane move the icon file over too. + add_custom_command( + TARGET simulate + POST_BUILD + COMMAND mkdir -p $/../Frameworks + COMMAND rm -rf $/../Frameworks/mujoco.framework + COMMAND cp -a $/../../../mujoco.framework + $/../Frameworks/ + # Delete the symlink and the TBD, otherwise we can't sign and notarize. + COMMAND rm -rf $/../Frameworks/mujoco.framework/mujoco.tbd + COMMAND rm -rf + $/../Frameworks/mujoco.framework/Versions/A/libmujoco.dylib + ) +endif() + +# Do not install if macOS Bundles are created as RPATH is managed manually there. +if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) + set(_INSTALL_SIMULATE OFF) +endif() + +if(_INSTALL_SIMULATE) + + include(TargetAddRpath) + + # Add support to RPATH for the samples. + target_add_rpath( + TARGETS + simulate + INSTALL_DIRECTORY + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}" + LIB_DIRS + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}" + DEPENDS + MUJOCO_ENABLE_RPATH + ) + + install( + TARGETS simulate + EXPORT ${PROJECT_NAME} + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT simulate + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT simulate + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT simulate + BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT simulate + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT simulate + ) + + if(NOT MUJOCO_SIMULATE_USE_SYSTEM_GLFW) + # We downloaded GLFW. Depending if it is a static or shared LIBRARY we might + # need to install it. + get_target_property(MJ_GLFW_LIBRARY_TYPE glfw TYPE) + if(MJ_GLFW_LIBRARY_TYPE STREQUAL SHARED_LIBRARY) + install( + TARGETS glfw + EXPORT ${PROJECT_NAME} + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT simulate + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT simulate + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT simulate + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT simulate + ) + endif() + endif() +endif() diff --git a/simulate/Makefile b/simulate/Makefile new file mode 100644 index 00000000..f0834cbe --- /dev/null +++ b/simulate/Makefile @@ -0,0 +1,11 @@ +# This Makefile assumes that you have GLFW libraries and headers installed on, +# which is commonly available through your distro's package manager. +# On Debian and Ubuntu, GLFW can be installed via `apt install libglfw3-dev`. + +COMMON=-O2 -I../include -L../lib -pthread -Wl,-no-as-needed -Wl,-rpath,'$$ORIGIN'/../lib + +all: + $(CXX) $(COMMON) -std=c++17 -c simulate.cc + $(CC) $(COMMON) -std=c11 -c uitools.c + $(CXX) $(COMMON) -std=c++17 main.cc simulate.o uitools.o -lmujoco -lglfw -o ../bin/simulate + rm uitools.o simulate.o diff --git a/simulate/Makefile.macos b/simulate/Makefile.macos new file mode 100644 index 00000000..ba0c307c --- /dev/null +++ b/simulate/Makefile.macos @@ -0,0 +1,18 @@ +# This Makefile assumes that GLFW is installed via Homebrew. +# If your setup is different, you will need to set GLFWROOT manually. + +# This Makefile also assumes that MuJoCo.app is present in /Applications. + +GLFWROOT?=$(shell brew --prefix) +MUJOCOPATH?=/Applications/MuJoCo.app/Contents/Frameworks + +CFLAGS=-O2 -F$(MUJOCOPATH) -I$(GLFWROOT)/include -pthread +CXXFLAGS=$(CFLAGS) -std=c++17 -stdlib=libc++ +ALLFLAGS=$(CXXFLAGS) -L$(GLFWROOT)/lib -Wl,-rpath,$(MUJOCOPATH) + +all: + clang++ $(CXXFLAGS) -c macos_save.mm + clang++ $(CXXFLAGS) -c simulate.cc + clang $(CFLAGS) -std=c11 -c uitools.c + clang++ $(CXXFLAGS) main.cc macos_save.o simulate.o uitools.o -framework mujoco -framework Cocoa -lglfw -o simulate + rm *.o diff --git a/simulate/Makefile.windows b/simulate/Makefile.windows new file mode 100644 index 00000000..957929e4 --- /dev/null +++ b/simulate/Makefile.windows @@ -0,0 +1,13 @@ +# A copy of the GLFW library is required to build some sample programs. +# If this is not already installed on your system, download the WIN64 archive +# from https://github.com/glfw/glfw/releases, and copy files as follows: +# - Copy the entire `include/GLFW` subdirectory to `mujoco/include/GLFW`. +# - Copy glfw3dll.lib from the subdirectory corresponding into your compiler +# choice to `mujoco/lib/glfw3dll.lib`. +# - Copy glfw3.dll from the same subdirectory into `mujoco/bin/glfw3.dll`. + +COMMON=/O2 /MT /EHsc /arch:AVX /I../include /Fe../bin/ + +all: + cl $(COMMON) main.cc simulate.cc uitools.c ../lib/glfw3dll.lib ../lib/mujoco.lib + del *.obj diff --git a/simulate/array_safety.h b/simulate/array_safety.h new file mode 100644 index 00000000..19881f8a --- /dev/null +++ b/simulate/array_safety.h @@ -0,0 +1,96 @@ +// Copyright 2021 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_SAMPLE_ARRAY_SAFETY_H_ +#define MUJOCO_SAMPLE_ARRAY_SAFETY_H_ + +#include +#include +#include +#include +#include + +// Provides safe alternatives to the sizeof() operator and standard library functions for handling +// null-terminated (C-style) strings in raw char arrays. +// +// These functions make use of compile-time array sizes to limit read and write operations to within +// the array bounds. They are designed to trigger a compile error if the array size cannot be +// determined at compile time (e.g. when an array has decayed into a pointer). +// +// They do not perform runtime bound checks. + +namespace mujoco { +namespace sample_util { + +// returns sizeof(arr) +// use instead of sizeof() to avoid unintended array-to-pointer decay +template +static constexpr std::size_t sizeof_arr(const T(&arr)[N]) { + return sizeof(arr); +} + +// like std::strcmp but it will not read beyond the bound of either lhs or rhs +template +static inline int strcmp_arr(const char (&lhs)[N1], const char (&rhs)[N2]) { + return std::strncmp(lhs, rhs, std::min(N1, N2)); +} + +// like std::strlen but it will not read beyond the bound of str +// if str is not null-terminated, returns sizeof(str) +template +static inline std::size_t strlen_arr(const char (&str)[N]) { + for (std::size_t i = 0; i < N; ++i) { + if (str[i] == '\0') { + return i; + } + } + return N; +} + +// like std::sprintf but will not write beyond the bound of dest +// dest is guaranteed to be null-terminated +template +static inline int sprintf_arr(char (&dest)[N], const char* format, ...) { + std::va_list vargs; + va_start(vargs, format); + int retval = std::vsnprintf(dest, N, format, vargs); + va_end(vargs); + return retval; +} + +// like std::strcat but will not write beyond the bound of dest +// dest is guaranteed to be null-terminated +template +static inline char* strcat_arr(char (&dest)[N], const char* src) { + return std::strncat(dest, src, sizeof_arr(dest) - strlen_arr(dest) - 1); +} + +// like std::strcpy but won't write beyond the bound of dest +// dest is guaranteed to be null-terminated +template +static inline char* strcpy_arr(char (&dest)[N], const char* src) { + { + std::size_t i = 0; + for (; src[i] && i < N - 1; ++i) { + dest[i] = src[i]; + } + dest[i] = '\0'; + } + return &dest[0]; +} + +} // namespace sample_util +} // namespace mujoco + +#endif // MUJOCO_SAMPLE_ARRAY_SAFETY_H_ diff --git a/simulate/cmake/CheckAvxSupport.cmake b/simulate/cmake/CheckAvxSupport.cmake new file mode 100644 index 00000000..ab8ad644 --- /dev/null +++ b/simulate/cmake/CheckAvxSupport.cmake @@ -0,0 +1,53 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include(CheckCSourceCompiles) + +# Assigns compiler options to the given variable based on availability of AVX. +function(get_avx_compile_options OUTPUT_VAR) + message(VERBOSE "Checking if AVX is available...") + + if(MSVC) + set(CMAKE_REQUIRED_FLAGS "/arch:AVX") + else() + set(CMAKE_REQUIRED_FLAGS "-mavx") + endif() + + if(APPLE AND "x86_64" IN_LIST CMAKE_OSX_ARCHITECTURES) + message(STATUS "Building x86_64 on macOS, forcing CAN_BUILD_AVX to TRUE.") + set(CAN_BUILD_AVX TRUE) + else() + check_c_source_compiles( + " + #include + int main(int argc, char* argv[]) { + __m256d ymm; + return 0; + } + " + CAN_BUILD_AVX + ) + endif() + + if(CAN_BUILD_AVX) + message(VERBOSE "Checking if AVX is available... AVX available.") + set("${OUTPUT_VAR}" + ${CMAKE_REQUIRED_FLAGS} + PARENT_SCOPE + ) + else() + message(VERBOSE "Checking if AVX is available... AVX not available.") + set("${OUTPUT_VAR}" PARENT_SCOPE) + endif() +endfunction() diff --git a/simulate/cmake/FindOrFetch.cmake b/simulate/cmake/FindOrFetch.cmake new file mode 100644 index 00000000..602601f5 --- /dev/null +++ b/simulate/cmake/FindOrFetch.cmake @@ -0,0 +1,139 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#.rst: +# FindOrFetch +# ---------------------- +# +# Find or fetch a package in order to satisfy target dependencies. +# +# FindOrFetch([USE_SYSTEM_PACKAGE [ON/OFF]] +# [PACKAGE_NAME [name]] +# [LIBRARY_NAME [name]] +# [GIT_REPO [repo]] +# [GIT_TAG [tag]] +# [PATCH_COMMAND [cmd] [args]] +# [TARGETS [targets]] +# [EXCLUDE_FROM_ALL]) +# +# The command has the following parameters: +# +# Arguments: +# - ``USE_SYSTEM_PACKAGE`` one-value argument on whether to search for the +# package in the system (ON) or whether to fetch the library using +# FetchContent from the specified Git repository (OFF). Note that +# FetchContent variables will override this behaviour. +# - ``PACKAGE_NAME`` name of the system-package. Ignored if +# ``USE_SYSTEM_PACKAGE`` is ``OFF``. +# - ``LIBRARY_NAME`` name of the library. Ignored if +# ``USE_SYSTEM_PACKAGE`` is ``ON``. +# - ``GIT_REPO`` git repository to fetch the library from. Ignored if +# ``USE_SYSTEM_PACKAGE`` is ``ON``. +# - ``GIT_TAG`` tag reference when fetching the library from the git +# repository. Ignored if ``USE_SYSTEM_PACKAGE`` is ``ON``. +# - ``PATCH_COMMAND`` Specifies a custom command to patch the sources after an +# update. See https://cmake.org/cmake/help/latest/module/ExternalProject.html#command:externalproject_add +# for details on the parameter. +# - ``TARGETS`` list of targets to be satisfied. If any of these targets are +# not currently defined, this macro will attempt to either find or fetch the +# package. +# - ``EXCLUDE_FROM_ALL`` if specified, the targets are not added to the ``all`` +# metatarget. +# +# Note: if ``USE_SYSTEM_PACKAGE`` is ``OFF``, FetchContent will be used to +# retrieve the specified targets. It is possible to specify any variable in +# https://cmake.org/cmake/help/latest/module/FetchContent.html#variables to +# override this macro behaviour. + +if(COMMAND FindOrFetch) + return() +endif() + +macro(FindOrFetch) + if(NOT FetchContent) + include(FetchContent) + endif() + + # Parse arguments. + set(options EXCLUDE_FROM_ALL) + set(one_value_args + USE_SYSTEM_PACKAGE + PACKAGE_NAME + LIBRARY_NAME + GIT_REPO + GIT_TAG + ) + set(multi_value_args PATCH_COMMAND TARGETS) + cmake_parse_arguments( + _ARGS + "${options}" + "${one_value_args}" + "${multi_value_args}" + ${ARGN} + ) + + # Check if all targets are found. + if(NOT _ARGS_TARGETS) + message(FATAL_ERROR "mujoco::FindOrFetch: TARGETS must be specified.") + endif() + + set(targets_found TRUE) + message(CHECK_START + "mujoco::FindOrFetch: checking for targets in package `${_ARGS_PACKAGE_NAME}`" + ) + foreach(target ${_ARGS_TARGETS}) + if(NOT TARGET ${target}) + message(CHECK_FAIL "target `${target}` not defined.") + set(targets_found FALSE) + break() + endif() + endforeach() + + # If targets are not found, use `find_package` or `FetchContent...` to get it. + if(NOT targets_found) + if(${_ARGS_USE_SYSTEM_PACKAGE}) + message(CHECK_START + "mujoco::FindOrFetch: finding `${_ARGS_PACKAGE_NAME}` in system packages..." + ) + find_package(${_ARGS_PACKAGE_NAME} REQUIRED) + message(CHECK_PASS "found") + else() + message(CHECK_START + "mujoco::FindOrFetch: Using FetchContent to retrieve `${_ARGS_LIBRARY_NAME}`" + ) + FetchContent_Declare( + ${_ARGS_LIBRARY_NAME} + GIT_REPOSITORY ${_ARGS_GIT_REPO} + GIT_TAG ${_ARGS_GIT_TAG} + GIT_SHALLOW FALSE + PATCH_COMMAND ${_ARGS_PATCH_COMMAND} + ) + if(${_ARGS_EXCLUDE_FROM_ALL}) + FetchContent_GetProperties(${_ARGS_LIBRARY_NAME}) + if(NOT ${${_ARGS_LIBRARY_NAME}_POPULATED}) + FetchContent_Populate(${_ARGS_LIBRARY_NAME}) + add_subdirectory( + ${${_ARGS_LIBRARY_NAME}_SOURCE_DIR} ${${_ARGS_LIBRARY_NAME}_BINARY_DIR} + EXCLUDE_FROM_ALL + ) + endif() + else() + FetchContent_MakeAvailable(${_ARGS_LIBRARY_NAME}) + endif() + message(CHECK_PASS "Done") + endif() + else() + message(CHECK_PASS "found") + endif() +endmacro() diff --git a/simulate/cmake/MujocoHarden.cmake b/simulate/cmake/MujocoHarden.cmake new file mode 100644 index 00000000..7beb88fe --- /dev/null +++ b/simulate/cmake/MujocoHarden.cmake @@ -0,0 +1,35 @@ +# Copyright 2022 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +option(MUJOCO_HARDEN "Enable build hardening for MuJoCo." OFF) +if(MUJOCO_HARDEN + AND NOT + CMAKE_CXX_COMPILER_ID + MATCHES + ".*Clang.*" +) + message(FATAL_ERROR "MUJOCO_HARDEN is only supported when building with Clang") +endif() + +if(MUJOCO_HARDEN) + set(MUJOCO_HARDEN_COMPILE_OPTIONS -D_FORTIFY_SOURCE=2 -fstack-protector) + if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(MUJOCO_HARDEN_LINK_OPTIONS -Wl,-bind_at_load) + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + set(MUJOCO_HARDEN_LINK_OPTIONS -Wl,-z,relro -Wl,-z,now) + endif() +else() + set(MUJOCO_HARDEN_COMPILE_OPTIONS "") + set(MUJOCO_HARDEN_LINK_OPTIONS "") +endif() diff --git a/simulate/cmake/MujocoLinkOptions.cmake b/simulate/cmake/MujocoLinkOptions.cmake new file mode 100644 index 00000000..242767f9 --- /dev/null +++ b/simulate/cmake/MujocoLinkOptions.cmake @@ -0,0 +1,67 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include(CheckCSourceCompiles) + +# Gets the appropriate linker options for building MuJoCo, based on features available on the +# linker. +function(get_mujoco_extra_link_options OUTPUT_VAR) + if(MSVC) + set(EXTRA_LINK_OPTIONS /OPT:REF /OPT:ICF=5) + else() + set(EXTRA_LINK_OPTIONS) + + if(WIN32) + set(CMAKE_REQUIRED_FLAGS "-fuse-ld=lld-link") + check_c_source_compiles("int main() {}" SUPPORTS_LLD) + if(SUPPORTS_LLD) + set(EXTRA_LINK_OPTIONS + ${EXTRA_LINK_OPTIONS} + -fuse-ld=lld-link + -Wl,/OPT:REF + -Wl,/OPT:ICF + ) + endif() + else() + set(CMAKE_REQUIRED_FLAGS "-fuse-ld=lld") + check_c_source_compiles("int main() {}" SUPPORTS_LLD) + if(SUPPORTS_LLD) + set(EXTRA_LINK_OPTIONS ${EXTRA_LINK_OPTIONS} -fuse-ld=lld) + else() + set(CMAKE_REQUIRED_FLAGS "-fuse-ld=gold") + check_c_source_compiles("int main() {}" SUPPORTS_GOLD) + if(SUPPORTS_GOLD) + set(EXTRA_LINK_OPTIONS ${EXTRA_LINK_OPTIONS} -fuse-ld=gold) + endif() + endif() + + set(CMAKE_REQUIRED_FLAGS ${EXTRA_LINK_OPTIONS} "-Wl,--gc-sections") + check_c_source_compiles("int main() {}" SUPPORTS_GC_SECTIONS) + if(SUPPORTS_GC_SECTIONS) + set(EXTRA_LINK_OPTIONS ${EXTRA_LINK_OPTIONS} -Wl,--gc-sections) + else() + set(CMAKE_REQUIRED_FLAGS ${EXTRA_LINK_OPTIONS} "-Wl,-dead_strip") + check_c_source_compiles("int main() {}" SUPPORTS_DEAD_STRIP) + if(SUPPORTS_DEAD_STRIP) + set(EXTRA_LINK_OPTIONS ${EXTRA_LINK_OPTIONS} -Wl,-dead_strip) + endif() + endif() + endif() + endif() + + set("${OUTPUT_VAR}" + ${EXTRA_LINK_OPTIONS} + PARENT_SCOPE + ) +endfunction() diff --git a/simulate/cmake/MujocoMacOS.cmake b/simulate/cmake/MujocoMacOS.cmake new file mode 100644 index 00000000..d2f378f6 --- /dev/null +++ b/simulate/cmake/MujocoMacOS.cmake @@ -0,0 +1,38 @@ +# Copyright 2022 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if(APPLE) + # 10.12 is the oldest version of macOS that supports C++17, launched 2016. + set(MUJOCO_MACOSX_VERSION_MIN 10.12) + + # We are setting the -mmacosx-version-min compiler flag directly rather than using the + # CMAKE_OSX_DEPLOYMENT_TARGET variable since we do not want to affect choice of SDK, + # and also we only want to apply the version restriction locally. + set(MUJOCO_MACOS_COMPILE_OPTIONS -mmacosx-version-min=${MUJOCO_MACOSX_VERSION_MIN} + -Werror=partial-availability -Werror=unguarded-availability + ) + set(MUJOCO_MACOS_LINK_OPTIONS -mmacosx-version-min=${MUJOCO_MACOSX_VERSION_MIN} + -Wl,-no_weak_imports + ) +else() + set(MUJOCO_MACOS_COMPILE_OPTIONS "") + set(MUJOCO_MACOS_LINK_OPTIONS "") +endif() + +function(enforce_mujoco_macosx_min_version) + if(APPLE) + add_compile_options(${MUJOCO_MACOS_COMPILE_OPTIONS}) + add_link_options(${MUJOCO_MACOS_LINK_OPTIONS}) + endif() +endfunction() diff --git a/simulate/cmake/SimulateDependencies.cmake b/simulate/cmake/SimulateDependencies.cmake new file mode 100644 index 00000000..9fba6a28 --- /dev/null +++ b/simulate/cmake/SimulateDependencies.cmake @@ -0,0 +1,104 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include(FindOrFetch) + +if(SIMULATE_STANDALONE) + # If standalone, by default look for MuJoCo binary version. + set(DEFAULT_USE_SYSTEM_MUJOCO ON) +else() + set(DEFAULT_USE_SYSTEM_MUJOCO OFF) +endif() + +option(MUJOCO_SIMULATE_USE_SYSTEM_MUJOCO "Use installed MuJoCo version." + ${DEFAULT_USE_SYSTEM_MUJOCO} +) +unset(DEFAULT_USE_SYSTEM_MUJOCO) + +option(MUJOCO_SIMULATE_USE_SYSTEM_MUJOCO "Use installed MuJoCo version." OFF) +option(MUJOCO_SIMULATE_USE_SYSTEM_GLFW "Use installed GLFW version." OFF) + +set(MUJOCO_DEP_VERSION_glfw + 7d5a16ce714f0b5f4efa3262de22e4d948851525 # 3.3.6 + CACHE STRING "Version of `glfw` to be fetched." +) +mark_as_advanced(MUJOCO_DEP_VERSION_glfw) + +find_package(Threads REQUIRED) + +set(MUJOCO_BUILD_EXAMPLES OFF) +set(MUJOCO_BUILD_TESTS OFF) +set(MUJOCO_BUILD_PYTHON OFF) +set(MUJOCO_TEST_PYTHON_UTIL OFF) + +findorfetch( + USE_SYSTEM_PACKAGE + MUJOCO_SIMULATE_USE_SYSTEM_MUJOCO + PACKAGE_NAME + mujoco + LIBRARY_NAME + mujoco + GIT_REPO + https://github.com/deepmind/mujoco.git + GIT_TAG + main + TARGETS + mujoco + EXCLUDE_FROM_ALL +) + +option(MUJOCO_EXTRAS_STATIC_GLFW + "Link MuJoCo sample apps and simulate libraries against GLFW statically." ON +) +if(MUJOCO_EXTRAS_STATIC_GLFW) + set(BUILD_SHARED_LIBS_OLD ${BUILD_SHARED_LIBS}) + set(BUILD_SHARED_LIBS + OFF + CACHE INTERNAL "Build SHARED libraries" + ) +endif() + +set(GLFW_BUILD_EXAMPLES OFF) +set(GLFW_BUILD_TESTS OFF) +set(GLFW_BUILD_DOCS OFF) +set(GLFW_INSTALL OFF) + +findorfetch( + USE_SYSTEM_PACKAGE + MUJOCO_SIMULATE_USE_SYSTEM_GLFW + PACKAGE_NAME + glfw + LIBRARY_NAME + glfw + GIT_REPO + https://github.com/glfw/glfw.git + GIT_TAG + ${MUJOCO_DEP_VERSION_glfw} + TARGETS + glfw + EXCLUDE_FROM_ALL +) + +if(MUJOCO_EXTRAS_STATIC_GLFW) + set(BUILD_SHARED_LIBS + ${BUILD_SHARED_LIBS_OLD} + CACHE BOOL "Build SHARED libraries" FORCE + ) + unset(BUILD_SHARED_LIBS_OLD) +endif() + +if(NOT SIMULATE_STANDALONE) + target_compile_options(glfw PRIVATE ${MUJOCO_MACOS_COMPILE_OPTIONS}) + target_link_options(glfw PRIVATE ${MUJOCO_MACOS_LINK_OPTIONS}) +endif() diff --git a/simulate/cmake/SimulateOptions.cmake b/simulate/cmake/SimulateOptions.cmake new file mode 100644 index 00000000..2b0ce938 --- /dev/null +++ b/simulate/cmake/SimulateOptions.cmake @@ -0,0 +1,106 @@ +# Copyright 2021 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Global compilation settings +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling + +if(NOT CMAKE_CONFIGURATION_TYPES) + if(NOT CMAKE_BUILD_TYPE) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE + "Release" + CACHE STRING "Choose the type of build, recommanded options are: Debug or Release" FORCE + ) + endif() + set(BUILD_TYPES + "Debug" + "Release" + "MinSizeRel" + "RelWithDebInfo" + ) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ${BUILD_TYPES}) +endif() + +include(GNUInstallDirs) + +# Change the default output directory in the build structure. This is not stricly needed, but helps +# running in Windows, such that all built executables have DLLs in the same folder as the .exe +# files. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") + +set(OpenGL_GL_PREFERENCE GLVND) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_C_VISIBILITY_PRESET hidden) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) + +if(MSVC) + add_compile_options(/Gy /Gw /Oi) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-fdata-sections -ffunction-sections) +endif() + +# We default to shared library. +set(BUILD_SHARED_LIBS + ON + CACHE BOOL "Build Mujoco as shared library." +) + +option(MUJOCO_ENABLE_AVX "Build binaries that require AVX instructions, if possible." ON) +option(MUJOCO_ENABLE_AVX_INTRINSICS "Make use of hand-written AVX intrinsics, if possible." ON) +option(MUJOCO_ENABLE_RPATH "Enable RPath support when installing Mujoco." ON) +mark_as_advanced(MUJOCO_ENABLE_RPATH) + +if(MUJOCO_ENABLE_AVX) + include(CheckAvxSupport) + get_avx_compile_options(AVX_COMPILE_OPTIONS) +else() + set(AVX_COMPILE_OPTIONS) +endif() + +option(MUJOCO_BUILD_MACOS_FRAMEWORKS "Build libraries as macOS Frameworks" OFF) + +# Get some extra link options. +include(MujocoLinkOptions) +get_mujoco_extra_link_options(EXTRA_LINK_OPTIONS) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT MSVC)) + set(EXTRA_COMPILE_OPTIONS -Wall -Werror) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(EXTRA_COMPILE_OPTIONS + -Wno-int-in-bool-context + -Wno-maybe-uninitialized + -Wno-sign-compare + -Wno-stringop-overflow + -Wno-stringop-truncation + ) + endif() +endif() + +if(WIN32) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) +endif() + +include(MujocoHarden) +set(EXTRA_COMPILE_OPTIONS ${EXTRA_COMPILE_OPTIONS} ${MUJOCO_HARDEN_COMPILE_OPTIONS}) +set(EXTRA_LINK_OPTIONS ${EXTRA_LINK_OPTIONS} ${MUJOCO_HARDEN_LINK_OPTIONS}) diff --git a/sample/macos_save.mm b/simulate/macos_save.mm similarity index 100% rename from sample/macos_save.mm rename to simulate/macos_save.mm diff --git a/simulate/main.cc b/simulate/main.cc new file mode 100644 index 00000000..b48ff5b8 --- /dev/null +++ b/simulate/main.cc @@ -0,0 +1,293 @@ +// Copyright 2021 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "simulate.h" +#include "array_safety.h" + +namespace { +namespace mj = ::mujoco; +namespace mju = ::mujoco::sample_util; + +// constants +const double syncmisalign = 0.1; // maximum time mis-alignment before re-sync +const double refreshfactor = 0.7; // fraction of refresh available for simulation +const int kErrorLength = 1024; + +// model and data +mjModel* m = nullptr; +mjData* d = nullptr; + +// control noise variables +mjtNum* ctrlnoise = nullptr; + + + +//------------------------------------------- simulation ------------------------------------------- + + +mjModel* LoadModel(const char* file, mj::Simulate& sim) { + // this copy is needed so that the mju::strlen call below compiles + char filename[mj::Simulate::kMaxFilenameLength]; + mju::strcpy_arr(filename, file); + + // make sure filename is not empty + if (!filename[0]) { + return nullptr; + } + + // load and compile + char loadError[kErrorLength] = ""; + mjModel* mnew = 0; + if (mju::strlen_arr(filename)>4 && + !std::strncmp(filename + mju::strlen_arr(filename) - 4, ".mjb", + mju::sizeof_arr(filename) - mju::strlen_arr(filename)+4)) { + mnew = mj_loadModel(filename, nullptr); + if (!mnew) { + mju::strcpy_arr(loadError, "could not load binary model"); + } + } else { + mnew = mj_loadXML(filename, nullptr, loadError, mj::Simulate::kMaxFilenameLength); + // remove trailing newline character from loadError + if (loadError[0]) { + int error_length = mju::strlen_arr(loadError); + if (loadError[error_length-1] == '\n') { + loadError[error_length-1] = '\0'; + } + } + } + + mju::strcpy_arr(sim.loadError, loadError); + + if (!mnew) { + std::printf("%s\n", loadError); + return nullptr; + } + + // compiler warning: print and pause + if (loadError[0]) { + // mj_forward() below will print the warning message + std::printf("Model compiled, but simulation warning (paused):\n %s\n", loadError); + sim.run = 0; + } + + return mnew; +} + +// simulate in background thread (while rendering in main thread) +void PhysicsLoop(mj::Simulate& sim) { + // cpu-sim syncronization point + double cpusync = 0; + mjtNum simsync = 0; + + // run until asked to exit + while (!sim.exitrequest.load()) { + if (sim.droploadrequest.load()) { + mjModel* mnew = LoadModel(sim.dropfilename, sim); + sim.droploadrequest.store(false); + + mjData* dnew = nullptr; + if (mnew) dnew = mj_makeData(mnew); + if (dnew) { + sim.load(sim.dropfilename, mnew, dnew, true); + + m = mnew; + d = dnew; + mj_forward(m, d); + + // allocate ctrlnoise + free(ctrlnoise); + ctrlnoise = (mjtNum*) malloc(sizeof(mjtNum)*m->nu); + mju_zero(ctrlnoise, m->nu); + } + } + + if (sim.uiloadrequest.load()) { + sim.uiloadrequest.fetch_sub(1); + mjModel* mnew = LoadModel(sim.filename, sim); + mjData* dnew = nullptr; + if (mnew) dnew = mj_makeData(mnew); + if (dnew) { + sim.load(sim.filename, mnew, dnew, true); + + m = mnew; + d = dnew; + mj_forward(m, d); + + // allocate ctrlnoise + free(ctrlnoise); + ctrlnoise = static_cast(malloc(sizeof(mjtNum)*m->nu)); + mju_zero(ctrlnoise, m->nu); + } + } + + // sleep for 1 ms or yield, to let main thread run + // yield results in busy wait - which has better timing but kills battery life + if (sim.run && sim.busywait) { + std::this_thread::yield(); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + { + const std::lock_guard lock(sim.mtx); + + // run only if model is present + if (m) { + // running + if (sim.run) { + // record cpu time at start of iteration + double tmstart = glfwGetTime(); + + // inject noise + if (sim.ctrlnoisestd) { + // convert rate and scale to discrete time given current timestep + mjtNum rate = mju_exp(-m->opt.timestep / sim.ctrlnoiserate); + mjtNum scale = sim.ctrlnoisestd * mju_sqrt(1-rate*rate); + + for (int i=0; inu; i++) { + // update noise + ctrlnoise[i] = rate * ctrlnoise[i] + scale * mju_standardNormal(nullptr); + // apply noise + d->ctrl[i] = ctrlnoise[i]; + } + } + + // out-of-sync (for any reason) + mjtNum offset = mju_abs((d->time*sim.slow_down-simsync)-(tmstart-cpusync)); + if (d->time*sim.slow_down syncmisalign*sim.slow_down || sim.speed_changed) { + // re-sync + cpusync = tmstart; + simsync = d->time*sim.slow_down; + sim.speed_changed = false; + + // clear old perturbations, apply new + mju_zero(d->xfrc_applied, 6*m->nbody); + sim.applyposepertubations(0); // move mocap bodies only + sim.applyforceperturbations(); + + // run single step, let next iteration deal with timing + mj_step(m, d); + } + + // in-sync + else { + // step while simtime lags behind cputime, and within safefactor + while ((d->time*sim.slow_down-simsync) < (glfwGetTime()-cpusync) && + (glfwGetTime()-tmstart) < refreshfactor/sim.vmode.refreshRate) { + // clear old perturbations, apply new + mju_zero(d->xfrc_applied, 6*m->nbody); + sim.applyposepertubations(0); // move mocap bodies only + sim.applyforceperturbations(); + + // run mj_step + mjtNum prevtm = d->time*sim.slow_down; + mj_step(m, d); + + // break on reset + if (d->time*sim.slow_down + } +} +} // namespace + +//-------------------------------------- physics_thread -------------------------------------------- + +void PhysicsThread(mj::Simulate* sim, const char* filename) { + // request loadmodel if file given (otherwise drag-and-drop) + if (filename != nullptr) { + m = LoadModel(filename, *sim); + if (m) d = mj_makeData(m); + if (d) { + sim->load(filename, m, d, true); + mj_forward(m, d); + + // allocate ctrlnoise + free(ctrlnoise); + ctrlnoise = static_cast(malloc(sizeof(mjtNum)*m->nu)); + mju_zero(ctrlnoise, m->nu); + } + } + + PhysicsLoop(*sim); + + // delete everything we allocated + free(ctrlnoise); + mj_deleteData(d); + mj_deleteModel(m); +} + +//------------------------------------------ main -------------------------------------------------- + +// run event loop +int main(int argc, const char** argv) { + // print version, check compatibility + std::printf("MuJoCo version %s\n", mj_versionString()); + if (mjVERSION_HEADER!=mj_version()) { + mju_error("Headers and library have different versions"); + } + + // simulate object encapsulates the UI + auto sim = std::make_unique(); + + // init GLFW + if (!glfwInit()) { + mju_error("could not initialize GLFW"); + } + + const char* filename = nullptr; + if (argc > 1) { + filename = argv[1]; + } + + // start physics thread + std::thread physicsthreadhandle = std::thread(&PhysicsThread, sim.get(), filename); + + // start simulation UI loop (blocking call) + sim->renderloop(); + physicsthreadhandle.join(); + + // terminate GLFW (crashes with Linux NVidia drivers) +#if defined(__APPLE__) || defined(_WIN32) + glfwTerminate(); +#endif + + return 0; +} diff --git a/simulate/simulate.cc b/simulate/simulate.cc new file mode 100644 index 00000000..af8b7304 --- /dev/null +++ b/simulate/simulate.cc @@ -0,0 +1,1904 @@ +// Copyright 2021 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "simulate.h" + +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include "uitools.h" +#include "array_safety.h" + +// When launched via an App Bundle on macOS, the working directory is the path to the App Bundle's +// resource directory. This causes files to be saved into the bundle, which is not the desired +// behavior. Instead, we open a save dialog box to ask the user where to put the file. +// Since the dialog box logic needs to be written in Objective-C, we separate it into a different +// source file. +#ifdef __APPLE__ +std::string getSavePath(const char* filename); +#else +static std::string getSavePath(const char* filename) { + return filename; +} +#endif + +namespace { +namespace mj = ::mujoco; +namespace mju = ::mujoco::sample_util; + +//------------------------------------------- global ----------------------------------------------- + +const int maxgeom = 5000; // preallocated geom array in mjvScene +const int max_slow_down = 128; // maximum slow-down quotient +const double zoom_increment = 0.02; // ratio of one click-wheel zoom increment to vertical extent + +// section ids +enum { + // left ui + SECT_FILE = 0, + SECT_OPTION, + SECT_SIMULATION, + SECT_WATCH, + SECT_PHYSICS, + SECT_RENDERING, + SECT_GROUP, + NSECT0, + + // right ui + SECT_JOINT = 0, + SECT_CONTROL, + NSECT1 +}; + +// file section of UI +const mjuiDef defFile[] = { + {mjITEM_SECTION, "File", 1, nullptr, "AF"}, + {mjITEM_BUTTON, "Save xml", 2, nullptr, ""}, + {mjITEM_BUTTON, "Save mjb", 2, nullptr, ""}, + {mjITEM_BUTTON, "Print model", 2, nullptr, "CM"}, + {mjITEM_BUTTON, "Print data", 2, nullptr, "CD"}, + {mjITEM_BUTTON, "Quit", 1, nullptr, "CQ"}, + {mjITEM_END} +}; + +// help strings +const char help_content[] = + "Space\n" + "+ -\n" + "Right arrow\n" + "[ ]\n" + "Esc\n" + "Double-click\n" + "Page Up\n" + "Right double-click\n" + "Ctrl Right double-click\n" + "Scroll, middle drag\n" + "Left drag\n" + "[Shift] right drag\n" + "Ctrl [Shift] drag\n" + "Ctrl [Shift] right drag\n" + "F1\n" + "F2\n" + "F3\n" + "F4\n" + "F5\n" + "UI right hold\n" + "UI title double-click"; + +const char help_title[] = + "Play / Pause\n" + "Speed up / down\n" + "Step\n" + "Cycle cameras\n" + "Free camera\n" + "Select\n" + "Select parent\n" + "Center\n" + "Tracking camera\n" + "Zoom\n" + "View rotate\n" + "View translate\n" + "Object rotate\n" + "Object translate\n" + "Help\n" + "Info\n" + "Profiler\n" + "Sensors\n" + "Full screen\n" + "Show UI shortcuts\n" + "Expand/collapse all"; + + +//-------------------------------- profiler, sensor, info, watch ----------------------------------- + +// init profiler figures +void profilerinit(mj::Simulate* sim) { + int i, n; + + // set figures to default + mjv_defaultFigure(&sim->figconstraint); + mjv_defaultFigure(&sim->figcost); + mjv_defaultFigure(&sim->figtimer); + mjv_defaultFigure(&sim->figsize); + + // titles + mju::strcpy_arr(sim->figconstraint.title, "Counts"); + mju::strcpy_arr(sim->figcost.title, "Convergence (log 10)"); + mju::strcpy_arr(sim->figsize.title, "Dimensions"); + mju::strcpy_arr(sim->figtimer.title, "CPU time (msec)"); + + // x-labels + mju::strcpy_arr(sim->figconstraint.xlabel, "Solver iteration"); + mju::strcpy_arr(sim->figcost.xlabel, "Solver iteration"); + mju::strcpy_arr(sim->figsize.xlabel, "Video frame"); + mju::strcpy_arr(sim->figtimer.xlabel, "Video frame"); + + // y-tick nubmer formats + mju::strcpy_arr(sim->figconstraint.yformat, "%.0f"); + mju::strcpy_arr(sim->figcost.yformat, "%.1f"); + mju::strcpy_arr(sim->figsize.yformat, "%.0f"); + mju::strcpy_arr(sim->figtimer.yformat, "%.2f"); + + // colors + sim->figconstraint.figurergba[0] = 0.1f; + sim->figcost.figurergba[2] = 0.2f; + sim->figsize.figurergba[0] = 0.1f; + sim->figtimer.figurergba[2] = 0.2f; + sim->figconstraint.figurergba[3] = 0.5f; + sim->figcost.figurergba[3] = 0.5f; + sim->figsize.figurergba[3] = 0.5f; + sim->figtimer.figurergba[3] = 0.5f; + + // legends + mju::strcpy_arr(sim->figconstraint.linename[0], "total"); + mju::strcpy_arr(sim->figconstraint.linename[1], "active"); + mju::strcpy_arr(sim->figconstraint.linename[2], "changed"); + mju::strcpy_arr(sim->figconstraint.linename[3], "evals"); + mju::strcpy_arr(sim->figconstraint.linename[4], "updates"); + mju::strcpy_arr(sim->figcost.linename[0], "improvement"); + mju::strcpy_arr(sim->figcost.linename[1], "gradient"); + mju::strcpy_arr(sim->figcost.linename[2], "lineslope"); + mju::strcpy_arr(sim->figsize.linename[0], "dof"); + mju::strcpy_arr(sim->figsize.linename[1], "body"); + mju::strcpy_arr(sim->figsize.linename[2], "constraint"); + mju::strcpy_arr(sim->figsize.linename[3], "sqrt(nnz)"); + mju::strcpy_arr(sim->figsize.linename[4], "contact"); + mju::strcpy_arr(sim->figsize.linename[5], "iteration"); + mju::strcpy_arr(sim->figtimer.linename[0], "total"); + mju::strcpy_arr(sim->figtimer.linename[1], "collision"); + mju::strcpy_arr(sim->figtimer.linename[2], "prepare"); + mju::strcpy_arr(sim->figtimer.linename[3], "solve"); + mju::strcpy_arr(sim->figtimer.linename[4], "other"); + + // grid sizes + sim->figconstraint.gridsize[0] = 5; + sim->figconstraint.gridsize[1] = 5; + sim->figcost.gridsize[0] = 5; + sim->figcost.gridsize[1] = 5; + sim->figsize.gridsize[0] = 3; + sim->figsize.gridsize[1] = 5; + sim->figtimer.gridsize[0] = 3; + sim->figtimer.gridsize[1] = 5; + + // minimum ranges + sim->figconstraint.range[0][0] = 0; + sim->figconstraint.range[0][1] = 20; + sim->figconstraint.range[1][0] = 0; + sim->figconstraint.range[1][1] = 80; + sim->figcost.range[0][0] = 0; + sim->figcost.range[0][1] = 20; + sim->figcost.range[1][0] = -15; + sim->figcost.range[1][1] = 5; + sim->figsize.range[0][0] = -200; + sim->figsize.range[0][1] = 0; + sim->figsize.range[1][0] = 0; + sim->figsize.range[1][1] = 100; + sim->figtimer.range[0][0] = -200; + sim->figtimer.range[0][1] = 0; + sim->figtimer.range[1][0] = 0; + sim->figtimer.range[1][1] = 0.4f; + + // init x axis on history figures (do not show yet) + for (n=0; n<6; n++) + for (i=0; ifigtimer.linedata[n][2*i] = -i; + sim->figsize.linedata[n][2*i] = -i; + } +} + +// update profiler figures +void profilerupdate(mj::Simulate* sim) { + int i, n; + + // update constraint figure + sim->figconstraint.linepnt[0] = mjMIN(mjMIN(sim->d->solver_iter, mjNSOLVER), mjMAXLINEPNT); + for (i=1; i<5; i++) { + sim->figconstraint.linepnt[i] = sim->figconstraint.linepnt[0]; + } + if (sim->m->opt.solver==mjSOL_PGS) { + sim->figconstraint.linepnt[3] = 0; + sim->figconstraint.linepnt[4] = 0; + } + if (sim->m->opt.solver==mjSOL_CG) { + sim->figconstraint.linepnt[4] = 0; + } + for (i=0; ifigconstraint.linepnt[0]; i++) { + // x + sim->figconstraint.linedata[0][2*i] = i; + sim->figconstraint.linedata[1][2*i] = i; + sim->figconstraint.linedata[2][2*i] = i; + sim->figconstraint.linedata[3][2*i] = i; + sim->figconstraint.linedata[4][2*i] = i; + + // y + sim->figconstraint.linedata[0][2*i+1] = sim->d->nefc; + sim->figconstraint.linedata[1][2*i+1] = sim->d->solver[i].nactive; + sim->figconstraint.linedata[2][2*i+1] = sim->d->solver[i].nchange; + sim->figconstraint.linedata[3][2*i+1] = sim->d->solver[i].neval; + sim->figconstraint.linedata[4][2*i+1] = sim->d->solver[i].nupdate; + } + + // update cost figure + sim->figcost.linepnt[0] = mjMIN(mjMIN(sim->d->solver_iter, mjNSOLVER), mjMAXLINEPNT); + for (i=1; i<3; i++) { + sim->figcost.linepnt[i] = sim->figcost.linepnt[0]; + } + if (sim->m->opt.solver==mjSOL_PGS) { + sim->figcost.linepnt[1] = 0; + sim->figcost.linepnt[2] = 0; + } + + for (i=0; ifigcost.linepnt[0]; i++) { + // x + sim->figcost.linedata[0][2*i] = i; + sim->figcost.linedata[1][2*i] = i; + sim->figcost.linedata[2][2*i] = i; + + // y + sim->figcost.linedata[0][2*i+1] = mju_log10(mju_max(mjMINVAL, sim->d->solver[i].improvement)); + sim->figcost.linedata[1][2*i+1] = mju_log10(mju_max(mjMINVAL, sim->d->solver[i].gradient)); + sim->figcost.linedata[2][2*i+1] = mju_log10(mju_max(mjMINVAL, sim->d->solver[i].lineslope)); + } + + // get timers: total, collision, prepare, solve, other + mjtNum total = sim->d->timer[mjTIMER_STEP].duration; + int number = sim->d->timer[mjTIMER_STEP].number; + if (!number) { + total = sim->d->timer[mjTIMER_FORWARD].duration; + number = sim->d->timer[mjTIMER_FORWARD].number; + } + number = mjMAX(1, number); + float tdata[5] = { + static_cast(total/number), + static_cast(sim->d->timer[mjTIMER_POS_COLLISION].duration/number), + static_cast(sim->d->timer[mjTIMER_POS_MAKE].duration/number) + + static_cast(sim->d->timer[mjTIMER_POS_PROJECT].duration/number), + static_cast(sim->d->timer[mjTIMER_CONSTRAINT].duration/number), + 0 + }; + tdata[4] = tdata[0] - tdata[1] - tdata[2] - tdata[3]; + + // update figtimer + int pnt = mjMIN(201, sim->figtimer.linepnt[0]+1); + for (n=0; n<5; n++) { + // shift data + for (i=pnt-1; i>0; i--) { + sim->figtimer.linedata[n][2*i+1] = sim->figtimer.linedata[n][2*i-1]; + } + + // assign new + sim->figtimer.linepnt[n] = pnt; + sim->figtimer.linedata[n][1] = tdata[n]; + } + + // get sizes: nv, nbody, nefc, sqrt(nnz), ncont, iter + float sdata[6] = { + static_cast(sim->m->nv), + static_cast(sim->m->nbody), + static_cast(sim->d->nefc), + static_cast(mju_sqrt(sim->d->solver_nnz)), + static_cast(sim->d->ncon), + static_cast(sim->d->solver_iter) + }; + + // update figsize + pnt = mjMIN(201, sim->figsize.linepnt[0]+1); + for (n=0; n<6; n++) { + // shift data + for (i=pnt-1; i>0; i--) { + sim->figsize.linedata[n][2*i+1] = sim->figsize.linedata[n][2*i-1]; + } + + // assign new + sim->figsize.linepnt[n] = pnt; + sim->figsize.linedata[n][1] = sdata[n]; + } +} + +// show profiler figures +void profilershow(mj::Simulate* sim, mjrRect rect) { + mjrRect viewport = { + rect.left + rect.width - rect.width/4, + rect.bottom, + rect.width/4, + rect.height/4 + }; + mjr_figure(viewport, &sim->figtimer, &sim->con); + viewport.bottom += rect.height/4; + mjr_figure(viewport, &sim->figsize, &sim->con); + viewport.bottom += rect.height/4; + mjr_figure(viewport, &sim->figcost, &sim->con); + viewport.bottom += rect.height/4; + mjr_figure(viewport, &sim->figconstraint, &sim->con); +} + + +// init sensor figure +void sensorinit(mj::Simulate* sim) { + mjvFigure& figsensor = sim->figsensor; + + // set figure to default + mjv_defaultFigure(&figsensor); + figsensor.figurergba[3] = 0.5f; + + // set flags + figsensor.flg_extend = 1; + figsensor.flg_barplot = 1; + figsensor.flg_symmetric = 1; + + // title + mju::strcpy_arr(figsensor.title, "Sensor data"); + + // y-tick nubmer format + mju::strcpy_arr(figsensor.yformat, "%.0f"); + + // grid size + figsensor.gridsize[0] = 2; + figsensor.gridsize[1] = 3; + + // minimum range + figsensor.range[0][0] = 0; + figsensor.range[0][1] = 0; + figsensor.range[1][0] = -1; + figsensor.range[1][1] = 1; +} + +// update sensor figure +void sensorupdate(mj::Simulate* sim) { + mjModel* m = sim->m; + mjvFigure& figsensor = sim->figsensor; + static const int maxline = 10; + + // clear linepnt + for (int i=0; insensor; n++) { + // go to next line if type is different + if (n>0 && m->sensor_type[n]!=m->sensor_type[n-1]) { + lineid = mjMIN(lineid+1, maxline-1); + } + + // get info about this sensor + mjtNum cutoff = (m->sensor_cutoff[n]>0 ? m->sensor_cutoff[n] : 1); + int adr = m->sensor_adr[n]; + int dim = m->sensor_dim[n]; + + // data pointer in line + int p = figsensor.linepnt[lineid]; + + // fill in data for this sensor + for (int i=0; i=mjMAXLINEPNT/2) { + break; + } + + // x + figsensor.linedata[lineid][2*p+4*i] = adr+i; + figsensor.linedata[lineid][2*p+4*i+2] = adr+i; + + // y + figsensor.linedata[lineid][2*p+4*i+1] = 0; + figsensor.linedata[lineid][2*p+4*i+3] = sim->d->sensordata[adr+i]/cutoff; + } + + // update linepnt + figsensor.linepnt[lineid] = mjMIN(mjMAXLINEPNT-1, + figsensor.linepnt[lineid]+2*dim); + } +} + +// show sensor figure +void sensorshow(mj::Simulate* sim, mjrRect rect) { + // constant width with and without profiler + int width = sim->profiler ? rect.width/3 : rect.width/4; + + // render figure on the right + mjrRect viewport = { + rect.left + rect.width - width, + rect.bottom, + width, + rect.height/3 + }; + mjr_figure(viewport, &sim->figsensor, &sim->con); +} + +// prepare info text +void infotext(mj::Simulate* sim, + char (&title)[mj::Simulate::kMaxFilenameLength], + char (&content)[mj::Simulate::kMaxFilenameLength], + double interval) { + mjModel* m = sim->m; + mjData* d = sim->d; + char tmp[20]; + + // compute solver error + mjtNum solerr = 0; + if (d->solver_iter) { + int ind = mjMIN(d->solver_iter-1, mjNSOLVER-1); + solerr = mju_min(d->solver[ind].improvement, d->solver[ind].gradient); + if (solerr==0) { + solerr = mju_max(d->solver[ind].improvement, d->solver[ind].gradient); + } + } + solerr = mju_log10(mju_max(mjMINVAL, solerr)); + + // prepare info text + const std::string realtime_nominator = sim->slow_down == 1 ? "" : "1/"; + mju::strcpy_arr(title, "Time\nSize\nCPU\nSolver \nFPS\nstack\nconbuf\nefcbuf"); + mju::sprintf_arr(content, + "%-9.3f %s%d x\n%d (%d con)\n%.3f\n%.1f (%d it)\n%.0f\n%.3f\n%.3f\n%.3f", + d->time, realtime_nominator.c_str(), sim->slow_down, + d->nefc, d->ncon, + sim->run ? + d->timer[mjTIMER_STEP].duration / mjMAX(1, d->timer[mjTIMER_STEP].number) : + d->timer[mjTIMER_FORWARD].duration / mjMAX(1, d->timer[mjTIMER_FORWARD].number), + solerr, d->solver_iter, + 1/interval, + d->maxuse_stack/(double)d->nstack, + d->maxuse_con/(double)m->nconmax, + d->maxuse_efc/(double)m->njmax); + + // add Energy if enabled + { + if (mjENABLED(mjENBL_ENERGY)) { + mju::sprintf_arr(tmp, "\n%.3f", d->energy[0]+d->energy[1]); + mju::strcat_arr(content, tmp); + mju::strcat_arr(title, "\nEnergy"); + } + + // add FwdInv if enabled + if (mjENABLED(mjENBL_FWDINV)) { + mju::sprintf_arr(tmp, "\n%.1f %.1f", + mju_log10(mju_max(mjMINVAL, d->solver_fwdinv[0])), + mju_log10(mju_max(mjMINVAL, d->solver_fwdinv[1]))); + mju::strcat_arr(content, tmp); + mju::strcat_arr(title, "\nFwdInv"); + } + } +} + +// sprintf forwarding, to avoid compiler warning in x-macro +void printfield(char (&str)[mjMAXUINAME], void* ptr) { + mju::sprintf_arr(str, "%g", *static_cast(ptr)); +} + +// update watch +void watch(mj::Simulate* sim) { + // clear + sim->ui0.sect[SECT_WATCH].item[2].multi.nelem = 1; + mju::strcpy_arr(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0], "invalid field"); + + // prepare symbols needed by xmacro + MJDATA_POINTERS_PREAMBLE(sim->m); + + // find specified field in mjData arrays, update value + #define X(TYPE, NAME, NR, NC) \ + if (!mju::strcmp_arr(#NAME, sim->field) && \ + !mju::strcmp_arr(#TYPE, "mjtNum")) { \ + if (sim->index >= 0 && sim->index < sim->m->NR * NC) { \ + printfield(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0], sim->d->NAME + sim->index); \ + } else { \ + mju::strcpy_arr(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0], "invalid index"); \ + } \ + return; \ + } + + MJDATA_POINTERS +#undef X +} + + +//---------------------------------- UI construction ----------------------------------------------- + +// make physics section of UI +void makephysics(mj::Simulate* sim, int oldstate) { + int i; + mjOption& opt = sim->m->opt; + + mjuiDef defPhysics[] = { + {mjITEM_SECTION, "Physics", oldstate, nullptr, "AP"}, + {mjITEM_SELECT, "Integrator", 2, &(opt.integrator), "Euler\nRK4\nimplicit"}, + {mjITEM_SELECT, "Collision", 2, &(opt.collision), "All\nPair\nDynamic"}, + {mjITEM_SELECT, "Cone", 2, &(opt.cone), "Pyramidal\nElliptic"}, + {mjITEM_SELECT, "Jacobian", 2, &(opt.jacobian), "Dense\nSparse\nAuto"}, + {mjITEM_SELECT, "Solver", 2, &(opt.solver), "PGS\nCG\nNewton"}, + {mjITEM_SEPARATOR, "Algorithmic Parameters", 1}, + {mjITEM_EDITNUM, "Timestep", 2, &(opt.timestep), "1 0 1"}, + {mjITEM_EDITINT, "Iterations", 2, &(opt.iterations), "1 0 1000"}, + {mjITEM_EDITNUM, "Tolerance", 2, &(opt.tolerance), "1 0 1"}, + {mjITEM_EDITINT, "Noslip Iter", 2, &(opt.noslip_iterations), "1 0 1000"}, + {mjITEM_EDITNUM, "Noslip Tol", 2, &(opt.noslip_tolerance), "1 0 1"}, + {mjITEM_EDITINT, "MRR Iter", 2, &(opt.mpr_iterations), "1 0 1000"}, + {mjITEM_EDITNUM, "MPR Tol", 2, &(opt.mpr_tolerance), "1 0 1"}, + {mjITEM_EDITNUM, "API Rate", 2, &(opt.apirate), "1 0 1000"}, + {mjITEM_SEPARATOR, "Physical Parameters", 1}, + {mjITEM_EDITNUM, "Gravity", 2, opt.gravity, "3"}, + {mjITEM_EDITNUM, "Wind", 2, opt.wind, "3"}, + {mjITEM_EDITNUM, "Magnetic", 2, opt.magnetic, "3"}, + {mjITEM_EDITNUM, "Density", 2, &(opt.density), "1"}, + {mjITEM_EDITNUM, "Viscosity", 2, &(opt.viscosity), "1"}, + {mjITEM_EDITNUM, "Imp Ratio", 2, &(opt.impratio), "1"}, + {mjITEM_SEPARATOR, "Disable Flags", 1}, + {mjITEM_END} + }; + mjuiDef defEnableFlags[] = { + {mjITEM_SEPARATOR, "Enable Flags", 1}, + {mjITEM_END} + }; + mjuiDef defOverride[] = { + {mjITEM_SEPARATOR, "Contact Override", 1}, + {mjITEM_EDITNUM, "Margin", 2, &(opt.o_margin), "1"}, + {mjITEM_EDITNUM, "Sol Imp", 2, &(opt.o_solimp), "5"}, + {mjITEM_EDITNUM, "Sol Ref", 2, &(opt.o_solref), "2"}, + {mjITEM_END} + }; + + // add physics + mjui_add(&sim->ui0, defPhysics); + + // add flags programmatically + mjuiDef defFlag[] = { + {mjITEM_CHECKINT, "", 2, nullptr, ""}, + {mjITEM_END} + }; + for (i=0; idisable + i; + mjui_add(&sim->ui0, defFlag); + } + mjui_add(&sim->ui0, defEnableFlags); + for (i=0; ienable + i; + mjui_add(&sim->ui0, defFlag); + } + + // add contact override + mjui_add(&sim->ui0, defOverride); +} + + + +// make rendering section of UI +void makerendering(mj::Simulate* sim, int oldstate) { + int i, j; + + mjuiDef defRendering[] = { + { + mjITEM_SECTION, + "Rendering", + oldstate, + nullptr, + "AR" + }, + { + mjITEM_SELECT, + "Camera", + 2, + &(sim->camera), + "Free\nTracking" + }, + { + mjITEM_SELECT, + "Label", + 2, + &(sim->vopt.label), + "None\nBody\nJoint\nGeom\nSite\nCamera\nLight\nTendon\n" + "Actuator\nConstraint\nSkin\nSelection\nSel Pnt\nForce" + }, + { + mjITEM_SELECT, + "Frame", + 2, + &(sim->vopt.frame), + "None\nBody\nGeom\nSite\nCamera\nLight\nContact\nWorld" + }, + { + mjITEM_BUTTON, + "Copy camera", + 2, + nullptr, + "" + }, + { + mjITEM_SEPARATOR, + "Model Elements", + 1 + }, + { + mjITEM_END + } + }; + mjuiDef defOpenGL[] = { + {mjITEM_SEPARATOR, "OpenGL Effects", 1}, + {mjITEM_END} + }; + + // add model cameras, up to UI limit + for (i=0; im->ncam, mjMAXUIMULTI-2); i++) { + // prepare name + char camname[mjMAXUITEXT] = "\n"; + if (sim->m->names[sim->m->name_camadr[i]]) { + mju::strcat_arr(camname, sim->m->names+sim->m->name_camadr[i]); + } else { + mju::sprintf_arr(camname, "\nCamera %d", i); + } + + // check string length + if (mju::strlen_arr(camname) + mju::strlen_arr(defRendering[1].other)>=mjMAXUITEXT-1) { + break; + } + + // add camera + mju::strcat_arr(defRendering[1].other, camname); + } + + // add rendering standard + mjui_add(&sim->ui0, defRendering); + + // add flags programmatically + mjuiDef defFlag[] = { + {mjITEM_CHECKBYTE, "", 2, nullptr, ""}, + {mjITEM_END} + }; + for (i=0; ivopt.flags + i; + mjui_add(&sim->ui0, defFlag); + } + mjui_add(&sim->ui0, defOpenGL); + for (i=0; iscn.flags + i; + mjui_add(&sim->ui0, defFlag); + } +} + + + +// make group section of UI +void makegroup(mj::Simulate* sim, int oldstate) { + mjvOption& vopt = sim->vopt; + mjuiDef defGroup[] = { + {mjITEM_SECTION, "Group enable", oldstate, nullptr, "AG"}, + {mjITEM_SEPARATOR, "Geom groups", 1}, + {mjITEM_CHECKBYTE, "Geom 0", 2, vopt.geomgroup, " 0"}, + {mjITEM_CHECKBYTE, "Geom 1", 2, vopt.geomgroup+1, " 1"}, + {mjITEM_CHECKBYTE, "Geom 2", 2, vopt.geomgroup+2, " 2"}, + {mjITEM_CHECKBYTE, "Geom 3", 2, vopt.geomgroup+3, " 3"}, + {mjITEM_CHECKBYTE, "Geom 4", 2, vopt.geomgroup+4, " 4"}, + {mjITEM_CHECKBYTE, "Geom 5", 2, vopt.geomgroup+5, " 5"}, + {mjITEM_SEPARATOR, "Site groups", 1}, + {mjITEM_CHECKBYTE, "Site 0", 2, vopt.sitegroup, "S0"}, + {mjITEM_CHECKBYTE, "Site 1", 2, vopt.sitegroup+1, "S1"}, + {mjITEM_CHECKBYTE, "Site 2", 2, vopt.sitegroup+2, "S2"}, + {mjITEM_CHECKBYTE, "Site 3", 2, vopt.sitegroup+3, "S3"}, + {mjITEM_CHECKBYTE, "Site 4", 2, vopt.sitegroup+4, "S4"}, + {mjITEM_CHECKBYTE, "Site 5", 2, vopt.sitegroup+5, "S5"}, + {mjITEM_SEPARATOR, "Joint groups", 1}, + {mjITEM_CHECKBYTE, "Joint 0", 2, vopt.jointgroup, ""}, + {mjITEM_CHECKBYTE, "Joint 1", 2, vopt.jointgroup+1, ""}, + {mjITEM_CHECKBYTE, "Joint 2", 2, vopt.jointgroup+2, ""}, + {mjITEM_CHECKBYTE, "Joint 3", 2, vopt.jointgroup+3, ""}, + {mjITEM_CHECKBYTE, "Joint 4", 2, vopt.jointgroup+4, ""}, + {mjITEM_CHECKBYTE, "Joint 5", 2, vopt.jointgroup+5, ""}, + {mjITEM_SEPARATOR, "Tendon groups", 1}, + {mjITEM_CHECKBYTE, "Tendon 0", 2, vopt.tendongroup, ""}, + {mjITEM_CHECKBYTE, "Tendon 1", 2, vopt.tendongroup+1, ""}, + {mjITEM_CHECKBYTE, "Tendon 2", 2, vopt.tendongroup+2, ""}, + {mjITEM_CHECKBYTE, "Tendon 3", 2, vopt.tendongroup+3, ""}, + {mjITEM_CHECKBYTE, "Tendon 4", 2, vopt.tendongroup+4, ""}, + {mjITEM_CHECKBYTE, "Tendon 5", 2, vopt.tendongroup+5, ""}, + {mjITEM_SEPARATOR, "Actuator groups", 1}, + {mjITEM_CHECKBYTE, "Actuator 0", 2, vopt.actuatorgroup, ""}, + {mjITEM_CHECKBYTE, "Actuator 1", 2, vopt.actuatorgroup+1, ""}, + {mjITEM_CHECKBYTE, "Actuator 2", 2, vopt.actuatorgroup+2, ""}, + {mjITEM_CHECKBYTE, "Actuator 3", 2, vopt.actuatorgroup+3, ""}, + {mjITEM_CHECKBYTE, "Actuator 4", 2, vopt.actuatorgroup+4, ""}, + {mjITEM_CHECKBYTE, "Actuator 5", 2, vopt.actuatorgroup+5, ""}, + {mjITEM_SEPARATOR, "Skin groups", 1}, + {mjITEM_CHECKBYTE, "Skin 0", 2, vopt.skingroup, ""}, + {mjITEM_CHECKBYTE, "Skin 1", 2, vopt.skingroup+1, ""}, + {mjITEM_CHECKBYTE, "Skin 2", 2, vopt.skingroup+2, ""}, + {mjITEM_CHECKBYTE, "Skin 3", 2, vopt.skingroup+3, ""}, + {mjITEM_CHECKBYTE, "Skin 4", 2, vopt.skingroup+4, ""}, + {mjITEM_CHECKBYTE, "Skin 5", 2, vopt.skingroup+5, ""}, + {mjITEM_END} + }; + + // add section + mjui_add(&sim->ui0, defGroup); +} + +// make joint section of UI +void makejoint(mj::Simulate* sim, int oldstate) { + int i; + + mjuiDef defJoint[] = { + {mjITEM_SECTION, "Joint", oldstate, nullptr, "AJ"}, + {mjITEM_END} + }; + mjuiDef defSlider[] = { + {mjITEM_SLIDERNUM, "", 2, nullptr, "0 1"}, + {mjITEM_END} + }; + + // add section + mjui_add(&sim->ui1, defJoint); + defSlider[0].state = 4; + + // add scalar joints, exit if UI limit reached + int itemcnt = 0; + for (i=0; im->njnt && itemcntm->jnt_type[i]==mjJNT_HINGE || sim->m->jnt_type[i]==mjJNT_SLIDE)) { + // skip if joint group is disabled + if (!sim->vopt.jointgroup[mjMAX(0, mjMIN(mjNGROUP-1, sim->m->jnt_group[i]))]) { + continue; + } + + // set data and name + defSlider[0].pdata = sim->d->qpos + sim->m->jnt_qposadr[i]; + if (sim->m->names[sim->m->name_jntadr[i]]) { + mju::strcpy_arr(defSlider[0].name, sim->m->names+sim->m->name_jntadr[i]); + } else { + mju::sprintf_arr(defSlider[0].name, "joint %d", i); + } + + // set range + if (sim->m->jnt_limited[i]) + mju::sprintf_arr(defSlider[0].other, "%.4g %.4g", + sim->m->jnt_range[2*i], sim->m->jnt_range[2*i+1]); + else if (sim->m->jnt_type[i]==mjJNT_SLIDE) { + mju::strcpy_arr(defSlider[0].other, "-1 1"); + } else { + mju::strcpy_arr(defSlider[0].other, "-3.1416 3.1416"); + } + + // add and count + mjui_add(&sim->ui1, defSlider); + itemcnt++; + } +} + +// make control section of UI +void makecontrol(mj::Simulate* sim, int oldstate) { + int i; + + mjuiDef defControl[] = { + {mjITEM_SECTION, "Control", oldstate, nullptr, "AC"}, + {mjITEM_BUTTON, "Clear all", 2}, + {mjITEM_END} + }; + mjuiDef defSlider[] = { + {mjITEM_SLIDERNUM, "", 2, nullptr, "0 1"}, + {mjITEM_END} + }; + + // add section + mjui_add(&sim->ui1, defControl); + defSlider[0].state = 2; + + // add controls, exit if UI limit reached (Clear button already added) + int itemcnt = 1; + for (i=0; im->nu && itemcntvopt.actuatorgroup[mjMAX(0, mjMIN(mjNGROUP-1, sim->m->actuator_group[i]))]) { + continue; + } + + // set data and name + defSlider[0].pdata = sim->d->ctrl + i; + if (sim->m->names[sim->m->name_actuatoradr[i]]) { + mju::strcpy_arr(defSlider[0].name, sim->m->names+sim->m->name_actuatoradr[i]); + } else { + mju::sprintf_arr(defSlider[0].name, "control %d", i); + } + + // set range + if (sim->m->actuator_ctrllimited[i]) + mju::sprintf_arr(defSlider[0].other, "%.4g %.4g", + sim->m->actuator_ctrlrange[2*i], sim->m->actuator_ctrlrange[2*i+1]); + else { + mju::strcpy_arr(defSlider[0].other, "-1 1"); + } + + // add and count + mjui_add(&sim->ui1, defSlider); + itemcnt++; + } +} + +// make model-dependent UI sections +void makesections(mj::Simulate* sim) { + int i; + + // get section open-close state, UI 0 + int oldstate0[NSECT0]; + for (i=0; iui0.nsect>i) { + oldstate0[i] = sim->ui0.sect[i].state; + } + } + + // get section open-close state, UI 1 + int oldstate1[NSECT1]; + for (i=0; iui1.nsect>i) { + oldstate1[i] = sim->ui1.sect[i].state; + } + } + + // clear model-dependent sections of UI + sim->ui0.nsect = SECT_PHYSICS; + sim->ui1.nsect = 0; + + // make + makephysics(sim, oldstate0[SECT_PHYSICS]); + makerendering(sim, oldstate0[SECT_RENDERING]); + makegroup(sim, oldstate0[SECT_GROUP]); + makejoint(sim, oldstate1[SECT_JOINT]); + makecontrol(sim, oldstate1[SECT_CONTROL]); +} + +//---------------------------------- utility functions --------------------------------------------- + +// align and scale view +void alignscale(mj::Simulate* sim) { + // autoscale + sim->cam.lookat[0] = sim->m->stat.center[0]; + sim->cam.lookat[1] = sim->m->stat.center[1]; + sim->cam.lookat[2] = sim->m->stat.center[2]; + sim->cam.distance = 1.5 * sim->m->stat.extent; + + // set to free camera + sim->cam.type = mjCAMERA_FREE; +} + +// copy qpos to clipboard as key +void copykey(mj::Simulate* sim) { + char clipboard[5000] = ""); + + // copy to clipboard + glfwSetClipboardString(sim->window, clipboard); +} + +// millisecond timer, for MuJoCo built-in profiler +mjtNum timer() { + return 1000*glfwGetTime(); +} + +// clear all times +void cleartimers(mjData* d) { + for (int i=0; itimer[i].duration = 0; + d->timer[i].number = 0; + } +} + +// copy current camera to clipboard as MJCF specification +void copycamera(mj::Simulate* sim) { + mjvGLCamera* camera = sim->scn.camera; + + char clipboard[500]; + mjtNum cam_right[3]; + mjtNum cam_forward[3]; + mjtNum cam_up[3]; + + // get camera spec from the GLCamera + mju_f2n(cam_forward, camera[0].forward, 3); + mju_f2n(cam_up, camera[0].up, 3); + mju_cross(cam_right, cam_forward, cam_up); + + // make MJCF camera spec + mju::sprintf_arr(clipboard, + "\n", + (camera[0].pos[0] + camera[1].pos[0]) / 2, + (camera[0].pos[1] + camera[1].pos[1]) / 2, + (camera[0].pos[2] + camera[1].pos[2]) / 2, + cam_right[0], cam_right[1], cam_right[2], + camera[0].up[0], camera[0].up[1], camera[0].up[2]); + + // copy spec into clipboard + glfwSetClipboardString(sim->window, clipboard); +} + +// update UI 0 when MuJoCo structures change (except for joint sliders) +void updatesettings(mj::Simulate* sim) { + int i; + + // physics flags + for (i=0; idisable[i] = ((sim->m->opt.disableflags & (1<enable[i] = ((sim->m->opt.enableflags & (1<cam.type==mjCAMERA_FIXED) { + sim->camera = 2 + sim->cam.fixedcamid; + } else if (sim->cam.type==mjCAMERA_TRACKING) { + sim->camera = 1; + } else { + sim->camera = 0; + } + + // update UI + mjui_update(-1, -1, &sim->ui0, &sim->uistate, &sim->con); +} + + +//---------------------------------- UI hooks (for uitools.c) -------------------------------------- + +// determine enable/disable item state given category +int uiPredicate(int category, void* userdata) { + mj::Simulate* sim = static_cast(userdata); + + switch (category) { + case 2: // require model + return (sim->m != nullptr); + + case 3: // require model and nkey + return (sim->m && sim->m->nkey); + + case 4: // require model and paused + return (sim->m && !sim->run); + + default: + return 1; + } +} + +// set window layout +void uiLayout(mjuiState* state) { + mj::Simulate* sim = static_cast(state->userdata); + mjrRect* rect = state->rect; + + // set number of rectangles + state->nrect = 4; + + // rect 0: entire framebuffer + rect[0].left = 0; + rect[0].bottom = 0; + glfwGetFramebufferSize(sim->window, &rect[0].width, &rect[0].height); + + // rect 1: UI 0 + rect[1].left = 0; + rect[1].width = sim->ui0_enable ? sim->ui0.width : 0; + rect[1].bottom = 0; + rect[1].height = rect[0].height; + + // rect 2: UI 1 + rect[2].width = sim->ui1_enable ? sim->ui1.width : 0; + rect[2].left = mjMAX(0, rect[0].width - rect[2].width); + rect[2].bottom = 0; + rect[2].height = rect[0].height; + + // rect 3: 3D plot (everything else is an overlay) + rect[3].left = rect[1].width; + rect[3].width = mjMAX(0, rect[0].width - rect[1].width - rect[2].width); + rect[3].bottom = 0; + rect[3].height = rect[0].height; +} + +// handle UI event +void uiEvent(mjuiState* state) { + mj::Simulate* sim = static_cast(state->userdata); + mjModel* m = sim->m; + mjData* d = sim->d; + int i; + char err[200]; + + // call UI 0 if event is directed to it + if ((state->dragrect==sim->ui0.rectid) || + (state->dragrect==0 && state->mouserect==sim->ui0.rectid) || + state->type==mjEVENT_KEY) { + // process UI event + mjuiItem* it = mjui_event(&sim->ui0, state, &sim->con); + + // file section + if (it && it->sectionid==SECT_FILE) { + switch (it->itemid) { + case 0: // Save xml + { + const std::string path = getSavePath("mjmodel.xml"); + if (!path.empty() && !mj_saveLastXML(path.c_str(), m, err, 200)) { + std::printf("Save XML error: %s", err); + } + } + break; + + case 1: // Save mjb + { + const std::string path = getSavePath("mjmodel.mjb"); + if (!path.empty()) { + mj_saveModel(m, path.c_str(), NULL, 0); + } + } + break; + + case 2: // Print model + mj_printModel(m, "MJMODEL.TXT"); + break; + + case 3: // Print data + mj_printData(m, d, "MJDATA.TXT"); + break; + + case 4: // Quit + sim->exitrequest.store(1); + break; + } + } + + // option section + else if (it && it->sectionid==SECT_OPTION) { + switch (it->itemid) { + case 0: // Spacing + sim->ui0.spacing = mjui_themeSpacing(sim->spacing); + sim->ui1.spacing = mjui_themeSpacing(sim->spacing); + break; + + case 1: // Color + sim->ui0.color = mjui_themeColor(sim->color); + sim->ui1.color = mjui_themeColor(sim->color); + break; + + case 2: // Font + mjr_changeFont(50*(sim->font+1), &sim->con); + break; + + case 9: // Full screen + if (glfwGetWindowMonitor(sim->window)) { + // restore window from saved data + glfwSetWindowMonitor(sim->window, nullptr, sim->windowpos[0], sim->windowpos[1], + sim->windowsize[0], sim->windowsize[1], 0); + } + + // currently windowed: switch to full screen + else { + // save window data + glfwGetWindowPos(sim->window, sim->windowpos, sim->windowpos+1); + glfwGetWindowSize(sim->window, sim->windowsize, sim->windowsize+1); + + // switch + glfwSetWindowMonitor(sim->window, glfwGetPrimaryMonitor(), 0, 0, + sim->vmode.width, sim->vmode.height, sim->vmode.refreshRate); + } + + // reinstante vsync, just in case + glfwSwapInterval(sim->vsync); + break; + + case 10: // Vertical sync + glfwSwapInterval(sim->vsync); + break; + } + + // modify UI + uiModify(sim->window, &sim->ui0, state, &sim->con); + uiModify(sim->window, &sim->ui1, state, &sim->con); + } + + // simulation section + else if (it && it->sectionid==SECT_SIMULATION) { + switch (it->itemid) { + case 1: // Reset + if (m) { + mj_resetData(m, d); + mj_forward(m, d); + profilerupdate(sim); + sensorupdate(sim); + updatesettings(sim); + } + break; + + case 2: // Reload + sim->uiloadrequest.fetch_add(1); + break; + + case 3: // Align + alignscale(sim); + updatesettings(sim); + break; + + case 4: // Copy pose + copykey(sim); + break; + + case 5: // Adjust key + case 6: // Load key + i = sim->key; + d->time = m->key_time[i]; + mju_copy(d->qpos, m->key_qpos+i*m->nq, m->nq); + mju_copy(d->qvel, m->key_qvel+i*m->nv, m->nv); + mju_copy(d->act, m->key_act+i*m->na, m->na); + mju_copy(d->mocap_pos, m->key_mpos+i*3*m->nmocap, 3*m->nmocap); + mju_copy(d->mocap_quat, m->key_mquat+i*4*m->nmocap, 4*m->nmocap); + mju_copy(d->ctrl, m->key_ctrl+i*m->nu, m->nu); + mj_forward(m, d); + profilerupdate(sim); + sensorupdate(sim); + updatesettings(sim); + break; + + case 7: // Save key + i = sim->key; + m->key_time[i] = d->time; + mju_copy(m->key_qpos+i*m->nq, d->qpos, m->nq); + mju_copy(m->key_qvel+i*m->nv, d->qvel, m->nv); + mju_copy(m->key_act+i*m->na, d->act, m->na); + mju_copy(m->key_mpos+i*3*m->nmocap, d->mocap_pos, 3*m->nmocap); + mju_copy(m->key_mquat+i*4*m->nmocap, d->mocap_quat, 4*m->nmocap); + mju_copy(m->key_ctrl+i*m->nu, d->ctrl, m->nu); + break; + } + } + + // physics section + else if (it && it->sectionid==SECT_PHYSICS) { + // update disable flags in mjOption + m->opt.disableflags = 0; + for (i=0; idisable[i]) { + m->opt.disableflags |= (1<opt.enableflags = 0; + for (i=0; ienable[i]) { + m->opt.enableflags |= (1<sectionid==SECT_RENDERING) { + // set camera in mjvCamera + if (sim->camera==0) { + sim->cam.type = mjCAMERA_FREE; + } else if (sim->camera==1) { + if (sim->pert.select>0) { + sim->cam.type = mjCAMERA_TRACKING; + sim->cam.trackbodyid = sim->pert.select; + sim->cam.fixedcamid = -1; + } else { + sim->cam.type = mjCAMERA_FREE; + sim->camera = 0; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + } else { + sim->cam.type = mjCAMERA_FIXED; + sim->cam.fixedcamid = sim->camera - 2; + } + // copy camera spec to clipboard (as MJCF element) + if (it->itemid == 3) { + copycamera(sim); + } + } + + // group section + else if (it && it->sectionid==SECT_GROUP) { + // remake joint section if joint group changed + if (it->name[0]=='J' && it->name[1]=='o') { + sim->ui1.nsect = SECT_JOINT; + makejoint(sim, sim->ui1.sect[SECT_JOINT].state); + sim->ui1.nsect = NSECT1; + uiModify(sim->window, &sim->ui1, state, &sim->con); + } + + // remake control section if actuator group changed + if (it->name[0]=='A' && it->name[1]=='c') { + sim->ui1.nsect = SECT_CONTROL; + makecontrol(sim, sim->ui1.sect[SECT_CONTROL].state); + sim->ui1.nsect = NSECT1; + uiModify(sim->window, &sim->ui1, state, &sim->con); + } + } + + // stop if UI processed event + if (it!=nullptr || (state->type==mjEVENT_KEY && state->key==0)) { + return; + } + } + + // call UI 1 if event is directed to it + if ((state->dragrect==sim->ui1.rectid) || + (state->dragrect==0 && state->mouserect==sim->ui1.rectid) || + state->type==mjEVENT_KEY) { + // process UI event + mjuiItem* it = mjui_event(&sim->ui1, state, &sim->con); + + // control section + if (it && it->sectionid==SECT_CONTROL) { + // clear controls + if (it->itemid==0) { + mju_zero(d->ctrl, m->nu); + mjui_update(SECT_CONTROL, -1, &sim->ui1, &sim->uistate, &sim->con); + } + } + + // stop if UI processed event + if (it!=nullptr || (state->type==mjEVENT_KEY && state->key==0)) { + return; + } + } + + // shortcut not handled by UI + if (state->type==mjEVENT_KEY && state->key!=0) { + switch (state->key) { + case ' ': // Mode + if (m) { + sim->run = 1 - sim->run; + sim->pert.active = 0; + mjui_update(-1, -1, &sim->ui0, state, &sim->con); + } + break; + + case mjKEY_RIGHT: // step forward + if (m && !sim->run) { + cleartimers(d); + mj_step(m, d); + profilerupdate(sim); + sensorupdate(sim); + updatesettings(sim); + } + break; + + case mjKEY_PAGE_UP: // select parent body + if (m && sim->pert.select>0) { + sim->pert.select = m->body_parentid[sim->pert.select]; + sim->pert.skinselect = -1; + + // stop perturbation if world reached + if (sim->pert.select<=0) { + sim->pert.active = 0; + } + } + + break; + + case ']': // cycle up fixed cameras + if (m && m->ncam) { + sim->cam.type = mjCAMERA_FIXED; + // simulate->camera = {0 or 1} are reserved for the free and tracking cameras + if (sim->camera < 2 || sim->camera == 2 + m->ncam-1) { + sim->camera = 2; + } else { + sim->camera += 1; + } + sim->cam.fixedcamid = sim->camera - 2; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + break; + + case '[': // cycle down fixed cameras + if (m && m->ncam) { + sim->cam.type = mjCAMERA_FIXED; + // settings.camera = {0 or 1} are reserved for the free and tracking cameras + if (sim->camera <= 2) { + sim->camera = 2 + m->ncam-1; + } else { + sim->camera -= 1; + } + sim->cam.fixedcamid = sim->camera - 2; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + break; + + case mjKEY_F6: // cycle frame visualisation + if (m) { + sim->vopt.frame = (sim->vopt.frame + 1) % mjNFRAME; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + break; + + case mjKEY_F7: // cycle label visualisation + if (m) { + sim->vopt.label = (sim->vopt.label + 1) % mjNLABEL; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + break; + + case mjKEY_ESCAPE: // free camera + sim->cam.type = mjCAMERA_FREE; + sim->camera = 0; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + break; + + case '-': // slow down + if (sim->slow_down < max_slow_down && !state->shift) { + sim->slow_down *= 2; + sim->speed_changed = true; + } + break; + + case '=': // speed up + if (sim->slow_down > 1 && !state->shift) { + sim->slow_down /= 2; + sim->speed_changed = true; + } + break; + } + + return; + } + + // 3D scroll + if (state->type==mjEVENT_SCROLL && state->mouserect==3 && m) { + // emulate vertical mouse motion = 2% of window height + mjv_moveCamera(m, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam); + + return; + } + + // 3D press + if (state->type==mjEVENT_PRESS && state->mouserect==3 && m) { + // set perturbation + int newperturb = 0; + if (state->control && sim->pert.select>0) { + // right: translate; left: rotate + if (state->right) { + newperturb = mjPERT_TRANSLATE; + } else if (state->left) { + newperturb = mjPERT_ROTATE; + } + + // perturbation onset: reset reference + if (newperturb && !sim->pert.active) { + mjv_initPerturb(m, d, &sim->scn, &sim->pert); + } + } + sim->pert.active = newperturb; + + // handle double-click + if (state->doubleclick) { + // determine selection mode + int selmode; + if (state->button==mjBUTTON_LEFT) { + selmode = 1; + } else if (state->control) { + selmode = 3; + } else { + selmode = 2; + } + + // find geom and 3D click point, get corresponding body + mjrRect r = state->rect[3]; + mjtNum selpnt[3]; + int selgeom, selskin; + int selbody = mjv_select(m, d, &sim->vopt, + static_cast(r.width)/r.height, + (state->x - r.left)/r.width, + (state->y - r.bottom)/r.height, + &sim->scn, selpnt, &selgeom, &selskin); + + // set lookat point, start tracking is requested + if (selmode==2 || selmode==3) { + // copy selpnt if anything clicked + if (selbody>=0) { + mju_copy3(sim->cam.lookat, selpnt); + } + + // switch to tracking camera if dynamic body clicked + if (selmode==3 && selbody>0) { + // mujoco camera + sim->cam.type = mjCAMERA_TRACKING; + sim->cam.trackbodyid = selbody; + sim->cam.fixedcamid = -1; + + // UI camera + sim->camera = 1; + mjui_update(SECT_RENDERING, -1, &sim->ui0, &sim->uistate, &sim->con); + } + } + + // set body selection + else { + if (selbody>=0) { + // record selection + sim->pert.select = selbody; + sim->pert.skinselect = selskin; + + // compute localpos + mjtNum tmp[3]; + mju_sub3(tmp, selpnt, d->xpos+3*sim->pert.select); + mju_mulMatTVec(sim->pert.localpos, d->xmat+9*sim->pert.select, tmp, 3, 3); + } else { + sim->pert.select = 0; + sim->pert.skinselect = -1; + } + } + + // stop perturbation on select + sim->pert.active = 0; + } + + return; + } + + // 3D release + if (state->type==mjEVENT_RELEASE && state->dragrect==3 && m) { + // stop perturbation + sim->pert.active = 0; + + return; + } + + // 3D move + if (state->type==mjEVENT_MOVE && state->dragrect==3 && m) { + // determine action based on mouse button + mjtMouse action; + if (state->right) { + action = state->shift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V; + } else if (state->left) { + action = state->shift ? mjMOUSE_ROTATE_H : mjMOUSE_ROTATE_V; + } else { + action = mjMOUSE_ZOOM; + } + + // move perturb or camera + mjrRect r = state->rect[3]; + if (sim->pert.active) + mjv_movePerturb(m, d, action, state->dx/r.height, -state->dy/r.height, + &sim->scn, &sim->pert); + else + mjv_moveCamera(m, action, state->dx/r.height, -state->dy/r.height, + &sim->scn, &sim->cam); + + return; + } +} + +void uiRender(mjuiState* state) { + mj::Simulate* sim = static_cast(state->userdata); + sim->render(); +} + +// drop file callback +void drop(mj::Simulate* sim, int count, const char** paths) { + // make sure list is non-empty + if (count>0) { + while (sim->droploadrequest.load()) {} + mju::strcpy_arr(sim->dropfilename, paths[0]); + sim->droploadrequest.store(true); + } +} + +void uiDrop(mjuiState* state, int count, const char** paths) { + mj::Simulate* simulate = static_cast(state->userdata); + drop(simulate, count, paths); +} + +} // namespace + +namespace mujoco { +namespace mju = ::mujoco::sample_util; + +//------------------------------------ apply pose perturbations ------------------------------------ +void Simulate::applyposepertubations(int flg_paused) { + if (this->m != nullptr) { + mjv_applyPerturbPose(this->m, this->d, &this->pert, flg_paused); // move mocap bodies only + } +} + +//----------------------------------- apply force perturbations ------------------------------------ +void Simulate::applyforceperturbations() { + if (this->m != nullptr) { + mjv_applyPerturbForce(this->m, this->d, &this->pert); + } +} + +//------------------------- Tell the render thread to load a file and wait ------------------------- +void Simulate::load(const char* file, + mjModel* mnew, + mjData* dnew, + bool delete_old_m_d) { + this->mnew = mnew; + this->dnew = dnew; + this->delete_old_m_d = delete_old_m_d; + mju::strcpy_arr(this->filename, file); + + { + std::unique_lock lock(mtx); + this->loadrequest = 2; + + // Wait for the render thread to be done loading + // so that we know the old model and data's memory can + // be free'd by the other thread (sometimes python) + cond_loadrequest.wait(lock, [this]() { return this->loadrequest > 0; }); + } +} + +//------------------------------------- load mjb or xml model -------------------------------------- +void Simulate::loadmodel() { + if (this->delete_old_m_d) { + // delete old model if requested + if (this->d) { + mj_deleteData(d); + } + if (this->m) { + mj_deleteModel(m); + } + } + + this->m = this->mnew; + this->d = this->dnew; + + // re-create scene and context + mjv_makeScene(this->m, &this->scn, maxgeom); + mjr_makeContext(this->m, &this->con, 50*(this->font+1)); + + // clear perturbation state + this->pert.active = 0; + this->pert.select = 0; + this->pert.skinselect = -1; + + // align and scale view unless reloading the same file + if (mju::strcmp_arr(this->filename, this->previous_filename)) { + alignscale(this); + mju::strcpy_arr(this->previous_filename, this->filename); + } + + // update scene + mjv_updateScene(this->m, this->d, &this->vopt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); + + // set window title to model name + if (this->window && this->m->names) { + char title[200] = "this : "; + mju::strcat_arr(title, this->m->names); + glfwSetWindowTitle(this->window, title); + } + + // set keyframe range and divisions + this->ui0.sect[SECT_SIMULATION].item[5].slider.range[0] = 0; + this->ui0.sect[SECT_SIMULATION].item[5].slider.range[1] = mjMAX(0, this->m->nkey - 1); + this->ui0.sect[SECT_SIMULATION].item[5].slider.divisions = mjMAX(1, this->m->nkey - 1); + + // rebuild UI sections + makesections(this); + + // full ui update + uiModify(this->window, &this->ui0, &this->uistate, &this->con); + uiModify(this->window, &this->ui1, &this->uistate, &this->con); + updatesettings(this); + + // clear request + this->loadrequest = 0; + cond_loadrequest.notify_all(); +} + + +//------------------------------------------- rendering -------------------------------------------- + + +// prepare to render +void Simulate::prepare() { + // data for FPS calculation + static double lastupdatetm = 0; + + // update interval, save update time + double tmnow = glfwGetTime(); + double interval = tmnow - lastupdatetm; + interval = mjMIN(1, mjMAX(0.0001, interval)); + lastupdatetm = tmnow; + + // no model: nothing to do + if (!this->m) { + return; + } + + // update scene + mjv_updateScene(this->m, this->d, &this->vopt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); + + // update watch + if (this->ui0_enable && this->ui0.sect[SECT_WATCH].state) { + watch(this); + mjui_update(SECT_WATCH, -1, &this->ui0, &this->uistate, &this->con); + } + + // update joint + if (this->ui1_enable && this->ui1.sect[SECT_JOINT].state) { + mjui_update(SECT_JOINT, -1, &this->ui1, &this->uistate, &this->con); + } + + // update info text + if (this->info) { + infotext(this, this->info_title, this->info_content, interval); + } + + // update control + if (this->ui1_enable && this->ui1.sect[SECT_CONTROL].state) { + mjui_update(SECT_CONTROL, -1, &this->ui1, &this->uistate, &this->con); + } + + // update profiler + if (this->profiler && this->run) { + profilerupdate(this); + } + + // update sensor + if (this->sensor && this->run) { + sensorupdate(this); + } + + // clear timers once profiler info has been copied + cleartimers(this->d); +} + +// render the ui to the window +void Simulate::render() { + // get 3D rectangle and reduced for profiler + mjrRect rect = this->uistate.rect[3]; + mjrRect smallrect = rect; + if (this->profiler) { + smallrect.width = rect.width - rect.width/4; + } + + // no model + if (!this->m) { + // blank screen + mjr_rectangle(rect, 0.2f, 0.3f, 0.4f, 1); + + // label + if (this->loadrequest) { + mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, "loading", nullptr, &this->con); + } else { + char intro_message[Simulate::kMaxFilenameLength]; + mju::sprintf_arr(intro_message, + "MuJoCo version %s\nDrag-and-drop model file here", mj_versionString()); + mjr_overlay(mjFONT_NORMAL, mjGRID_TOPLEFT, rect, intro_message, 0, &this->con); + } + + // show last loading error + if (this->loadError[0]) { + mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, this->loadError, 0, &this->con); + } + + // render uis + if (this->ui0_enable) { + mjui_render(&this->ui0, &this->uistate, &this->con); + } + if (this->ui1_enable) { + mjui_render(&this->ui1, &this->uistate, &this->con); + } + + // finalize + glfwSwapBuffers(this->window); + + return; + } + + // render scene + mjr_render(rect, &this->scn, &this->con); + + // show last loading error + if (this->loadError[0]) { + mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, this->loadError, 0, &this->con); + } + + // show pause/loading label + if (!this->run || this->loadrequest) { + mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, + this->loadrequest ? "loading" : "pause", nullptr, &this->con); + } + + // show realtime label + if (this->run && this->slow_down != 1) { + std::string realtime_label = "1/" + std::to_string(this->slow_down) + " x"; + mjr_overlay(mjFONT_BIG, mjGRID_TOPRIGHT, smallrect, realtime_label.c_str(), nullptr, &this->con); + } + + // show ui 0 + if (this->ui0_enable) { + mjui_render(&this->ui0, &this->uistate, &this->con); + } + + // show ui 1 + if (this->ui1_enable) { + mjui_render(&this->ui1, &this->uistate, &this->con); + } + + // show help + if (this->help) { + mjr_overlay(mjFONT_NORMAL, mjGRID_TOPLEFT, rect, help_title, help_content, &this->con); + } + + // show info + if (this->info) { + mjr_overlay(mjFONT_NORMAL, mjGRID_BOTTOMLEFT, rect, this->info_title, this->info_content, &this->con); + } + + // show profiler + if (this->profiler) { + profilershow(this, rect); + } + + // show sensor + if (this->sensor) { + sensorshow(this, smallrect); + } + + // finalize + glfwSwapBuffers(this->window); +} + + +// clear callbacks registered in external structures +void Simulate::clearcallback() { + uiClearCallback(this->window); +} + +void Simulate::renderloop() { + // Set timer callback (milliseconds) + mjcb_time = timer; + + // multisampling + glfwWindowHint(GLFW_SAMPLES, 4); + glfwWindowHint(GLFW_VISIBLE, 1); + + // get videomode and save + this->vmode = *glfwGetVideoMode(glfwGetPrimaryMonitor()); + + // create window + this->window = glfwCreateWindow((2*this->vmode.width)/3, (2*this->vmode.height)/3, + "Simulate", nullptr, nullptr); + if (!this->window) { + glfwTerminate(); + mju_error("could not create window"); + } + + // save window position and size + glfwGetWindowPos(this->window, this->windowpos, this->windowpos+1); + glfwGetWindowSize(this->window, this->windowsize, this->windowsize+1); + + // make context current, set v-sync + glfwMakeContextCurrent(this->window); + glfwSwapInterval(this->vsync); + + // init abstract visualization + mjv_defaultCamera(&this->cam); + mjv_defaultOption(&this->vopt); + profilerinit(this); + sensorinit(this); + + // make empty scene + mjv_defaultScene(&this->scn); + mjv_makeScene(nullptr, &this->scn, maxgeom); + + // select default font + int fontscale = uiFontScale(this->window); + this->font = fontscale/50 - 1; + + // make empty context + mjr_defaultContext(&this->con); + mjr_makeContext(nullptr, &this->con, fontscale); + + // init state and uis + std::memset(&this->uistate, 0, sizeof(mjuiState)); + std::memset(&this->ui0, 0, sizeof(mjUI)); + std::memset(&this->ui1, 0, sizeof(mjUI)); + this->ui0.spacing = mjui_themeSpacing(this->spacing); + this->ui0.color = mjui_themeColor(this->color); + this->ui0.predicate = uiPredicate; + this->ui0.rectid = 1; + this->ui0.auxid = 0; + this->ui1.spacing = mjui_themeSpacing(this->spacing); + this->ui1.color = mjui_themeColor(this->color); + this->ui1.predicate = uiPredicate; + this->ui1.rectid = 2; + this->ui1.auxid = 1; + + // set GLFW callbacks + this->uistate.userdata = this; + uiSetCallback(this->window, &this->uistate, uiEvent, uiLayout, uiRender, uiDrop); + + // populate uis with standard sections + this->ui0.userdata = this; + this->ui1.userdata = this; + mjui_add(&this->ui0, defFile); + mjui_add(&this->ui0, this->defOption); + mjui_add(&this->ui0, this->defSimulation); + mjui_add(&this->ui0, this->defWatch); + uiModify(this->window, &this->ui0, &this->uistate, &this->con); + uiModify(this->window, &this->ui1, &this->uistate, &this->con); + + // run event loop + while (!glfwWindowShouldClose(this->window) && !this->exitrequest.load()) { + { + const std::lock_guard lock(this->mtx); + + // load model (not on first pass, to show "loading" label) + if (this->loadrequest==1) { + this->loadmodel(); + } else if (this->loadrequest>1) { + this->loadrequest = 1; + } + + // handle events (calls all callbacks) + glfwPollEvents(); + + // prepare to render + this->prepare(); + } // std::lock_guard (unblocks simulation thread) + + // render while simulation is running + this->render(); + } + + this->exitrequest.store(true); + + this->clearcallback(); + mjv_freeScene(&this->scn); + mjr_freeContext(&this->con); +} + +} // namespace mujoco diff --git a/simulate/simulate.h b/simulate/simulate.h new file mode 100644 index 00000000..8abffc68 --- /dev/null +++ b/simulate/simulate.h @@ -0,0 +1,214 @@ +// Copyright 2021 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_SIMULATE_SIMULATE_H_ +#define MUJOCO_SIMULATE_SIMULATE_H_ + +#include +#include +#include +#include + +#include +#include + +#ifdef MJSIMULATE_STATIC + // static library + #define MJSIMULATEAPI + #define MJSIMULATELOCAL +#else + #ifdef MJSIMULATE_DLL_EXPORTS + #define MJSIMULATEAPI MUJOCO_HELPER_DLL_EXPORT + #else + #define MJSIMULATEAPI MUJOCO_HELPER_DLL_IMPORT + #endif + #define MJSIMULATELOCAL MUJOCO_HELPER_DLL_LOCAL +#endif + +namespace mujoco { + +//-------------------------------- global ----------------------------------------------- + +// Simulate states not contained in MuJoCo structures +class MJSIMULATEAPI Simulate { + public: + // create object and initialize the simulate ui + Simulate() = default; + + // Apply UI pose perturbations to model and data + void applyposepertubations(int flg_paused); + + // Apply UI force perturbations to model and data + void applyforceperturbations(); + + // Request that the Simulate UI thread render a new model + // optionally delete the old model and data when done + void load(const char* file, mjModel* m, mjData* d, bool delete_old_m_d); + + // functions below are used by the renderthread + // load mjb or xml model that has been requested by load() + void loadmodel(); + + // prepare to render + void prepare(); + + // render the ui to the window + void render(); + + // clear callbacks registered in external structures + void clearcallback(); + + // loop to render the UI (must be called from main thread because of MacOS) + // https://discourse.glfw.org/t/multithreading-glfw/573/5 + void renderloop(); + + // constants + static constexpr int kMaxFilenameLength = 1000; + + // model and data to be visualized + mjModel* mnew = nullptr; + mjData* dnew = nullptr; + bool delete_old_m_d = false; + + mjModel* m = nullptr; + mjData* d = nullptr; + std::mutex mtx; + std::condition_variable cond_loadrequest; + + std::atomic_bool exitrequest = false; + + // option + int spacing = 0; + int color = 0; + int font = 0; + int ui0_enable = 1; + int ui1_enable = 1; + int help = 0; + int info = 0; + int profiler = 0; + int sensor = 0; + int fullscreen = 0; + int vsync = 1; + int busywait = 0; + + // simulation + int run = 1; + // keyframe index + int key = 0; + std::atomic_int uiloadrequest = 0; + std::atomic_bool droploadrequest = false; + // 2: render thread asked to update its model + // 1: showing "loading" label, about to load + // 0: model loaded or no load requested. + int loadrequest = 0; + // strings + char loadError[kMaxFilenameLength] = ""; + char dropfilename[kMaxFilenameLength] = ""; + char filename[kMaxFilenameLength] = ""; + char previous_filename[kMaxFilenameLength] = ""; + int slow_down = 1; + bool speed_changed = true; + double ctrlnoisestd = 0.0; + double ctrlnoiserate = 0.0; + + // watch + char field[mjMAXUITEXT] = "qpos"; + int index = 0; + + // physics: need sync + int disable[mjNDISABLE] = {0}; + int enable[mjNENABLE] = {0}; + + // rendering: need sync + int camera = 0; + + // abstract visualization + mjvScene scn = {}; + mjvCamera cam = {}; + mjvOption vopt = {}; + mjvPerturb pert = {}; + mjvFigure figconstraint = {}; + mjvFigure figcost = {}; + mjvFigure figtimer = {}; + mjvFigure figsize = {}; + mjvFigure figsensor = {}; + + // OpenGL rendering and UI + GLFWvidmode vmode = {}; + int windowpos[2] = {0}; + int windowsize[2] = {0}; + mjrContext con = {}; + GLFWwindow* window = nullptr; + mjuiState uistate = {}; + mjUI ui0 = {}; + mjUI ui1 = {}; + + // Constant arrays needed for the option section of UI and the UI interface + // TODO setting the size here is not ideal + const mjuiDef defOption[14] = { + {mjITEM_SECTION, "Option", 1, nullptr, "AO"}, + {mjITEM_SELECT, "Spacing", 1, &this->spacing, "Tight\nWide"}, + {mjITEM_SELECT, "Color", 1, &this->color, "Default\nOrange\nWhite\nBlack"}, + {mjITEM_SELECT, "Font", 1, &this->font, "50 %\n100 %\n150 %\n200 %\n250 %\n300 %"}, + {mjITEM_CHECKINT, "Left UI (Tab)", 1, &this->ui0_enable, " #258"}, + {mjITEM_CHECKINT, "Right UI", 1, &this->ui1_enable, "S#258"}, + {mjITEM_CHECKINT, "Help", 2, &this->help, " #290"}, + {mjITEM_CHECKINT, "Info", 2, &this->info, " #291"}, + {mjITEM_CHECKINT, "Profiler", 2, &this->profiler, " #292"}, + {mjITEM_CHECKINT, "Sensor", 2, &this->sensor, " #293"}, + #ifdef __APPLE__ + {mjITEM_CHECKINT, "Fullscreen", 0, &this->fullscreen, " #294"}, + #else + {mjITEM_CHECKINT, "Fullscreen", 1, &this->fullscreen, " #294"}, + #endif + {mjITEM_CHECKINT, "Vertical Sync", 1, &this->vsync, ""}, + {mjITEM_CHECKINT, "Busy Wait", 1, &this->busywait, ""}, + {mjITEM_END} + }; + + + // simulation section of UI + const mjuiDef defSimulation[12] = { + {mjITEM_SECTION, "Simulation", 1, nullptr, "AS"}, + {mjITEM_RADIO, "", 2, &this->run, "Pause\nRun"}, + {mjITEM_BUTTON, "Reset", 2, nullptr, " #259"}, + {mjITEM_BUTTON, "Reload", 2, nullptr, "CL"}, + {mjITEM_BUTTON, "Align", 2, nullptr, "CA"}, + {mjITEM_BUTTON, "Copy pose", 2, nullptr, "CC"}, + {mjITEM_SLIDERINT, "Key", 3, &this->key, "0 0"}, + {mjITEM_BUTTON, "Load key", 3}, + {mjITEM_BUTTON, "Save key", 3}, + {mjITEM_SLIDERNUM, "Noise scale", 2, &this->ctrlnoisestd, "0 2"}, + {mjITEM_SLIDERNUM, "Noise rate", 2, &this->ctrlnoiserate, "0 2"}, + {mjITEM_END} + }; + + + // watch section of UI + const mjuiDef defWatch[5] = { + {mjITEM_SECTION, "Watch", 0, nullptr, "AW"}, + {mjITEM_EDITTXT, "Field", 2, this->field, "qpos"}, + {mjITEM_EDITINT, "Index", 2, &this->index, "1"}, + {mjITEM_STATIC, "Value", 2, nullptr, " "}, + {mjITEM_END} + }; + + // info strings + char info_title[Simulate::kMaxFilenameLength] = {0}; + char info_content[Simulate::kMaxFilenameLength] = {0}; +}; + +} // namespace mujoco + +#endif diff --git a/sample/uitools.c b/simulate/uitools.c similarity index 89% rename from sample/uitools.c rename to simulate/uitools.c index 3fa7f048..6548bd0b 100644 --- a/sample/uitools.c +++ b/simulate/uitools.c @@ -13,11 +13,12 @@ // limitations under the License. #include "uitools.h" + #include #include -//-------------------------------- Internal GLFW callbacks ------------------------------ +//------------------------------------ internal GLFW callbacks ------------------------------------- // update state static void uiUpdateState(GLFWwindow* wnd) { @@ -226,8 +227,19 @@ static void uiResize(GLFWwindow* wnd, int width, int height) { } +static void uiRender(GLFWwindow* wnd) { + uiUserPointer* ptr = (uiUserPointer*)glfwGetWindowUserPointer(wnd); + mjuiState* state = ptr->state; + ptr->uiRender(state); +} -//----------------------------------- Public API ---------------------------------------- +static void uiDrop(GLFWwindow* wnd, int count, const char** paths) { + uiUserPointer* ptr = (uiUserPointer*)glfwGetWindowUserPointer(wnd); + mjuiState* state = ptr->state; + ptr->uiDrop(state, count, paths); +} + +//------------------------------------------- public API ------------------------------------------- // Compute suitable font scale. int uiFontScale(GLFWwindow* wnd) { @@ -262,12 +274,15 @@ int uiFontScale(GLFWwindow* wnd) { // Set internal and user-supplied UI callbacks in GLFW window. void uiSetCallback(GLFWwindow* wnd, mjuiState* state, - uiEventFn uiEvent, uiLayoutFn uiLayout) { + uiEventFn uiEvent, uiLayoutFn uiLayout, + uiRenderFn uiUserRender, uiDropFn uiUserDrop) { // make container with user-supplied objects and set window pointer uiUserPointer* ptr = (uiUserPointer*) mju_malloc(sizeof(uiUserPointer)); ptr->state = state; ptr->uiEvent = uiEvent; ptr->uiLayout = uiLayout; + ptr->uiRender = uiUserRender; + ptr->uiDrop = uiUserDrop; glfwSetWindowUserPointer(wnd, ptr); // compute framebuffer-to-window pixel ratio @@ -282,6 +297,8 @@ void uiSetCallback(GLFWwindow* wnd, mjuiState* state, glfwSetMouseButtonCallback(wnd, uiMouseButton); glfwSetScrollCallback(wnd, uiScroll); glfwSetWindowSizeCallback(wnd, uiResize); + glfwSetWindowRefreshCallback(wnd, uiRender); + glfwSetDropCallback(wnd, uiDrop); } @@ -300,6 +317,8 @@ void uiClearCallback(GLFWwindow* wnd) { glfwSetMouseButtonCallback(wnd, NULL); glfwSetScrollCallback(wnd, NULL); glfwSetWindowSizeCallback(wnd, NULL); + glfwSetWindowRefreshCallback(wnd, NULL); + glfwSetDropCallback(wnd, NULL); } diff --git a/sample/uitools.h b/simulate/uitools.h similarity index 80% rename from sample/uitools.h rename to simulate/uitools.h index 4e94bcce..92720221 100644 --- a/sample/uitools.h +++ b/simulate/uitools.h @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef MUJOCO_UITOOLS_H_ -#define MUJOCO_UITOOLS_H_ - +#ifndef MUJOCO_SIMULATE_UITOOLS_H_ +#define MUJOCO_SIMULATE_UITOOLS_H_ #include #include @@ -28,19 +27,24 @@ extern "C" { // User-supplied callback function types. typedef void (*uiEventFn)(mjuiState* state); typedef void (*uiLayoutFn)(mjuiState* state); +typedef void (*uiRenderFn)(mjuiState* state); +typedef void (*uiDropFn) (mjuiState* state, int count, const char** paths); // Container for GLFW window pointer. struct _uiUserPointer { mjuiState* state; uiEventFn uiEvent; uiLayoutFn uiLayout; + uiRenderFn uiRender; + uiDropFn uiDrop; double buffer2window; }; typedef struct _uiUserPointer uiUserPointer; // Set internal and user-supplied UI callbacks in GLFW window. void uiSetCallback(GLFWwindow* wnd, mjuiState* state, - uiEventFn uiEvent, uiLayoutFn uiLayout); + uiEventFn uiEvent, uiLayoutFn uiLayout, + uiRenderFn uiUserRender, uiDropFn uiUserDrop); // Clear UI callbacks in GLFW window. void uiClearCallback(GLFWwindow* wnd);