// MythTV headers
#include "openglvideo.h"
#include "mythcontext.h"
#include "tv.h"
#include "mythrender_opengl.h"
#include "mythavutil.h"

#define LOC QString("GLVid: ")
#define COLOUR_UNIFORM "m_colourMatrix"
#define MYTHTV_YV12 0x8a20

enum DisplayBuffer
{
    kDefaultBuffer,
    kFrameBufferObject
};

class OpenGLFilter
{
    public:
        vector<GLuint> fragmentPrograms;
        uint           numInputs;
        vector<GLuint> frameBuffers;
        vector<GLuint> frameBufferTextures;
        DisplayBuffer  outputBuffer;
};

/**
 * \class OpenGLVideo
 *  A class used to display video frames and associated imagery
 *  using the OpenGL API.
 *
 *  The basic operational concept is to use a series of filter stages to
 *  generate the desired video output, using limited software assistance
 *  alongside OpenGL fragment programs (deinterlacing and YUV->RGB conversion)
 *  , FrameBuffer Objects (flexible GPU storage) and PixelBuffer Objects
 *  (faster CPU->GPU memory transfers).
 *
 *  In the most basic case, for example, a YV12 frame pre-converted in software
 *  to BGRA format is simply blitted to the frame buffer.
 *  Currently, the most complicated example is the rendering of a standard
 *  definition, interlaced frame to a high(er) definition display using
 *  OpenGL (i.e. hardware based) deinterlacing, colourspace conversion and
 *  bicubic upsampling.
 *
 *  Higher level tasks such as coordination between OpenGLVideo instances,
 *  video buffer management, audio/video synchronisation etc are handled by
 *  the higher level classes VideoOutput and NuppelVideoPlayer. The bulk of
 *  the lower level interface with the window system and OpenGL is handled by
 *  MythRenderOpenGL.
 *
 *  N.B. Direct use of OpenGL calls is minimised to maintain platform
 *  independance. The only member function where this is impractical is
 *  PrepareFrame().
 *
 *  \warning Any direct OpenGL calls must be wrapped by calls to
 *  gl_context->MakeCurrent(). Alternatively use the convenience class
 *  OpenGLLocker.
 */

/**
 *  Create a new OpenGLVideo instance that must be initialised
 *  with a call to OpenGLVideo::Init()
 */

OpenGLVideo::OpenGLVideo() :
    gl_context(nullptr),      video_disp_dim(0,0),
    video_dim(0,0),           viewportSize(0,0),
    masterViewportSize(0,0),  display_visible_rect(0,0,0,0),
    display_video_rect(0,0,0,0), video_rect(0,0,0,0),
    frameBufferRect(0,0,0,0), hardwareDeinterlacing(false),
    colourSpace(nullptr),     viewportControl(false),
    inputTextureSize(0,0),    currentFrameNum(0),
    inputUpdated(false),      refsNeeded(0),
    textureRects(false),      textureType(GL_TEXTURE_2D),
    helperTexture(0),         defaultUpsize(kGLFilterResize),
    gl_features(0),           videoTextureType(GL_BGRA),
    preferYCBCR(false)
{
}

OpenGLVideo::~OpenGLVideo()
{
    OpenGLLocker ctx_lock(gl_context);
    Teardown();
}

void OpenGLVideo::Teardown(void)
{
    if (helperTexture)
        gl_context->DeleteTexture(helperTexture);
    helperTexture = 0;

    DeleteTextures(&inputTextures);
    DeleteTextures(&referenceTextures);

    while (!filters.empty())
    {
        RemoveFilter(filters.begin()->first);
        filters.erase(filters.begin());
    }
}

/**
 *  \param glcontext          the MythRenderOpenGL object responsible for lower
 *   levelwindow and OpenGL context integration
 *  \param colourspace        the colourspace management object
 *  \param videoDim           the size of the video source
 *  \param videoDispDim       the size of the display
 *  \param displayVisibleRect the bounding rectangle of the OpenGL window
 *  \param displayVideoRect   the bounding rectangle for the area to display
 *   the video frame
 *  \param videoRect          the portion of the video frame to display in
     displayVideoRect
 *  \param viewport_control   if true, this instance may permanently change
     the OpenGL viewport
 *  \param options            a string defining OpenGL features to disable
 *  \param hw_accel           if true, a GPU decoder will copy frames directly
     to an RGBA texture
 */

bool OpenGLVideo::Init(MythRenderOpenGL *glcontext, VideoColourSpace *colourspace,
                       QSize videoDim, QSize videoDispDim,
                       QRect displayVisibleRect,
                       QRect displayVideoRect, QRect videoRect,
                       bool viewport_control, QString options,
                       bool hw_accel)
{
    if (!glcontext)
        return false;

    gl_context            = glcontext;
    OpenGLLocker ctx_lock(gl_context);

    video_dim             = videoDim;
    video_disp_dim        = videoDispDim;
    display_visible_rect  = displayVisibleRect;
    display_video_rect    = displayVideoRect;
    video_rect            = videoRect;
    masterViewportSize    = display_visible_rect.size();
    frameBufferRect       = QRect(QPoint(0,0), video_disp_dim);
    softwareDeinterlacer  = "";
    hardwareDeinterlacing = false;
    colourSpace           = colourspace;
    viewportControl       = viewport_control;
    inputTextureSize      = QSize(0,0);
    currentFrameNum       = -1;
    inputUpdated          = false;

    // OpenGL-Lite - use implementation specific extensions for updating frames
    if (options.contains("preferycbcr"))
        preferYCBCR = true;

    // Set OpenGL feature support
    gl_features = gl_context->GetFeatures();

    if (viewportControl)
        gl_context->SetFence();

    SetViewPort(display_visible_rect.size());

    bool glsl    = gl_features & kGLSL;
    bool shaders = glsl || (gl_features & kGLExtFragProg);
    bool fbos    = gl_features & kGLExtFBufObj;
    bool pbos    = gl_features & kGLExtPBufObj;

    #ifdef ANDROID
    #define YV12DEFAULT false
    #else
    #define YV12DEFAULT true
    #endif

    bool yv12 = gCoreContext->GetBoolSetting("OpenGLYV12", YV12DEFAULT)
        && !getenv("OPENGL_NOYV12");
    bool uyvy = gCoreContext->GetBoolSetting("OpenGLUYVY", true)
        && !getenv("OPENGL_NOUYVY");
    bool ycbcr   = (gl_features & kGLMesaYCbCr) || (gl_features & kGLAppleYCbCr);

    // warn about the lite profile when it offers no benefit
    if (!ycbcr && preferYCBCR)
    {
        LOG(VB_GENERAL, LOG_WARNING, LOC +
            "You have selected the opengl-lite profile but no required OpenGL "
            "extensions are available.");
    }

    // decide on best video input texture format
    videoTextureType = GL_BGRA;
    if (hw_accel)
        videoTextureType = GL_RGBA;
    else if ((!shaders || preferYCBCR) && (gl_features & kGLMesaYCbCr))
        videoTextureType = GL_YCBCR_MESA;
    else if ((!shaders || preferYCBCR) && (gl_features & kGLAppleYCbCr))
        videoTextureType = GL_YCBCR_422_APPLE;
    else if (glsl && fbos && !(pbos && uyvy) && yv12)
        videoTextureType = MYTHTV_YV12;
    else if (shaders && fbos && uyvy)
        videoTextureType = MYTHTV_UYVY;

    // colourspace adjustments require shaders to operate on YUV textures
    if ((GL_BGRA != videoTextureType) &&
        (MYTHTV_UYVY != videoTextureType) &&
        (MYTHTV_YV12 != videoTextureType))
    {
        colourSpace->SetSupportedAttributes(kPictureAttributeSupported_None);
    }

    // turn on bicubic filtering
    if (options.contains("openglbicubic"))
    {
        if (shaders && fbos)
            defaultUpsize = kGLFilterBicubic;
        else
            LOG(VB_PLAYBACK, LOG_ERR, LOC +
                "No OpenGL feature support for Bicubic filter.");
    }

    // decide on best input texture type
    if ((GL_RGBA != videoTextureType) &&
        (MYTHTV_YV12 != videoTextureType) &&
        (defaultUpsize != kGLFilterBicubic) &&
        (gl_features & kGLExtRect))
    {
        textureType = gl_context->GetTextureType(textureRects);
    }

    // Create initial input texture and associated filter stage
    GLuint tex = CreateVideoTexture(video_dim, inputTextureSize);
    bool    ok = false;

    if ((GL_BGRA == videoTextureType) || (MYTHTV_UYVY == videoTextureType))
        ok = tex && AddFilter(kGLFilterYUV2RGB);
    else if (MYTHTV_YV12 == videoTextureType)
        ok = tex && AddFilter(kGLFilterYV12RGB);
    else
        ok = tex && AddFilter(kGLFilterResize);

    if (ok)
    {
        if (GL_RGBA == videoTextureType)
            LOG(VB_GENERAL, LOG_INFO, LOC + "Using raw RGBA input textures.");
        else if ((GL_YCBCR_MESA == videoTextureType) ||
                 (GL_YCBCR_422_APPLE == videoTextureType))
            LOG(VB_GENERAL, LOG_INFO, LOC +
                "Using YCbCr->BGRA input textures.");
        else if (MYTHTV_YV12 == videoTextureType)
            LOG(VB_GENERAL, LOG_INFO, LOC +
                "Using YV12 input textures.");
        else if (MYTHTV_UYVY == videoTextureType)
            LOG(VB_GENERAL, LOG_INFO, LOC +
                "Using custom UYVY input textures.");
        else
            LOG(VB_GENERAL, LOG_INFO, LOC +
                "Using plain BGRA input textures.");
        inputTextures.push_back(tex);
    }
    else
        Teardown();

    if (filters.empty())
    {
        LOG(VB_PLAYBACK, LOG_INFO, LOC +
                "Failed to setup colourspace conversion.\n\t\t\t"
                "Falling back to software conversion.\n\t\t\t"
                "Any opengl filters will also be disabled.");

        videoTextureType = GL_BGRA;
        GLuint bgra32tex = CreateVideoTexture(video_dim, inputTextureSize);

        if (bgra32tex && AddFilter(kGLFilterResize))
        {
            inputTextures.push_back(bgra32tex);
            colourSpace->SetSupportedAttributes(kPictureAttributeSupported_None);
        }
        else
        {
            LOG(VB_GENERAL, LOG_ERR, LOC + "Fatal error");
            Teardown();
            return false;
        }
    }

    bool mmx = false;
#ifdef MMX
    // cppcheck-suppress redundantAssignment
    mmx = true;
#endif

    CheckResize(false);

    LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("MMX: %1 PBO: %2")
            .arg(mmx).arg((gl_features & kGLExtPBufObj) > 0));

    return true;
}

