2026-07-14 19:27:27 +02:00
/*
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"
2026-07-14 20:16:18 +02:00
# include "gtk-uring-app.hpp"
2026-07-14 19:27:27 +02:00
2026-07-15 18:56:37 +02:00
# include <functional>
2026-07-14 19:27:27 +02:00
# include <algorithm>
# include <cassert>
# include <string>
2026-07-15 18:56:37 +02:00
# include <array>
2026-07-14 19:27:27 +02:00
# 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 .
*/
2026-07-15 18:56:37 +02:00
template < typename PIX , uint WIDTH , uint HEIGHT >
2026-07-14 19:27:27 +02:00
class EglDisplay
2026-07-15 20:40:59 +02:00
: NonCopyable
2026-07-14 19:27:27 +02:00
{
2026-07-15 18:56:37 +02:00
public :
using Buffer = std : : array < PIX , WIDTH * HEIGHT * 3 > ;
2026-07-15 20:40:59 +02:00
/**
2026-08-09 20:11:26 +02:00
* Interface : retrieve pixel data for video frames to display .
2026-07-15 20:40:59 +02:00
* - 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 }
2026-07-15 18:56:37 +02:00
{
openDisplay ( appWindow ) ;
2026-07-15 20:40:59 +02:00
pixelFeed_ . connect ( [ this ] { displayFrame ( ) ; } ) ;
2026-07-15 18:56:37 +02:00
}
void cleanUp ( ) ;
private :
2026-07-14 19:27:27 +02:00
/** EGL / X11 connection. */
2026-07-14 20:16:18 +02:00
EGLDisplay display_ { nullptr } ;
EGLSurface surface_ { nullptr } ;
2026-07-14 19:27:27 +02:00
2026-07-14 20:16:18 +02:00
EGLContext egl_ { nullptr } ;
2026-07-14 19:27:27 +02:00
2026-07-14 20:16:18 +02:00
uint texID_ { 0 } ;
uint vboID_ { 0 } ;
uint vaoID_ { 0 } ;
2026-07-14 19:27:27 +02:00
2026-07-14 20:16:18 +02:00
float scaleX_ { 1 } ;
float scaleY_ { 1 } ;
2026-07-14 19:27:27 +02:00
2026-08-09 20:11:26 +02:00
/** Callback to retrieve pixel data */
2026-07-15 20:40:59 +02:00
PixelFeed & pixelFeed_ { } ;
2026-07-14 20:16:18 +02:00
void openDisplay ( Gtk : : Window & ) ;
void displayFrame ( ) ;
2026-07-14 19:27:27 +02:00
} ;
2026-07-15 18:56:37 +02:00
template < typename PIX , uint WIDTH , uint HEIGHT >
2026-07-14 20:16:18 +02:00
void
2026-07-15 18:56:37 +02:00
EglDisplay < PIX , WIDTH , HEIGHT > : : openDisplay ( Gtk : : Window & appWindow )
2026-07-14 19:27:27 +02:00
{
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 } ;
2026-07-14 20:16:18 +02:00
display_ = eglGetPlatformDisplay ( EGL_PLATFORM_X11_EXT , xDisplay , SCREEN_SPEC ) ;
if ( EGL_NO_DISPLAY = = display_
or not eglInitialize ( display_ , NULL , NULL ) )
2026-07-14 19:27:27 +02:00
__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 ;
2026-07-14 20:16:18 +02:00
if ( not eglChooseConfig ( display_
2026-07-14 19:27:27 +02:00
, DESIRED_ATTRIBS
, & config
, 1 , & _cnt )
or not config )
__FAIL ( " unable to select a EGL display config with the required attributes " ) ;
2026-07-14 20:16:18 +02:00
surface_ = eglCreateWindowSurface ( display_ , config , xWindow , nullptr ) ;
if ( not surface_ )
2026-07-14 19:27:27 +02:00
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 } ;
2026-07-14 20:16:18 +02:00
egl_ = eglCreateContext ( display_ , config
, EGL_NO_CONTEXT // do not share definitions and data with another context
, CTX_ATTRIBS ) ;
if ( not egl_ )
2026-07-14 19:27:27 +02:00
__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
2026-07-14 20:16:18 +02:00
if ( not eglMakeCurrent ( display_ , surface_ , surface_ , egl_ ) )
2026-07-14 19:27:27 +02:00
__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 ( ) ;
2026-07-16 19:36:49 +02:00
////////////////////////////////////////////////////////////////////////////////////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))});
2026-07-14 19:27:27 +02:00
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
2026-07-14 20:16:18 +02:00
glGenTextures ( 1 , & texID_ ) ; // allocate 1 new texture ID
glBindTexture ( GL_TEXTURE_2D , texID_ ) ; // use this textureID as "the" TEXTURE_2D
2026-07-14 19:27:27 +02:00
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
2026-07-15 18:56:37 +02:00
glViewport ( 0 , 0 , WIDTH , HEIGHT ) ; // Origin in the middle of the window (note Y points upwards)
2026-07-14 19:27:27 +02:00
// 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
2026-07-14 20:16:18 +02:00
GLfloat sX { scaleX_ } ;
GLfloat sY { scaleY_ } ; // Note: positions use »normalised device coordinates« (NDC)
2026-07-14 19:27:27 +02:00
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.
} ;
2026-07-14 20:16:18 +02:00
glGenBuffers ( 1 , & vboID_ ) ; // allocate 1 new vertex-buffer-object
glGenVertexArrays ( 1 , & vaoID_ ) ; // allocate 1 new vertex-array-object (to attach all definitions)
2026-07-14 19:27:27 +02:00
2026-07-14 20:16:18 +02:00
glBindVertexArray ( vaoID_ ) ; // activate the VAO to pick up the following definitions...
glBindBuffer ( GL_ARRAY_BUFFER , vboID_ ) ;
2026-07-14 19:27:27 +02:00
// 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
2026-07-15 18:56:37 +02:00
std : : cout < < " Successfully established OpenGL display ( " < < WIDTH < < " x " < < HEIGHT < < " px) within GTK-Window. " < < std : : endl ;
2026-07-14 19:27:27 +02:00
}
2026-07-15 18:56:37 +02:00
template < typename PIX , uint WIDTH , uint HEIGHT >
2026-07-14 19:27:27 +02:00
void
2026-07-15 18:56:37 +02:00
EglDisplay < PIX , WIDTH , HEIGHT > : : displayFrame ( )
2026-07-14 19:27:27 +02:00
{
2026-07-16 19:31:34 +02:00
auto * currFrame = pixelFeed_ . retrieveFrame ( ) ;
2026-07-15 18:56:37 +02:00
if ( not currFrame ) return ;
// pick up current buffer with RGB data
// and send it to the GPU (into the prepared texture)...
2026-07-14 19:27:27 +02:00
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
2026-07-15 18:56:37 +02:00
, WIDTH , HEIGHT , /*border*/ 0
2026-07-14 19:27:27 +02:00
, GL_RGB // data layout of the provided pixels
, GL_UNSIGNED_BYTE // data format / size of the pixels
2026-07-15 18:56:37 +02:00
, currFrame
2026-07-14 19:27:27 +02:00
) ;
// Draw the quadrilateral
glDrawArrays ( GL_QUADS , 0 , 4 ) ;
// double-buffer flip, automatically invokes glFlush()
2026-07-14 20:16:18 +02:00
eglSwapBuffers ( display_ , surface_ ) ;
2026-07-15 20:40:59 +02:00
// signal that processing of this frame is complete
pixelFeed_ . completedFrame ( ) ;
2026-07-14 19:27:27 +02:00
}
2026-07-15 18:56:37 +02:00
template < typename PIX , uint WIDTH , uint HEIGHT >
2026-07-14 19:27:27 +02:00
void
2026-07-15 18:56:37 +02:00
EglDisplay < PIX , WIDTH , HEIGHT > : : cleanUp ( )
2026-07-14 19:27:27 +02:00
{
2026-07-15 18:56:37 +02:00
// std::cout << "STOP " << imgGen_.getFrameNr() << " frames displayed." << std::endl;
2026-07-14 19:27:27 +02:00
// detach binding with OpenGL context
2026-07-14 20:16:18 +02:00
eglMakeCurrent ( display_ , EGL_NO_SURFACE , EGL_NO_SURFACE , EGL_NO_CONTEXT ) ;
eglDestroySurface ( display_ , surface_ ) ;
eglDestroyContext ( display_ , egl_ ) ;
eglTerminate ( display_ ) ; // detach EGL from display
2026-07-14 19:27:27 +02:00
}