- rename the states to better reflect their purpose - introduce getters for in BufferDispatcher
376 lines
15 KiB
C++
376 lines
15 KiB
C++
/*
|
||
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 functor to access current 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();
|
||
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
|
||
}
|