/**
 *   Determine if the output is to be scaled at all and create or destroy
 *   the appropriate filter as necessary.
 */

void OpenGLVideo::CheckResize(bool deinterlacing, bool allow)
{
    // to improve performance on slower cards
    bool resize_up = ((video_disp_dim.height() < display_video_rect.height()) ||
                     (video_disp_dim.width() < display_video_rect.width())) && allow;

    // to ensure deinterlacing works correctly
    bool resize_down = (video_disp_dim.height() > display_video_rect.height()) &&
                        deinterlacing && allow;

    // UYVY packed pixels must be sampled exactly and any overscan settings will
    // break sampling - so always force an extra stage
    resize_down |= videoTextureType == MYTHTV_UYVY;
    // Extra stage needed on Fire Stick 4k, maybe others, because of blank screen when playing.
    resize_down |= gCoreContext->GetBoolSetting("OpenGLExtraStage", false);

    if (resize_up && (defaultUpsize == kGLFilterBicubic))
    {
        RemoveFilter(kGLFilterResize);
        filters.erase(kGLFilterResize);
        AddFilter(kGLFilterBicubic);
        OptimiseFilters();
        return;
    }

    if ((resize_up && (defaultUpsize == kGLFilterResize)) || resize_down)
    {
        RemoveFilter(kGLFilterBicubic);
        filters.erase(kGLFilterBicubic);
        AddFilter(kGLFilterResize);
        OptimiseFilters();
        return;
    }

    RemoveFilter(kGLFilterBicubic);
    filters.erase(kGLFilterBicubic);
    OptimiseFilters();
}

/**
 *  Ensure the current chain of OpenGLFilters is logically correct
 *  and has the resources required to complete rendering.
 */

bool OpenGLVideo::OptimiseFilters(void)
{
    glfilt_map_t::reverse_iterator it;

    // add/remove required frame buffer objects
    // and link filters
    uint buffers_needed = 1;
    bool last_filter    = true;
    for (it = filters.rbegin(); it != filters.rend(); ++it)
    {
        if (!last_filter)
        {
            it->second->outputBuffer = kFrameBufferObject;
            uint buffers_have = it->second->frameBuffers.size();
            int buffers_diff = buffers_needed - buffers_have;
            if (buffers_diff > 0)
            {
                uint tmp_buf, tmp_tex;
                for (int i = 0; i < buffers_diff; i++)
                {
                    if (!AddFrameBuffer(tmp_buf, tmp_tex, video_disp_dim))
                        return false;
                    else
                    {
                        it->second->frameBuffers.push_back(tmp_buf);
                        it->second->frameBufferTextures.push_back(tmp_tex);
                    }
                }
            }
            else if (buffers_diff < 0)
            {
                for (int i = 0; i > buffers_diff; i--)
                {
                    OpenGLFilter *filt = it->second;

                    gl_context->DeleteFrameBuffer(
                        filt->frameBuffers.back());
                    gl_context->DeleteTexture(
                        filt->frameBufferTextures.back());

                    filt->frameBuffers.pop_back();
                    filt->frameBufferTextures.pop_back();
                }
            }
        }
        else
        {
            it->second->outputBuffer = kDefaultBuffer;
            last_filter = false;
        }
        buffers_needed = it->second->numInputs;
    }

    SetFiltering();

    return true;
}

/**
 *  Set the OpenGL texture mapping functions to optimise speed and quality.
 */

void OpenGLVideo::SetFiltering(void)
{
    if (filters.size() < 2)
    {
        SetTextureFilters(&inputTextures, GL_LINEAR, GL_CLAMP_TO_EDGE);
        SetTextureFilters(&referenceTextures, GL_LINEAR, GL_CLAMP_TO_EDGE);
        return;
    }

    SetTextureFilters(&inputTextures, GL_NEAREST, GL_CLAMP_TO_EDGE);
    SetTextureFilters(&referenceTextures, GL_NEAREST, GL_CLAMP_TO_EDGE);

    glfilt_map_t::reverse_iterator rit;
    int last_filter = 0;

    for (rit = filters.rbegin(); rit != filters.rend(); ++rit)
    {
        if (last_filter == 1)
        {
            SetTextureFilters(&(rit->second->frameBufferTextures),
                              GL_LINEAR, GL_CLAMP_TO_EDGE);
        }
        else if (last_filter > 1)
        {
            SetTextureFilters(&(rit->second->frameBufferTextures),
                              GL_NEAREST, GL_CLAMP_TO_EDGE);
        }
        ++last_filter;
    }
}

/**
 *  Add a new filter stage and create any additional resources needed.
 */

bool OpenGLVideo::AddFilter(OpenGLFilterType filter)
{
    if (filters.count(filter))
        return true;

    switch (filter)
    {
      case kGLFilterNone:
          // Nothing to do. Prevents compiler warning.
          break;

      case kGLFilterResize:
        if (!(gl_features & kGLExtFBufObj) && !filters.empty())
        {
            LOG(VB_PLAYBACK, LOG_ERR, LOC +
                "GL_EXT_framebuffer_object not available "
                "for scaling/resizing filter.");
            return false;
        }
        break;

      case kGLFilterBicubic:
        if (!(gl_features & kGLExtFragProg) || !(gl_features & kGLExtFBufObj))
        {
            LOG(VB_PLAYBACK, LOG_ERR, LOC +
                "Features not available for bicubic filter.");
            return false;
        }
        break;

      case kGLFilterYUV2RGB:
        if (!(gl_features & kGLExtFragProg) && !(gl_features & kGLSL))
        {
            LOG(VB_PLAYBACK, LOG_ERR, LOC +
                "No shader support for OpenGL deinterlacing.");
            return false;
        }
        break;

      case kGLFilterYV12RGB:
        if (!(gl_features & kGLSL))
        {
            LOG(VB_PLAYBACK, LOG_ERR, LOC +
                "No shader support for OpenGL deinterlacing.");
            return false;
        }
        break;
    }

    bool success = true;

    LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Creating %1 filter.")
            .arg(FilterToString(filter)));

    OpenGLFilter *temp = new OpenGLFilter();

    temp->numInputs = 1;
    GLuint program = 0;

    if (filter == kGLFilterBicubic)
    {
        if (helperTexture)
            gl_context->DeleteTexture(helperTexture);

        helperTexture = gl_context->CreateHelperTexture();
        if (!helperTexture)
            success = false;
    }

    if (success &&
        (((filter != kGLFilterNone) && (filter != kGLFilterResize)) ||
         ((gl_features & kGLSL) && (filter == kGLFilterResize))))
    {
        program = AddFragmentProgram(filter);
        if (!program)
            success = false;
        else
            temp->fragmentPrograms.push_back(program);
    }

    if (success)
    {
        temp->outputBuffer = kDefaultBuffer;
        temp->frameBuffers.clear();
        temp->frameBufferTextures.clear();
        filters[filter] = temp;
        temp = nullptr;
        success &= OptimiseFilters();
    }

    if (!success)
    {
        RemoveFilter(filter);
        filters.erase(filter);
        delete temp; // If temp wasn't added to the filter list, we need to delete
        return false;
    }

    return true;
}

