Skip to content

Commit 3b4552d

Browse files
committed
Add an environment variable that can be used to globally disable OpenGL rendering support
Setting `DISABLE_MUJOCO_RENDERING` before launching the Python interpreter will cause OpenGL context creation to be skipped when `mujoco.Physics` is instantiated. This allows the MuJoCo Python bindings to be used on platforms where an OpenGL context cannot be created, provided that no actual rendering calls are made. PiperOrigin-RevId: 183136732
1 parent 78dd720 commit 3b4552d

3 files changed

Lines changed: 75 additions & 44 deletions

File tree

dm_control/mujoco/engine.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class Physics(_control.Physics):
102102
[0] http://www.mujoco.org/book/modeling.html
103103
"""
104104

105+
_contexts = None
106+
105107
def __init__(self, data):
106108
"""Initializes a new `Physics` instance.
107109
@@ -275,7 +277,8 @@ def _reload_from_model(self, model):
275277
def _reload_from_data(self, data):
276278
"""Initializes a new or existing `Physics` instance from a `wrapper.MjData`.
277279
278-
Assigns all attributes and sets up rendering contexts and named indexing.
280+
Assigns all attributes, sets up named indexing, and creates rendering
281+
contexts if rendering is enabled.
279282
280283
The default constructor as well as the other `reload_from` methods should
281284
delegate to this method.
@@ -285,20 +288,8 @@ def _reload_from_data(self, data):
285288
"""
286289
self._data = data
287290

288-
# Forcibly clear the previous context to avoid problems with GL
289-
# implementations which do not support multiple contexts on a given device.
290-
if hasattr(self, '_contexts'):
291-
self._contexts.gl.free()
292-
293-
# Set up rendering context. Need to provide at least one rendering api in
294-
# the BUILD target.
295-
render_context = render.Renderer(_MAX_WIDTH, _MAX_HEIGHT)
296-
mujoco_context = wrapper.MjrContext()
297-
with render_context.make_current(_MAX_WIDTH, _MAX_HEIGHT):
298-
mjlib.mjr_makeContext(self.model.ptr, mujoco_context.ptr, _FONT_SCALE)
299-
mjlib.mjr_setBuffer(
300-
enums.mjtFramebuffer.mjFB_OFFSCREEN, mujoco_context.ptr)
301-
self._contexts = Contexts(gl=render_context, mujoco=mujoco_context)
291+
if not render.DISABLED:
292+
self._make_rendering_contexts()
302293

303294
# Call kinematics update to enable rendering.
304295
self.after_reset()
@@ -407,9 +398,27 @@ def reload_from_xml_path(self, file_path):
407398
def named(self):
408399
return self._named
409400

401+
def _make_rendering_contexts(self):
402+
"""Creates the OpenGL and MuJoCo rendering contexts."""
403+
# Forcibly clear the previous GL context to avoid problems with GL
404+
# implementations which do not support multiple contexts on a given device.
405+
if self._contexts:
406+
self._contexts.gl.free()
407+
# Create the OpenGL context.
408+
render_context = render.Renderer(_MAX_WIDTH, _MAX_HEIGHT)
409+
# Create the MuJoCo context.
410+
mujoco_context = wrapper.MjrContext()
411+
with render_context.make_current(_MAX_WIDTH, _MAX_HEIGHT):
412+
mjlib.mjr_makeContext(self.model.ptr, mujoco_context.ptr, _FONT_SCALE)
413+
mjlib.mjr_setBuffer(
414+
enums.mjtFramebuffer.mjFB_OFFSCREEN, mujoco_context.ptr)
415+
self._contexts = Contexts(gl=render_context, mujoco=mujoco_context)
416+
410417
@property
411418
def contexts(self):
412419
"""Returns a `Contexts` namedtuple, used in `Camera`s and rendering code."""
420+
if render.DISABLED:
421+
raise RuntimeError(render.DISABLED_MESSAGE)
413422
return self._contexts
414423

415424
@property

dm_control/mujoco/engine_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,15 @@ def testActionSpec(self):
325325
np.testing.assert_array_equal(spec.minimum, [-np.inf, -1.0])
326326
np.testing.assert_array_equal(spec.maximum, [np.inf, 2.0])
327327

