问题背景:
最近因为项目开发需要,需要在服务端构建OpenGL渲染环境,但由于缺乏服务端开发经验,我们遇到了一些相对棘手的问题,如何在服务端构建稳定可行的OpenGL context上下文环境便是其中一个较为棘手的一个,由于服务端环境相较于客户端环境相对复杂,因为在服务端没有用于画面渲染的显示设备,而OpenGL Context 上下文环境的构建又强依赖于display。所以为了解决这个问题我们前后实验了多种方案,包括在我们的服务器环境下安装虚拟显示设备、安装X window等方案但均无法正常构建OpenGL Context上下文环境。为此我们经过查阅相关资料最终采取了英伟达(NVIDIA)的技术方案。在GPU服务器上安装英伟达的显卡,在构建OpenGL context上下文环境时使用外接Display构建OpenGL环境的方法。具体流程如下:
下面我们先看一下在正常存在display设备环境下如何创建OpenGL contxt
#include static const EGLint configAttribs[] = { EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 8, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE }; static const int pbufferWidth = 9; static const int pbufferHeight = 9; static const EGLint pbufferAttribs[] = { EGL_WIDTH, pbufferWidth, EGL_HEIGHT, pbufferHeight, EGL_NONE, };int main(int argc, char *argv[]){ // 1. Initialize EGL EGLDisplay eglDpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); EGLint major, minor; eglInitialize(eglDpy, &major, &minor); // 2. Select an appropriate configuration EGLint numConfigs; EGLConfig eglCfg; eglChooseConfig(eglDpy, configAttribs, &eglCfg, 1, &numConfigs); // 3. Create a surface EGLSurface eglSurf = eglCreatePbufferSurface(eglDpy, eglCfg, pbufferAttribs); // 4. Bind the API eglBindAPI(EGL_OPENGL_API); // 5. Create a context and make it current EGLContext eglCtx = eglCreateContext(eglDpy, eglCfg, EGL_NO_CONTEXT, NULL); eglMakeCurrent(eglDpy, eglSurf, eglSurf, eglCtx); // from now on use your OpenGL context // 6. Terminate EGL when finished eglTerminate(eglDpy); return 0;}
我们先简单看一下在EGL环境下创建OpenGL conext的具体步骤,第一步是获取一个显示对象(eglGetDisplay),在上述代码中是获取默认显示设备,display可以(但非必须)是附加于系统上的物理显示设备。接下来我们需要在display上初始化EGL。除了初始化EGL之外,这个函数还会返回这个特定的display所支持的EGL版本,如果应用程序依赖于特定的EGL版本这将非常方便。一个display设备可以支持一系列的配置,因此我们需要指定应用程序中需要使用的配置。在我们的应用示例中,对于我们的配置需要一系列的属性配置,比如8-bits位深的颜色通道以及生成一个离屏渲染buffer的能力或者生成一个特定尺寸的surface。然后EGL能够返回一组能够匹配这些要求的配置。
现在我们开始创建一个surface,什么是surface呢?粗略的说一个surface就代表了屏幕上的一个窗口,只是你不需要屏幕。所以呢这是一个有限大小的画布。除了实际的像素数据,surface还可以包含辅助缓冲区,比如模版或者深度缓冲区。这里我们仅仅创建一个被称之为pbuffer(eglCreatePBufferSurface())用于离屏渲染的EGL Surface。
现在基本所有的工作准备就绪,唯一需要的就是我们需要告诉EGL我们想要用什么API来渲染这个surface。在这个示例中,我们选择了标准的OpenGL(eglBindAPI()),另一种选择是使用OpenGL es。此时我们可以在这个surface上创建OpenGL context(eglCreateContext()),并切换到当前环境(eglMakeCurrent())以便于进行相关openGL操作。
上面的示例是我们通过eglGetDisplay()去获取一个默认的display目标,但是如果我们系统中有多个gpu,并且我们希望使用一个特定的gpu(可能不是通过X server进行管理)进行渲染任务那该怎么办呢?又或者我们希望在工作站上基于一个不是由X server管理的display设备创建离屏的context和buffer又该怎么做呢?
EGL提供了另外一种更加强大的获取display设备的机制eglGetPlatformDisplay(),它允许我们指定确切的display设备,因此我们可以在多GPU配置中配置工作的GPU。
下面是通过eglGetPlatformDisplay()获取display的示例代码。
#define EGL_EGLEXT_PROTOTYPES#include #include main(){ static const int MAX_DEVICES = 4; EGLDeviceEXT eglDevs[MAX_DEVICES]; EGLint numDevices; PFNEGLQUERYDEVICESEXTPROC eglQueryDevicesEXT = (PFNEGLQUERYDEVICESEXTPROC) eglGetProcAddress("eglQueryDevicesEXT"); eglQueryDevicesEXT(MAX_DEVICES, eglDevs, &numDevices); printf(“Detected %d devices\n”, numDevices); PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC) eglGetProcAddress("eglGetPlatformDisplayEXT"); eglDpy = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, eglDevs[0], 0); // ...}
这个示例首先通过eglQueryDevicesEXT()查询可用的EGL设备,与所有的EGL创建例程一样,eglQueryDevicesEXT填充一个设备数组并返回找到的设备数量。注意eglQueryDevicesEXT()是一个EGL扩展,对应的EGL可能支持也可能不支持,所以你需要引入对应EGL扩展的头文件,并且通过设置EGL_EXT_PROTOTYPES预处理宏lai启用对应的扩展。为了得到想要的扩展函数,我们必须通过eglGetProcAddress()来获得对应的函数指针,它可以通过传入函数名称作为型参并最终返回函数指针。这里为了清晰起见,这里省略了所有的错误检查,但是为了代码的质量我们还是需要处理eglGetProcAddress()返回为NULL的情况。
在获得一系列的有效设备后,我们可以选择一个合适的设备作为我们的目标设备。另外一个扩展函数eglGetPlatformDisplayEXT()可以允许我们使用我们选择的设备作为EGL的display设备。其他后续的创建OpenGL context的步骤与我们第一的示例的后续步骤一致。
如果想详细研究可以参考NVIDIA的官方文档:https://devblogs.nvidia.com/egl-eye-opengl-visualization-without-x-server/
具体实现代码请参考:https://github.com/multimedia-advanced-org/CreateGLContextNoDisplay