bool OpenGLVideo::RemoveFilter(OpenGLFilterType filter)
{
    if (!filters.count(filter))
        return true;

    LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Removing %1 filter")
            .arg(FilterToString(filter)));

    vector<GLuint> temp;
    vector<GLuint>::iterator it;

    temp = filters[filter]->fragmentPrograms;
    for (it = temp.begin(); it != temp.end(); ++it)
        gl_context->DeleteShaderObject(*it);
    filters[filter]->fragmentPrograms.clear();

    temp = filters[filter]->frameBuffers;
    for (it = temp.begin(); it != temp.end(); ++it)
        gl_context->DeleteFrameBuffer(*it);
    filters[filter]->frameBuffers.clear();

    DeleteTextures(&(filters[filter]->frameBufferTextures));

    delete filters[filter];
    filters[filter] = nullptr;

    return true;
}

void OpenGLVideo::TearDownDeinterlacer(void)
{
    glfilt_map_t::iterator it;
    if (filters.end() == (it = filters.find(kGLFilterYUV2RGB)) &&
        filters.end() == (it = filters.find(kGLFilterYV12RGB)) )
    {
        return;
    }

    OpenGLFilter *tmp = it->second;

    if (tmp->fragmentPrograms.size() == 3)
    {
        gl_context->DeleteShaderObject(tmp->fragmentPrograms[2]);
        tmp->fragmentPrograms.pop_back();
    }

    if (tmp->fragmentPrograms.size() == 2)
    {
        gl_context->DeleteShaderObject(tmp->fragmentPrograms[1]);
        tmp->fragmentPrograms.pop_back();
    }

    DeleteTextures(&referenceTextures);
    refsNeeded = 0;
}

/**
 *  Extends the functionality of the basic YUV->RGB filter stage to include
 *  deinterlacing (combining the stages is significantly more efficient than
 *  2 separate stages). Create 2 deinterlacing fragment programs, 1 for each
 *  required field.
 */

bool OpenGLVideo::AddDeinterlacer(const QString &deinterlacer)
{
    if (!(gl_features & kGLExtFragProg) && !(gl_features & kGLSL))
    {
        LOG(VB_PLAYBACK, LOG_ERR, LOC +
            "No shader support for OpenGL deinterlacing.");
        return false;
    }

    OpenGLLocker ctx_lock(gl_context);

    if (filters.end() == filters.find(kGLFilterYUV2RGB) &&
        filters.end() == filters.find(kGLFilterYV12RGB) )
    {
        LOG(VB_PLAYBACK, LOG_ERR, LOC +
            "No YUV2RGB filter stage for OpenGL deinterlacing.");
        return false;
    }

    if (hardwareDeinterlacer == deinterlacer)
        return true;

    TearDownDeinterlacer();

    bool success = true;

    uint ref_size = 2;

    if (deinterlacer == "openglbobdeint" ||
        deinterlacer == "openglonefield" ||
        deinterlacer == "opengllinearblend" ||
        deinterlacer == "opengldoubleratelinearblend" ||
        deinterlacer == "opengldoubleratefieldorder")
    {
        ref_size = 0;
    }

    refsNeeded = ref_size;
    if (ref_size > 0)
    {
        for (; ref_size > 0; ref_size--)
        {
            GLuint tex = CreateVideoTexture(video_dim, inputTextureSize);
            if (tex)
            {
                referenceTextures.push_back(tex);
            }
            else
            {
                success = false;
            }
        }
    }

    OpenGLFilterType type = (MYTHTV_YV12 == videoTextureType) ?
                            kGLFilterYV12RGB : kGLFilterYUV2RGB;

    uint prog1 = AddFragmentProgram(type, deinterlacer, kScan_Interlaced);
    uint prog2 = AddFragmentProgram(type, deinterlacer, kScan_Intr2ndField);

    if (prog1 && prog2)
    {
        filters[type]->fragmentPrograms.push_back(prog1);
        filters[type]->fragmentPrograms.push_back(prog2);
    }
    else
    {
        success = false;
    }

    if (success)
    {
        CheckResize(hardwareDeinterlacing);
        hardwareDeinterlacer = deinterlacer;
        return true;
    }

    hardwareDeinterlacer = "";
    TearDownDeinterlacer();

    return false;
}

/**
 *  Create the correct fragment program for the given filter type
 */

uint OpenGLVideo::AddFragmentProgram(OpenGLFilterType name,
                                     QString deint, FrameScanType field)
{
    if (!gl_context)
        return 0;

    QString vertex, fragment;
    if (gl_features & kGLSL)
    {
        GetProgramStrings(vertex, fragment, name, deint, field);
    }
    else if (gl_features & kGLExtFragProg)
    {
        fragment = GetProgramString(name, deint, field);
    }
    else
    {
        LOG(VB_PLAYBACK, LOG_ERR, LOC + "No OpenGL shader/program support");
        return 0;
    }

    return gl_context->CreateShaderObject(vertex, fragment);
}

/**
 *  Add a FrameBuffer object of the correct size to the given texture.
 */

bool OpenGLVideo::AddFrameBuffer(uint &framebuffer,
                                 uint &texture, QSize vid_size)
{
    if (!(gl_features & kGLExtFBufObj))
    {
        LOG(VB_PLAYBACK, LOG_ERR, LOC + "Framebuffer binding not supported.");
        return false;
    }

    texture = gl_context->CreateTexture(vid_size, false, textureType);

    bool ok = gl_context->CreateFrameBuffer(framebuffer, texture);

    if (!ok)
        gl_context->DeleteTexture(texture);

    return ok;
}

void OpenGLVideo::SetViewPort(const QSize &viewPortSize)
{
    uint w = max(viewPortSize.width(),  video_disp_dim.width());
    uint h = max(viewPortSize.height(), video_disp_dim.height());

    viewportSize = QSize(w, h);

    if (!viewportControl)
        return;

    LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Viewport: %1x%2") .arg(w).arg(h));
    gl_context->SetViewPort(QRect(QPoint(),viewportSize));
}

/**
 *  Create and initialise an OpenGL texture suitable for a YV12 video frame
 *  of the given size.
 */

uint OpenGLVideo::CreateVideoTexture(QSize size, QSize &tex_size)
{
    uint tmp_tex = 0;
    bool use_pbo = gl_features & kGLExtPBufObj;
    if (GL_YCBCR_MESA == videoTextureType)
    {
        tmp_tex = gl_context->CreateTexture(size, use_pbo, textureType,
                                            GL_UNSIGNED_SHORT_8_8_MESA,
                                            GL_YCBCR_MESA, GL_RGBA);
    }
    else if (GL_YCBCR_422_APPLE == videoTextureType)
    {
        tmp_tex = gl_context->CreateTexture(size, use_pbo, textureType,
                                            GL_UNSIGNED_SHORT_8_8_MESA,
                                            GL_YCBCR_422_APPLE, GL_RGBA);
    }
    else if (MYTHTV_UYVY == videoTextureType)
    {
        QSize fix(size.width() / 2, size.height());
        tmp_tex = gl_context->CreateTexture(fix, use_pbo, textureType,
                                            GL_UNSIGNED_BYTE, GL_RGBA, GL_RGBA);
    }
    else if (MYTHTV_YV12 == videoTextureType)
    {
        // 4:1:1 YVU planar (12bpp)
        size.setHeight((3 * size.height() + 1) / 2);
        tmp_tex = gl_context->CreateTexture(size, use_pbo, textureType,
                                            GL_UNSIGNED_BYTE,   // data_type
                                            GL_LUMINANCE,       // data_fmt
                                            GL_LUMINANCE        // internal_fmt
                                            );
    }
    else
        tmp_tex = gl_context->CreateTexture(size, use_pbo, textureType);

    tex_size = gl_context->GetTextureSize(textureType, size);
    if (!tmp_tex)
        return 0;

    return tmp_tex;
}