328+
def testErrorOnContextAccessIfRenderingDisabled(self):
329+
expected_message = 'Error message'
330+
with mock.patch(engine.__name__ + '.render') as mock_render:
331+
mock_render.DISABLED = True
332+
mock_render.DISABLED_MESSAGE = expected_message
333+
physics = engine.Physics.from_xml_path(MODEL_PATH)
334+
with self.assertRaisesWithLiteralMatch(RuntimeError, expected_message):
335+
_ = physics.contexts
336+
328337

329338
if __name__ == '__main__':
330339
absltest.main()

dm_control/render/__init__.py

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,52 @@
1717
1818
The `Renderer` class will use one of the following rendering APIs, in order of
1919
descending priority: EGL > GLFW > OSMesa.
20+
21+
Rendering support can be disabled globally by setting the
22+
`DISABLE_MUJOCO_RENDERING` environment variable before launching the Python
23+
interpreter. This allows the MuJoCo bindings in `dm_control.mujoco` to be used
24+
on platforms where an OpenGL context cannot be created. Attempting to render
25+
when rendering has been disabled will result in a `RuntimeError`.
2026
"""
2127

22-
# pylint: disable=g-import-not-at-top
23-
try:
24-
from dm_control.render.glfw_renderer import GLFWContext as _GLFWRenderer
25-
except (ImportError, IOError):
26-
_GLFWRenderer = None
27-
try:
28-
from dm_control.render.egl_renderer import EGLContext as _EGLRenderer
29-
except ImportError:
30-
_EGLRenderer = None
31-
try:
32-
from dm_control.render.osmesa_renderer import OSMesaContext as _OSMesaRenderer
33-
except ImportError:
34-
_OSMesaRenderer = None
35-
# pylint: enable=g-import-not-at-top
28+
import os
29+
DISABLED = bool(os.environ.get('DISABLE_MUJOCO_RENDERING', ''))
30+
del os
3631

37-
# pylint: disable=invalid-name
38-
if _EGLRenderer:
39-
Renderer = _EGLRenderer
40-
elif _GLFWRenderer:
41-
Renderer = _GLFWRenderer
42-
elif _OSMesaRenderer:
43-
Renderer = _OSMesaRenderer
44-
else:
45-
# This is a workaround that allows imports from `dm_control.render` to succeed
46-
# even when there is no rendering API available. We need this in order to run
47-
# integration tests on headless servers.
32+
DISABLED_MESSAGE = (
33+
'Rendering support has been disabled by the `DISABLE_MUJOCO_RENDERING` '
34+
'environment variable')
4835

49-
def Renderer(*args, **kwargs):
50-
del args, kwargs # Unused.
51-
raise ImportError('No OpenGL rendering backend could be imported.')
36+
# pylint: disable=g-import-not-at-top
37+
# pylint: disable=invalid-name
5238

53-
# pylint: enable=invalid-name
39+
_GLFWRenderer = None
40+
_EGLRenderer = None
41+
_OSMesaRenderer = None
5442

43+
if not DISABLED:
44+
try:
45+
from dm_control.render.glfw_renderer import GLFWContext as _GLFWRenderer
46+
except ImportError:
47+
pass
48+
try:
49+
from dm_control.render.egl_renderer import EGLContext as _EGLRenderer
50+
except ImportError:
51+
pass
52+
try:
53+
from dm_control.render.osmesa_renderer import OSMesaContext as _OSMesaRenderer
54+
except ImportError:
55+
pass
5556

57+
if _EGLRenderer:
58+
Renderer = _EGLRenderer
59+
elif _GLFWRenderer:
60+
Renderer = _GLFWRenderer
61+
elif _OSMesaRenderer:
62+
Renderer = _OSMesaRenderer
63+
else:
64+
raise ImportError(
65+
'No OpenGL rendering backend could be imported. To use '
66+
'`dm_control.mujoco` without rendering support, set the '
67+
'`DISABLE_MUJOCO_RENDERING` environment variable before launching your '
68+
'interpreter.')

0 commit comments

Comments
 (0)