Skip to content

Commit

Permalink
Support querying and selecting EGL device.
Browse files Browse the repository at this point in the history
This commit uses the EGL query extension to query devices and allows for
selecting a specific GPU. This is crucial when using shared GPU
resources.

The GPU index can be set using the environment variable 'EGL_DEVICE_ID'.
  • Loading branch information
Keunhong Park committed Aug 21, 2019
1 parent 1cb07d9 commit 5ae1bb1
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 3 deletions.
11 changes: 8 additions & 3 deletions pyrender/offscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import os

from .renderer import Renderer
from .platforms import EGLPlatform, OSMesaPlatform, PygletPlatform
from .constants import RenderFlags


Expand Down Expand Up @@ -114,12 +113,18 @@ def delete(self):

def _create(self):
if 'PYOPENGL_PLATFORM' not in os.environ:
from pyrender.platforms.pyglet import PygletPlatform
self._platform = PygletPlatform(self.viewport_width,
self.viewport_height)
elif os.environ['PYOPENGL_PLATFORM'] == 'egl':
self._platform = EGLPlatform(self.viewport_width,
self.viewport_height)
from pyrender.platforms import egl
device_id = int(os.environ.get('EGL_DEVICE_ID', '0'))
egl_device = egl.get_device_by_index(device_id)
self._platform = egl.EGLPlatform(self.viewport_width,
self.viewport_height,
device=egl_device)
elif os.environ['PYOPENGL_PLATFORM'] == 'osmesa':
from pyrender.platforms.osmesa import OSMesaPlatform
self._platform = OSMesaPlatform(self.viewport_width,
self.viewport_height)
else:
Expand Down
6 changes: 6 additions & 0 deletions pyrender/platforms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Platforms for generating offscreen OpenGL contexts for rendering.
Author: Matthew Matl
"""

from pyrender.platforms.base import Platform
70 changes: 70 additions & 0 deletions pyrender/platforms/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import abc

import six


@six.add_metaclass(abc.ABCMeta)
class Platform(object):
"""Base class for all OpenGL platforms.
Parameters
----------
viewport_width : int
The width of the main viewport, in pixels.
viewport_height : int
The height of the main viewport, in pixels
"""

def __init__(self, viewport_width, viewport_height):
self.viewport_width = viewport_width
self.viewport_height = viewport_height

@property
def viewport_width(self):
"""int : The width of the main viewport, in pixels.
"""
return self._viewport_width

@viewport_width.setter
def viewport_width(self, value):
self._viewport_width = value

@property
def viewport_height(self):
"""int : The height of the main viewport, in pixels.
"""
return self._viewport_height

@viewport_height.setter
def viewport_height(self, value):
self._viewport_height = value

@abc.abstractmethod
def init_context(self):
"""Create an OpenGL context.
"""
pass

@abc.abstractmethod
def make_current(self):
"""Make the OpenGL context current.
"""
pass

@abc.abstractmethod
def delete_context(self):
"""Delete the OpenGL context.
"""
pass

@abc.abstractmethod
def supports_framebuffers(self):
"""Returns True if the method supports framebuffer rendering.
"""
pass

def __del__(self):
try:
self.delete_context()
except Exception:
pass
214 changes: 214 additions & 0 deletions pyrender/platforms/egl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import ctypes
import os

import OpenGL.platform

from .base import Platform

EGL_PLATFORM_DEVICE_EXT = 0x313F
EGL_DRM_DEVICE_FILE_EXT = 0x3233


def _ensure_egl_loaded():
plugin = OpenGL.platform.PlatformPlugin.by_name('egl')
if plugin is None:
raise RuntimeError("EGL platform plugin is not available.")

plugin_class = plugin.load()
plugin.loaded = True
# create instance of this platform implementation
plugin = plugin_class()

plugin.install(vars(OpenGL.platform))


_ensure_egl_loaded()
from OpenGL import EGL as egl


def _get_egl_func(func_name, res_type, *arg_types):
address = egl.eglGetProcAddress(func_name)
if address is None:
return None

proto = ctypes.CFUNCTYPE(res_type)
proto.argtypes = arg_types
func = proto(address)
return func


def _get_egl_struct(struct_name):
from OpenGL._opaque import opaque_pointer_cls
return opaque_pointer_cls(struct_name)


# These are not defined in PyOpenGL by default.
_EGLDeviceEXT = _get_egl_struct('EGLDeviceEXT')
_eglGetPlatformDisplayEXT = _get_egl_func('eglGetPlatformDisplayEXT', egl.EGLDisplay)
_eglQueryDevicesEXT = _get_egl_func('eglQueryDevicesEXT', egl.EGLBoolean)
_eglQueryDeviceStringEXT = _get_egl_func('eglQueryDeviceStringEXT', ctypes.c_char_p)


def query_devices():
if _eglQueryDevicesEXT is None:
raise RuntimeError("EGL query extension is not loaded or is not supported.")

num_devices = egl.EGLint()
success = _eglQueryDevicesEXT(0, None, ctypes.pointer(num_devices))
if not success or num_devices.value < 1:
return []

devices = (_EGLDeviceEXT * num_devices.value)() # array of size num_devices
success = _eglQueryDevicesEXT(num_devices.value, devices, ctypes.pointer(num_devices))
if not success or num_devices.value < 1:
return []

return [EGLDevice(devices[i]) for i in range(num_devices.value)]


def get_default_device():
# Fall back to not using query extension.
if _eglQueryDevicesEXT is None:
return EGLDevice(None)

return query_devices()[0]


def get_device_by_index(device_id):
if _eglQueryDevicesEXT is None and device_id == 0:
return get_default_device()

devices = query_devices()
if device_id >= len(devices):
raise ValueError('Invalid device ID ({})'.format(device_id, len(devices)))
return devices[device_id]


class EGLDevice:

def __init__(self, display=None):
self._display = display

def get_display(self):
if self._display is None:
return egl.eglGetDisplay(egl.EGL_DEFAULT_DISPLAY)

return _eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, self._display, None)

@property
def name(self):
if self._display is None:
return 'default'

name = _eglQueryDeviceStringEXT(self._display, EGL_DRM_DEVICE_FILE_EXT)
if name is None:
return None

return name.decode('ascii')

def __repr__(self):
return f"<EGLDevice(name={self.name!r})>"


class EGLPlatform(Platform):
"""Renders using EGL (not currently working on Ubuntu).
"""

def __init__(self, viewport_width, viewport_height, device: EGLDevice = None):
super(EGLPlatform, self).__init__(viewport_width, viewport_height)
if device is None:
device = get_default_device()

self._egl_device = device
self._egl_display = None
self._egl_context = None

def init_context(self):
_ensure_egl_loaded()

from OpenGL.EGL import (
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE,
EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_DEPTH_SIZE,
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_CONFORMANT,
EGL_NONE, EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT,
EGL_OPENGL_API, EGL_CONTEXT_MAJOR_VERSION,
EGL_CONTEXT_MINOR_VERSION,
EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
eglGetDisplay, eglInitialize, eglChooseConfig,
eglBindAPI, eglCreateContext, EGLConfig
)
from OpenGL import arrays

config_attributes = arrays.GLintArray.asArray([
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_BLUE_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_CONFORMANT, EGL_OPENGL_BIT,
EGL_NONE
])
context_attributes = arrays.GLintArray.asArray([
EGL_CONTEXT_MAJOR_VERSION, 4,
EGL_CONTEXT_MINOR_VERSION, 1,
EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
EGL_NONE
])
major, minor = ctypes.c_long(), ctypes.c_long()
num_configs = ctypes.c_long()
configs = (EGLConfig * 1)()

# Cache DISPLAY if necessary and get an off-screen EGL display
orig_dpy = None
if 'DISPLAY' in os.environ:
orig_dpy = os.environ['DISPLAY']
del os.environ['DISPLAY']

self._egl_display = self._egl_device.get_display()
if orig_dpy is not None:
os.environ['DISPLAY'] = orig_dpy

# Initialize EGL
assert eglInitialize(self._egl_display, major, minor)
assert eglChooseConfig(
self._egl_display, config_attributes, configs, 1, num_configs
)

# Bind EGL to the OpenGL API
assert eglBindAPI(EGL_OPENGL_API)

# Create an EGL context
self._egl_context = eglCreateContext(
self._egl_display, configs[0],
EGL_NO_CONTEXT, context_attributes
)

# Make it current
self.make_current()

def make_current(self):
from OpenGL.EGL import eglMakeCurrent, EGL_NO_SURFACE
assert eglMakeCurrent(
self._egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
self._egl_context
)

def delete_context(self):
from OpenGL.EGL import eglDestroyContext, eglTerminate
if self._egl_display is not None:
if self._egl_context is not None:
eglDestroyContext(self._egl_display, self._egl_context)
self._egl_context = None
eglTerminate(self._egl_display)
self._egl_display = None

def supports_framebuffers(self):
return True


__all__ = ['EGLPlatform']
54 changes: 54 additions & 0 deletions pyrender/platforms/osmesa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from .base import Platform


__all__ = ['OSMesaPlatform']


class OSMesaPlatform(Platform):
"""Renders into a software buffer using OSMesa. Requires special versions
of OSMesa to be installed, plus PyOpenGL upgrade.
"""

def __init__(self, viewport_width, viewport_height):
super(OSMesaPlatform, self).__init__(viewport_width, viewport_height)
self._context = None
self._buffer = None

def init_context(self):
from OpenGL import arrays
from OpenGL.osmesa import (
OSMesaCreateContextAttribs, OSMESA_FORMAT,
OSMESA_RGBA, OSMESA_PROFILE, OSMESA_CORE_PROFILE,
OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION,
OSMESA_DEPTH_BITS
)

attrs = arrays.GLintArray.asArray([
OSMESA_FORMAT, OSMESA_RGBA,
OSMESA_DEPTH_BITS, 24,
OSMESA_PROFILE, OSMESA_CORE_PROFILE,
OSMESA_CONTEXT_MAJOR_VERSION, 3,
OSMESA_CONTEXT_MINOR_VERSION, 3,
0
])
self._context = OSMesaCreateContextAttribs(attrs, None)
self._buffer = arrays.GLubyteArray.zeros(
(self.viewport_height, self.viewport_width, 4)
)

def make_current(self):
from OpenGL import GL as gl
from OpenGL.osmesa import OSMesaMakeCurrent
assert(OSMesaMakeCurrent(
self._context, self._buffer, gl.GL_UNSIGNED_BYTE,
self.viewport_width, self.viewport_height
))

def delete_context(self):
from OpenGL.osmesa import OSMesaDestroyContext
OSMesaDestroyContext(self._context)
self._context = None
self._buffer = None

def supports_framebuffers(self):
return False
Loading

0 comments on commit 5ae1bb1

Please sign in to comment.