QSize OpenGLVideo::GetTextureSize(const QSize &size)
{
    if (textureRects)
        return size;

    int w = 64;
    int h = 64;

    while (w < size.width())
    {
        w *= 2;
    }

    while (h < size.height())
    {
        h *= 2;
    }

    return QSize(w, h);
}

uint OpenGLVideo::GetInputTexture(void) const
{
    return inputTextures[0];
}

uint OpenGLVideo::GetTextureType(void) const
{
    return textureType;
}

void OpenGLVideo::SetInputUpdated(void)
{
    inputUpdated = true;
}

/**
 *  Update the current input texture using the data from the given YV12 video
 *  frame. If the required hardware support is not available, fall back to
 *  software YUV->RGB conversion.
 */

void OpenGLVideo::UpdateInputFrame(const VideoFrame *frame, bool soft_bob)
{
    OpenGLLocker ctx_lock(gl_context);

    if (frame->width  != video_dim.width()  ||
        frame->height != video_dim.height() ||
        frame->width  < 1 || frame->height < 1 ||
        frame->codec != FMT_YV12)
    {
        return;
    }
    if (hardwareDeinterlacing)
        RotateTextures();

    // We need to convert frames here to avoid dependencies in MythRenderOpenGL
    void* buf = gl_context->GetTextureBuffer(inputTextures[0]);
    if (!buf)
        return;

    if (MYTHTV_YV12 == videoTextureType)
    {

        if (gl_features & kGLExtPBufObj)
        {
            // Copy the frame to the pixel buffer which updates the texture
            copybuffer((uint8_t*)buf, frame, video_dim.width());
        }
        else if (video_dim.width() != frame->pitches[0])
        {
            // Re-packing is needed
            copybuffer((uint8_t*)buf, frame, video_dim.width());
        }
        else
        {
            // UpdateTexture will copy the frame to the texture
            buf = frame->buf;
        }
    }
    else if (!filters.count(kGLFilterYUV2RGB) ||
        MYTHTV_UYVY == videoTextureType)
    {
        // software conversion
        AVFrame img_out;
        AVPixelFormat out_fmt = AV_PIX_FMT_BGRA;
        if ((GL_YCBCR_MESA == videoTextureType) ||
            (GL_YCBCR_422_APPLE == videoTextureType) ||
            (MYTHTV_UYVY == videoTextureType))
        {
            out_fmt = AV_PIX_FMT_UYVY422;
        }
        m_copyCtx.Copy(&img_out, frame, (unsigned char*)buf, out_fmt);
    }
    else if (frame->interlaced_frame && !soft_bob)
    {
        pack_yv12interlaced(frame->buf, (unsigned char*)buf, frame->offsets,
                            frame->pitches, video_dim);
    }
    else
    {
        pack_yv12progressive(frame->buf, (unsigned char*)buf, frame->offsets,
                             frame->pitches, video_dim);
    }

    gl_context->UpdateTexture(inputTextures[0], buf);
    inputUpdated = true;
}

void OpenGLVideo::SetDeinterlacing(bool deinterlacing)
{
    hardwareDeinterlacing = deinterlacing;
    OpenGLLocker ctx_lock(gl_context);
    CheckResize(hardwareDeinterlacing);
}

void OpenGLVideo::SetSoftwareDeinterlacer(const QString &filter)
{
    if (softwareDeinterlacer != filter)
        CheckResize(false, filter != "bobdeint");
    softwareDeinterlacer = filter;
    softwareDeinterlacer.detach();
}

/**
 *  Render the contents of the current input texture to the framebuffer
 *  using the currently enabled filters.
 *  \param topfieldfirst         the frame is interlaced and top_field_first
 *                               is set
 *  \param scan                  interlaced or progressive?
 *  \param softwareDeinterlacing the frame has been deinterlaced in software
 *  \param frame                 the frame number
 *  \param stereo                Whether/how to drop stereo video information
 *  \param draw_border           if true, draw a red border around the frame
 *  \warning This function is a finely tuned, sensitive beast. Tinker at
 *   your own risk.
 */

