diff --git a/doc/changelog.rst b/doc/changelog.rst index 1c05a6ec..200a9548 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,12 @@ Changelog Upcoming version (not yet released) ----------------------------------- +Python bindings +^^^^^^^^^^^^^^^ + +- The ``simulate`` GUI is now available through the ``mujoco`` Python package. See :ref:`documentation` for + details. (Contribution by `Levi Burner `_.) + General ^^^^^^^ diff --git a/doc/python.rst b/doc/python.rst index a2942e42..2b0bf8b2 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -152,6 +152,38 @@ Minimal example mujoco.mj_step(model, data) print(data.geom_xpos) +.. _PyGUI: + +Interactive visualizer +---------------------- + +MuJoCo's interactive GUI (also known as the ``simulate`` application) is available as part of the Python package. +Three distinct use cases are supported: + +- Launching as a standalone application: + + * ``python -m mujoco.simulate`` launches an empty visualization session, where a model can be loaded by drag-and-drop. + * ``python -m mujoco.simulate --mjcf=/path/to/some/mjcf.xml`` launches a visualization session for the specified + model file. + +- Launching from a Python program/script -- import the module via ``from mujoco import simulate`` and launch the GUI + using one of the following invocations: + + * ``simulate.launch()`` launches an empty visualization session, where a model can be loaded by drag-and-drop. + * ``simulate.launch(model)`` launches a visualzation session for the given ``mjModel`` where the visualizer + internally creates its own instance of ``mjData`` + * ``simulate.launch(model, data)`` is the same as above, except that the visualizer operates directly on the given + ``mjData`` instance -- upon exit the ``data`` object will have been modified. + +- Launching from an interactive Python session (aka REPL): when working interactively either in a ``python`` or + ``ipython`` shell, the visualizer can be launched in a "passive" mode via ``simulate.launch_repl(model, data)``, where + the user remains in full control of modifying or stepping the physics. In this mode, the user can interact with the + visualizer using the mouse and keyboard as usual, however the physics will be frozen unless the user explicitly calls + ``mj_step`` (or perform any other modification of the ``mjData`` or ``mjModel``) in the REPL terminal. Note that since + the visualizer does not modify ``mjData`` in this mode, mouse-drag perturbations will not work unless the user + explicitly handles incoming GUI perturbation events in the REPL session. + + .. _PyNamed: Named access diff --git a/python/MANIFEST.in b/python/MANIFEST.in index b3dc4021..83d8996a 100644 --- a/python/MANIFEST.in +++ b/python/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE *.md -recursive-include mujoco *.h *.cc CMakeLists.txt +recursive-include mujoco *.h *.cc *.mm CMakeLists.txt Simulate*.cmake recursive-include cmake *.cmake diff --git a/python/make_sdist.sh b/python/make_sdist.sh index 3308dee6..326cb23a 100755 --- a/python/make_sdist.sh +++ b/python/make_sdist.sh @@ -53,6 +53,9 @@ cp "${package_dir}"/../LICENSE . mkdir cmake cp "${package_dir}"/../cmake/*.cmake cmake +# Copy over Simulate source code. +cp -r "${package_dir}"/../simulate mujoco + python setup.py sdist --formats=gztar tar -tf dist/mujoco-*.tar.gz popd diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index eec5d717..1296c8a0 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -119,6 +119,7 @@ if(NOT TARGET mujoco) ) set_target_properties(mujoco PROPERTIES IMPORTED_SONAME "${MUJOCO_SONAME}") endif() + add_library(mujoco::mujoco ALIAS mujoco) endif() # ==================== ABSEIL ================================================== @@ -190,6 +191,9 @@ findorfetch( ) # ==================== MUJOCO PYTHON BINDINGS ================================== +set(SIMULATE_BUILD_EXECUTABLE OFF) +set(SIMULATE_GLFW_DYNAMIC_SYMBOLS ON) +add_subdirectory(simulate) add_subdirectory(util) @@ -376,6 +380,14 @@ target_link_libraries( structs_header ) +mujoco_pybind11_module(_simulate simulate.cc) +target_link_libraries( + _simulate + PRIVATE mujoco + mujoco::libsimulate + raw + structs_header) + set(LIBRARIES_FOR_WHEEL "$" "$" @@ -384,6 +396,7 @@ set(LIBRARIES_FOR_WHEEL "$" "$" "$" + "$" "$" "$" ) @@ -411,6 +424,7 @@ if(MUJOCO_PYTHON_MAKE_WHEEL) _functions _render _rollout + _simulate _structs mujoco ) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc new file mode 100644 index 00000000..3d301b17 --- /dev/null +++ b/python/mujoco/simulate.cc @@ -0,0 +1,143 @@ +// 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 "raw.h" +#include "structs.h" +#include +#include + +namespace mujoco::python { +namespace { +PYBIND11_MODULE(_simulate, pymodule) { + namespace py = ::pybind11; + using SimulateMutex = decltype(mujoco::Simulate::mtx); + + py::class_(pymodule, "SimulateMutex") + .def( + "__enter__", [](SimulateMutex& mtx) { mtx.lock(); }, + py::call_guard()) + .def( + "__exit__", + [](SimulateMutex& mtx, py::handle, py::handle, py::handle) { + mtx.unlock(); + }, + py::call_guard()); + + py::class_(pymodule, "Simulate") + .def(py::init<>()) + .def( + "renderloop", + [](mujoco::Simulate& simulate) { simulate.renderloop(); }, + py::call_guard()) + .def( + "load", + [](mujoco::Simulate& simulate, MjModelWrapper& m, MjDataWrapper& d) { + simulate.load("", m.get(), d.get()); + }, + py::call_guard()) + .def("applyposepertubations", &mujoco::Simulate::applyposepertubations, + py::call_guard()) + .def("applyforceperturbations", + &mujoco::Simulate::applyforceperturbations, + py::call_guard()) + + .def( + "lock", + [](mujoco::Simulate& simulate) -> SimulateMutex& { + return simulate.mtx; + }, + py::call_guard(), + py::return_value_policy::reference) + .def_readonly("ctrlnoisestd", &mujoco::Simulate::ctrlnoisestd, + py::call_guard()) + .def_readonly("ctrlnoiserate", &mujoco::Simulate::ctrlnoiserate, + py::call_guard()) + + .def_readonly("real_time_index", &mujoco::Simulate::realTimeIndex, + py::call_guard()) + .def_readwrite("speed_changed", &mujoco::Simulate::speedChanged, + py::call_guard()) + .def_readwrite("measured_slowdown", &mujoco::Simulate::measuredSlowdown, + py::call_guard()) + .def_readonly("refresh_rate", &mujoco::Simulate::refreshRate, + py::call_guard()) + + .def_readonly("busywait", &mujoco::Simulate::busywait, + py::call_guard()) + .def_readonly("run", &mujoco::Simulate::run, + py::call_guard()) + + .def_property_readonly( + "exitrequest", + [](mujoco::Simulate& simulate) { + return simulate.exitrequest.load(); + }, + py::call_guard()) + + .def_property_readonly( + "uiloadrequest", + [](mujoco::Simulate& simulate) { + return simulate.uiloadrequest.load(); + }, + py::call_guard()) + .def( + "uiloadrequest_decrement", + [](mujoco::Simulate& simulate) { + simulate.uiloadrequest.fetch_sub(1); + }, + py::call_guard()) + + .def_property( + "droploadrequest", + [](mujoco::Simulate& simulate) { + return simulate.droploadrequest.load(); + }, + [](mujoco::Simulate& simulate, bool droploadrequest) { + simulate.droploadrequest.store(droploadrequest); + }, + py::call_guard()) + .def_property_readonly( + "dropfilename", + [](mujoco::Simulate& simulate) -> std::string { + return simulate.dropfilename; + }, + py::call_guard()) + .def_property_readonly( + "filename", + [](mujoco::Simulate& simulate) -> std::string { + return simulate.filename; + }, + py::call_guard()) + .def_property( + "load_error", + [](mujoco::Simulate& simulate) -> std::string { + return simulate.loadError; + }, + [](mujoco::Simulate& simulate, const std::string& error) { + std::strncpy(simulate.loadError, error.c_str(), + simulate.kMaxFilenameLength); + }); + + pymodule.def("setglfwdlhandle", [](std::uintptr_t dlhandle) { + mujoco::setglfwdlhandle(reinterpret_cast(dlhandle)); + }); +} + +} // namespace +} // namespace mujoco::python diff --git a/python/mujoco/simulate.py b/python/mujoco/simulate.py new file mode 100644 index 00000000..8c1f89ee --- /dev/null +++ b/python/mujoco/simulate.py @@ -0,0 +1,266 @@ +# 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 bindings for the Simulate GUI.""" + +import atexit +import code +import inspect +import math +import os +import threading +import time +import typing +from typing import Callable, Optional + +import glfw +import mujoco +from mujoco import _simulate +import numpy as np + +if not glfw._glfw: # pylint: disable=protected-access + raise RuntimeError('GLFW dynamic library handle is not available') +else: + _simulate.setglfwdlhandle(glfw._glfw._handle) # pylint: disable=protected-access + +# Logarithmically spaced realtime slow-down coefficients (percent). +PERCENT_REALTIME = ( + 100, 80, 66, 50, 40, 33, 25, 20, 16, 13, + 10, 8, 6.6, 5, 4, 3.3, 2.5, 2, 1.6, 1.3, + 1, 0.8, 0.66, 0.5, 0.4, 0.33, 0.25, 0.2, 0.16, 0.13, + 0.1 +) + +# Maximum time mis-alignment before re-sync. +MAX_SYNC_MISALIGN = 0.1 + +# Fraction of refresh available for simulation. +SIM_REFRESH_FRACTION = 0.7 + +CallbackType = Callable[[mujoco.MjModel, mujoco.MjData], None] + +Simulate = _simulate.Simulate + + +def _reload_from_file(simulate: Simulate, filename: str): + """Loads an MJCF model into the Simulate GUI.""" + try: + m = mujoco.MjModel.from_xml_path(filename) + except mujoco.FatalError as e: + m = None + simulate.load_error = str(e) + + if m is not None: + d = mujoco.MjData(m) + simulate.load(m, d) + mujoco.mj_forward(m, d) + else: + d = None + + return m, d + + +def _physics_loop(simulate: Simulate, + m: Optional[mujoco.MjModel], + d: Optional[mujoco.MjData]): + """Physics loop for the Simulate GUI, to be run in a separate thread.""" + ctrlnoise = None + if m is not None: + ctrlnoise = np.zeros((m.nu,)) + + # CPU-sim synchronization point. + synccpu = 0.0 + syncsim = 0.0 + + # Run until asked to exit. + while not simulate.exitrequest: + if simulate.droploadrequest: + simulate.droploadrequest = 0 + new_m, new_d = _reload_from_file(simulate, simulate.dropfilename) + if new_m is not None: + m = new_m + d = new_d + ctrlnoise = np.zeros((m.nu,)) + + if simulate.uiloadrequest: + simulate.uiloadrequest_decrement() + new_m, new_d = _reload_from_file(simulate, simulate.dropfilename) + if new_m is not None: + m = new_m + d = new_d + ctrlnoise = np.zeros((m.nu,)) + + # Sleep for 1 ms or yield, to let main thread run. + if simulate.run != 0 and simulate.busywait != 0: + time.sleep(0) + else: + time.sleep(0.001) + + with simulate.lock(): + if m is not None: + assert d is not None + if simulate.run: + # Record CPU time at start of iteration. + 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 (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] + + # Requested slow-down factor. + slowdown = 100 / PERCENT_REALTIME[simulate.real_time_index] + + # Misalignment: distance from target sim time > MAX_SYNC_MISALIGN. + misaligned = abs(elapsedcpu / slowdown - + elapsedsim) > MAX_SYNC_MISALIGN + + # Out-of-sync (for any reason): reset sync times, step. + if (elapsedsim < 0 or elapsedcpu < 0 or synccpu == 0 or misaligned or + simulate.speed_changed): + # Re-sync. + synccpu = startcpu + syncsim = d.time + 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: step until ahead of cpu. + else: + measured = False + prevsim = d.time + refreshtime = SIM_REFRESH_FRACTION / simulate.refresh_rate + # 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.measured_slowdown = elapsedcpu / elapsedsim + measured = True + + # Clear old perturbations, apply new. + d.xfrc_applied[:, :] = 0 + simulate.applyposepertubations(0) # Move mocap bodies only. + simulate.applyforceperturbations() + + # Call mj_step. + mujoco.mj_step(m, d) + + # Break if reset. + if d.time < prevsim: + break + else: # simulate.run is False: GUI is paused. + # 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) + + +def launch(model: Optional[mujoco.MjModel] = None, + data: Optional[mujoco.MjData] = None, + *, + run_physics_thread: bool = True) -> None: + """Launches the Simulate GUI.""" + if model is None and data is not None: + raise ValueError('mjData is specified but mjModel is not') + elif model is not None and data is None: + data = mujoco.MjData(model) + + # The simulate object encapsulates the UI. + simulate = Simulate() + + # Initialize GLFW. + if not glfw.init(): + raise mujoco.FatalError('could not initialize GLFW') + + atexit.register(glfw.terminate) + + if run_physics_thread: + physics_thread = threading.Thread( + target=_physics_loop, args=(simulate, model, data)) + physics_thread.start() + + # Load the initial model, if one is given. + if model is not None: + t = threading.Thread(target=simulate.load, args=(model, data)) + t.start() + del t + + simulate.renderloop() + + if run_physics_thread: + physics_thread.join() + + +def launch_repl(model: mujoco.MjModel, data: mujoco.MjData) -> None: + """EXPERIMENTAL FEATURE: Launches the Simulate GUI in REPL mode.""" + if typing.TYPE_CHECKING: + launch(model, data, run_physics_thread=False) + return + + try: + import IPython # pylint: disable=g-import-not-at-top + has_ipython = True + except ImportError: + has_ipython = False + + def start_shell(global_variables): + if has_ipython and IPython.get_ipython() is not None: + locals().update(global_variables) + IPython.embed() + else: + code.InteractiveConsole(locals=global_variables).interact() + + repl_thread = threading.Thread( + target=start_shell, args=(inspect.stack()[1][0].f_globals,)) + repl_thread.start() + launch(model, data, run_physics_thread=False) + repl_thread.join() + + +if __name__ == '__main__': + from absl import app # pylint: disable=g-import-not-at-top + from absl import flags # pylint: disable=g-import-not-at-top + + _MJCF_PATH = flags.DEFINE_string('mjcf', None, 'Path to MJCF file.') + + def main(argv) -> None: + del argv + if _MJCF_PATH.value is not None: + model = mujoco.MjModel.from_xml_path(os.path.expanduser(_MJCF_PATH.value)) + launch(model) + else: + launch() + + app.run(main) diff --git a/python/setup.py b/python/setup.py index e622cf7f..2327f35c 100644 --- a/python/setup.py +++ b/python/setup.py @@ -70,7 +70,6 @@ def get_external_lib_patterns(): else: return ['libmujoco.so.*'] - def get_plugin_lib_patterns(): if platform.system() == 'Windows': return ['*.dll'] @@ -322,6 +321,7 @@ setup( CMakeExtension('mujoco._functions'), CMakeExtension('mujoco._render'), CMakeExtension('mujoco._rollout'), + CMakeExtension('mujoco._simulate'), CMakeExtension('mujoco._structs'), ], python_requires='>=3.7', diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index e80e1bc7..dfbffcb2 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -41,6 +41,9 @@ if(APPLE) enable_language(OBJCXX) endif() +option(SIMULATE_BUILD_EXECUTABLE "Build the simulate executable binary." ON) +option(SIMULATE_GLFW_DYNAMIC_SYMBOLS "Whether to resolve GLFW symbols dynamically." OFF) + # Check if we are building as standalone project. set(SIMULATE_STANDALONE OFF) set(_INSTALL_SIMULATE ON) @@ -114,7 +117,8 @@ target_sources( target_include_directories(libsimulate PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(libsimulate PUBLIC MJSIMULATE_STATIC) target_compile_options(libsimulate PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) -target_link_libraries(libsimulate PUBLIC glfw lodepng mujoco::mujoco) +target_link_libraries(libsimulate PUBLIC lodepng mujoco::mujoco) +target_include_directories(libsimulate PUBLIC $) target_link_options(libsimulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) if(APPLE) @@ -122,126 +126,132 @@ if(APPLE) target_link_libraries(libsimulate PUBLIC "-framework Cocoa") endif() +if(SIMULATE_GLFW_DYNAMIC_SYMBOLS) + target_compile_definitions(libsimulate PUBLIC mjGLFW_DYNAMIC_SYMBOLS) +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/simulate.rc) -else() - set(SIMULATE_RESOURCE_FILES "") -endif() +if(SIMULATE_BUILD_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/simulate.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() + 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 - libsimulate - mujoco::mujoco - glfw - Threads::Threads - lodepng -) - -target_link_options(simulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) - -if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) - set_target_properties( + target_link_libraries( 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." + libsimulate + mujoco::mujoco + glfw + Threads::Threads + lodepng ) - macro(embed_in_bundle target) - add_dependencies(${target} simulate) + target_link_options(simulate PRIVATE ${MUJOCO_SIMULATE_LINK_OPTIONS}) + + if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) set_target_properties( - ${target} + simulate PROPERTIES INSTALL_RPATH @executable_path/../Frameworks BUILD_WITH_INSTALL_RPATH TRUE - RUNTIME_OUTPUT_DIRECTORY $ + 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." ) - 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 + 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() endif() 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..6b908bc1 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); + + mj_deleteData(d); + 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); + + mj_deleteData(d); + 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 326c2798..792a937e 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -1575,11 +1575,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); { @@ -1595,16 +1593,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; @@ -1618,7 +1606,8 @@ void Simulate::loadmodel() { this->pert.skinselect = -1; // align and scale view unless reloading the same file - if (mju::strcmp_arr(this->filename, this->previous_filename)) { + if (this->filename[0] && + mju::strcmp_arr(this->filename, this->previous_filename)) { alignscale(this); mju::strcpy_arr(this->previous_filename, this->filename); } @@ -1980,6 +1969,13 @@ void Simulate::renderloop() { this->clearcallback(); mjv_freeScene(&this->scn); mjr_freeContext(&this->con); + + Glfw().glfwDestroyWindow(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..a1ca5787 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -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; @@ -228,6 +227,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 +MJSIMULATEAPI void setglfwdlhandle(void* dlhandle); + } // namespace mujoco #endif