From 0b6bf42840552e0100a375adceb2581ff6e5a30a Mon Sep 17 00:00:00 2001 From: Levi Burner Date: Wed, 30 Mar 2022 12:51:56 -0400 Subject: [PATCH 1/4] WIP Python bindings for Simulate tested on Linux only --- python/mujoco/CMakeLists.txt | 53 +++++++- python/mujoco/constants.cc | 4 + python/mujoco/raw.h | 4 + python/mujoco/simulate.cc | 142 +++++++++++++++++++ python/mujoco/simulate.py | 256 +++++++++++++++++++++++++++++++++++ python/setup.py | 9 +- simulate/CMakeLists.txt | 25 ++++ simulate/main.py | 30 ++++ 8 files changed, 517 insertions(+), 6 deletions(-) create mode 100644 python/mujoco/simulate.cc create mode 100644 python/mujoco/simulate.py create mode 100644 simulate/main.py diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 4bf4b5cf..dbe7b1fc 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -62,6 +62,7 @@ add_compile_options("${MUJOCO_HARDEN_COMPILE_OPTIONS}") add_link_options("${MUJOCO_HARDEN_LINK_OPTIONS}") find_package(Python3 COMPONENTS Interpreter Development) +find_package(glfw3 3.3 REQUIRED) include(FindOrFetch) @@ -115,6 +116,40 @@ if(NOT TARGET mujoco) endif() endif() +# ==================== MJSIMULATE LIBRARY ========================================== +if(NOT TARGET mjsimulate) + find_library(MJSIMULATE_LIBRARY mjsimulate mjsimulate HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED) + find_path(MJSIMULATE_INCLUDE mujoco/simulate.h mujoco/uitools.h mujoco/array_safety.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) + message("MuJoCo Simulate is at ${MJSIMULATE_LIBRARY}") + message("MuJoCo Simulate headers are at ${MJSIMULATE_INCLUDE}") + add_library(mjsimulate SHARED IMPORTED) + if(WIN32) + set_target_properties(mjsimulate PROPERTIES IMPORTED_IMPLIB "${MJSIMULATE_LIBRARY}") + else() + set_target_properties(mjsimulate PROPERTIES IMPORTED_LOCATION "${MJSIMULATE_LIBRARY}") + endif() + target_include_directories(mjsimulate INTERFACE "${MJSIMULATE_INCLUDE}") + if(APPLE) + execute_process( + COMMAND otool -XD ${MJSIMULATE_LIBRARY} + COMMAND head -n 1 + COMMAND xargs dirname + COMMAND xargs echo -n + OUTPUT_VARIABLE MJSIMULATE_INSTALL_NAME_DIR + ) + set_target_properties(mjsimulate PROPERTIES INSTALL_NAME_DIR "${MJSIMULATE_INSTALL_NAME_DIR}") + elseif(UNIX) + execute_process( + COMMAND objdump -p ${MJSIMULATE_LIBRARY} + COMMAND grep SONAME + COMMAND grep -Po [^\\s]+$ + COMMAND xargs echo -n + OUTPUT_VARIABLE MJSIMULATE_SONAME + ) + set_target_properties(mjsimulate PROPERTIES IMPORTED_SONAME "${MJSIMULATE_SONAME}") + endif() +endif() + # ==================== ABSEIL ================================================== set(MUJOCO_PYTHON_ABSL_TARGETS absl::core_headers absl::flat_hash_map absl::span) findorfetch( @@ -231,7 +266,7 @@ target_link_libraries(errors_header INTERFACE crossplatform func_wrap mujoco) add_library(raw INTERFACE) target_sources(raw INTERFACE raw.h) set_target_properties(raw PROPERTIES PUBLIC_HEADER raw.h) -target_link_libraries(raw INTERFACE mujoco) +target_link_libraries(raw INTERFACE mujoco mjsimulate) add_library(structs_header INTERFACE) target_sources( @@ -309,7 +344,7 @@ target_link_libraries( ) mujoco_pybind11_module(_constants constants.cc) -target_link_libraries(_constants PRIVATE mujoco) +target_link_libraries(_constants PRIVATE mujoco mjsimulate) mujoco_pybind11_module(_enums enums.cc) target_link_libraries( @@ -370,6 +405,16 @@ target_link_libraries( structs_header ) +mujoco_pybind11_module(_simulate simulate.cc) +target_link_libraries( + _simulate + PRIVATE mjsimulate + mujoco + raw + glfw + structs_header) +target_link_options(_simulate PRIVATE -Wl,-no-as-needed) + set(LIBRARIES_FOR_WHEEL "$" "$" @@ -378,8 +423,10 @@ set(LIBRARIES_FOR_WHEEL "$" "$" "$" + "$" "$" "$" + "$" ) if(MUJOCO_PYTHON_MAKE_WHEEL) @@ -405,7 +452,9 @@ if(MUJOCO_PYTHON_MAKE_WHEEL) _functions _render _rollout + _simulate _structs mujoco + mjsimulate ) endif() diff --git a/python/mujoco/constants.cc b/python/mujoco/constants.cc index 2ffe73bb..3fcbd279 100644 --- a/python/mujoco/constants.cc +++ b/python/mujoco/constants.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -78,6 +79,9 @@ PYBIND11_MODULE(_constants, pymodule) { // from mujoco.h X(mjVERSION_HEADER); + // from simulate.h + X(mujoco::Simulate::kMaxFilenameLength); + #undef X pymodule.attr("mjDISABLESTRING") = MakeTuple(mjDISABLESTRING); pymodule.attr("mjENABLESTRING") = MakeTuple(mjENABLESTRING); diff --git a/python/mujoco/raw.h b/python/mujoco/raw.h index 5aa146f4..3399540a 100644 --- a/python/mujoco/raw.h +++ b/python/mujoco/raw.h @@ -19,6 +19,7 @@ #include #include #include +#include // Type aliases for MuJoCo C structs to allow us refer to consistently refer // to them under the "raw" namespace. @@ -55,6 +56,9 @@ using MjvOption = ::mjvOption; using MjvScene = ::mjvScene; using MjvFigure = ::mjvFigure; +// From simulate.h +using Simulate = ::mujoco::Simulate; + } // namespace mujoco::raw #endif // MUJOCO_PYTHON_RAW_H_ diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc new file mode 100644 index 00000000..46540e8b --- /dev/null +++ b/python/mujoco/simulate.cc @@ -0,0 +1,142 @@ +// 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 +// +// 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 "raw.h" +#include "structs.h" +#include +#include +#include +#include + +namespace mujoco::python { + +namespace { + +namespace py = ::pybind11; + +const auto simulate_doc = R"( +Python wrapper for the Simulate class +)"; + +// We define SimulateWrapper here instead of in structs because +// we do not want to make _structs dependent on gflw + +PYBIND11_MODULE(_simulate, pymodule) { + namespace py = ::pybind11; + + py::class_(pymodule, "Simulate") + .def(py::init<>()) + .def("renderloop", + [](mujoco::raw::Simulate& simulate) { + simulate.renderloop(); + }, + py::call_guard()) + .def("load", + [](mujoco::raw::Simulate& simulate, std::string filename, const MjModelWrapper& m, MjDataWrapper& d, bool delete_old_m_d) { + const raw::MjModel* m_ptr = m.get(); + raw::MjData* d_ptr = d.get(); + simulate.load(filename.c_str(), (mjModel*)m_ptr, d_ptr, delete_old_m_d); + }, + py::call_guard()) + .def("applyposepertubations", &mujoco::raw::Simulate::applyposepertubations) + .def("applyforceperturbations", &mujoco::raw::Simulate::applyforceperturbations) + + .def("lock", // TODO wrap mutex properly as as seperate pybind11 object? + [](mujoco::raw::Simulate& simulate) { + simulate.mtx.lock(); + }, + py::call_guard()) + .def("unlock", + [](mujoco::raw::Simulate& simulate) { + simulate.mtx.unlock(); + }, + py::call_guard()) + .def_readwrite("ctrlnoisestd", &mujoco::raw::Simulate::ctrlnoisestd) + .def_readwrite("ctrlnoiserate", &mujoco::raw::Simulate::ctrlnoiserate) + .def_readwrite("slow_down", &mujoco::raw::Simulate::slow_down) + .def_readwrite("speed_changed", &mujoco::raw::Simulate::speed_changed) + .def("getrefreshRate", + [](mujoco::raw::Simulate& simulate) { + return simulate.vmode.refreshRate; + }) + + .def_readwrite("busywait", &mujoco::raw::Simulate::busywait) + .def_readwrite("run", &mujoco::raw::Simulate::run) + //.def_readwrite("exitrequest", &mujoco::raw::Simulate::exitrequest) + .def("getexitrequest", + [](mujoco::raw::Simulate& simulate) { + return simulate.exitrequest.load(); + } + ) + .def("setexitrequest", + [](mujoco::raw::Simulate& simulate, bool exitrequest) { + simulate.exitrequest.store(exitrequest); + } + ) + // .def_readwrite("uiloadrequest", &mujoco::raw::Simulate::uiloadrequest) + .def("getuiloadrequest", + [](mujoco::raw::Simulate& simulate) { + return simulate.uiloadrequest.load(); + } + ) + .def("setuiloadrequest", + [](mujoco::raw::Simulate& simulate, int uiloadrequest) { + simulate.uiloadrequest.store(uiloadrequest); + } + ) + .def("uiloadrequest_fetch_sub", + [](mujoco::raw::Simulate& simulate, int arg) { + simulate.uiloadrequest.fetch_sub(arg); + } + ) + // .def_readwrite("droploadrequest", &mujoco::raw::Simulate::droploadrequest) + .def("getdroploadrequest", + [](mujoco::raw::Simulate& simulate) { + return simulate.droploadrequest.load(); + } + ) + .def("setdroploadrequest", + [](mujoco::raw::Simulate& simulate, bool droploadrequest) { + simulate.droploadrequest.store(droploadrequest); + } + ) + .def("getdropfilename", + [](mujoco::raw::Simulate& simulate) { + return (char*)simulate.dropfilename; + } + ) + .def("getfilename", + [](mujoco::raw::Simulate& simulate) { + return (char*)simulate.filename; + } + ) + .def("setloadError", + [](mujoco::raw::Simulate& simulate, std::string& loadError) { + strncpy(simulate.loadError, loadError.c_str(), simulate.kMaxFilenameLength); + } + ); +} + +} // namespace + +} diff --git a/python/mujoco/simulate.py b/python/mujoco/simulate.py new file mode 100644 index 00000000..d4c37f14 --- /dev/null +++ b/python/mujoco/simulate.py @@ -0,0 +1,256 @@ +# 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 +# +# 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. +# ============================================================================== +"""Wrap the pybind11 bound Simulate class to provide type conversions""" + +import mujoco +from mujoco import _simulate + +import glfw +import numpy as np + +import ctypes +import math +import threading +import time + +class Simulate(_simulate.Simulate): + def __init__(self): + super().__init__() + + def _get_refreshRate(self): + return self.getrefreshRate() + + refreshRate = property( + fget=_get_refreshRate) + + def _get_drop_file_name(self): + return self.getdropfilename() + + dropfilename = property( + fget=_get_drop_file_name, + doc="char* from Simulate" + ) + + def _get_file_name(self): + return self.getfilename() + + filename = property( + fget=_get_file_name, + doc="char* from Simulate" + ) + + def _set_loadError(self, loadError): + self.setloadError(loadError) + + loadError = property( + fset=_set_loadError, + doc="set string to Simulate" + ) + + def _get_exit_request(self): + return self.getexitrequest() + + def _set_exit_request(self, exitrequest): + self.setexitrequest(exitrequest) + + exitrequest = property( + fget=_get_exit_request, + fset=_set_exit_request, + doc="atomic bool from Simulate") + + def _get_ui_load_request(self): + return self.getuiloadrequest() + + def _set_ui_load_request(self, uiloadrequest): + self.setuiloadrequest(uiloadrequest) + + uiloadrequest = property( + fget=_get_ui_load_request, + fset=_set_ui_load_request, + doc="atomic int from Simulate") + + def _get_drop_load_request(self): + return self.getdroploadrequest() + + def _set_drop_load_request(self, droploadrequest): + self.setdroploadrequest(droploadrequest) + + droploadrequest = property( + fget=_get_drop_load_request, + fset=_set_drop_load_request, + doc="atomic bool from Simulate") + +def load_and_step_model(m, d, simulate, filename, preload_callback=None, load_callback=None): + if preload_callback is not None: + preload_callback(m, d) # Call with old model/data so user can do cleanup + + try: + mnew = mujoco.MjModel.from_xml_path(filename) + loadError = None + except Exception as e: + print('Error loading using from_xml_path') + print(e) + mnew = None + loadError = str(e) + + if mnew is not None: + dnew = mujoco.MjData(mnew) + simulate.load(filename, mnew, dnew, False) + mujoco.mj_forward(mnew, dnew) + else: + mnew = None + dnew = None + + if load_callback is not None: + load_callback(mnew, dnew, loadError) + + return mnew, dnew, loadError + +def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=None): + # request loadmodel if file given (otherwise drag-and-drop) + if file is not None: + m, d, loadError = load_and_step_model(None, None, simulate, file, preload_callback, load_callback) + ctrlnoise = np.zeros((m.nu,)) + if loadError is not None: + simulate.loadError = loadError + else: + m = None + d = None + ctrlnoise = None + + # constants + syncmisalign = 0.1 # maximum time mis-alignment before re-sync + refreshfactor = 0.7 # fraction of refresh available for simulation + + # cpu-sim synchronization point + cpusync = 0.0 + simsync = 0.0 + + # run until asked to exit + while not simulate.exitrequest: + if simulate.droploadrequest: + simulate.droploadrequest = 0 + m_, d_, loadError = load_and_step_model(m, d, simulate, simulate.dropfilename, + preload_callback, load_callback) + + if m_ is not None: + m = m_ + d = d_ + ctrlnoise = np.zeros((m.nu,)) + else: + simulate.loadError = loadError + + if simulate.uiloadrequest: + simulate.uiloadrequest_fetch_sub(1) + m_, d_, loadError = load_and_step_model(m, d, simulate, simulate.filename, + preload_callback, load_callback) + if m_ is not None: + m = m_ + d = d_ + ctrlnoise = np.zeros((m.nu,)) + else: + simulate.loadError = loadError + + # 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 simulate.run != 0 and simulate.busywait != 0: + time.sleep(0) + else: + time.sleep(0.001) + + # Start exclusive access + simulate.lock() + + # run only if model is present + if m is not None: + # running + if simulate.run != 0: + # record cpu time at start of iteration + tmstart = glfw.get_time() + + # inject noise + if simulate.ctrlnoisestd != 0.0: + # convert rate and scale to discrete time given current timestep + rate = math.exp(-m.opt.timestep / simulate.ctrlnoiserate) + scale = simulate.ctrlnoisestd * math.sqrt(1-rate*rate) + + for i in range(m.nu): + # update noise + ctrlnoise[i] = rate * ctrlnoise[i] + scale * mujoco.mju_standardNormal(None) + # apply noise + d.ctrl[i] = ctrlnoise[i] + + # out-of-sync (for any reason) + offset = abs((d.time*simulate.slow_down - simsync) - (tmstart - cpusync)) + if (d.time*simulate.slow_down < simsync or tmstart < cpusync or cpusync == 0.0 or + offset > syncmisalign*simulate.slow_down or simulate.speed_changed): + # re-sync + cpusync = tmstart + simsync = d.time*simulate.slow_down + simulate.speed_changed = False + + # clear old perturbations, apply new + d.xfrc_applied[:, :] = 0 + simulate.applyposepertubations(0) # move mocap bodies only + simulate.applyforceperturbations() + + # run single step, let next iteration deal with timing + mujoco.mj_step(m, d) + + # in-sync + else: + while ((d.time*simulate.slow_down - simsync) < (glfw.get_time() - cpusync) and + (glfw.get_time() - tmstart) < (refreshfactor/simulate.refreshRate)): + # clear old perturbations, apply new + d.xfrc_applied[:, :] = 0 + simulate.applyposepertubations(0) # move mocap bodies only + simulate.applyforceperturbations() + + # run mj_step + prevtm = d.time*simulate.slow_down + mujoco.mj_step(m, d) + + # break on reset + if d.time*simulate.slow_down < prevtm: + break + + # paused + else: + # apply pose perturbation + simulate.applyposepertubations(1) # move mocap and dynamic bodies + + # run mj_forward, to update rendering and joint sliders + mujoco.mj_forward(m, d) + + # end exclusive access + simulate.unlock() + +def run_simulate_and_physics(file=None, preload_callback=None, load_callback=None): + # simulate object encapsulates the UI + simulate = Simulate() + + # init GLFW + if not glfw.init(): + raise mujoco.FatalError('could not initialize GLFW') + + # if m is not None: + physics_thread = threading.Thread(target=lambda: run_physics_loop(simulate, preload_callback=preload_callback, load_callback=load_callback, file=file)) + physics_thread.start() + + # start simulation thread (this creates the UI) + simulate.renderloop() + physics_thread.join() + + glfw.terminate() diff --git a/python/setup.py b/python/setup.py index fad9204e..e0380108 100644 --- a/python/setup.py +++ b/python/setup.py @@ -63,12 +63,11 @@ def get_mujoco_lib_pattern(): def get_external_lib_patterns(): if platform.system() == 'Windows': - return ['mujoco.dll'] + return ['mujoco.dll', 'mjsimulate.dll'] elif platform.system() == 'Darwin': - return ['libmujoco.*.dylib'] + return ['libmujoco.*.dylib', 'libmjsimulate.*.dylib'] else: - return ['libmujoco.so.*'] - + return ['libmujoco.so.*', 'libmjsimulate.so*'] def start_and_end(iterable): it = iter(iterable) @@ -292,6 +291,7 @@ setup( CMakeExtension('mujoco._functions'), CMakeExtension('mujoco._render'), CMakeExtension('mujoco._rollout'), + CMakeExtension('mujoco._simulate'), CMakeExtension('mujoco._structs'), ], python_requires='>=3.7', @@ -318,6 +318,7 @@ setup( 'libmujoco*.so.*', 'mujoco.dll', 'include/mujoco/*.h', + 'libmjsimulate*.so*', ]), }, ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index ae02fdc8..9a74fd77 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -117,6 +117,10 @@ target_compile_options(libsimulate PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) target_link_libraries(libsimulate PUBLIC glfw lodepng mujoco::mujoco) target_link_options(libsimulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) +set_target_properties( + mjsimulate PROPERTIES VERSION "${mujoco_VERSION}" PUBLIC_HEADER "simulate.h" +) + if(APPLE) target_sources(libsimulate PRIVATE macos_save.mm) target_link_libraries(libsimulate PUBLIC "-framework Cocoa") @@ -219,6 +223,17 @@ if(_INSTALL_SIMULATE) MUJOCO_ENABLE_RPATH ) + target_add_rpath( + TARGETS + mjsimulate + 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} @@ -229,6 +244,16 @@ if(_INSTALL_SIMULATE) PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT simulate ) + install( + TARGETS mjsimulate + 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}/mujoco 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. diff --git a/simulate/main.py b/simulate/main.py new file mode 100644 index 00000000..da1be97c --- /dev/null +++ b/simulate/main.py @@ -0,0 +1,30 @@ +# 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 +# +# 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. +# ============================================================================== +"""Python version of the simulate C++ sample""" + +import mujoco +from mujoco.simulate import run_simulate_and_physics +import sys + +if __name__ == '__main__': + # print version, check compatibility + print('MuJoCo version {}'.format(mujoco.mj_versionString())) + if mujoco.mjVERSION_HEADER != mujoco.mj_version(): + raise mujoco.FatalError('Headers and library have different versions') + + if len(sys.argv) > 1: + run_simulate_and_physics(sys.argv[1]) + else: + run_simulate_and_physics() From 95a4b1c551cdb29dd2fda4627720093eb95ee96c Mon Sep 17 00:00:00 2001 From: Levi Burner Date: Tue, 4 Oct 2022 14:37:06 -0400 Subject: [PATCH 2/4] simulate python: fix cmake, glfw usage, update Python version of physics loop --- python/mujoco/CMakeLists.txt | 44 +++++++++--------- python/mujoco/constants.cc | 4 -- python/mujoco/raw.h | 4 -- python/mujoco/simulate.cc | 70 +++++++++++++--------------- python/mujoco/simulate.py | 81 +++++++++++++++++++++----------- python/setup.py | 9 ++-- simulate/CMakeLists.txt | 44 +++++++++++++----- simulate/glfw_dispatch.cc | 5 +- simulate/glfw_dispatch.h | 1 + simulate/main.cc | 12 +++-- simulate/simulate.cc | 90 ++++++++++++++++++------------------ simulate/simulate.h | 29 +++++++----- 12 files changed, 219 insertions(+), 174 deletions(-) diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index dbe7b1fc..07757aec 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -62,7 +62,6 @@ add_compile_options("${MUJOCO_HARDEN_COMPILE_OPTIONS}") add_link_options("${MUJOCO_HARDEN_LINK_OPTIONS}") find_package(Python3 COMPONENTS Interpreter Development) -find_package(glfw3 3.3 REQUIRED) include(FindOrFetch) @@ -116,37 +115,37 @@ if(NOT TARGET mujoco) endif() endif() -# ==================== MJSIMULATE LIBRARY ========================================== -if(NOT TARGET mjsimulate) - find_library(MJSIMULATE_LIBRARY mjsimulate mjsimulate HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED) - find_path(MJSIMULATE_INCLUDE mujoco/simulate.h mujoco/uitools.h mujoco/array_safety.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) - message("MuJoCo Simulate is at ${MJSIMULATE_LIBRARY}") - message("MuJoCo Simulate headers are at ${MJSIMULATE_INCLUDE}") - add_library(mjsimulate SHARED IMPORTED) +# ==================== LIBSIMULATE LIBRARY ========================================== +if(NOT TARGET libsimulate) + find_library(LIBSIMULATE_LIBRARY libsimulate libsimulate HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED) + find_path(LIBSIMULATE_INCLUDE mujoco/simulate.h mujoco/uitools.h mujoco/array_safety.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) + message("MuJoCo Simulate is at ${LIBSIMULATE_LIBRARY}") + message("MuJoCo Simulate headers are at ${LIBSIMULATE_INCLUDE}") + add_library(libsimulate SHARED IMPORTED) if(WIN32) - set_target_properties(mjsimulate PROPERTIES IMPORTED_IMPLIB "${MJSIMULATE_LIBRARY}") + set_target_properties(libsimulate PROPERTIES IMPORTED_IMPLIB "${LIBSIMULATE_LIBRARY}") else() - set_target_properties(mjsimulate PROPERTIES IMPORTED_LOCATION "${MJSIMULATE_LIBRARY}") + set_target_properties(libsimulate PROPERTIES IMPORTED_LOCATION "${LIBSIMULATE_LIBRARY}") endif() - target_include_directories(mjsimulate INTERFACE "${MJSIMULATE_INCLUDE}") + target_include_directories(libsimulate INTERFACE "${LIBSIMULATE_INCLUDE}") if(APPLE) execute_process( - COMMAND otool -XD ${MJSIMULATE_LIBRARY} + COMMAND otool -XD ${LIBSIMULATE_LIBRARY} COMMAND head -n 1 COMMAND xargs dirname COMMAND xargs echo -n - OUTPUT_VARIABLE MJSIMULATE_INSTALL_NAME_DIR + OUTPUT_VARIABLE LIBSIMULATE_INSTALL_NAME_DIR ) - set_target_properties(mjsimulate PROPERTIES INSTALL_NAME_DIR "${MJSIMULATE_INSTALL_NAME_DIR}") + set_target_properties(libsimulate PROPERTIES INSTALL_NAME_DIR "${LIBSIMULATE_INSTALL_NAME_DIR}") elseif(UNIX) execute_process( - COMMAND objdump -p ${MJSIMULATE_LIBRARY} + COMMAND objdump -p ${LIBSIMULATE_LIBRARY} COMMAND grep SONAME COMMAND grep -Po [^\\s]+$ COMMAND xargs echo -n - OUTPUT_VARIABLE MJSIMULATE_SONAME + OUTPUT_VARIABLE LIBSIMULATE_SONAME ) - set_target_properties(mjsimulate PROPERTIES IMPORTED_SONAME "${MJSIMULATE_SONAME}") + set_target_properties(libsimulate PROPERTIES IMPORTED_SONAME "${LIBSIMULATE_SONAME}") endif() endif() @@ -266,7 +265,7 @@ target_link_libraries(errors_header INTERFACE crossplatform func_wrap mujoco) add_library(raw INTERFACE) target_sources(raw INTERFACE raw.h) set_target_properties(raw PROPERTIES PUBLIC_HEADER raw.h) -target_link_libraries(raw INTERFACE mujoco mjsimulate) +target_link_libraries(raw INTERFACE mujoco) add_library(structs_header INTERFACE) target_sources( @@ -344,7 +343,7 @@ target_link_libraries( ) mujoco_pybind11_module(_constants constants.cc) -target_link_libraries(_constants PRIVATE mujoco mjsimulate) +target_link_libraries(_constants PRIVATE mujoco) mujoco_pybind11_module(_enums enums.cc) target_link_libraries( @@ -408,10 +407,9 @@ target_link_libraries( mujoco_pybind11_module(_simulate simulate.cc) target_link_libraries( _simulate - PRIVATE mjsimulate + PRIVATE libsimulate mujoco raw - glfw structs_header) target_link_options(_simulate PRIVATE -Wl,-no-as-needed) @@ -426,7 +424,7 @@ set(LIBRARIES_FOR_WHEEL "$" "$" "$" - "$" + "$" ) if(MUJOCO_PYTHON_MAKE_WHEEL) @@ -455,6 +453,6 @@ if(MUJOCO_PYTHON_MAKE_WHEEL) _simulate _structs mujoco - mjsimulate + libsimulate ) endif() diff --git a/python/mujoco/constants.cc b/python/mujoco/constants.cc index 3fcbd279..2ffe73bb 100644 --- a/python/mujoco/constants.cc +++ b/python/mujoco/constants.cc @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -79,9 +78,6 @@ PYBIND11_MODULE(_constants, pymodule) { // from mujoco.h X(mjVERSION_HEADER); - // from simulate.h - X(mujoco::Simulate::kMaxFilenameLength); - #undef X pymodule.attr("mjDISABLESTRING") = MakeTuple(mjDISABLESTRING); pymodule.attr("mjENABLESTRING") = MakeTuple(mjENABLESTRING); diff --git a/python/mujoco/raw.h b/python/mujoco/raw.h index 3399540a..5aa146f4 100644 --- a/python/mujoco/raw.h +++ b/python/mujoco/raw.h @@ -19,7 +19,6 @@ #include #include #include -#include // Type aliases for MuJoCo C structs to allow us refer to consistently refer // to them under the "raw" namespace. @@ -56,9 +55,6 @@ using MjvOption = ::mjvOption; using MjvScene = ::mjvScene; using MjvFigure = ::mjvFigure; -// From simulate.h -using Simulate = ::mujoco::Simulate; - } // namespace mujoco::raw #endif // MUJOCO_PYTHON_RAW_H_ diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 46540e8b..28182ea3 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -34,107 +34,101 @@ namespace { namespace py = ::pybind11; -const auto simulate_doc = R"( -Python wrapper for the Simulate class -)"; - -// We define SimulateWrapper here instead of in structs because -// we do not want to make _structs dependent on gflw - PYBIND11_MODULE(_simulate, pymodule) { namespace py = ::pybind11; - py::class_(pymodule, "Simulate") + py::class_(pymodule, "Simulate") .def(py::init<>()) .def("renderloop", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { simulate.renderloop(); }, py::call_guard()) .def("load", - [](mujoco::raw::Simulate& simulate, std::string filename, const MjModelWrapper& m, MjDataWrapper& d, bool delete_old_m_d) { + [](mujoco::Simulate& simulate, std::string filename, const MjModelWrapper& m, MjDataWrapper& d) { const raw::MjModel* m_ptr = m.get(); raw::MjData* d_ptr = d.get(); - simulate.load(filename.c_str(), (mjModel*)m_ptr, d_ptr, delete_old_m_d); + simulate.load(filename.c_str(), (mjModel*)m_ptr, d_ptr); }, py::call_guard()) - .def("applyposepertubations", &mujoco::raw::Simulate::applyposepertubations) - .def("applyforceperturbations", &mujoco::raw::Simulate::applyforceperturbations) + .def("applyposepertubations", &mujoco::Simulate::applyposepertubations) + .def("applyforceperturbations", &mujoco::Simulate::applyforceperturbations) .def("lock", // TODO wrap mutex properly as as seperate pybind11 object? - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { simulate.mtx.lock(); }, py::call_guard()) .def("unlock", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { simulate.mtx.unlock(); }, py::call_guard()) - .def_readwrite("ctrlnoisestd", &mujoco::raw::Simulate::ctrlnoisestd) - .def_readwrite("ctrlnoiserate", &mujoco::raw::Simulate::ctrlnoiserate) - .def_readwrite("slow_down", &mujoco::raw::Simulate::slow_down) - .def_readwrite("speed_changed", &mujoco::raw::Simulate::speed_changed) - .def("getrefreshRate", - [](mujoco::raw::Simulate& simulate) { - return simulate.vmode.refreshRate; - }) + .def_readwrite("ctrlnoisestd", &mujoco::Simulate::ctrlnoisestd) + .def_readwrite("ctrlnoiserate", &mujoco::Simulate::ctrlnoiserate) + + .def_readwrite("realtimeindex", &mujoco::Simulate::realTimeIndex) + .def_readwrite("speedchanged", &mujoco::Simulate::speedChanged) + .def_readwrite("measuredslowdown", &mujoco::Simulate::measuredSlowdown) + .def_readwrite("refreshrate", &mujoco::Simulate::refreshRate) + + .def_readwrite("busywait", &mujoco::Simulate::busywait) + .def_readwrite("run", &mujoco::Simulate::run) - .def_readwrite("busywait", &mujoco::raw::Simulate::busywait) - .def_readwrite("run", &mujoco::raw::Simulate::run) - //.def_readwrite("exitrequest", &mujoco::raw::Simulate::exitrequest) .def("getexitrequest", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { return simulate.exitrequest.load(); } ) .def("setexitrequest", - [](mujoco::raw::Simulate& simulate, bool exitrequest) { + [](mujoco::Simulate& simulate, bool exitrequest) { simulate.exitrequest.store(exitrequest); } ) - // .def_readwrite("uiloadrequest", &mujoco::raw::Simulate::uiloadrequest) + .def("getuiloadrequest", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { return simulate.uiloadrequest.load(); } ) .def("setuiloadrequest", - [](mujoco::raw::Simulate& simulate, int uiloadrequest) { + [](mujoco::Simulate& simulate, int uiloadrequest) { simulate.uiloadrequest.store(uiloadrequest); } ) .def("uiloadrequest_fetch_sub", - [](mujoco::raw::Simulate& simulate, int arg) { + [](mujoco::Simulate& simulate, int arg) { simulate.uiloadrequest.fetch_sub(arg); } ) - // .def_readwrite("droploadrequest", &mujoco::raw::Simulate::droploadrequest) + .def("getdroploadrequest", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { return simulate.droploadrequest.load(); } ) .def("setdroploadrequest", - [](mujoco::raw::Simulate& simulate, bool droploadrequest) { + [](mujoco::Simulate& simulate, bool droploadrequest) { simulate.droploadrequest.store(droploadrequest); } ) .def("getdropfilename", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { return (char*)simulate.dropfilename; } ) .def("getfilename", - [](mujoco::raw::Simulate& simulate) { + [](mujoco::Simulate& simulate) { return (char*)simulate.filename; } ) .def("setloadError", - [](mujoco::raw::Simulate& simulate, std::string& loadError) { + [](mujoco::Simulate& simulate, std::string& loadError) { strncpy(simulate.loadError, loadError.c_str(), simulate.kMaxFilenameLength); } ); + + pymodule.def("setglfwdlhandle", [](std::uintptr_t dlhandle) { mujoco::setglfwdlhandle(reinterpret_cast(dlhandle)); }); } } // namespace diff --git a/python/mujoco/simulate.py b/python/mujoco/simulate.py index d4c37f14..42142b3d 100644 --- a/python/mujoco/simulate.py +++ b/python/mujoco/simulate.py @@ -18,6 +18,9 @@ import mujoco from mujoco import _simulate import glfw +from glfw import _glfw +_simulate.setglfwdlhandle(_glfw._handle) + import numpy as np import ctypes @@ -29,6 +32,14 @@ class Simulate(_simulate.Simulate): def __init__(self): super().__init__() + # logarithmically spaced realtime slow-down coefficients (percent) + self.percentrealtime = [ + 100, 80, 66, 50, 40, 33, 25, 20, 16, 13, + 10, 8, 6.6, 5.0, 4, 3.3, 2.5, 2, 1.6, 1.3, + 1, .8, .66, .5, .4, .33, .25, .2, .16, .13, + .1 + ] + def _get_refreshRate(self): return self.getrefreshRate() @@ -107,14 +118,14 @@ def load_and_step_model(m, d, simulate, filename, preload_callback=None, load_ca if mnew is not None: dnew = mujoco.MjData(mnew) - simulate.load(filename, mnew, dnew, False) + simulate.load(filename, mnew, dnew) mujoco.mj_forward(mnew, dnew) else: mnew = None dnew = None if load_callback is not None: - load_callback(mnew, dnew, loadError) + load_callback(mnew, dnew) return mnew, dnew, loadError @@ -132,11 +143,11 @@ def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=N # constants syncmisalign = 0.1 # maximum time mis-alignment before re-sync - refreshfactor = 0.7 # fraction of refresh available for simulation + simrefreshfraction = 0.7 # fraction of refresh available for simulation # cpu-sim synchronization point - cpusync = 0.0 - simsync = 0.0 + synccpu = 0.0 + syncsim = 0.0 # run until asked to exit while not simulate.exitrequest: @@ -144,7 +155,6 @@ def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=N simulate.droploadrequest = 0 m_, d_, loadError = load_and_step_model(m, d, simulate, simulate.dropfilename, preload_callback, load_callback) - if m_ is not None: m = m_ d = d_ @@ -178,28 +188,36 @@ def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=N # running if simulate.run != 0: # record cpu time at start of iteration - tmstart = glfw.get_time() + startcpu = glfw.get_time() + + elapsedcpu = startcpu - synccpu + elapsedsim = d.time - syncsim # inject noise if simulate.ctrlnoisestd != 0.0: - # convert rate and scale to discrete time given current timestep + # convert rate and scale to discrete time (Ornstein–Uhlenbeck) rate = math.exp(-m.opt.timestep / simulate.ctrlnoiserate) scale = simulate.ctrlnoisestd * math.sqrt(1-rate*rate) for i in range(m.nu): # update noise ctrlnoise[i] = rate * ctrlnoise[i] + scale * mujoco.mju_standardNormal(None) + # apply noise d.ctrl[i] = ctrlnoise[i] - # out-of-sync (for any reason) - offset = abs((d.time*simulate.slow_down - simsync) - (tmstart - cpusync)) - if (d.time*simulate.slow_down < simsync or tmstart < cpusync or cpusync == 0.0 or - offset > syncmisalign*simulate.slow_down or simulate.speed_changed): + # requested slow-down factor + slowdown = 100 / simulate.percentrealtime[simulate.realtimeindex] + + # misalignment condition: distance from target sim time is bigger than syncmisalign + misaligned = abs(elapsedcpu/slowdown - elapsedsim) > syncmisalign + + # out-of-sync (for any reason): reset sync times, step + if elapsedsim < 0 or elapsedcpu < 0 or synccpu == 0 or misaligned or simulate.speedchanged: # re-sync - cpusync = tmstart - simsync = d.time*simulate.slow_down - simulate.speed_changed = False + synccpu = startcpu + syncsim = d.time + simulate.speedchanged = False # clear old perturbations, apply new d.xfrc_applied[:, :] = 0 @@ -209,21 +227,30 @@ def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=N # run single step, let next iteration deal with timing mujoco.mj_step(m, d) - # in-sync + # in-sync: step until ahead of cpu else: - while ((d.time*simulate.slow_down - simsync) < (glfw.get_time() - cpusync) and - (glfw.get_time() - tmstart) < (refreshfactor/simulate.refreshRate)): + measured = False + prevsim = d.time + refreshtime = simrefreshfraction/simulate.refreshrate; + + # step while sim lags behind cpu and within refreshtime + while (((d.time - syncsim)*slowdown < (glfw.get_time() - synccpu)) + and ((glfw.get_time() - startcpu) < refreshtime)): + # measure slowdown before first step + if not measured and elapsedsim: + simulate.measuredslowdown = elapsedcpu / elapsedsim + measured = True + # clear old perturbations, apply new d.xfrc_applied[:, :] = 0 simulate.applyposepertubations(0) # move mocap bodies only simulate.applyforceperturbations() - # run mj_step - prevtm = d.time*simulate.slow_down + # call mj_step mujoco.mj_step(m, d) - # break on reset - if d.time*simulate.slow_down < prevtm: + # break if reset + if d.time < prevsim: break # paused @@ -237,13 +264,14 @@ def run_physics_loop(simulate, preload_callback=None, load_callback=None, file=N # end exclusive access simulate.unlock() -def run_simulate_and_physics(file=None, preload_callback=None, load_callback=None): +def run_simulate_and_physics(file=None, preload_callback=None, load_callback=None, init=True, terminate=True): # simulate object encapsulates the UI simulate = Simulate() # init GLFW - if not glfw.init(): - raise mujoco.FatalError('could not initialize GLFW') + if init: + if not glfw.init(): + raise mujoco.FatalError('could not initialize GLFW') # if m is not None: physics_thread = threading.Thread(target=lambda: run_physics_loop(simulate, preload_callback=preload_callback, load_callback=load_callback, file=file)) @@ -253,4 +281,5 @@ def run_simulate_and_physics(file=None, preload_callback=None, load_callback=Non simulate.renderloop() physics_thread.join() - glfw.terminate() + if terminate: + glfw.terminate() diff --git a/python/setup.py b/python/setup.py index e0380108..9223208d 100644 --- a/python/setup.py +++ b/python/setup.py @@ -63,11 +63,11 @@ def get_mujoco_lib_pattern(): def get_external_lib_patterns(): if platform.system() == 'Windows': - return ['mujoco.dll', 'mjsimulate.dll'] + return ['mujoco.dll', 'libsimulate.dll'] elif platform.system() == 'Darwin': - return ['libmujoco.*.dylib', 'libmjsimulate.*.dylib'] + return ['libmujoco.*.dylib', 'liblibsimulate.*.dylib'] else: - return ['libmujoco.so.*', 'libmjsimulate.so*'] + return ['libmujoco.so.*', 'liblibsimulate.so*'] def start_and_end(iterable): it = iter(iterable) @@ -318,7 +318,8 @@ setup( 'libmujoco*.so.*', 'mujoco.dll', 'include/mujoco/*.h', - 'libmjsimulate*.so*', + 'liblibsimulate.*.dylib', + 'liblibsimulate*.so*', ]), }, ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 9a74fd77..70ddb5b3 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -97,8 +97,8 @@ if(NOT TARGET lodepng) endif() endif() -# Simulate library -add_library(libsimulate STATIC) +# Simulate shared library +add_library(libsimulate SHARED simulate.cc uitools.cc glfw_dispatch.cc) add_library(mujoco::libsimulate ALIAS libsimulate) target_sources( @@ -106,19 +106,16 @@ target_sources( PUBLIC simulate.h array_safety.h glfw_dispatch.h - glfw_dispatch.cc - simulate.cc uitools.h - uitools.cc ) target_include_directories(libsimulate PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_compile_definitions(libsimulate PUBLIC MJSIMULATE_STATIC) +target_compile_definitions(libsimulate PUBLIC LIBSIMULATE_DLL_EXPORTS mjGLFW_DYNAMIC_SYMBOLS) target_compile_options(libsimulate PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) -target_link_libraries(libsimulate PUBLIC glfw lodepng mujoco::mujoco) +target_link_libraries(libsimulate PUBLIC lodepng glfw mujoco::mujoco) target_link_options(libsimulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) set_target_properties( - mjsimulate PROPERTIES VERSION "${mujoco_VERSION}" PUBLIC_HEADER "simulate.h" + libsimulate PROPERTIES VERSION "${mujoco_VERSION}" PUBLIC_HEADER "simulate.h" ) if(APPLE) @@ -126,6 +123,31 @@ if(APPLE) target_link_libraries(libsimulate PUBLIC "-framework Cocoa") endif() +# Simulate static library +add_library(libsimulatestatic STATIC simulate.cc uitools.cc glfw_dispatch.cc) +add_library(mujoco::libsimulatestatic ALIAS libsimulatestatic) +target_sources( + libsimulatestatic + PUBLIC simulate.h + array_safety.h + glfw_dispatch.h + uitools.h +) +target_include_directories(libsimulatestatic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(libsimulatestatic PUBLIC LIBSIMULATE_STATIC) +target_compile_options(libsimulatestatic PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) +target_link_libraries(libsimulatestatic PUBLIC lodepng glfw mujoco::mujoco) +target_link_options(libsimulatestatic PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) + +set_target_properties( + libsimulatestatic PROPERTIES VERSION "${mujoco_VERSION}" PUBLIC_HEADER "simulate.h" +) + +if(APPLE) + target_sources(libsimulatestatic PRIVATE macos_save.mm) + target_link_libraries(libsimulatestatic PUBLIC "-framework Cocoa") +endif() + # Build simulate executable if(APPLE) set(SIMULATE_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../dist/mujoco.icns) @@ -150,7 +172,7 @@ endif() target_link_libraries( simulate - libsimulate + libsimulatestatic mujoco::mujoco glfw Threads::Threads @@ -225,7 +247,7 @@ if(_INSTALL_SIMULATE) target_add_rpath( TARGETS - mjsimulate + libsimulate INSTALL_DIRECTORY "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}" LIB_DIRS @@ -245,7 +267,7 @@ if(_INSTALL_SIMULATE) ) install( - TARGETS mjsimulate + TARGETS libsimulate EXPORT ${PROJECT_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT simulate LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT simulate diff --git a/simulate/glfw_dispatch.cc b/simulate/glfw_dispatch.cc index 2d6e7bcd..c6a1d753 100644 --- a/simulate/glfw_dispatch.cc +++ b/simulate/glfw_dispatch.cc @@ -80,6 +80,7 @@ const struct Glfw& Glfw(void* dlhandle) { // go/keep-sorted start mjGLFW_INITIALIZE_SYMBOL(glfwCreateWindow); + mjGLFW_INITIALIZE_SYMBOL(glfwDestroyWindow); mjGLFW_INITIALIZE_SYMBOL(glfwGetCursorPos); mjGLFW_INITIALIZE_SYMBOL(glfwGetFramebufferSize); mjGLFW_INITIALIZE_SYMBOL(glfwGetKey); @@ -115,10 +116,6 @@ const struct Glfw& Glfw(void* dlhandle) { #undef mjGLFW_INITIALIZE_SYMBOL -#if defined(mjGLFW_DYNAMIC_SYMBOLS) && !defined(_MSC_VER) - dlclose(dlhandle); -#endif - return glfw; }(); return glfw; diff --git a/simulate/glfw_dispatch.h b/simulate/glfw_dispatch.h index 5dfc1d98..3a44ead2 100644 --- a/simulate/glfw_dispatch.h +++ b/simulate/glfw_dispatch.h @@ -25,6 +25,7 @@ struct Glfw { #define mjGLFW_DECLARE_SYMBOL(func) decltype(&::func) func // go/keep-sorted start mjGLFW_DECLARE_SYMBOL(glfwCreateWindow); + mjGLFW_DECLARE_SYMBOL(glfwDestroyWindow); mjGLFW_DECLARE_SYMBOL(glfwGetCursorPos); mjGLFW_DECLARE_SYMBOL(glfwGetFramebufferSize); mjGLFW_DECLARE_SYMBOL(glfwGetKey); diff --git a/simulate/main.cc b/simulate/main.cc index 266430e3..926ba732 100644 --- a/simulate/main.cc +++ b/simulate/main.cc @@ -337,7 +337,10 @@ void PhysicsLoop(mj::Simulate& sim) { mjData* dnew = nullptr; if (mnew) dnew = mj_makeData(mnew); if (dnew) { - sim.load(sim.dropfilename, mnew, dnew, true); + sim.load(sim.dropfilename, mnew, dnew); + + if (d) mj_deleteData(d); + if (m) mj_deleteModel(m); m = mnew; d = dnew; @@ -356,7 +359,10 @@ void PhysicsLoop(mj::Simulate& sim) { mjData* dnew = nullptr; if (mnew) dnew = mj_makeData(mnew); if (dnew) { - sim.load(sim.filename, mnew, dnew, true); + sim.load(sim.filename, mnew, dnew); + + if (d) mj_deleteData(d); + if (m) mj_deleteModel(m); m = mnew; d = dnew; @@ -482,7 +488,7 @@ void PhysicsThread(mj::Simulate* sim, const char* filename) { m = LoadModel(filename, *sim); if (m) d = mj_makeData(m); if (d) { - sim->load(filename, m, d, true); + sim->load(filename, m, d); mj_forward(m, d); // allocate ctrlnoise diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 5f13406e..941f5de5 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -934,7 +934,7 @@ void copykey(mj::Simulate* sim) { mju::strcat_arr(clipboard, "'/>"); // copy to clipboard - Glfw().glfwSetClipboardString(sim->window, clipboard); + Glfw().glfwSetClipboardString(reinterpret_cast(sim->window), clipboard); } // millisecond timer, for MuJoCo built-in profiler @@ -974,7 +974,7 @@ void copycamera(mj::Simulate* sim) { camera[0].up[0], camera[0].up[1], camera[0].up[2]); // copy spec into clipboard - Glfw().glfwSetClipboardString(sim->window, clipboard); + Glfw().glfwSetClipboardString(reinterpret_cast(sim->window), clipboard); } // update UI 0 when MuJoCo structures change (except for joint sliders) @@ -1035,7 +1035,7 @@ void uiLayout(mjuiState* state) { // rect 0: entire framebuffer rect[0].left = 0; rect[0].bottom = 0; - Glfw().glfwGetFramebufferSize(sim->window, &rect[0].width, &rect[0].height); + Glfw().glfwGetFramebufferSize(reinterpret_cast(sim->window), &rect[0].width, &rect[0].height); // rect 1: UI 0 rect[1].left = 0; @@ -1128,21 +1128,24 @@ void uiEvent(mjuiState* state) { break; case 9: // Full screen - if (Glfw().glfwGetWindowMonitor(sim->window)) { + if (Glfw().glfwGetWindowMonitor(reinterpret_cast(sim->window))) { // restore window from saved data - Glfw().glfwSetWindowMonitor(sim->window, nullptr, sim->windowpos[0], sim->windowpos[1], + Glfw().glfwSetWindowMonitor(reinterpret_cast(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 - Glfw().glfwGetWindowPos(sim->window, sim->windowpos, sim->windowpos+1); - Glfw().glfwGetWindowSize(sim->window, sim->windowsize, sim->windowsize+1); + Glfw().glfwGetWindowPos(reinterpret_cast(sim->window), sim->windowpos, sim->windowpos+1); + Glfw().glfwGetWindowSize(reinterpret_cast(sim->window), sim->windowsize, sim->windowsize+1); // switch - Glfw().glfwSetWindowMonitor(sim->window, Glfw().glfwGetPrimaryMonitor(), 0, 0, - sim->vmode.width, sim->vmode.height, sim->vmode.refreshRate); + Glfw().glfwSetWindowMonitor(reinterpret_cast(sim->window), Glfw().glfwGetPrimaryMonitor(), 0, 0, + reinterpret_cast(sim->vmode)->width, + reinterpret_cast(sim->vmode)->height, + reinterpret_cast(sim->vmode)->refreshRate); } // reinstante vsync, just in case @@ -1155,8 +1158,8 @@ void uiEvent(mjuiState* state) { } // modify UI - uiModify(sim->window, &sim->ui0, state, &sim->con); - uiModify(sim->window, &sim->ui1, state, &sim->con); + uiModify(reinterpret_cast(sim->window), &sim->ui0, state, &sim->con); + uiModify(reinterpret_cast(sim->window), &sim->ui1, state, &sim->con); } // simulation section @@ -1263,7 +1266,7 @@ void uiEvent(mjuiState* state) { 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); + uiModify(reinterpret_cast(sim->window), &sim->ui1, state, &sim->con); } // remake control section if actuator group changed @@ -1271,7 +1274,7 @@ void uiEvent(mjuiState* state) { 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); + uiModify(reinterpret_cast(sim->window), &sim->ui1, state, &sim->con); } } @@ -1574,11 +1577,9 @@ void Simulate::applyforceperturbations() { //------------------------- 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) { + mjData* dnew) { this->mnew = mnew; this->dnew = dnew; - this->delete_old_m_d = delete_old_m_d; mju::strcpy_arr(this->filename, file); { @@ -1594,16 +1595,6 @@ void Simulate::load(const char* file, //------------------------------------- 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; @@ -1626,10 +1617,10 @@ void Simulate::loadmodel() { 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) { + if (reinterpret_cast(this->window) && this->m->names) { char title[200] = "Simulate : "; mju::strcat_arr(title, this->m->names); - Glfw().glfwSetWindowTitle(this->window, title); + Glfw().glfwSetWindowTitle(reinterpret_cast(this->window), title); } // set keyframe range and divisions @@ -1641,8 +1632,8 @@ void Simulate::loadmodel() { makesections(this); // full ui update - uiModify(this->window, &this->ui0, &this->uistate, &this->con); - uiModify(this->window, &this->ui1, &this->uistate, &this->con); + uiModify(reinterpret_cast(this->window), &this->ui0, &this->uistate, &this->con); + uiModify(reinterpret_cast(this->window), &this->ui1, &this->uistate, &this->con); updatesettings(this); // clear request @@ -1758,7 +1749,7 @@ void Simulate::render() { } // finalize - Glfw().glfwSwapBuffers(this->window); + Glfw().glfwSwapBuffers(reinterpret_cast(this->window)); return; } @@ -1865,13 +1856,13 @@ void Simulate::render() { } // finalize - Glfw().glfwSwapBuffers(this->window); + Glfw().glfwSwapBuffers(reinterpret_cast(this->window)); } // clear callbacks registered in external structures void Simulate::clearcallback() { - uiClearCallback(this->window); + uiClearCallback(reinterpret_cast(this->window)); } void Simulate::renderloop() { @@ -1883,25 +1874,27 @@ void Simulate::renderloop() { Glfw().glfwWindowHint(GLFW_VISIBLE, 1); // get videomode and save - this->vmode = *Glfw().glfwGetVideoMode(Glfw().glfwGetPrimaryMonitor()); + this->vmode = Glfw().glfwGetVideoMode(Glfw().glfwGetPrimaryMonitor()); // use videomode refreshrate if nonzero - if (this->vmode.refreshRate) this->refreshRate = this->vmode.refreshRate; + if (reinterpret_cast(this->vmode)->refreshRate) + this->refreshRate = reinterpret_cast(this->vmode)->refreshRate; // create window - this->window = Glfw().glfwCreateWindow((2*this->vmode.width)/3, (2*this->vmode.height)/3, + this->window = Glfw().glfwCreateWindow((2*reinterpret_cast(this->vmode)->width)/3, + (2*reinterpret_cast(this->vmode)->height)/3, "Simulate", nullptr, nullptr); - if (!this->window) { + if (!reinterpret_cast(this->window)) { Glfw().glfwTerminate(); mju_error("could not create window"); } // save window position and size - Glfw().glfwGetWindowPos(this->window, this->windowpos, this->windowpos+1); - Glfw().glfwGetWindowSize(this->window, this->windowsize, this->windowsize+1); + Glfw().glfwGetWindowPos(reinterpret_cast(this->window), this->windowpos, this->windowpos+1); + Glfw().glfwGetWindowSize(reinterpret_cast(this->window), this->windowsize, this->windowsize+1); // make context current, set v-sync - Glfw().glfwMakeContextCurrent(this->window); + Glfw().glfwMakeContextCurrent(reinterpret_cast(this->window)); Glfw().glfwSwapInterval(this->vsync); // init abstract visualization @@ -1915,7 +1908,7 @@ void Simulate::renderloop() { mjv_makeScene(nullptr, &this->scn, maxgeom); // select default font - int fontscale = uiFontScale(this->window); + int fontscale = uiFontScale(reinterpret_cast(this->window)); this->font = fontscale/50 - 1; // make empty context @@ -1939,7 +1932,7 @@ void Simulate::renderloop() { // set GLFW callbacks this->uistate.userdata = this; - uiSetCallback(this->window, &this->uistate, uiEvent, uiLayout, uiRender, uiDrop); + uiSetCallback(reinterpret_cast(this->window), &this->uistate, uiEvent, uiLayout, uiRender, uiDrop); // populate uis with standard sections this->ui0.userdata = this; @@ -1948,11 +1941,11 @@ void Simulate::renderloop() { 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); + uiModify(reinterpret_cast(this->window), &this->ui0, &this->uistate, &this->con); + uiModify(reinterpret_cast(this->window), &this->ui1, &this->uistate, &this->con); // run event loop - while (!Glfw().glfwWindowShouldClose(this->window) && !this->exitrequest.load()) { + while (!Glfw().glfwWindowShouldClose(reinterpret_cast(this->window)) && !this->exitrequest.load()) { { const std::lock_guard lock(this->mtx); @@ -1979,6 +1972,13 @@ void Simulate::renderloop() { this->clearcallback(); mjv_freeScene(&this->scn); mjr_freeContext(&this->con); + + Glfw().glfwDestroyWindow(reinterpret_cast(this->window)); +} + +//------------------------------------ setup the glfw dispatch table ------------------------------- +void setglfwdlhandle(void* dlhandle) { + Glfw(dlhandle); } } // namespace mujoco diff --git a/simulate/simulate.h b/simulate/simulate.h index 4091ba6e..f61a3021 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -23,17 +23,17 @@ #include #include -#ifdef MJSIMULATE_STATIC +#ifdef LIBSIMULATE_STATIC // static library - #define MJSIMULATEAPI - #define MJSIMULATELOCAL + #define LIBSIMULATEAPI + #define LIBSIMULATELOCAL #else - #ifdef MJSIMULATE_DLL_EXPORTS - #define MJSIMULATEAPI MUJOCO_HELPER_DLL_EXPORT + #ifdef LIBSIMULATE_DLL_EXPORTS + #define LIBSIMULATEAPI MUJOCO_HELPER_DLL_EXPORT #else - #define MJSIMULATEAPI MUJOCO_HELPER_DLL_IMPORT + #define LIBSIMULATEAPI MUJOCO_HELPER_DLL_IMPORT #endif - #define MJSIMULATELOCAL MUJOCO_HELPER_DLL_LOCAL + #define LIBSIMULATELOCAL MUJOCO_HELPER_DLL_LOCAL #endif namespace mujoco { @@ -41,7 +41,7 @@ namespace mujoco { //-------------------------------- global ----------------------------------------------- // Simulate states not contained in MuJoCo structures -class MJSIMULATEAPI Simulate { +class LIBSIMULATEAPI Simulate { public: // create object and initialize the simulate ui Simulate() = default; @@ -54,7 +54,7 @@ class MJSIMULATEAPI Simulate { // 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); + void load(const char* file, mjModel* m, mjData* d); // functions below are used by the renderthread // load mjb or xml model that has been requested by load() @@ -79,7 +79,6 @@ class MJSIMULATEAPI Simulate { // model and data to be visualized mjModel* mnew = nullptr; mjData* dnew = nullptr; - bool delete_old_m_d = false; mjModel* m = nullptr; mjData* d = nullptr; @@ -163,12 +162,14 @@ class MJSIMULATEAPI Simulate { mjvFigure figsensor = {}; // OpenGL rendering and UI - GLFWvidmode vmode = {}; + // Use void* for GLFW objects to avoid requiring users of the shared library + // from needing GLFW headers (in particular Python should not need them) + const void* vmode = {}; // const GLFWvidmode* int refreshRate = 60; int windowpos[2] = {0}; int windowsize[2] = {0}; mjrContext con = {}; - GLFWwindow* window = nullptr; + void* window = nullptr; // GLFWwindow* mjuiState uistate = {}; mjUI ui0 = {}; mjUI ui1 = {}; @@ -228,6 +229,10 @@ class MJSIMULATEAPI Simulate { char info_content[Simulate::kMaxFilenameLength] = {0}; }; +// setup the glfw dispatch table +// if set, must be called prior to other Simulate functions +LIBSIMULATEAPI void setglfwdlhandle(void* dlhandle); + } // namespace mujoco #endif From e12343ae517e85ddb238a15c87468c3c6261198a Mon Sep 17 00:00:00 2001 From: Levi Burner Date: Tue, 4 Oct 2022 14:55:26 -0400 Subject: [PATCH 3/4] remove stray #include --- simulate/simulate.h | 1 - 1 file changed, 1 deletion(-) diff --git a/simulate/simulate.h b/simulate/simulate.h index f61a3021..f9c03d08 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -20,7 +20,6 @@ #include #include -#include #include #ifdef LIBSIMULATE_STATIC From 8cc983af5b727aec657b04f7010eff50784e6dcc Mon Sep 17 00:00:00 2001 From: Levi Burner Date: Tue, 4 Oct 2022 15:11:19 -0400 Subject: [PATCH 4/4] Remove stray build option --- python/mujoco/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 07757aec..b3b9ce68 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -411,7 +411,6 @@ target_link_libraries( mujoco raw structs_header) -target_link_options(_simulate PRIVATE -Wl,-no-as-needed) set(LIBRARIES_FOR_WHEEL "$"