void OpenGLVideo::PrepareFrame(bool topfieldfirst, FrameScanType scan,
                               bool softwareDeinterlacing,
                               long long frame, StereoscopicMode stereo,
                               bool draw_border)
{
    if (inputTextures.empty() || filters.empty())
        return;

    OpenGLLocker ctx_lock(gl_context);

    // we need to special case software bobdeint for 1080i
    bool softwarebob = softwareDeinterlacer == "bobdeint" &&
                       softwareDeinterlacing;

    vector<GLuint> inputs = inputTextures;
    QSize inputsize = inputTextureSize;
    QSize realsize  = GetTextureSize(video_disp_dim);

//#define RECT_TEST 1
#define LOG_RECT(r, s) \
        if (currentFrameNum <= 0) {   \
            LOG(VB_GENERAL, LOG_INFO, LOC + \
                QString("PrepareFrame %1 %2 %3,%4,%5,%6")  \
                .arg(s) \
                .arg(currentFrameNum)   \
                .arg(r.left())  \
                .arg(r.top())   \
                .arg(r.right()) \
                .arg(r.bottom())); }
#define LOG_SIZE(r, s) \
        if (currentFrameNum <= 0) {   \
            LOG(VB_GENERAL, LOG_INFO, LOC + \
                QString("PrepareFrame %1 %2 %3,%4")  \
                .arg(s) \
                .arg(currentFrameNum)   \
                .arg(r.width())  \
                .arg(r.height())); }
#define LOG_TINFO(s) \
        if (currentFrameNum <= 0) {   \
            LOG(VB_GENERAL, LOG_INFO, LOC + s ); }

    if (currentFrameNum <= 0)
    {
        LOG(VB_GENERAL, LOG_INFO, LOC +
            QString("PrepareFrame frame num %1").arg(frame));
    }
    LOG_SIZE(video_dim, "A video_dim");
    LOG_SIZE(video_disp_dim, "A video_disp_dim");

    glfilt_map_t::iterator it;
    for (it = filters.begin(); it != filters.end(); ++it)
    {
        OpenGLFilterType type = it->first;
        OpenGLFilter *filter = it->second;

        bool actual = softwarebob && (filter->outputBuffer == kDefaultBuffer);

        if (currentFrameNum <= 0)
        {
            LOG(VB_GENERAL, LOG_INFO, LOC +
                QString("PrepareFrame filter %1").arg(FilterToString(type)));
        }
        // texture coordinates
        float trueheight = (float)(actual ? video_dim.height() :
                                            video_disp_dim.height());
        float width = video_disp_dim.width();
        if ((type == kGLFilterYUV2RGB) && (videoTextureType == MYTHTV_UYVY))
            width /= 2.0f;

        QRectF trect(QPoint(0, 0), QSize(width, trueheight));

        LOG_RECT(trect, "A trect");
        // only apply overscan on last filter
        if (filter->outputBuffer == kDefaultBuffer)
        {
            trect.setRect(video_rect.left(),  video_rect.top(),
                          video_rect.width(), video_rect.height());
            LOG_RECT(trect, "B trect default");
        }

        if (!textureRects && (inputsize.height() > 0))
            trueheight /= inputsize.height();

        // software bobdeint
        if (actual)
        {
            bool top = (scan == kScan_Intr2ndField && topfieldfirst) ||
                       (scan == kScan_Interlaced && !topfieldfirst);
            bool bot = (scan == kScan_Interlaced && topfieldfirst) ||
                       (scan == kScan_Intr2ndField && !topfieldfirst);
            bool first = filters.size() < 2;
            float bob = (trueheight / (float)video_disp_dim.height()) / 4.0f;
            if ((top && !first) || (bot && first))
            {
                trect.setBottom(trect.bottom() / 2);
                trect.setTop(trect.top() / 2);
                LOG_RECT(trect, "C trect int a");
                trect.adjust(0, bob, 0, bob);
            }
            if ((bot && !first) || (top && first))
            {
                trect.setTop(static_cast<qreal>(trueheight / 2) + (trect.top() / 2));
                trect.setBottom(static_cast<qreal>(trueheight / 2) + (trect.bottom() / 2));
                LOG_RECT(trect, "D trect int b");
                trect.adjust(0, -bob, 0, -bob);
            }
        }

        // discard stereoscopic fields
        if (filter->outputBuffer == kDefaultBuffer)
        {
            if (kStereoscopicModeSideBySideDiscard == stereo)
                trect = QRectF(trect.left() / 2.0,  trect.top(),
                               trect.width() / 2.0, trect.height());
            if (kStereoscopicModeTopAndBottomDiscard == stereo)
                trect = QRectF(trect.left(),  trect.top() / 2.0,
                               trect.width(), trect.height() / 2.0);
        }

        // vertex coordinates
        QRect display = (filter->outputBuffer == kDefaultBuffer) ?
                         display_video_rect : frameBufferRect;
        QRect visible = (filter->outputBuffer == kDefaultBuffer) ?
                         display_visible_rect : frameBufferRect;
#ifdef RECT_TEST
        QRectF vrect(trect);
        draw_border = true;
#else
        QRectF vrect(display);
#endif

#ifndef RECT_TEST
        // invert if first filter
        if (it == filters.begin())
        {
            LOG_RECT(display, "E display");
            LOG_RECT(visible, "F visible");
            if (filters.size() > 1)
            {
                vrect.setTop(visible.height() - display.top());
                vrect.setBottom(vrect.top() - display.height());
                LOG_RECT(vrect, "G vrect sf");
            }
            else
            {
                vrect.setBottom(display.top());
                vrect.setTop(display.top() + display.height());
                LOG_RECT(vrect, "H vrect bf");
            }
        }
#endif

        // hardware bobdeint
        if (filter->outputBuffer == kDefaultBuffer &&
            hardwareDeinterlacing &&
            hardwareDeinterlacer == "openglbobdeint")
        {
            float bob = ((float)display.height() / (float)video_rect.height())
                        / 2.0f;
            float field = kScan_Interlaced ? -1.0f : 1.0f;
            bob = bob * (topfieldfirst ? field : -field);
            vrect.adjust(0, bob, 0, bob);
        }

        uint target = 0;
        // bind correct frame buffer (default onscreen) and set viewport
        switch (filter->outputBuffer)
        {
            case kDefaultBuffer:
                gl_context->BindFramebuffer(0);
                if (viewportControl)
                    gl_context->SetViewPort(QRect(QPoint(), display_visible_rect.size()));
                else
                    gl_context->SetViewPort(QRect(QPoint(), masterViewportSize));
                break;
            case kFrameBufferObject:
                if (!filter->frameBuffers.empty())
                {
                    gl_context->BindFramebuffer(filter->frameBuffers[0]);
                    gl_context->SetViewPort(QRect(QPoint(), frameBufferRect.size()));
                    target = filter->frameBuffers[0];
                }
                break;

            default:
                continue;
        }

        if (draw_border && filter->outputBuffer == kDefaultBuffer)
        {
            LOG_RECT(vrect, "I vrect");
#ifdef RECT_TEST
            vrect = vrect.adjusted(+10, +10, +10, +10);
            QRectF piprectf = vrect.adjusted(-1, -1, +1, +1);
#else
            QRectF piprectf = vrect.adjusted(-10, -10, +10, +10);
#endif
            QRect  piprect(piprectf.left(), piprectf.top(),
                           piprectf.width(), piprectf.height());
            static const QPen nopen(Qt::NoPen);
            static const QBrush redbrush(QBrush(QColor(127, 0, 0, 255)));
            gl_context->DrawRect(piprect, redbrush, nopen, 255);
        }

        // bind correct textures
        uint textures[4]; // NB
        uint texture_count = 0;
        for (uint i = 0; i < inputs.size(); i++)
            textures[texture_count++] = inputs[i];

        if (!referenceTextures.empty() &&
            hardwareDeinterlacing &&
            (type == kGLFilterYUV2RGB || type == kGLFilterYV12RGB))
        {
            for (uint i = 0; i < referenceTextures.size(); i++)
                textures[texture_count++] = referenceTextures[i];
        }

        if (helperTexture && type == kGLFilterBicubic)
            textures[texture_count++] = helperTexture;

        // enable fragment program and set any environment variables
        GLuint program = 0;
        if (((type != kGLFilterNone) && (type != kGLFilterResize)) ||
            ((gl_features & kGLSL) && (type == kGLFilterResize)))
        {
            GLuint prog_ref = 0;

            if (type == kGLFilterYUV2RGB || type == kGLFilterYV12RGB)
            {
                if (hardwareDeinterlacing &&
                    filter->fragmentPrograms.size() == 3 &&
                    !refsNeeded)
                {
                    if (scan == kScan_Interlaced)
                        prog_ref = topfieldfirst ? 1 : 2;
                    else if (scan == kScan_Intr2ndField)
                        prog_ref = topfieldfirst ? 2 : 1;
                }
            }
            program = filter->fragmentPrograms[prog_ref];
        }

        if (type == kGLFilterYUV2RGB || type == kGLFilterYV12RGB)
        {
            gl_context->SetShaderParams(program,
                GLMatrix4x4(reinterpret_cast<float*>(colourSpace->GetMatrix())),
                COLOUR_UNIFORM);
        }

        LOG_RECT(trect, "Final trect");
        LOG_RECT(vrect, "Final vrect");
        gl_context->DrawBitmap(textures, texture_count, target, &trect, &vrect,
                               program);

        inputs = filter->frameBufferTextures;
        inputsize = realsize;
    }

    currentFrameNum = frame;
    inputUpdated = false;
}

void OpenGLVideo::RotateTextures(void)
{
    if (referenceTextures.size() < 2)
        return;

    if (refsNeeded > 0)
        refsNeeded--;

    GLuint tmp = referenceTextures[referenceTextures.size() - 1];

    for (uint i = referenceTextures.size() - 1; i > 0;  i--)
        referenceTextures[i] = referenceTextures[i - 1];

    referenceTextures[0] = inputTextures[0];
    inputTextures[0] = tmp;
}

void OpenGLVideo::DeleteTextures(vector<GLuint> *textures)
{
    if ((*textures).empty())
        return;

    for (uint i = 0; i < (*textures).size(); i++)
        gl_context->DeleteTexture((*textures)[i]);
    (*textures).clear();
}

void OpenGLVideo::SetTextureFilters(vector<GLuint> *textures,
                                    int filt, int wrap)
{
    if (textures->empty())
        return;

    for (uint i = 0; i < textures->size(); i++)
        gl_context->SetTextureFilters((*textures)[i], filt, wrap);
}

OpenGLVideo::OpenGLFilterType OpenGLVideo::StringToFilter(const QString &filter)
{
    OpenGLFilterType ret = kGLFilterNone;

    if (filter.contains("master"))
        ret = kGLFilterYUV2RGB;
    else if (filter.contains("resize"))
        ret = kGLFilterResize;
    else if (filter.contains("bicubic"))
        ret = kGLFilterBicubic;
    else if (filter.contains("yv12rgb"))
        ret = kGLFilterYV12RGB;

    return ret;
}

QString OpenGLVideo::FilterToString(OpenGLFilterType filt)
{
    switch (filt)
    {
        case kGLFilterNone:
            break;
        case kGLFilterYUV2RGB:
            return "master";
        case kGLFilterResize:
            return "resize";
        case kGLFilterBicubic:
            return "bicubic";
        case kGLFilterYV12RGB:
            return "yv12rgb";
    }

    return "";
}

static const QString attrib_fast =
"ATTRIB tex   = fragment.texcoord[0];\n"
"PARAM yuv[3] = { program.local[0..2] };\n";

static const QString tex_fast =
"TEX res, tex, texture[0], %1;\n";

static const QString var_fast =
"TEMP tmp, res;\n";

static const QString var_col =
"TEMP col;\n";

static const QString select_col =
"MUL col, tex.xxxx, %8;\n"
"FRC col, col;\n"
"SUB col, col, 0.5;\n"
"CMP res, col, res.rabg, res.rgba;\n";

static const QString end_fast =
"DPH tmp.r, res.arbg, yuv[0];\n"
"DPH tmp.g, res.arbg, yuv[1];\n"
"DPH tmp.b, res.arbg, yuv[2];\n"
"MOV tmp.a, 1.0;\n"
"MOV result.color, tmp;\n";

static const QString var_deint =
"TEMP other, current, mov, prev;\n";

static const QString field_calc =
"MUL prev, tex.yyyy, %2;\n"
"FRC prev, prev;\n"
"SUB prev, prev, 0.5;\n";

static const QString bobdeint[2] = {
field_calc +
"ADD other, tex, {0.0, %3, 0.0, 0.0};\n"
"MIN other, other, {10000.0, %9, 10000.0, 10000.0};\n"
"TEX other, other, texture[0], %1;\n"
"CMP res, prev, res, other;\n",
field_calc +
"SUB other, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX other, other, texture[0], %1;\n"
"CMP res, prev, other, res;\n"
};

static const QString deint_end_top =
"CMP res,  prev, current, other;\n";

static const QString deint_end_bot =
"CMP res,  prev, other, current;\n";

