Lumiera
The new emerging NLE for GNU/Linux
/*
  demoXV.cpp  -  output video from a GTK application, using the X-Video standard

   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-xv-app.hpp"
#include "image-generator.hpp"

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cassert>
#include <string>
#include <set>

// for low-level access -> X-Window
#include <gdk/gdkx.h>

// X11 and XVideo extension
#include <X11/Xlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <X11/extensions/XShm.h>
#include <X11/extensions/Xvlib.h>

using std::string;



/**
 * Sequence of letters of a "fourCC" format ID,
 * packaged numerically into a single 32-bit int.
 * @param id the human-readable 4-character literal string of the fourCC
 * @return ASCII values of these characters packaged in little-endian order.
 */
constexpr int
fourCC (const char id[5])
{
  uint32_t code{0};
  for (uint c=0; c<4; ++c)
      code |= uint(id[c]) << c*8;
  return code;
}

/** display fourCC code in human readable form */
string
fourCCstring (int fcc)
{
  string id{"????"};
  for (uint c=0; c<4; ++c)
    id[c] = 0xFF & (fcc >> c*8);
  return id;
}


const std::set<int> SUPPORTED_FORMATS = {fourCC("I420")
                                        ,fourCC("YV12")
                                        ,fourCC("YUY2")
                                        ,fourCC("UYVY")
                                        ,fourCC("YVYU")
                                       };

/**
 * Output connection context used for opening X-Video display.
 */
struct XvCtx
  {
    /** X11 connection. */
    Display* display{nullptr};
    Window window{0};
    uint port{0};
    GC gc{nullptr};

    int format{0};

    // hard wired here (should be configurable in real-world usage)
    constexpr static uint VIDEO_WIDTH {320};
    constexpr static uint VIDEO_HEIGHT{240};

    /** shared memory image descriptor for the video output */
    XvImage* xvImage{nullptr};

    /** descriptor of the shared memory segment used for data exchange */
    XShmSegmentInfo shmInfo;

    using ImgGen = ImageGenerator<VIDEO_WIDTH,VIDEO_HEIGHT>;
    ImgGen imgGen_;

    XvCtx(FrameRate fps)
      : imgGen_{fps}
      { }
  };


namespace { // implementation details : pixel format conversion

  using std::clamp;

  using ImgGen    = XvCtx::ImgGen;
  using PackedRGB = ImgGen::PackedRGB;


  /** slightly simplified conversion from RGB components to Y'CbCr with Rec.601 (MPEG style) */
  inline Trip
  rgb_to_yuv (Trip const& rgb)
  {
    auto r = int(rgb[0]);
    auto g = int(rgb[1]);
    auto b = int(rgb[2]);
    Trip yuv;
    auto& [y,u,v] = yuv;
    y = byte(clamp (  0 + (    299 * r +    587 * g +    114 * b) /    1000, 16,235));   // Luma clamped to MPEG scan range
    u = byte(clamp (128 + (-168736 * r - 331264 * g + 500000 * b) / 1000000, 0, 255));   // Chroma components mapped according to Rec.601
    v = byte(clamp (128 + ( 500000 * r - 418688 * g -  81312 * b) / 1000000, 0, 255));   // (but with integer arithmetics and truncating)
    return yuv;
  }


  /**
   * These 4:2:2 _packed formats_ have in common that they process two consecutive pixels,
   * while outputting only the chroma information from the first pixel; they differ in
   * the actual arrangement of the four output elements into the packed data sequence.
   */
  void
  rgb_buffer_to_packed (int format, PackedRGB const& in, byte* out)
  {
    assert (format == fourCC ("YUY2") or
            format == fourCC ("UYVY") or
            format == fourCC ("YVYU"));
    uint cntPix = in.size();
    assert (cntPix % 2 == 0);

    for (uint i = 0; i < cntPix; i += 2)
      {// convert and interleave 2 pixels in one step
        uint op = i * 2;                           // Output packed in groups with 2 bytes
        Trip const& rgb0 = in[i];
        Trip const& rgb1 = in[i+1];
        Trip yuv0 = rgb_to_yuv (rgb0);
        Trip yuv1 = rgb_to_yuv (rgb1);

        auto& [y0,u0,v0] = yuv0;
        auto& [y1,_u,_v] = yuv1;                   // note: these formats discard half of the chroma information

        switch (format) {
          case fourCC ("YUY2"):
            out[op    ] = y0;
            out[op + 1] = u0;
            out[op + 2] = y1;
            out[op + 3] = v0;
            break;
          case fourCC ("UYVY"):
            out[op    ] = u0;
            out[op + 1] = y0;
            out[op + 2] = v0;
            out[op + 3] = y1;
            break;
          case fourCC ("YVYU"):
            out[op    ] = y0;
            out[op + 1] = v0;
            out[op + 2] = y1;
            out[op + 3] = u0;
            break;
          default:
            __FAIL ("Logic broken");
            break;
        }
      }
  }


