Lumiera
The new emerging NLE for GNU/Linux
/*
  demoSDL1.cpp  -  output video from a GTK application, using the SDL Abstraction (v1.2)

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

#include <iostream>
#include <algorithm>
#include <cassert>
#include <array>

#include <SDL/SDL.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;
}

constexpr auto SUPPORTED_FORMATS = std::array{fourCC("YUY2")
//                                           ,fourCC("UYVY")    ///////TODO implement
//                                           ,fourCC("YVYU")
//                                           ,fourCC("IYUV")    ///////TODO implement  (this is equivalent to I420)
//                                           ,fourCC("YV12")    ///////TODO implement
                                             };



/**
 * Output connection context used for video display via SDL.
 */
struct SdlCtx
  {
    SDL_Surface* surface_{nullptr};
    SDL_Overlay* overlay_{nullptr};
    SDL_Rect windowPos_{};
    SDL_Rect targetPos_{};


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

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

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


namespace { // implementation details : pixel format conversion

  using std::clamp;

  using ImgGen    = SdlCtx::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;
  }


  void
  rgb_buffer_to_yuy2 (PackedRGB const& in, byte* out)
  {
    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: this format discards half of the chroma information

        out[op    ] = y0;
        out[op + 1] = u0;
        out[op + 2] = y1;
        out[op + 3] = v0;
  }   }


} // (End) implementation details


void
convert_RGB_intoBuffer (SDL_Overlay& target
                       ,PackedRGB const& inputFrame)
{
  static_assert (sizeof(byte) == sizeof(uint8_t));

  if (target.format == fourCC("YUY2"))
    {
      // this format discards 1/3 of the information
      // input comes in RGB triplets, output discards 50% chroma
      assert (target.planes == 1);
      assert (target.pitches[0] == 2 * SdlCtx::VIDEO_WIDTH);
      byte* outputData = reinterpret_cast<byte*> (target.pixels[0]);
      rgb_buffer_to_yuy2 (inputFrame, outputData);
    }
  else
    __FAIL ("Logic broken: unsupported output target format");
}


SDL_Rect
determinePosition (Gtk::Window const& appWindow)
{
  int x,y,w,h;
  appWindow.get_position (x, y);
  appWindow.get_size (w,h);
  SDL_Rect pos;
  pos.x = x; // note narrowing conversion int -> int16_t
  pos.y = y;
  pos.w = w;
  pos.h = h;
  return pos;
}


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

  SdlCtx ctx{fps};
  int success = SDL_Init(SDL_INIT_VIDEO);
  if (success < 0)
    __FAIL ("SDL framework not usable.");

  // Note: when calling GetVideoInfo _before_ SetVideoMode,
  //       SDL will fill in the _best available_ video format.
  SDL_VideoInfo const* videoInfo = SDL_GetVideoInfo();
  uint8_t bpp = videoInfo->vfmt->BitsPerPixel;
  if (bpp == 8)
    __FAIL ("this system only supports palette colours.");

  ctx.windowPos_ = determinePosition (appWindow);
  std::cout << ".... System supports "<<uint(bpp)<<" bit-per-pixel colour\n"
            << ".... Screen size: "
            << videoInfo->current_w << " x "
            << videoInfo->current_h << " px\n"
            << ".... Window: at ("
            << ctx.windowPos_.x <<","<< ctx.windowPos_.y << ") size "
            << ctx.windowPos_.w<<" x "<<ctx.windowPos_.h << " px"
            << std::endl;

  assert (bpp == 16 or bpp == 24 or bpp == 32);
  assert (ctx.windowPos_.w == ctx.VIDEO_WIDTH and // ◁────────────────────┨ specifically arranged for this demo
          ctx.windowPos_.h == ctx.VIDEO_HEIGHT);

  const auto DISPLAY_FLAGS = SDL_HWSURFACE        // framebuffer surface backed by video hardware memory
                           | SDL_DOUBLEBUF        // request hardware supported double-buffering
                           | SDL_NOFRAME;         // request surface without window decoration

  ctx.surface_ = SDL_SetVideoMode (ctx.VIDEO_WIDTH, ctx.VIDEO_HEIGHT, bpp, DISPLAY_FLAGS);
  if (not ctx.surface_)
    __FAIL ("access to hardware backed framebuffer not possible");

  for (int formatCode : SUPPORTED_FORMATS)
    {
      ctx.overlay_ = SDL_CreateYUVOverlay (ctx.VIDEO_WIDTH
                                          ,ctx.VIDEO_HEIGHT
                                          ,formatCode     // ◁────────────┨ supported formats are defined in sdl.h (but happen to be fourCC codes)
                                          ,ctx.surface_); // ◁────────────┨ this establishes a link to the underlying surface
      if (ctx.overlay_
          and ctx.overlay_->hw_overlay)
        {
          std::cout << ".... Format: " << fourCCstring (formatCode) << std::endl;
          break;
        }
    }
  if (not ctx.overlay_)
    __FAIL ("unable to setup a YUV converter with a supported format");

   //////////////////////////////////////TODO find out how to position a transparent overly into the App window
  ctx.targetPos_ = ctx.windowPos_;
  ctx.targetPos_.x = 0;
  ctx.targetPos_.y = 0;

   // hand-over the activated connection context
  //  to be managed by the GTK application...
  std::cout << "\nStarted playback at "<<fps<<" frames/sec." << std::endl;
  return ctx;
}


void
displayFrame (SdlCtx& ctx)
{
  uint frameNr = ctx.imgGen_.getFrameNr();
  uint fps     = ctx.imgGen_.getFps();
  if (0 == frameNr % fps)
    std::cout << "tick ... " << ctx.imgGen_.getFrameNr() << std::endl;

  SDL_LockYUVOverlay (ctx.overlay_);
  convert_RGB_intoBuffer (*ctx.overlay_
                         ,ctx.imgGen_.buildNext()
                         );
  SDL_UnlockYUVOverlay (ctx.overlay_);

  //  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;
  //        In such a case, you'd pass the desired target position here, and SDL would scale
  SDL_DisplayYUVOverlay (ctx.overlay_, & ctx.targetPos_);
}


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

  if (ctx.overlay_)
    SDL_FreeYUVOverlay (ctx.overlay_);
  // note: ctx.surface_ is a system resource and will be managed/freed by SDL
  SDL_Quit();
}



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