static const QString linearblend[2] = {
"TEX current, tex, texture[0], %1;\n"
"ADD other, tex, {0.0, %3, 0.0, 0.0};\n"
"MIN other, other, {10000.0, %9, 10000.0, 10000.0};\n"
"TEX other, other, texture[0], %1;\n"
"SUB mov, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX mov, mov, texture[0], %1;\n"
"LRP other, 0.5, other, mov;\n"
+ field_calc + deint_end_top,

"TEX current, tex, texture[0], %1;\n"
"SUB other, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX other, other, texture[0], %1;\n"
"ADD mov, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX mov, mov, texture[0], %1;\n"
"LRP other, 0.5, other, mov;\n"
+ field_calc + deint_end_bot
};

static const QString kerneldeint[2] = {
"TEX current, tex, texture[1], %1;\n"
"TEX prev, tex, texture[2], %1;\n"
"MUL other, 0.125, prev;\n"
"MAD other, 0.125, current, other;\n"
"ADD prev, tex, {0.0, %3, 0.0, 0.0};\n"
"MIN prev, prev, {10000.0, %9, 10000.0, 10000.0};\n"
"TEX prev, prev, texture[1], %1;\n"
"MAD other, 0.5, prev, other;\n"
"SUB prev, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX prev, prev, texture[1], %1;\n"
"MAD other, 0.5, prev, other;\n"
"ADD prev, tex, {0.0, %4, 0.0, 0.0};\n"
"TEX tmp, prev, texture[1], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"TEX tmp, prev, texture[2], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"SUB prev, tex, {0.0, %4, 0.0, 0.0};\n"
"TEX tmp, prev, texture[1], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"TEX tmp, prev, texture[2], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
+ field_calc + deint_end_top,

"TEX current, tex, texture[1], %1;\n"
"MUL other, 0.125, res;\n"
"MAD other, 0.125, current, other;\n"
"ADD prev, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX prev, prev, texture[1], %1;\n"
"MAD other, 0.5, prev, other;\n"
"SUB prev, tex, {0.0, %3, 0.0, 0.0};\n"
"TEX prev, prev, texture[1], %1;\n"
"MAD other, 0.5, prev, other;\n"
"ADD prev, tex, {0.0, %4, 0.0, 0.0};\n"
"TEX tmp, prev, texture[1], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"TEX tmp, prev, texture[0], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"SUB prev, tex, {0.0, %4, 0.0, 0.0};\n"
"TEX tmp, prev, texture[1], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
"TEX tmp, prev, texture[0], %1;\n"
"MAD other, -0.0625, tmp, other;\n"
+ field_calc + deint_end_bot
};

static const QString bicubic =
"TEMP coord, coord2, cdelta, parmx, parmy, a, b, c, d;\n"
"MAD coord.xy, fragment.texcoord[0], {%6, %7}, {0.5, 0.5};\n"
"TEX parmx, coord.x, texture[1], 1D;\n"
"TEX parmy, coord.y, texture[1], 1D;\n"
"MUL cdelta.xz, parmx.rrgg, {-%5, 0, %5, 0};\n"
"MUL cdelta.yw, parmy.rrgg, {0, -%3, 0, %3};\n"
"ADD coord, fragment.texcoord[0].xyxy, cdelta.xyxw;\n"
"ADD coord2, fragment.texcoord[0].xyxy, cdelta.zyzw;\n"
"TEX a, coord.xyxy, texture[0], 2D;\n"
"TEX b, coord.zwzw, texture[0], 2D;\n"
"TEX c, coord2.xyxy, texture[0], 2D;\n"
"TEX d, coord2.zwzw, texture[0], 2D;\n"
"LRP a, parmy.b, a, b;\n"
"LRP c, parmy.b, c, d;\n"
"LRP result.color, parmx.b, a, c;\n";

QString OpenGLVideo::GetProgramString(OpenGLFilterType name,
                                      QString deint, FrameScanType field)
{
    QString ret =
        "!!ARBfp1.0\n"
        "OPTION ARB_precision_hint_fastest;\n";

    switch (name)
    {
        case kGLFilterYUV2RGB:
        {
            bool need_tex = true;
            bool packed = MYTHTV_UYVY == videoTextureType;
            QString deint_bit = "";
            if (deint != "")
            {
                uint tmp_field = 0;
                if (field == kScan_Intr2ndField)
                    tmp_field = 1;
                if (deint == "openglbobdeint" ||
                    deint == "openglonefield" ||
                    deint == "opengldoubleratefieldorder")
                {
                    deint_bit = bobdeint[tmp_field];
                }
                else if (deint == "opengllinearblend" ||
                         deint == "opengldoubleratelinearblend")
                {
                    deint_bit = linearblend[tmp_field];
                    if (!tmp_field) { need_tex = false; }
                }
                else if (deint == "openglkerneldeint" ||
                         deint == "opengldoubleratekerneldeint")
                {
                    deint_bit = kerneldeint[tmp_field];
                    if (!tmp_field) { need_tex = false; }
                }
                else
                {
                    LOG(VB_PLAYBACK, LOG_ERR, LOC +
                        "Unrecognised OpenGL deinterlacer");
                }
            }

            ret += attrib_fast;
            ret += (deint != "") ? var_deint : "";
            ret += packed ? var_col : "";
            ret += var_fast + (need_tex ? tex_fast : "");
            ret += deint_bit;
            ret += packed ? select_col : "";
            ret += end_fast;
        }
            break;

        case kGLFilterNone:
        case kGLFilterResize:
            break;

        case kGLFilterBicubic:

            ret += bicubic;
            break;

        case kGLFilterYV12RGB: // TODO: extend this for opengl1
        default:
            LOG(VB_PLAYBACK, LOG_ERR, LOC + "Unknown fragment program.");
            break;
    }

    CustomiseProgramString(ret);
    ret += "END";

    LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Created %1 fragment program %2")
                .arg(FilterToString(name)).arg(deint));

    return ret;
}

void OpenGLVideo::CustomiseProgramString(QString &string)
{
    string.replace("%1", textureRects ? "RECT" : "2D");

    if (!textureRects)
    {
        string.replace("GLSL_SAMPLER", "sampler2D");
        string.replace("GLSL_TEXTURE", "texture2D");
    }

    float lineHeight = 1.0f;
    float colWidth   = 1.0f;
    float yselect    = 1.0f;
    QSize fb_size = GetTextureSize(video_disp_dim);

    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("CustomiseProgramString fbsize %1,%2").arg(fb_size.width()).arg(fb_size.height()));
    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("CustomiseProgramString inputTextureSize %1,%2 tr %3")
            .arg(inputTextureSize.width()).arg(inputTextureSize.height()).arg(textureRects));

    if (!textureRects &&
       (inputTextureSize.height() > 0))
    {
        lineHeight /= inputTextureSize.height();
        colWidth   /= inputTextureSize.width();
        yselect    /= ((float)inputTextureSize.width() / 2.0f);
    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("CustomiseProgramString lineHeight %1 colWidth %2 yselect %3")
            .arg(QString::number(lineHeight, 'f', 16))
                .arg(QString::number(colWidth, 'f', 16))
                .arg(QString::number(yselect, 'f', 16)));
    }

    float maxheight  = (float)(min(inputTextureSize.height(), 2160) - 1) *
                       lineHeight;
    float fieldSize = 1.0f / (lineHeight * 2.0f);
    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("CustomiseProgramString maxHeight %1 fieldSize %2 %3")
            .arg(maxheight).arg(fieldSize, 0, 'f', 8).arg(fieldSize, 0, 'f', 16));

    string.replace("%2", QString::number(fieldSize, 'f', 8));
    string.replace("%3", QString::number(lineHeight, 'f', 16));
    string.replace("%4", QString::number(lineHeight * 2.0f, 'f', 16));
    string.replace("%5", QString::number(colWidth, 'f', 16));
    string.replace("%6", QString::number((float)fb_size.width(), 'f', 1));
    string.replace("%7", QString::number((float)fb_size.height(), 'f', 1));
    string.replace("%8", QString::number(1.0f / yselect, 'f', 16));
    // make sure truncation errors dont affect clamping
    string.replace("%9", QString::number(maxheight, 'f', 16));

    float width = float(video_dim.width()) / inputTextureSize.width();
    string.replace("%WIDTH%", QString::number(width, 'f', 8));

    float height = float(video_dim.height()) / inputTextureSize.height();
    string.replace("%HEIGHT%", QString::number(height, 'f', 8));

    LOG(VB_GENERAL, LOG_INFO, LOC +
        QString("CustomiseProgramString width %1 height %2")
            .arg(height, 0, 'f', 8).arg(width, 0, 'f', 8));

    string.replace("COLOUR_UNIFORM", COLOUR_UNIFORM);
}

