From 6d63e046fdf89987b06fb77110e0cf7ded2698d9 Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Mon, 10 Apr 2023 10:09:06 -0700 Subject: [PATCH] Add low-level CGL context management for macOS. This allows rendering from non-main threads as the context is not tied to a Cocoa window. Fixes #742 Fixes #798 PiperOrigin-RevId: 523144359 Change-Id: I98432debdd034814c313eda94e38c248c3c1f76c --- doc/changelog.rst | 7 ++ python/mujoco/cgl/__init__.py | 71 +++++++++++++++++ python/mujoco/cgl/cgl.py | 142 ++++++++++++++++++++++++++++++++++ python/mujoco/gl_context.py | 3 + 4 files changed, 223 insertions(+) create mode 100644 python/mujoco/cgl/__init__.py create mode 100644 python/mujoco/cgl/cgl.py diff --git a/doc/changelog.rst b/doc/changelog.rst index 4aef61cc..c92fc350 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -20,6 +20,13 @@ General equivalent body inertias with ellipsoids instead of the default boxes. - Added documentation for :ref:`engine plugins`. +Python bindings +^^^^^^^^^^^^^^^ + +- Offscreen rendering on macOS is no longer restricted to the main thread. This is achieved by using the low-level + Core OpenGL (CGL) API to create the OpenGL context, rather than going via GLFW which relies on Cocoa's NSOpenGL. + The resulting context is not tied to a Cocoa window, and is therefore not tied to the main thread. + Bug fixes ^^^^^^^^^ diff --git a/python/mujoco/cgl/__init__.py b/python/mujoco/cgl/__init__.py new file mode 100644 index 00000000..ba1e36a6 --- /dev/null +++ b/python/mujoco/cgl/__init__.py @@ -0,0 +1,71 @@ +# Copyright 2023 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. +# ============================================================================== +"""An Apple CGL context for offscreen rendering on macOS.""" + +import atexit +import ctypes +import os + +from mujoco.cgl import cgl + +_ATTRIB = cgl.CGLPixelFormatAttribute +_PROFILE = cgl.CGLOpenGLProfile + + +class GLContext: + """An EGL context for headless accelerated OpenGL rendering on GPU devices.""" + + def __init__(self, max_width, max_height): + del max_width, max_height # unused + attrib_values = ( + _ATTRIB.CGLPFAOpenGLProfile, _PROFILE.CGLOGLPVersion_Legacy, + _ATTRIB.CGLPFAColorSize, 24, + _ATTRIB.CGLPFAAlphaSize, 8, + _ATTRIB.CGLPFADepthSize, 24, + _ATTRIB.CGLPFAStencilSize, 8, + _ATTRIB.CGLPFAMultisample, + _ATTRIB.CGLPFASampleBuffers, 1, + _ATTRIB.CGLPFASample, 4, + _ATTRIB.CGLPFAAccelerated, + 0, + ) + attribs = (ctypes.c_int * len(attrib_values))(*attrib_values) + self._pix = cgl.CGLPixelFormatObj() + npix = cgl.GLint() + cgl.CGLChoosePixelFormat( + attribs, ctypes.byref(self._pix), ctypes.byref(npix) + ) + + self._context = cgl.CGLContextObj() + cgl.CGLCreateContext(self._pix, 0, ctypes.byref(self._context)) + + def make_current(self): + cgl.CGLSetCurrentContext(self._context) + cgl.CGLLockContext(self._context) + + def free(self): + """Frees resources associated with this context.""" + if self._context: + cgl.CGLUnlockContext(self._context) + cgl.CGLSetCurrentContext(None) + cgl.CGLReleaseContext(self._context) + self._context = None + + if self._pix: + cgl.CGLReleasePixelFormat(self._pix) + self._context = None + + def __del__(self): + self.free() diff --git a/python/mujoco/cgl/cgl.py b/python/mujoco/cgl/cgl.py new file mode 100644 index 00000000..5a8e2b5b --- /dev/null +++ b/python/mujoco/cgl/cgl.py @@ -0,0 +1,142 @@ +# Copyright 2023 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. +# ============================================================================== +"""Bindings for Apple CGL.""" + +import ctypes +import enum + +_CGL = ctypes.CDLL('/System/Library/OpenGL.framework/OpenGL') + +CGLContextObj = ctypes.c_void_p +CGLPixelFormatObj = ctypes.c_void_p +GLint = ctypes.c_int + +_CGLChoosePixelFormat = _CGL.CGLChoosePixelFormat +_CGLChoosePixelFormat.argtypes = ( + ctypes.POINTER(ctypes.c_int), + ctypes.POINTER(CGLPixelFormatObj), + ctypes.POINTER(GLint), +) + +_CGLCreateContext = _CGL.CGLCreateContext +_CGLCreateContext.argtypes = ( + CGLPixelFormatObj, + ctypes.c_int, + CGLContextObj, +) + +_CGLErrorString = _CGL.CGLErrorString +_CGLErrorString.restype = ctypes.c_char_p +_CGLErrorString.argtype = (ctypes.c_int,) + +_CGLLockContext = _CGL.CGLLockContext +_CGLLockContext.argtypes = (CGLContextObj,) + +_CGLReleaseContext = _CGL.CGLReleaseContext +_CGLReleaseContext.restype = None +_CGLReleaseContext.argtypes = (CGLContextObj,) + +_CGLReleasePixelFormat = _CGL.CGLReleasePixelFormat +_CGLReleasePixelFormat.restype = None +_CGLReleasePixelFormat.argtypes = (CGLPixelFormatObj,) + +_CGLSetCurrentContext = _CGL.CGLSetCurrentContext +_CGLSetCurrentContext.argtypes = (CGLContextObj,) + +_CGLUnlockContext = _CGL.CGLUnlockContext +_CGLUnlockContext.argtypes = (CGLContextObj,) + + +# pylint: disable=invalid-name + + +class CGLOpenGLProfile(enum.IntEnum): + CGLOGLPVersion_Legacy = 0x1000 # renderer compatible with GL1.0 + CGLOGLPVersion_3_2_Core = 0x3200 # renderer capable of GL3.2 or later + CGLOGLPVersion_GL3_Core = 0x3200 # renderer capable of GL3.2 or later + CGLOGLPVersion_GL4_Core = 0x4100 # renderer capable of GL4.1 or later + + +class CGLPixelFormatAttribute(enum.IntEnum): + """CGLPixelFormatAttribute enum values.""" + CGLPFAAllRenderers = 1 # choose from all available renderers + CGLPFATripleBuffer = 3 # choose a triple buffered pixel format + CGLPFADoubleBuffer = 5 # choose a double buffered pixel format + CGLPFAColorSize = 8 # number of color buffer bits + CGLPFAAlphaSize = 11 # number of alpha component bits + CGLPFADepthSize = 12 # number of depth buffer bits + CGLPFAStencilSize = 13 # number of stencil buffer bits + CGLPFAMinimumPolicy = 51 # never choose smaller buffers than requested + CGLPFAMaximumPolicy = 52 # choose largest buffers of type requested + CGLPFASampleBuffers = 55 # number of multi sample buffers + CGLPFASample = 56 # number of samples per multi sample buffer + CGLPFAColorFloat = 58 # color buffers store floating point pixels + CGLPFAMultisample = 59 # choose multisampling + CGLPFASupersample = 60 # choose supersampling + CGLPFASampleAlpha = 61 # request alpha filtering + CGLPFARendererID = 70 # request renderer by ID + CGLPFANoRecovery = 72 # disable all failure recovery systems + CGLPFAAccelerated = 73 # choose a hardware accelerated renderer + CGLPFAClosestPolicy = 74 # choose the closest color buffer to request + CGLPFABackingStore = 76 # back buffer contents are valid after swap + CGLPFABackingVolatile = 77 # back buffer contents are volatile after swap + CGLPFADisplayMask = 84 # mask limiting supported displays + CGLPFAAllowOfflineRenderers = 96 # show offline renderers in pixel formats + CGLPFAAcceleratedCompute = 97 # choose a hardware accelerated compute device + CGLPFAOpenGLProfile = 99 # specify an OpenGL Profile to use + CGLPFASupportsAutomaticGraphicsSwitching = 101 # responds to display changes + CGLPFAVirtualScreenCount = 128 # number of virtual screens in this format + + # Note: the following attributes are deprecated in Core Profile + CGLPFAAuxBuffers = 7 # number of aux buffers + CGLPFAAccumSize = 14 # number of accum buffer bits + CGLPFAAuxDepthStencil = 57 # each aux buffer has its own depth stencil + + CGLPFAStereo = 6 + CGLPFAOffScreen = 53 + CGLPFAWindow = 80 + CGLPFACompliant = 83 + CGLPFAPBuffer = 90 + CGLPFARemotePBuffer = 91 + + CGLPFASingleRenderer = 71 + CGLPFARobust = 75 + CGLPFAMPSafe = 78 + CGLPFAMultiScreen = 81 + CGLPFAFullScreen = 54 + + +# pylint: enable=invalid-name + + +class CGLError(RuntimeError): # pylint: disable=g-bad-exception-name + pass + + +def _make_checked(func): + def checked_func(*args): + err = func(*args) + if err: + raise CGLError(_CGLErrorString(err).decode()) + return checked_func + + +CGLChoosePixelFormat = _make_checked(_CGLChoosePixelFormat) +CGLCreateContext = _make_checked(_CGLCreateContext) +CGLLockContext = _make_checked(_CGLLockContext) +CGLReleaseContext = _CGLReleaseContext +CGLReleasePixelFormat = _CGLReleasePixelFormat +CGLSetCurrentContext = _make_checked(_CGLSetCurrentContext) +CGLUnlockContext = _make_checked(_CGLUnlockContext) diff --git a/python/mujoco/gl_context.py b/python/mujoco/gl_context.py index 3a5335ea..61b861ec 100644 --- a/python/mujoco/gl_context.py +++ b/python/mujoco/gl_context.py @@ -40,6 +40,9 @@ if _MUJOCO_GL not in ('disable', 'disabled', 'off', 'false', '0'): elif _SYSTEM == 'Linux' and _MUJOCO_GL == 'egl': from mujoco.egl import GLContext as _GLContext GLContext = _GLContext + elif _SYSTEM == 'Darwin': + from mujoco.cgl import GLContext as _GLContext + GLContext = _GLContext else: from mujoco.glfw import GLContext as _GLContext GLContext = _GLContext