Skip to content

Commit af2e506

Browse files
authored
Merge pull request #191 from utn-mi/krack/egl
Headless support
2 parents 3655927 + 78e447d commit af2e506

File tree

19 files changed

+272
-232
lines changed

19 files changed

+272
-232
lines changed

CMakeLists.txt

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ set(BUILD_DOCUMENTATION OFF)
4444
include(FetchContent)
4545

4646
find_package(Eigen3 REQUIRED)
47-
find_package(glfw3 REQUIRED)
4847
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
4948
find_package(MuJoCo REQUIRED)
5049

@@ -67,29 +66,6 @@ FetchContent_Declare(pybind11
6766
GIT_PROGRESS TRUE
6867
EXCLUDE_FROM_ALL
6968
)
70-
set(MuJoCo_UI_URL https://raw.githubusercontent.com/google-deepmind/mujoco/${MuJoCo_VERSION}/simulate/)
71-
set(MuJoCo_UI_FILES platform_ui_adapter.cc platform_ui_adapter.h glfw_adapter.cc glfw_adapter.h glfw_adapter.cc glfw_corevideo.h glfw_corevideo.mm glfw_dispatch.cc glfw_dispatch.h)
72-
# Set the destination directory for the downloaded files
73-
set(MuJoCo_UI_DIR "${CMAKE_BINARY_DIR}/mujoco_ui")
74-
75-
# Create the directory if it doesn't exist
76-
file(MAKE_DIRECTORY ${MuJoCo_UI_DIR})
77-
78-
# Loop through each file and download it only if it doesn't already exist
79-
foreach(file ${MuJoCo_UI_FILES})
80-
set(output_file "${MuJoCo_UI_DIR}/${file}")
81-
if (NOT EXISTS ${output_file})
82-
message(STATUS "Downloading ${file}...")
83-
file(DOWNLOAD ${MuJoCo_UI_URL}${file} ${output_file})
84-
else()
85-
message(STATUS "File ${file} already exists, skipping download.")
86-
endif()
87-
endforeach()
88-
89-
# Add the include directories or link the files as needed
90-
add_library(mujoco_ui_lib ${MuJoCo_UI_DIR}/glfw_adapter.cc ${MuJoCo_UI_DIR}/platform_ui_adapter.cc ${MuJoCo_UI_DIR}/glfw_dispatch.cc)
91-
target_link_libraries(mujoco_ui_lib MuJoCo::MuJoCo glfw)
92-
target_include_directories(mujoco_ui_lib INTERFACE ${MuJoCo_UI_DIR})
9369

9470
FetchContent_MakeAvailable(libfranka rl pybind11)
9571
include(compile_scenes)

python/rcs/_core/common.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def FrankaHandTCPOffset() -> numpy.ndarray[tuple[typing.Literal[4], typing.Liter
232232
def IdentityRotMatrix() -> numpy.ndarray[tuple[typing.Literal[3], typing.Literal[3]], numpy.dtype[numpy.float64]]: ...
233233
def IdentityRotQuatVec() -> numpy.ndarray[typing.Literal[4], numpy.dtype[numpy.float64]]: ...
234234
def IdentityTranslation() -> numpy.ndarray[typing.Literal[3], numpy.dtype[numpy.float64]]: ...
235+
def _bootstrap_egl(fn_addr: int, display: int, context: int) -> None: ...
235236
def robots_meta_config(robot_type: RobotType) -> RobotMetaConfig: ...
236237

237238
FR3: RobotType # value = <RobotType.FR3: 0>

python/rcs/_core/sim.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import rcs._core.common
1212
__all__ = [
1313
"CameraType",
1414
"FrameSet",
15+
"GuiClient",
1516
"Sim",
1617
"SimCameraConfig",
1718
"SimCameraSet",
@@ -24,7 +25,6 @@ __all__ = [
2425
"default_free",
2526
"fixed",
2627
"free",
27-
"open_gui_window",
2828
"tracking",
2929
]
3030
M = typing.TypeVar("M", bound=int)
@@ -73,6 +73,12 @@ class FrameSet:
7373
@property
7474
def timestamp(self) -> float: ...
7575

76+
class GuiClient:
77+
def __init__(self, id: str) -> None: ...
78+
def get_model_bytes(self) -> bytes: ...
79+
def set_model_and_data(self, arg0: int, arg1: int) -> None: ...
80+
def sync(self) -> None: ...
81+
7682
class Sim:
7783
def __init__(self, mjmdl: int, mjdata: int) -> None: ...
7884
def _start_gui_server(self, id: str) -> None: ...
@@ -170,8 +176,6 @@ class SimRobotState(rcs._core.common.RobotState):
170176
@property
171177
def target_angles(self) -> numpy.ndarray[M, numpy.dtype[numpy.float64]]: ...
172178

173-
def open_gui_window(uuid: str) -> None: ...
174-
175179
default_free: CameraType # value = <CameraType.default_free: 3>
176180
fixed: CameraType # value = <CameraType.fixed: 2>
177181
free: CameraType # value = <CameraType.free: 0>

python/rcs/camera/hw.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import cv2
1010
import numpy as np
1111
from rcs._core.common import BaseCameraConfig
12-
from rcs.camera.interface import BaseCameraSet, Frame, FrameSet, SimpleFrameRate
12+
from rcs.camera.interface import BaseCameraSet, Frame, FrameSet
13+
from rcs.utils import SimpleFrameRate
1314

1415

1516
class HardwareCamera(typing.Protocol):

python/rcs/camera/interface.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
from dataclasses import dataclass
33
from datetime import datetime
4-
from time import sleep, time
54
from typing import Any, Protocol
65

76
from rcs._core.common import BaseCameraConfig
@@ -10,35 +9,6 @@
109
logger.setLevel(logging.INFO)
1110

1211

13-
class SimpleFrameRate:
14-
def __init__(self, frame_rate: int | float):
15-
self.t: float | None = None
16-
self._last_print: float | None = None
17-
self.frame_rate = frame_rate
18-
19-
def reset(self):
20-
self.t = None
21-
22-
def __call__(self):
23-
if self.t is None:
24-
self.t = time()
25-
self._last_print = self.t
26-
sleep(1 / self.frame_rate if isinstance(self.frame_rate, int) else self.frame_rate)
27-
return
28-
sleep_time = (
29-
1 / self.frame_rate - (time() - self.t)
30-
if isinstance(self.frame_rate, int)
31-
else self.frame_rate - (time() - self.t)
32-
)
33-
if sleep_time > 0:
34-
sleep(sleep_time)
35-
if self._last_print is None or time() - self._last_print > 10:
36-
self._last_print = time()
37-
logger.info(f"FPS: {1 / (time() - self.t)}")
38-
39-
self.t = time()
40-
41-
4212
@dataclass(kw_only=True)
4313
class DataFrame:
4414
data: Any

python/rcs/sim.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

python/rcs/sim/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from rcs._core.sim import (
2+
SimGripper,
3+
SimGripperConfig,
4+
SimGripperState,
5+
SimRobot,
6+
SimRobotConfig,
7+
SimRobotState,
8+
)
9+
from rcs.sim.sim import Sim, gui_loop
10+
11+
__all__ = [
12+
"Sim",
13+
"SimRobot",
14+
"SimRobotConfig",
15+
"SimRobotState",
16+
"SimGripper",
17+
"SimGripperConfig",
18+
"SimGripperState",
19+
"gui_loop",
20+
]

python/rcs/sim/egl_bootstrap.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
Load the EGL library, create a persistent GLContext, and register it with the C++ backend.
3+
4+
Globals prevent the library and context from being garbage-collected.
5+
Call `bootstrap()` to complete initialization.
6+
"""
7+
8+
import ctypes
9+
import ctypes.util
10+
import os
11+
12+
import mujoco.egl
13+
import rcs._core as _cxx
14+
from mujoco.egl import GLContext
15+
16+
name = ctypes.util.find_library("EGL")
17+
if name is None:
18+
msg = "libEGL not found"
19+
raise OSError(msg)
20+
21+
_egl = ctypes.CDLL(name, mode=os.RTLD_LOCAL | os.RTLD_NOW)
22+
23+
addr_make_current = ctypes.cast(_egl.eglMakeCurrent, ctypes.c_void_p).value
24+
25+
ctx = GLContext(max_width=3840, max_height=2160)
26+
27+
egl_display = mujoco.egl.EGL_DISPLAY.address
28+
egl_context = ctx._context.address
29+
30+
31+
def bootstrap():
32+
assert addr_make_current is not None
33+
_cxx.common._bootstrap_egl(addr_make_current, egl_display, egl_context)

python/rcs/sim/sim.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import atexit
2+
import multiprocessing as mp
3+
import uuid
4+
from logging import getLogger
5+
from multiprocessing.synchronize import Event as EventClass
6+
from os import PathLike
7+
from pathlib import Path
8+
from tempfile import NamedTemporaryFile
9+
from typing import Optional
10+
11+
import mujoco as mj
12+
import mujoco.viewer
13+
from rcs._core.sim import GuiClient as _GuiClient
14+
from rcs._core.sim import Sim as _Sim
15+
from rcs.sim import egl_bootstrap
16+
from rcs.utils import SimpleFrameRate
17+
18+
egl_bootstrap.bootstrap()
19+
logger = getLogger(__name__)
20+
21+
22+
# Target frames per second
23+
FPS = 60
24+
25+
26+
def gui_loop(gui_uuid: str, close_event):
27+
frame_rate = SimpleFrameRate(FPS, "gui_loop")
28+
gui_client = _GuiClient(gui_uuid)
29+
model_bytes = gui_client.get_model_bytes()
30+
with NamedTemporaryFile(mode="wb") as f:
31+
f.write(model_bytes)
32+
model = mujoco.MjModel.from_binary_path(f.name)
33+
data = mujoco.MjData(model)
34+
gui_client.set_model_and_data(model._address, data._address)
35+
mujoco.mj_step(model, data)
36+
with mujoco.viewer.launch_passive(model, data) as viewer:
37+
while not close_event.is_set():
38+
mujoco.mj_step(model, data)
39+
viewer.sync()
40+
gui_client.sync()
41+
frame_rate()
42+
43+
44+
class Sim(_Sim):
45+
def __init__(self, mjmdl: str | PathLike):
46+
mjmdl = Path(mjmdl)
47+
if mjmdl.suffix == ".xml":
48+
self.model = mj.MjModel.from_xml_path(str(mjmdl))
49+
elif mjmdl.suffix == ".mjb":
50+
self.model = mj.MjModel.from_binary_path(str(mjmdl))
51+
else:
52+
msg = f"Filetype {mjmdl.suffix} is unknown"
53+
logger.error(msg)
54+
self.data = mj.MjData(self.model)
55+
super().__init__(self.model._address, self.data._address)
56+
self._mp_context = mp.get_context("spawn")
57+
self._gui_uuid: Optional[str] = None
58+
self._gui_client: Optional[_GuiClient] = None
59+
self._gui_process: Optional[mp.context.SpawnProcess] = None
60+
self._stop_event: Optional[EventClass] = None
61+
62+
def close_gui(self):
63+
if self._stop_event is not None:
64+
self._stop_event.set()
65+
if self._gui_process is not None:
66+
self._gui_process.join()
67+
self._stop_gui_server()
68+
69+
def open_gui(self):
70+
if self._gui_uuid is None:
71+
self._gui_uuid = "rcs_" + str(uuid.uuid4())
72+
self._start_gui_server(self._gui_uuid)
73+
if self._gui_client is None:
74+
ctx = mp.get_context("spawn")
75+
self._stop_event = ctx.Event()
76+
self._gui_process = ctx.Process(
77+
target=gui_loop,
78+
args=(self._gui_uuid, self._stop_event),
79+
)
80+
self._gui_process.start()
81+
atexit.register(self.close_gui)

python/rcs/utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import logging
2+
from time import perf_counter, sleep
3+
4+
logger = logging.getLogger(__name__)
5+
logger.setLevel(logging.INFO)
6+
7+
8+
class SimpleFrameRate:
9+
def __init__(self, frame_rate: int | float, loop_name: str = "SimpleFrameRate"):
10+
"""SimpleFrameRate is a utility class to manage frame rates in a simple way.
11+
It allows you to call it in a loop, and it will sleep the necessary time to maintain the desired frame rate.
12+
13+
Args:
14+
frame_rate (int | float): The desired frame rate in frames per second (int) or as a time interval in seconds (float).
15+
"""
16+
self.t: float | None = None
17+
self._last_print: float | None = None
18+
self.frame_rate = frame_rate
19+
self.loop_name = loop_name
20+
21+
def reset(self):
22+
self.t = None
23+
24+
def __call__(self):
25+
if self.t is None:
26+
self.t = perf_counter()
27+
self._last_print = self.t
28+
sleep(1 / self.frame_rate if isinstance(self.frame_rate, int) else self.frame_rate)
29+
return
30+
sleep_time = (
31+
1 / self.frame_rate - (perf_counter() - self.t)
32+
if isinstance(self.frame_rate, int)
33+
else self.frame_rate - (perf_counter() - self.t)
34+
)
35+
if sleep_time > 0:
36+
sleep(sleep_time)
37+
if self._last_print is None or perf_counter() - self._last_print > 10:
38+
self._last_print = perf_counter()
39+
logger.info(f"FPS {self.loop_name}: {1 / (perf_counter() - self.t)}")
40+
41+
self.t = perf_counter()

0 commit comments

Comments
 (0)