static const QString YUV2RGBVertexShader =
"GLSL_DEFINES"
"attribute vec2 a_position;\n"
"attribute vec2 a_texcoord0;\n"
"varying   vec2 v_texcoord0;\n"
"uniform   mat4 u_projection;\n"
"void main() {\n"
"    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n"
"    v_texcoord0 = a_texcoord0;\n"
"}\n";

static const QString SelectColumn =
"    if (fract(v_texcoord0.x * %8) < 0.5)\n"
"        yuva = yuva.rabg;\n";

static const QString YUV2RGBFragmentShader =
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec4 yuva    = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n";

static const QString OneFieldShader[2] = {
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    float field = v_texcoord0.y - (step(0.5, fract(v_texcoord0.y * %2)) * %3);\n"
"    field       = clamp(field, 0.0, %9);\n"
"    vec4 yuva   = GLSL_TEXTURE(s_texture0, vec2(v_texcoord0.x, field));\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n",

"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec2 field   = vec2(0.0, step(0.5, 1.0 - fract(v_texcoord0.y * %2)) * %3);\n"
"    vec4 yuva    = GLSL_TEXTURE(s_texture0, v_texcoord0 + field);\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n"
};

static const QString LinearBlendShader[2] = {
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec2 line1 = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %3, 0.0, %9));\n"
"    vec2 line2 = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %3, 0.0, %9));\n"
"    vec4 yuva  = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"    vec4 above = GLSL_TEXTURE(s_texture0, line2);\n"
"    vec4 below = GLSL_TEXTURE(s_texture0, line1);\n"
"    if (fract(v_texcoord0.y * %2) < 0.5)\n"
"        yuva = mix(above, below, 0.5);\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n",

"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec2 line1 = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %3, 0.0, %9));\n"
"    vec2 line2 = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %3, 0.0, %9));\n"
"    vec4 yuva  = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"    vec4 above = GLSL_TEXTURE(s_texture0, line2);\n"
"    vec4 below = GLSL_TEXTURE(s_texture0, line1);\n"
"    if (fract(v_texcoord0.y * %2) >= 0.5)\n"
"        yuva = mix(above, below, 0.5);\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n"
};

static const QString KernelShader[2] = {
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform GLSL_SAMPLER s_texture1;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec4 yuva    = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"    if (fract(v_texcoord0.y * %2) < 0.5)\n"
"    {\n"
"        vec2 twoup   = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %4, 0.0, %9));\n"
"        vec2 twodown = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %4, 0.0, %9));\n"
"        vec2 oneup   = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %3, 0.0, %9));\n"
"        vec2 onedown = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %3, 0.0, %9));\n"
"        vec4 line0   = GLSL_TEXTURE(s_texture0, twoup);\n"
"        vec4 line1   = GLSL_TEXTURE(s_texture0, oneup);\n"
"        vec4 line3   = GLSL_TEXTURE(s_texture0, onedown);\n"
"        vec4 line4   = GLSL_TEXTURE(s_texture0, twodown);\n"
"        vec4 line00  = GLSL_TEXTURE(s_texture1, twoup);\n"
"        vec4 line20  = GLSL_TEXTURE(s_texture1, v_texcoord0);\n"
"        vec4 line40  = GLSL_TEXTURE(s_texture1, twodown);\n"
"        yuva = (yuva   * 0.125);\n"
"        yuva = (line20 * 0.125) + yuva;\n"
"        yuva = (line1  * 0.5) + yuva;\n"
"        yuva = (line3  * 0.5) + yuva;\n"
"        yuva = (line0  * -0.0625) + yuva;\n"
"        yuva = (line4  * -0.0625) + yuva;\n"
"        yuva = (line00 * -0.0625) + yuva;\n"
"        yuva = (line40 * -0.0625) + yuva;\n"
"    }\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n",

"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"uniform GLSL_SAMPLER s_texture1;\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec4 yuva    = GLSL_TEXTURE(s_texture1, v_texcoord0);\n"
"    if (fract(v_texcoord0.y * %2) >= 0.5)\n"
"    {\n"
"        vec2 twoup   = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %4, 0.0, %9));\n"
"        vec2 twodown = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %4, 0.0, %9));\n"
"        vec2 oneup   = vec2(v_texcoord0.x, clamp(v_texcoord0.y - %3, 0.0, %9));\n"
"        vec2 onedown = vec2(v_texcoord0.x, clamp(v_texcoord0.y + %3, 0.0, %9));\n"
"        vec4 line0   = GLSL_TEXTURE(s_texture1, twoup);\n"
"        vec4 line1   = GLSL_TEXTURE(s_texture1, oneup);\n"
"        vec4 line3   = GLSL_TEXTURE(s_texture1, onedown);\n"
"        vec4 line4   = GLSL_TEXTURE(s_texture1, twodown);\n"
"        vec4 line00  = GLSL_TEXTURE(s_texture0, twoup);\n"
"        vec4 line20  = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"        vec4 line40  = GLSL_TEXTURE(s_texture0, twodown);\n"
"        yuva = (yuva   * 0.125);\n"
"        yuva = (line20 * 0.125) + yuva;\n"
"        yuva = (line1  * 0.5) + yuva;\n"
"        yuva = (line3  * 0.5) + yuva;\n"
"        yuva = (line0  * -0.0625) + yuva;\n"
"        yuva = (line4  * -0.0625) + yuva;\n"
"        yuva = (line00 * -0.0625) + yuva;\n"
"        yuva = (line40 * -0.0625) + yuva;\n"
"    }\n"
"SELECT_COLUMN"
"    gl_FragColor = vec4(yuva.arb, 1.0) * COLOUR_UNIFORM;\n"
"}\n"
};

static const QString BicubicShader =
"GLSL_DEFINES"
"uniform sampler2D s_texture0;\n"
"uniform sampler1D s_texture1;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec2 coord = (v_texcoord0 * vec2(%6, %7)) - vec2(0.5, 0.5);\n"
"    vec4 parmx = texture1D(s_texture1, coord.x);\n"
"    vec4 parmy = texture1D(s_texture1, coord.y);\n"
"    vec2 e_x = vec2(%5, 0.0);\n"
"    vec2 e_y = vec2(0.0, %3);\n"
"    vec2 coord10 = v_texcoord0 + parmx.x * e_x;\n"
"    vec2 coord00 = v_texcoord0 - parmx.y * e_x;\n"
"    vec2 coord11 = coord10     + parmy.x * e_y;\n"
"    vec2 coord01 = coord00     + parmy.x * e_y;\n"
"    coord10      = coord10     - parmy.y * e_y;\n"
"    coord00      = coord00     - parmy.y * e_y;\n"
"    vec4 tex00   = texture2D(s_texture0, coord00);\n"
"    vec4 tex10   = texture2D(s_texture0, coord10);\n"
"    vec4 tex01   = texture2D(s_texture0, coord01);\n"
"    vec4 tex11   = texture2D(s_texture0, coord11);\n"
"    tex00        = mix(tex00, tex01, parmy.z);\n"
"    tex10        = mix(tex10, tex11, parmy.z);\n"
"    gl_FragColor = mix(tex00, tex10, parmx.z);\n"
"}\n";

static const QString DefaultFragmentShader =
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0;\n"
"varying vec2 v_texcoord0;\n"
"void main(void)\n"
"{\n"
"    vec4 color   = GLSL_TEXTURE(s_texture0, v_texcoord0);\n"
"    gl_FragColor = vec4(color.xyz, 1.0);\n"
"}\n";

static const QString YV12RGBVertexShader =
"//YV12RGBVertexShader\n"
"GLSL_DEFINES"
"attribute vec2 a_position;\n"
"attribute vec2 a_texcoord0;\n"
"varying   vec2 v_texcoord0;\n"
"uniform   mat4 u_projection;\n"
"void main() {\n"
"    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n"
"    v_texcoord0 = a_texcoord0;\n"
"}\n";

#ifdef ANDROID
#define SAMPLEYVU "\
vec3 sampleYVU(in GLSL_SAMPLER texture, vec2 texcoordY)\n\
{\n\
    vec2 texcoordV, texcoordU;\n\
    texcoordV = vec2(texcoordY.s / 2.0, %HEIGHT% + texcoordY.t / 4.0);\n\
    texcoordU = vec2(texcoordV.s, texcoordV.t + %HEIGHT% / 4.0);\n\
    vec3 yvu;\n\
    yvu.r = GLSL_TEXTURE(texture, texcoordY).r;\n\
    yvu.g = GLSL_TEXTURE(texture, texcoordV).r;\n\
    yvu.b = GLSL_TEXTURE(texture, texcoordU).r;\n\
    return yvu;\n\
}\n"
#else

