Lumiera
The new emerging NLE for GNU/Linux
/*
  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
}