diff --git a/cmake/FindMuJoCo.cmake b/cmake/FindMuJoCo.cmake index 5b670758..39799a5b 100644 --- a/cmake/FindMuJoCo.cmake +++ b/cmake/FindMuJoCo.cmake @@ -38,18 +38,35 @@ if (NOT MuJoCo_FOUND) return() endif() - file(GLOB mujoco_library_path "${MUJOCO_PATH}/libmujoco.so.*") - if (NOT mujoco_library_path) + set(_mujoco_library_globs) + if (APPLE) + list(APPEND _mujoco_library_globs "${MUJOCO_PATH}/libmujoco.*.dylib") + elseif (WIN32) + list(APPEND _mujoco_library_globs "${MUJOCO_PATH}/mujoco.dll") + list(APPEND _mujoco_library_globs "${MUJOCO_PATH}/bin/mujoco.dll") + else() + list(APPEND _mujoco_library_globs "${MUJOCO_PATH}/libmujoco.so.*") + endif() + + file(GLOB mujoco_library_paths LIST_DIRECTORIES FALSE ${_mujoco_library_globs}) + list(LENGTH mujoco_library_paths _mujoco_library_count) + if (_mujoco_library_count EQUAL 0) set(MuJoCo_FOUND FALSE) if (MuJoCo_FIND_REQUIRED) - message(FATAL_ERROR "Could not find MuJoCo. Please install MuJoCo using pip 4.") + message(FATAL_ERROR "Could not find MuJoCo shared library. Searched: ${_mujoco_library_globs}") endif() return() endif() + list(GET mujoco_library_paths 0 mujoco_library_path) # Extract version from the library filename cmake_path(GET mujoco_library_path FILENAME mujoco_library_filename) - string(REPLACE "libmujoco.so." "" MuJoCo_VERSION "${mujoco_library_filename}") + set(MuJoCo_VERSION "") + if (mujoco_library_filename MATCHES "^libmujoco\\.so\\.(.+)$") + set(MuJoCo_VERSION "${CMAKE_MATCH_1}") + elseif (mujoco_library_filename MATCHES "^libmujoco\\.(.+)\\.dylib$") + set(MuJoCo_VERSION "${CMAKE_MATCH_1}") + endif() # Create the imported target add_library(MuJoCo::MuJoCo SHARED IMPORTED) @@ -59,6 +76,9 @@ if (NOT MuJoCo_FOUND) PROPERTIES IMPORTED_LOCATION "${mujoco_library_path}" ) + if (APPLE) + set_target_properties(MuJoCo::MuJoCo PROPERTIES IMPORTED_NO_SONAME TRUE) + endif() set(MuJoCo_FOUND TRUE) endif() diff --git a/cmake/Findpinocchio.cmake b/cmake/Findpinocchio.cmake index fcefcf5c..54e32c09 100644 --- a/cmake/Findpinocchio.cmake +++ b/cmake/Findpinocchio.cmake @@ -17,25 +17,44 @@ if (NOT pinocchio_FOUND) return() endif() - # Check if the library file exists - cmake_path(APPEND Python3_SITELIB cmeel.prefix lib libpinocchio_default.so OUTPUT_VARIABLE pinocchio_library_path) - if (NOT EXISTS ${pinocchio_library_path}) + cmake_path(APPEND Python3_SITELIB cmeel.prefix lib OUTPUT_VARIABLE pinocchio_LIBRARY_DIR) + + set(_pinocchio_default_globs) + set(_pinocchio_parsers_globs) + if (APPLE) + list(APPEND _pinocchio_default_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_default*.dylib") + list(APPEND _pinocchio_parsers_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_parsers*.dylib") + elseif (WIN32) + list(APPEND _pinocchio_default_globs "${pinocchio_LIBRARY_DIR}/pinocchio_default*.dll") + list(APPEND _pinocchio_default_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_default*.dll") + list(APPEND _pinocchio_parsers_globs "${pinocchio_LIBRARY_DIR}/pinocchio_parsers*.dll") + list(APPEND _pinocchio_parsers_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_parsers*.dll") + else() + list(APPEND _pinocchio_default_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_default.so*") + list(APPEND _pinocchio_parsers_globs "${pinocchio_LIBRARY_DIR}/libpinocchio_parsers.so*") + endif() + + file(GLOB pinocchio_library_paths LIST_DIRECTORIES FALSE ${_pinocchio_default_globs}) + list(LENGTH pinocchio_library_paths _pinocchio_library_count) + if (_pinocchio_library_count EQUAL 0) set(pinocchio_FOUND FALSE) if (pinocchio_FIND_REQUIRED) - message(FATAL_ERROR "Could not find pinocchio. Please install pinocchio using pip.") + message(FATAL_ERROR "Could not find pinocchio library. Searched: ${_pinocchio_default_globs}") endif() return() endif() + list(GET pinocchio_library_paths 0 pinocchio_library_path) - # Check if the library file exists - cmake_path(APPEND Python3_SITELIB cmeel.prefix lib libpinocchio_parsers.so OUTPUT_VARIABLE pinocchio_parsers_path) - if (NOT EXISTS ${pinocchio_parsers_path}) + file(GLOB pinocchio_parsers_paths LIST_DIRECTORIES FALSE ${_pinocchio_parsers_globs}) + list(LENGTH pinocchio_parsers_paths _pinocchio_parsers_count) + if (_pinocchio_parsers_count EQUAL 0) set(pinocchio_FOUND FALSE) if (pinocchio_FIND_REQUIRED) - message(FATAL_ERROR "Could not find pinocchio parsers path. Please install pinocchio using pip.") + message(FATAL_ERROR "Could not find pinocchio parsers library. Searched: ${_pinocchio_parsers_globs}") endif() return() endif() + list(GET pinocchio_parsers_paths 0 pinocchio_parsers_path) # Extract version from the library filename file(GLOB pinocchio_dist_info "${Python3_SITELIB}/pin-*.dist-info") diff --git a/extensions/rcs_fr3/src/pybind/CMakeLists.txt b/extensions/rcs_fr3/src/pybind/CMakeLists.txt index 38ac1483..bfa7462c 100644 --- a/extensions/rcs_fr3/src/pybind/CMakeLists.txt +++ b/extensions/rcs_fr3/src/pybind/CMakeLists.txt @@ -1,13 +1,19 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + pybind11_add_module(_core MODULE rcs.cpp) target_link_libraries(_core PRIVATE hw rcs pinocchio::all) target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) set_target_properties(franka PROPERTIES - INSTALL_RPATH "$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) set_target_properties(_core PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../rcs;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../rcs;${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) # in pip diff --git a/extensions/rcs_panda/src_fr3/pybind/CMakeLists.txt b/extensions/rcs_panda/src_fr3/pybind/CMakeLists.txt index a3304f9b..6ac9f674 100644 --- a/extensions/rcs_panda/src_fr3/pybind/CMakeLists.txt +++ b/extensions/rcs_panda/src_fr3/pybind/CMakeLists.txt @@ -1,13 +1,19 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + pybind11_add_module(_core MODULE rcs.cpp) target_link_libraries(_core PRIVATE hw rcs pinocchio::all) target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) set_target_properties(franka PROPERTIES - INSTALL_RPATH "$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) set_target_properties(_core PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../rcs;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../rcs;${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) # in pip diff --git a/extensions/rcs_robotics_library/src/pybind/CMakeLists.txt b/extensions/rcs_robotics_library/src/pybind/CMakeLists.txt index 09d0acae..976976b9 100644 --- a/extensions/rcs_robotics_library/src/pybind/CMakeLists.txt +++ b/extensions/rcs_robotics_library/src/pybind/CMakeLists.txt @@ -1,9 +1,15 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + pybind11_add_module(_core MODULE rcs.cpp) target_link_libraries(_core PRIVATE rcs mdl pinocchio::all) target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) set_target_properties(_core PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../rcs;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../rcs;${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) diff --git a/extensions/rcs_so101/src/pybind/CMakeLists.txt b/extensions/rcs_so101/src/pybind/CMakeLists.txt index de44bb17..9610f64e 100644 --- a/extensions/rcs_so101/src/pybind/CMakeLists.txt +++ b/extensions/rcs_so101/src/pybind/CMakeLists.txt @@ -1,9 +1,15 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + pybind11_add_module(_core MODULE rcs.cpp) target_link_libraries(_core PRIVATE rcs Eigen3::Eigen pinocchio::all) target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) set_target_properties(_core PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../rcs;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../rcs;${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) diff --git a/include/rcs/utils.h b/include/rcs/utils.h index 5ebc8cce..4f39ce83 100644 --- a/include/rcs/utils.h +++ b/include/rcs/utils.h @@ -37,6 +37,7 @@ Eigen::Matrix array2eigen( } void bootstrap_egl(std::uintptr_t fn_addr, std::uintptr_t display, std::uintptr_t context); +void bootstrap_gl_context(); void ensure_current(); } // namespace common diff --git a/python/rcs/_core/common.pyi b/python/rcs/_core/common.pyi index b2749517..50537ea5 100644 --- a/python/rcs/_core/common.pyi +++ b/python/rcs/_core/common.pyi @@ -321,6 +321,7 @@ def IdentityRotMatrix() -> numpy.ndarray[tuple[typing.Literal[3], typing.Literal def IdentityRotQuatVec() -> numpy.ndarray[tuple[typing.Literal[4]], numpy.dtype[numpy.float64]]: ... def IdentityTranslation() -> numpy.ndarray[tuple[typing.Literal[3]], numpy.dtype[numpy.float64]]: ... def _bootstrap_egl(fn_addr: int, display: int, context: int) -> None: ... +def _bootstrap_gl_context() -> None: ... HARDWARE: RobotPlatform # value = LATERAL_GRASP: GraspType # value = diff --git a/python/rcs/sim/egl_bootstrap.py b/python/rcs/sim/egl_bootstrap.py index 57884bde..18dd4618 100644 --- a/python/rcs/sim/egl_bootstrap.py +++ b/python/rcs/sim/egl_bootstrap.py @@ -1,41 +1,60 @@ """ -Load the EGL library, create a persistent GLContext, and register it with the C++ backend. +Bootstrap the rendering backend used by the C++ simulation camera path. -Globals prevent the library and context from being garbage-collected. -Call `bootstrap()` to complete initialization. +Linux uses MuJoCo's EGL backend. macOS uses a persistent MuJoCo GLContext +that is made current on the Python thread before C++ creates rendering +contexts. """ import ctypes import ctypes.util import os +import sys _egl_available = False _addr_make_current = None _egl_display = None _egl_context = None +_backend = None +_gl_context = None -name = ctypes.util.find_library("EGL") -if name is not None: +if sys.platform == "darwin": try: - import mujoco.egl - from mujoco.egl import GLContext - - _egl = ctypes.CDLL(name, mode=os.RTLD_LOCAL | os.RTLD_NOW) - _addr_make_current = ctypes.cast(_egl.eglMakeCurrent, ctypes.c_void_p).value - _ctx = GLContext(max_width=3840, max_height=2160) - _egl_display = int(mujoco.egl.EGL_DISPLAY.address) - _egl_context = int(_ctx._context.address) - _egl_available = True + import mujoco + + _gl_context = mujoco.GLContext(3840, 2160) + _backend = "gl_context" except Exception: pass +else: + name = ctypes.util.find_library("EGL") + if name is not None: + try: + import mujoco.egl + from mujoco.egl import GLContext + + _egl = ctypes.CDLL(name, mode=os.RTLD_LOCAL | os.RTLD_NOW) + _addr_make_current = ctypes.cast(_egl.eglMakeCurrent, ctypes.c_void_p).value + _ctx = GLContext(max_width=3840, max_height=2160) + _egl_display = int(mujoco.egl.EGL_DISPLAY.address) + _egl_context = int(_ctx._context.address) + _egl_available = True + _backend = "egl" + except Exception: + pass def bootstrap(): - if not _egl_available: - return import rcs._core as _cxx - assert _addr_make_current is not None - assert _egl_display is not None - assert _egl_context is not None - _cxx.common._bootstrap_egl(_addr_make_current, _egl_display, _egl_context) + if _backend == "gl_context": + assert _gl_context is not None + _gl_context.make_current() + _cxx.common._bootstrap_gl_context() + return + + if _backend == "egl": + assert _addr_make_current is not None + assert _egl_display is not None + assert _egl_context is not None + _cxx.common._bootstrap_egl(_addr_make_current, _egl_display, _egl_context) diff --git a/python/rcs/sim/sim.py b/python/rcs/sim/sim.py index 43a49a20..2b5faeb8 100644 --- a/python/rcs/sim/sim.py +++ b/python/rcs/sim/sim.py @@ -1,6 +1,8 @@ import atexit import contextlib import multiprocessing as mp +import shutil +import sys import typing import uuid from logging import getLogger @@ -25,6 +27,21 @@ logger = getLogger(__name__) +def _configure_spawn_executable() -> None: + if sys.platform != "darwin": + return + + mjpython = shutil.which("mjpython") + if mjpython is None: + logger.warning("mjpython was not found on PATH; passive MuJoCo viewer may fail on macOS") + return + + mp.set_executable(mjpython) + + +_configure_spawn_executable() + + # Target frames per second FPS = 60 RAW_STATE_ENCODING = "raw" diff --git a/src/pybind/CMakeLists.txt b/src/pybind/CMakeLists.txt index 3baf7822..8c5bba77 100644 --- a/src/pybind/CMakeLists.txt +++ b/src/pybind/CMakeLists.txt @@ -1,11 +1,30 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + pybind11_add_module(_core MODULE rcs.cpp) target_link_libraries(_core PRIVATE sim rcs) target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) set_target_properties(_core PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../mujoco;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../mujoco;${_rpath_origin}/../cmeel.prefix/lib" INTERPROCEDURAL_OPTIMIZATION TRUE ) + +if (APPLE) + get_target_property(_mujoco_library_path MuJoCo::MuJoCo IMPORTED_LOCATION) + cmake_path(GET _mujoco_library_path FILENAME _mujoco_library_name) + add_custom_command(TARGET _core POST_BUILD + COMMAND install_name_tool + -change "@rpath/mujoco.framework/Versions/A/${_mujoco_library_name}" + "@loader_path/../mujoco/${_mujoco_library_name}" + "$" + VERBATIM + ) +endif() + # in pip install(TARGETS _core rcs DESTINATION rcs COMPONENT python_package) install( diff --git a/src/pybind/rcs.cpp b/src/pybind/rcs.cpp index 9be15edf..25762b36 100644 --- a/src/pybind/rcs.cpp +++ b/src/pybind/rcs.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -253,6 +252,7 @@ PYBIND11_MODULE(_core, m) { common.def("_bootstrap_egl", &rcs::common::bootstrap_egl, py::arg("fn_addr"), py::arg("display"), py::arg("context")); + common.def("_bootstrap_gl_context", &rcs::common::bootstrap_gl_context); common.def("IdentityTranslation", &rcs::common::IdentityTranslation); common.def("IdentityRotMatrix", &rcs::common::IdentityRotMatrix); common.def("IdentityRotQuatVec", &rcs::common::IdentityRotQuatVec); diff --git a/src/rcs/CMakeLists.txt b/src/rcs/CMakeLists.txt index 141761d0..56391f74 100644 --- a/src/rcs/CMakeLists.txt +++ b/src/rcs/CMakeLists.txt @@ -1,7 +1,13 @@ +if (APPLE) + set(_rpath_origin "@loader_path") +else() + set(_rpath_origin "$ORIGIN") +endif() + add_library(rcs SHARED) target_include_directories(rcs PUBLIC ${CMAKE_SOURCE_DIR}/include) target_sources(rcs PRIVATE Pose.cpp Robot.cpp Kinematics.cpp utils.cpp) target_link_libraries(rcs PUBLIC Eigen3::Eigen pinocchio::all) set_target_properties(rcs PROPERTIES - INSTALL_RPATH "$ORIGIN;$ORIGIN/../cmeel.prefix/lib" + INSTALL_RPATH "${_rpath_origin};${_rpath_origin}/../cmeel.prefix/lib" ) diff --git a/src/rcs/utils.cpp b/src/rcs/utils.cpp index d348375b..cc1c82ed 100644 --- a/src/rcs/utils.cpp +++ b/src/rcs/utils.cpp @@ -6,20 +6,44 @@ namespace rcs { namespace common { +enum class RenderBackend { + none, + egl, + current_context, +}; + static PFNEGLMAKECURRENTPROC g_makeCurrent = nullptr; static EGLDisplay g_display = EGL_NO_DISPLAY; static EGLSurface g_surface = EGL_NO_SURFACE; static EGLContext g_context = EGL_NO_CONTEXT; +static RenderBackend g_render_backend = RenderBackend::none; void bootstrap_egl(uintptr_t fn_addr, uintptr_t dpy, uintptr_t ctx) { g_makeCurrent = reinterpret_cast(fn_addr); g_display = reinterpret_cast(dpy); g_context = reinterpret_cast(ctx); + g_render_backend = RenderBackend::egl; +} + +void bootstrap_gl_context() { + g_makeCurrent = nullptr; + g_display = EGL_NO_DISPLAY; + g_context = EGL_NO_CONTEXT; + g_render_backend = RenderBackend::current_context; } void ensure_current() { - if (!g_makeCurrent(g_display, g_surface, g_surface, g_context)) + if (g_render_backend == RenderBackend::current_context) { + return; + } + if (g_render_backend != RenderBackend::egl || g_makeCurrent == nullptr) { + throw std::runtime_error( + "No rendering context backend initialized. " + "Call the Python rendering bootstrap before using simulation cameras."); + } + if (!g_makeCurrent(g_display, g_surface, g_surface, g_context)) { throw std::runtime_error("eglMakeCurrent failed"); + } } } // namespace common } // namespace rcs diff --git a/src/sim/SimRobot.h b/src/sim/SimRobot.h index 99b23184..d4c43667 100644 --- a/src/sim/SimRobot.h +++ b/src/sim/SimRobot.h @@ -1,11 +1,14 @@ #ifndef RCS_SIMROBOT_H #define RCS_SIMROBOT_H + #include #include #include #include #include +#include + #include "sim/sim.h" namespace rcs {