#define SAMPLEYVU "\
vec3 sampleYVU(in GLSL_SAMPLER texture, vec2 texcoordY)\n\
{\n\
    vec2 texcoordV, texcoordU;\n\
    texcoordV = vec2(texcoordY.s / 2.0, %HEIGHT% + texcoordY.t / 4.0);\n\
    texcoordU = vec2(texcoordV.s, texcoordV.t + %HEIGHT% / 4.0);\n\
    if (fract(texcoordY.t * %2) >= 0.5)\n\
    {\n\
        texcoordV.s += %WIDTH% / 2.0;\n\
        texcoordU.s += %WIDTH% / 2.0;\n\
    }\n\
    vec3 yvu;\n\
    yvu.r = GLSL_TEXTURE(texture, texcoordY).r;\n\
    yvu.g = GLSL_TEXTURE(texture, texcoordV).r;\n\
    yvu.b = GLSL_TEXTURE(texture, texcoordU).r;\n\
    return yvu;\n\
}\n"
#endif

static const QString YV12RGBFragmentShader =
"//YV12RGBFragmentShader\n"
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0; // 4:1:1 YVU planar\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
SAMPLEYVU
"void main(void)\n"
"{\n"
"    vec3 yvu = sampleYVU(s_texture0, v_texcoord0);\n"
"    gl_FragColor = vec4(yvu, 1.0) * COLOUR_UNIFORM;\n"
"}\n";

static const QString YV12RGBOneFieldVertexShader[2] = {
"//YV12RGBOneFieldVertexShader 1\n"
"GLSL_DEFINES"
"attribute vec2 a_position;\n"
"attribute vec2 a_texcoord0;\n"
"varying   vec2 v_texcoord0;\n"
"uniform   mat4 u_projection;\n"
"void main() {\n"
"    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n"
"    v_texcoord0 = a_texcoord0;\n"
"    if (fract(v_texcoord0.t * %2) >= 0.5)\n"
"        v_texcoord0.t -= %3;\n"
"}\n",

"//YV12RGBOneFieldVertexShader 2\n"
"GLSL_DEFINES"
"attribute vec2 a_position;\n"
"attribute vec2 a_texcoord0;\n"
"varying   vec2 v_texcoord0;\n"
"uniform   mat4 u_projection;\n"
"void main() {\n"
"    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n"
"    v_texcoord0 = a_texcoord0;\n"
"    if (fract(v_texcoord0.t * %2) < 0.5)\n"
"    {\n"
"        v_texcoord0.t += %3;\n"
"        v_texcoord0.t = min(v_texcoord0.t, %HEIGHT% - %3);\n"
"    }\n"
"}\n"
};

static const QString YV12RGBLinearBlendFragmentShader =
"//YV12RGBLinearBlendFragmentShader\n"
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0; // 4:1:1 YVU planar\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
SAMPLEYVU
"void main(void)\n"
"{\n"
"    vec2 texcoord;\n"
"    texcoord = v_texcoord0 - vec2(0.0, %3);\n"
"    vec3 yvu1 = sampleYVU(s_texture0, texcoord);\n"
"    vec3 yvu2 = sampleYVU(s_texture0, v_texcoord0);\n"
"    texcoord = v_texcoord0 + vec2(0.0, %3);\n"
"    texcoord.t = min(texcoord.t, %HEIGHT% - %3);\n"
"    vec3 yvu3 = sampleYVU(s_texture0, texcoord);\n"
"    vec3 yvu = (yvu1 + 2.0 * yvu2 + yvu3) / 4.0;\n"
"    gl_FragColor = vec4(yvu, 1.0) * COLOUR_UNIFORM;\n"
"}\n";

#define KERNELYVU "\
vec3 kernelYVU(in vec3 yvu)\n\
{\n\
    vec2 twoup   = v_texcoord0 - vec2(0.0, %4);\n\
    vec2 twodown = v_texcoord0 + vec2(0.0, %4);\n\
    twodown.t = min(twodown.t, %HEIGHT% - %3);\n\
    vec2 onedown = v_texcoord0 + vec2(0.0, %3);\n\
    onedown.t = min(onedown.t, %HEIGHT% - %3);\n\
    vec3 line0   = sampleYVU(s_texture0, twoup);\n\
    vec3 line1   = sampleYVU(s_texture0, v_texcoord0 - vec2(0.0, %3));\n\
    vec3 line3   = sampleYVU(s_texture0, onedown);\n\
    vec3 line4   = sampleYVU(s_texture0, twodown);\n\
    vec3 line00  = sampleYVU(s_texture1, twoup);\n\
    vec3 line20  = sampleYVU(s_texture1, v_texcoord0);\n\
    vec3 line40  = sampleYVU(s_texture1, twodown);\n\
    yvu *=           0.125;\n\
    yvu += line20 *  0.125;\n\
    yvu += line1  *  0.5;\n\
    yvu += line3  *  0.5;\n\
    yvu += line0  * -0.0625;\n\
    yvu += line4  * -0.0625;\n\
    yvu += line00 * -0.0625;\n\
    yvu += line40 * -0.0625;\n\
    return yvu;\n\
}\n"

static const QString YV12RGBKernelShader[2] = {
"//YV12RGBKernelShader 1\n"
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0, s_texture1; // 4:1:1 YVU planar\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
SAMPLEYVU
KERNELYVU
"void main(void)\n"
"{\n"
"    vec3 yvu = sampleYVU(s_texture0, v_texcoord0);\n"
"    if (fract(v_texcoord0.t * %2) >= 0.5)\n"
"        yvu = kernelYVU(yvu);\n"
"    gl_FragColor = vec4(yvu, 1.0) * COLOUR_UNIFORM;\n"
"}\n",

"//YV12RGBKernelShader 2\n"
"GLSL_DEFINES"
"uniform GLSL_SAMPLER s_texture0, s_texture1; // 4:1:1 YVU planar\n"
"uniform mat4 COLOUR_UNIFORM;\n"
"varying vec2 v_texcoord0;\n"
SAMPLEYVU
KERNELYVU
"void main(void)\n"
"{\n"
"    vec3 yvu = sampleYVU(s_texture0, v_texcoord0);\n"
"    if (fract(v_texcoord0.t * %2) < 0.5)\n"
"        yvu = kernelYVU(yvu);\n"
"    gl_FragColor = vec4(yvu, 1.0) * COLOUR_UNIFORM;\n"
"}\n"
};

void OpenGLVideo::GetProgramStrings(QString &vertex, QString &fragment,
                                    OpenGLFilterType filter,
                                    QString deint, FrameScanType field)
{
    uint bottom = field == kScan_Intr2ndField;
    vertex = YUV2RGBVertexShader;
    switch (filter)
    {
        case kGLFilterYUV2RGB:
        {
            if (deint == "openglonefield" || deint == "openglbobdeint")
                fragment = OneFieldShader[bottom];
            else if (deint == "opengllinearblend" ||
                     deint == "opengldoubleratelinearblend")
                fragment = LinearBlendShader[bottom];
            else if (deint == "openglkerneldeint" ||
                     deint == "opengldoubleratekerneldeint")
                fragment = KernelShader[bottom];
            else
                fragment = YUV2RGBFragmentShader;

            fragment.replace("SELECT_COLUMN", MYTHTV_UYVY == videoTextureType ?
                                              SelectColumn : "");
            break;
        }
        case kGLFilterYV12RGB:
            if (deint == "openglonefield" || deint == "openglbobdeint")
            {
                vertex = YV12RGBOneFieldVertexShader[bottom];
                fragment = YV12RGBFragmentShader;
            }
            else if (deint == "opengllinearblend" ||
                     deint == "opengldoubleratelinearblend")
            {
                vertex = YV12RGBVertexShader;
                fragment = YV12RGBLinearBlendFragmentShader;
            }
            else if (deint == "openglkerneldeint" ||
                     deint == "opengldoubleratekerneldeint")
            {
                vertex = YV12RGBVertexShader;
                fragment = YV12RGBKernelShader[bottom];
            }
            else
            {
                vertex = YV12RGBVertexShader;
                fragment = YV12RGBFragmentShader;
            }
            break;
        case kGLFilterNone:
            break;
        case kGLFilterResize:
            fragment = DefaultFragmentShader;
            break;
        case kGLFilterBicubic:
            fragment = BicubicShader;
            break;
        default:
            LOG(VB_PLAYBACK, LOG_ERR, LOC + "Unknown filter");
            break;
    }
    CustomiseProgramString(vertex);
    CustomiseProgramString(fragment);
}