  /**
   * These 4:2:0 _planar formats_ place all Luma information into one data block,
   * followed by two chroma data blocks with 1/4 size each.
   * Every 2 x 2 Block of pixels shares a single Chroma sample:
   * - 2 adjacent horizontal Y components on the same line share a UV Chroma
   *   => store the UV component for all even columns and skip odd columns
   * - 2 stacked adjacent vertical Y components, one above the other,
   *   on consecutive lines share a single Chroma sample
   *   => do not store any UV data on every second line of pixels
   */
  void
  rgb_buffer_to_planar (int format, PackedRGB const& in, byte* out, uint width, uint height)
    {
      assert (format == fourCC("I420") or
              format == fourCC("YV12"));
      uint cntPix = in.size ();
      assert (cntPix % 4 == 0);
      assert (in.size() == width*height);

      byte* outU;
      byte* outV;
      if (format == fourCC("I420"))
        {
          outU = out + cntPix;
          outV = outU + cntPix /4;
        }
      else
      if (format == fourCC("YV12"))
        {
          outV = out + cntPix;
          outU = outV + cntPix /4;
        }
      else
        __FAIL ("Developer wake up");

      for (uint yIdx = 0, uvIdx = 0; yIdx < cntPix; ++yIdx)
        {
          bool evenLine = yIdx % (2*width) < width;
          bool evenCol  = yIdx % 2 == 0;

          Trip const& rgb = in[yIdx];
          Trip yuv = rgb_to_yuv (rgb);
          auto& [y, u, v] = yuv;

          out[yIdx] = y;
          if (evenLine and evenCol)
            {// output chroma
              outU[uvIdx] = u;
              outV[uvIdx] = v;
              ++uvIdx;
            }
        }
    }
} // (End) implementation details



void
convert_RGB_intoBuffer (int format
                       ,char* targetBuff, int targetSiz
                       ,PackedRGB const& inputFrame
                       ,uint width, uint height
                       )
{
  static_assert (sizeof(byte) == sizeof(char));
  byte* outputData = reinterpret_cast<byte*> (targetBuff);

  if (format == fourCC("YUY2") or
      format == fourCC("UYVY") or
      format == fourCC("YVYU"))
    { // Handle popular packed formats:
      // These formats discard 1/3 of the information
      // input comes in RGB triplets, output discards 50% chroma
      assert (targetSiz > 0);
      assert (static_cast<uint>(targetSiz) == 2 * inputFrame.size());
      rgb_buffer_to_packed (format, inputFrame, outputData);
    }
  else
  if (format == fourCC("I420") or
      format == fourCC("YV12"))
    { // Handle common planar formats:
      // These formats discard half of the information
      // input comes in RGB triplets, output discards 75% chroma
      assert (targetSiz > 0);
      assert (static_cast<uint>(targetSiz) == inputFrame.size() *3/2);
      rgb_buffer_to_planar (format, inputFrame, outputData, width, height);
    }
  else
    __FAIL ("Logic broken: unsupported output target format");
}



