/* egl-display.hpp - output video from a GTK application, using EGL - OpenGL binding Copyright (C) 2025, Benny Lyons <benny.lyons@gmx.net> 2025, Hermann Vosseler <Ichthyostega@web.de> This program is free software; you can redistribute it and/or modify it under the terms of the GNU GPL version 2+ See the LICENSE file for details. * ************************************************************************/ #include "commons.hpp" #include "gtk-uring-app.hpp" #include <functional> #include <algorithm> #include <cassert> #include <string> #include <array> #include <set> // for low-level access -> X-Window #include <gdk/gdkx.h> // X11, EGL and GLEW for OpenGL #include <X11/Xlib.h> #include <EGL/egl.h> #include <EGL/eglext.h> // defines plattform-specific config and extensions #include <GL/glew.h> // provides system-specific bindings for OpenGL extensions using std::string; /** * Connection and drawing context used for EGL based video display. */ template<typename PIX, uint WIDTH, uint HEIGHT> class EglDisplay : NonCopyable { public: using Buffer = std::array<PIX, WIDTH*HEIGHT * 3 >; /** * Interface: retrieve pixel data for video frames to display. * - this is a Glib::Dispatcher, and the attached slot shall be * triggered whenever data for a new frame is available * - pixel data is accessed (threadsafe) by invoking #retrieveFrame() * - when processing is complete and the data will no longer be needed, * access to the pixel buffer is yielded by invoking #completedFrame() */ class PixelFeed : public Glib::Dispatcher { public: virtual ~PixelFeed() { }; ///< this is an interface virtual Buffer* retrieveFrame() =0; virtual void completedFrame() =0; }; EglDisplay (Gtk::Window& appWindow, PixelFeed& pixelFeed) : pixelFeed_{pixelFeed} { openDisplay (appWindow); pixelFeed_.connect([this]{ displayFrame(); }); } void cleanUp(); private: /** EGL / X11 connection. */ EGLDisplay display_{nullptr}; EGLSurface surface_{nullptr}; EGLContext egl_{nullptr}; uint texID_{0}; uint vboID_{0}; uint vaoID_{0}; float scaleX_{1}; float scaleY_{1}; /** Callback to retrieve pixel data */ PixelFeed& pixelFeed_{}; void openDisplay (Gtk::Window&); void displayFrame(); }; template<typename PIX, uint WIDTH, uint HEIGHT> void EglDisplay<PIX,WIDTH,HEIGHT>::openDisplay (Gtk::Window& appWindow) { std::cout << "Open display-connection through EGL..." << std::endl; // use the X-Window as anchor to build an OpenGL context via EGL Glib::RefPtr<Gdk::Window> gdkWindow = appWindow.get_window(); Window xWindow = GDK_WINDOW_XID (gdkWindow->gobj()); Display* xDisplay = GDK_WINDOW_XDISPLAY (gdkWindow->gobj()); long xScreen; XWindowAttributes xWinAttrs; if (XGetWindowAttributes (xDisplay, xWindow, &xWinAttrs)) xScreen = XScreenNumberOfScreen (xWinAttrs.screen); else __FAIL ("unable to retrieve screen number from the X11 window attributes."); long SCREEN_SPEC[] {EGL_PLATFORM_X11_SCREEN_EXT, xScreen ,EGL_NONE}; display_ = eglGetPlatformDisplay (EGL_PLATFORM_X11_EXT, xDisplay, SCREEN_SPEC); if (EGL_NO_DISPLAY == display_ or not eglInitialize(display_, NULL,NULL)) __FAIL ("could not establish EGL Display connection."); EGLint DESIRED_ATTRIBS[] {EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER ,EGL_RED_SIZE, 4 ,EGL_GREEN_SIZE, 4 ,EGL_BLUE_SIZE, 4 ,EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT // config must support creating an OpenGL context ,EGL_SURFACE_TYPE, EGL_WINDOW_BIT // want to create a window surface ,EGL_DEPTH_SIZE, 0 // prefer config without depth buffer (occlusion testing not needed) ,EGL_STENCIL_SIZE, 0 // prefer config without stencil buffer (no advanced visual effects) ,EGL_NONE}; EGLint _cnt; EGLConfig config; if (not eglChooseConfig (display_ ,DESIRED_ATTRIBS ,& config ,1,&_cnt) or not config) __FAIL ("unable to select a EGL display config with the required attributes"); surface_ = eglCreateWindowSurface (display_,config,xWindow,nullptr); if (not surface_) switch (eglGetError()) { case EGL_BAD_CONFIG: __FAIL ("selected display configuration is not valid"); break; case EGL_BAD_ALLOC: __FAIL ("unable to allocate resources or collision with existing resources"); break; case EGL_BAD_MATCH: __FAIL ("mismatch between X11 window visuals and the capabilities of the selected config"); break; default: __FAIL ("unable to create EGL window surface, for unknown reasons"); break; } if (not eglBindAPI( EGL_OPENGL_API)) __FAIL ("unable to configure EGL for usage with the OpenGL API."); EGLint CTX_ATTRIBS[] // Note: require modern OpenGL {EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT // ,EGL_CONTEXT_MAJOR_VERSION, 3 // ,EGL_CONTEXT_MINOR_VERSION, 3 ,EGL_NONE}; egl_ = eglCreateContext (display_, config ,EGL_NO_CONTEXT // do not share definitions and data with another context ,CTX_ATTRIBS); if (not egl_) __FAIL ("failed to create OpenGL context for this display with desired visuals"); // create a binding for the current thread to use this context on this window if (not eglMakeCurrent (display_, surface_,surface_, egl_)) __FAIL ("failed to attach an OpenGL context to the application X-Window"); // use lib-GLEW to manage OpenGL extensions (notably GL Shader Language) GLenum glewErr = glewInit(); ////////////////////////////////////////////////////////////////////////////////////TODO for some reason I get glewErr == 4 ("unknown error"), but it works non the less //if (GLEW_OK != glewErr) // __FAIL ("could not bind OpenGL extensions through lib-GLEW:\n" // +string{reinterpret_cast<const char*> (glewGetErrorString(glewErr))}); auto compileShader = [](GLenum kind, string code) { uint shaderID = glCreateShader (kind); const GLchar* codeSegment = code.c_str(); glShaderSource (shaderID, 1, &codeSegment, NULL); glCompileShader(shaderID); // diagnostics... int success; glGetShaderiv (shaderID, GL_COMPILE_STATUS, &success); if (not success) { int logSiz{0}; glGetShaderiv (shaderID, GL_INFO_LOG_LENGTH, &logSiz); string report(logSiz,'.'); glGetShaderInfoLog (shaderID, logSiz, NULL, & report[0]); std::cout << "\n■□■□■□■□■----Shader-Code-------------------------\n" << code << "\n■□■□■□■□■----Failure-Report----------------------\n" << report.c_str() << std::endl; __FAIL ("shader compilation failure"); } return shaderID; }; auto checkLinkError = [](uint programID) { int success; glGetProgramiv (programID, GL_LINK_STATUS, &success); if (not success) { int logSiz{0}; glGetProgramiv (programID, GL_INFO_LOG_LENGTH, &logSiz); string report(logSiz,'.'); glGetProgramInfoLog (programID, logSiz, NULL, & report[0]); std::cout << "\n■□■□■□■□■----Failure-Report----------------------\n" << report.c_str() << std::endl; __FAIL ("unable to link shader code"); } }; uint vertexShader = compileShader (GL_VERTEX_SHADER, R"-(#version 330 core layout(location = 0) in vec2 position; layout(location = 1) in vec2 texCoord; out vec2 fragTexCoord; void main() { gl_Position = vec4 (position, 0.0, 1.0); fragTexCoord = texCoord; } )-"); uint fragmentShader = compileShader (GL_FRAGMENT_SHADER, R"-(#version 330 core in vec2 fragTexCoord; out vec4 colour; uniform sampler2D textureSampler; void main() { colour = texture (textureSampler, fragTexCoord); } )-"); uint gpuProgram = glCreateProgram(); glAttachShader (gpuProgram, vertexShader); glAttachShader (gpuProgram, fragmentShader); glLinkProgram (gpuProgram); checkLinkError (gpuProgram); glUseProgram (gpuProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); /* === setup a 2D texture, to be mapped into the viewport === */ glActiveTexture(GL_TEXTURE0); // activate texture-unit #0 on the GPU glGenTextures (1, &texID_); // allocate 1 new texture ID glBindTexture (GL_TEXTURE_2D, texID_); // use this textureID as "the" TEXTURE_2D glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // configure image scaling filter glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // clamp, don't wrap at the texture edges glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // configure the `textureSampler` parameter (»uniform« is a global parameter visible in all shaders) GLint samplerParam = glGetUniformLocation(gpuProgram, "textureSampler"); glUniform1i(samplerParam, 0); // configure the shader to use texture-unit #0 of the GPU /* === setup a fixed geometry to hold that texture === */ glDisable (GL_DEPTH_TEST); // we do not need 3D layering / positioning glViewport (0, 0, WIDTH, HEIGHT); // Origin in the middle of the window (note Y points upwards) // Note: this demo uses a fixed-size window and hard-coded video size; // a real-world implementation would have to place the video frame // dynamically into the available screen space, possibly scaling up/down GLfloat sX{scaleX_}; GLfloat sY{scaleY_}; // Note: positions use »normalised device coordinates« (NDC) GLfloat w{1}; // TEXTURE_2D points cover the range (0,0) ... (1,1) GLfloat h{1}; GLfloat vertexData[] // Geometry and mapping data to be sent to the GPU... //-point-+--texture // We define the 4 edges of a rectangular »projection screen« {-sX,-sY , 0,h // For each edge, we define the position in NDC [-1 ... +1] , sX,-sY , w,h // and we specify which point in the texture to map there , sX, sY , w,0 // Note: the rows of the video image run top-down, and thus we ,-sX, sY , 0,0 // have to map the texture flipped, since OpenGL orients Y upwards. }; glGenBuffers (1, &vboID_); // allocate 1 new vertex-buffer-object glGenVertexArrays(1, &vaoID_); // allocate 1 new vertex-array-object (to attach all definitions) glBindVertexArray(vaoID_); // activate the VAO to pick up the following definitions... glBindBuffer (GL_ARRAY_BUFFER, vboID_); // copy the vertexData into the vertex-buffer-object currently bound to GL_ARRAY_BUFFER glBufferData (GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); auto SIZ = sizeof(float); // parameters are transferred to the GPU as float data auto attrib = 2; // we use 2 input attributes for the shader pipeline auto coords = 2; // our data vectors have 2 coordinates (2D) auto stride = attrib * coords * SIZ; // stride is the offset in bytes between full data points auto offset = [&](uint elm){ return (void*) (elm * SIZ); }; // offset of one attribute relative to the start of a data point (packaged into a void*) auto normalise = GL_FALSE; // do not auto-normalise, positions are already [-1 ... +1] // define how two two attributes are packed into the data sequence we send to the GPU glVertexAttribPointer (0, coords, GL_FLOAT, normalise, stride, offset(0) ); glVertexAttribPointer (1, coords, GL_FLOAT, normalise, stride, offset(2) ); glEnableVertexAttribArray (0); // activate attribute #1 ≙ input argument `position` in vertexShader glEnableVertexAttribArray (1); // activate attribute #2 ≙ input argument `texCoord` in vertexShader std::cout << "Successfully established OpenGL display ("<<WIDTH<<"x"<<HEIGHT<<"px) within GTK-Window." << std::endl; } template<typename PIX, uint WIDTH, uint HEIGHT> void EglDisplay<PIX,WIDTH,HEIGHT>::displayFrame() { auto* currFrame = pixelFeed_.retrieveFrame(); if (not currFrame) return; // pick up current buffer with RGB data // and send it to the GPU (into the prepared texture)... glTexImage2D (GL_TEXTURE_2D // the target texture store to work on, here "the" TEXTURE_2D ,0 // detail level (when using mipmap reduction, which we don't) ,GL_RGB // internal format or features to use for this texture ,WIDTH,HEIGHT, /*border*/ 0 ,GL_RGB // data layout of the provided pixels ,GL_UNSIGNED_BYTE // data format / size of the pixels ,currFrame ); // Draw the quadrilateral glDrawArrays(GL_QUADS, 0, 4); // double-buffer flip, automatically invokes glFlush() eglSwapBuffers (display_, surface_); // signal that processing of this frame is complete pixelFeed_.completedFrame(); } template<typename PIX, uint WIDTH, uint HEIGHT> void EglDisplay<PIX,WIDTH,HEIGHT>::cleanUp() { // std::cout << "STOP " << imgGen_.getFrameNr() << " frames displayed." << std::endl; // detach binding with OpenGL context eglMakeCurrent (display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroySurface (display_, surface_); eglDestroyContext (display_, egl_); eglTerminate(display_); // detach EGL from display }