XvCtx
openDisplay (Gtk::Window& appWindow, FrameRate fps)
{
  std::cout << "Open X-Video display slot..." << std::endl;

  Glib::RefPtr<Gdk::Window> gdkWindow = appWindow.get_window();

  XvCtx ctx{fps};
  ctx.window  = GDK_WINDOW_XID      (gdkWindow->gobj());
  ctx.display = GDK_WINDOW_XDISPLAY (gdkWindow->gobj());

  if (not XShmQueryExtension(ctx.display))
    __FAIL ("X11 shared memory extension not available for this display.");


  std::set<int> formats{}; // collection of usable formats supported by this setup

  uint count;
  XvAdaptorInfo* adaptorInfo;
  if (Success != XvQueryAdaptors (ctx.display, ctx.window, &count, &adaptorInfo))
    __FAIL ("unable to query XVideo adapters -- XV extension not available.");
  else
    {
      bool foundPort{false};
      for (uint n = 0; n < count and not foundPort; ++n )
        {
          if (not (adaptorInfo[n].type & XvImageMask))
            continue;                 // supports output of (frame)image data
          for (uint port = adaptorInfo[n].base_id;
                  port < adaptorInfo[n].base_id + adaptorInfo[n].num_ports;
                  port ++ )
            {
              if (Success == XvGrabPort (ctx.display, port, CurrentTime))
                {
                  auto isSupportedFormat = [](int formatCode){ return contains (SUPPORTED_FORMATS, formatCode); };

                  int num_formats;
                  XvImageFormatValues* list = XvListImageFormats (ctx.display, port, &num_formats);
                  for (int i = 0; i < num_formats; ++i)
                      if (isSupportedFormat (list[i].id))
                        formats.insert (list[i].id);

                  foundPort = not formats.empty();
                  if (foundPort)
                    {
                      ctx.port = port;
                      break;
                    }
                  else
                    XvUngrabPort (ctx.display, port, CurrentTime );
                }
            }//for all ports
        }// for all adaptors
      XvFreeAdaptorInfo (adaptorInfo);

      if (not foundPort)
        __FAIL ("unable to allocate XV port or unsupported pixel format.");

      // after having established a connection to the X-server,
      // allocate resources and setup buffers for the actual output
      ctx.gc = XCreateGC (ctx.display, ctx.window, 0, nullptr);

      // select one of the supported output data formats
      ctx.format = *formats.begin();
      std::cout << "Using Format: " << fourCCstring(ctx.format) << std::endl;

      ctx.xvImage = static_cast<XvImage*> (XvShmCreateImage (ctx.display
                                                            ,ctx.port
                                                            ,ctx.format
                                                            ,nullptr          // shared-mem buffer will be attached later
                                                            ,ctx.VIDEO_WIDTH
                                                            ,ctx.VIDEO_HEIGHT
                                                            ,&ctx.shmInfo
                                                            ));
      // allocate a shared-memory buffer
      // with a size as indicated in xvImage
      ctx.shmInfo.shmid = shmget (IPC_PRIVATE, ctx.xvImage->data_size, IPC_CREAT | 0777);
      if (ctx.shmInfo.shmid < 0)
        __FAIL ("unable to allocate a shared memory buffer for image data exchange");


      ctx.xvImage->data =
      ctx.shmInfo.shmaddr = static_cast<char*> (shmat (ctx.shmInfo.shmid, nullptr, 0));
      ctx.shmInfo.readOnly = false;

      if (not XShmAttach (ctx.display, &ctx.shmInfo))
        __FAIL ("failed to establish shared-memory setup for communication with XServer");

      XSync (ctx.display, false);
      shmctl(ctx.shmInfo.shmid, IPC_RMID, 0);
    }
   // hand-over the activated connection context
  //  to be managed by the GTK application...
  std::cout << "Started playback at "<<fps<<" frames/sec." << std::endl;
  return ctx;
}


void
displayFrame (XvCtx& ctx)
{
  assert (ctx.xvImage and ctx.xvImage->data);
  uint frameNr = ctx.imgGen_.getFrameNr();
  uint fps     = ctx.imgGen_.getFps();
  if (0 == frameNr % fps)
    std::cout << "tick ... " << ctx.imgGen_.getFrameNr() << std::endl;

  int org_x = 0
    , org_y = 0
    , destW = ctx.VIDEO_WIDTH
    , destH = ctx.VIDEO_HEIGHT;
  //  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

  convert_RGB_intoBuffer (ctx.format
                         ,ctx.xvImage->data
                         ,ctx.xvImage->data_size
                         ,ctx.imgGen_.buildNext()
                         ,ctx.VIDEO_WIDTH
                         ,ctx.VIDEO_HEIGHT
                         );

  XvShmPutImage (ctx.display, ctx.port, ctx.window, ctx.gc
                ,ctx.xvImage
                ,0, 0, ctx.VIDEO_WIDTH, ctx.VIDEO_HEIGHT
                ,org_x, org_y, destW, destH
                ,false
                );
  XFlush (ctx.display);
}


void
cleanUp (XvCtx& ctx)
{
  std::cout << "STOP " << ctx.imgGen_.getFrameNr() << " frames displayed." << std::endl;

  XvStopVideo (ctx.display, ctx.port, ctx.window);
  XSync (ctx.display, false);
  if (ctx.shmInfo.shmaddr)
    {
      XShmDetach (ctx.display, &ctx.shmInfo);
      shmdt (ctx.shmInfo.shmaddr);
    }
  if (ctx.xvImage)
    XFree (ctx.xvImage);
  XFreeGC (ctx.display, ctx.gc);
  XvUngrabPort (ctx.display, ctx.port, CurrentTime);
}



int
main (int, const char*[])
{
    return GtkXvApp<XvCtx>{"demo.xv"}
            .onStart (openDisplay)
            .onFrame (displayFrame)
            .onClose (cleanUp)
            .run(30);
}