/*****************************************************************************
 * converter_sw.c: OpenGL converters for software video formats
 *****************************************************************************
 * Copyright (C) 2016,2017 VLC authors and VideoLAN
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <assert.h>
#include <limits.h>
#include <stdlib.h>

#include <vlc_common.h>
#include <vlc_arrays.h>
#include <vlc_plugin.h>
#include <vlc_opengl.h>
#include <vlc_opengl_interop.h>

#include "gl_util.h"
#include "interop.h"

#define PBO_DISPLAY_COUNT 2 /* Double buffering */
typedef struct
{
    PFNGLDELETEBUFFERSPROC DeleteBuffers;
    GLuint      buffers[PICTURE_PLANE_MAX];
    size_t      bytes[PICTURE_PLANE_MAX];
} picture_sys_t;

struct priv
{
    bool   has_gl_3;
    bool   has_texture_rg;
    bool   has_texture_integer;
    bool   has_unpack_subimage;
    bool   unsigned_sampler;
    void * texture_temp_buf;
    size_t texture_temp_buf_size;
    struct {
        picture_t *display_pics[PBO_DISPLAY_COUNT];
        size_t display_idx;
    } pbo;

#define OPENGL_VTABLE_F(X) \
        X(PFNGLGETERRORPROC,        GetError) \
        X(PFNGLGETINTEGERVPROC,     GetIntegerv) \
        X(PFNGLGETSTRINGPROC,       GetString) \
        \
        X(PFNGLACTIVETEXTUREPROC,   ActiveTexture) \
        X(PFNGLBINDTEXTUREPROC,     BindTexture) \
        X(PFNGLTEXIMAGE2DPROC,      TexImage2D) \
        X(PFNGLTEXSUBIMAGE2DPROC,   TexSubImage2D) \
        \
        X(PFNGLBINDBUFFERPROC,      BindBuffer) \
        X(PFNGLBUFFERDATAPROC,      BufferData) \
        X(PFNGLBUFFERSUBDATAPROC,   BufferSubData) \
        X(PFNGLDELETEBUFFERSPROC,   DeleteBuffers) \
        X(PFNGLGENBUFFERSPROC,      GenBuffers) \
        X(PFNGLPIXELSTOREIPROC,     PixelStorei) \
        X(PFNGLTEXPARAMETERIPROC,   TexParameteri)
    struct {
#define DECLARE_SYMBOL(type, name) type name;
        OPENGL_VTABLE_F(DECLARE_SYMBOL)
    } gl;
};

static void
pbo_picture_destroy(picture_t *pic)
{
    picture_sys_t *picsys = pic->p_sys;

    picsys->DeleteBuffers(pic->i_planes, picsys->buffers);

    free(picsys);
}

static picture_t *
pbo_picture_create(const struct vlc_gl_interop *interop)
{
    const struct priv *priv = interop->priv;
    picture_sys_t *picsys = calloc(1, sizeof(*picsys));
    if (unlikely(picsys == NULL))
        return NULL;

    picture_resource_t rsc = {
        .p_sys = picsys,
        .pf_destroy = pbo_picture_destroy,
    };
    picture_t *pic = picture_NewFromResource(&interop->fmt_out, &rsc);
    if (pic == NULL)
    {
        free(picsys);
        return NULL;
    }

    priv->gl.GenBuffers(pic->i_planes, picsys->buffers);
    picsys->DeleteBuffers = priv->gl.DeleteBuffers;

    /* XXX: needed since picture_NewFromResource override pic planes */
    if (picture_Setup(pic, &interop->fmt_out))
    {
        picture_Release(pic);
        return NULL;
    }

    assert(pic->i_planes > 0
        && (unsigned) pic->i_planes <= interop->tex_count);

    for (int i = 0; i < pic->i_planes; ++i)
    {
        const plane_t *p = &pic->p[i];

        if( p->i_pitch < 0 || p->i_lines <= 0 ||
            (size_t)p->i_pitch > SIZE_MAX/p->i_lines )
        {
            picture_Release(pic);
            return NULL;
        }
        picsys->bytes[i] = p->i_pitch * p->i_lines;
    }
    return pic;
}

static int
pbo_data_alloc(const struct vlc_gl_interop *interop, picture_t *pic)
{
    const struct priv *priv = interop->priv;
    picture_sys_t *picsys = pic->p_sys;

    priv->gl.GetError();

    for (int i = 0; i < pic->i_planes; ++i)
    {
        priv->gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER, picsys->buffers[i]);
        priv->gl.BufferData(GL_PIXEL_UNPACK_BUFFER, picsys->bytes[i], NULL,
                                GL_DYNAMIC_DRAW);

        if (priv->gl.GetError() != GL_NO_ERROR)
        {
            msg_Err(interop->gl, "could not alloc PBO buffers");
            priv->gl.DeleteBuffers(i, picsys->buffers);
            return VLC_EGENERIC;
        }
    }
    return VLC_SUCCESS;
}

static int
pbo_pics_alloc(const struct vlc_gl_interop *interop)
{
    struct priv *priv = interop->priv;
    for (size_t i = 0; i < PBO_DISPLAY_COUNT; ++i)
    {
        picture_t *pic = priv->pbo.display_pics[i] =
            pbo_picture_create(interop);
        if (pic == NULL)
            goto error;

        if (pbo_data_alloc(interop, pic) != VLC_SUCCESS)
            goto error;
    }

    /* turn off pbo */
    priv->gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    return VLC_SUCCESS;
error:
    for (size_t i = 0; i < PBO_DISPLAY_COUNT && priv->pbo.display_pics[i]; ++i)
        picture_Release(priv->pbo.display_pics[i]);
    return VLC_EGENERIC;
}

static int
tc_pbo_update(const struct vlc_gl_interop *interop, uint32_t textures[],
              const int32_t tex_width[], const int32_t tex_height[],
              picture_t *pic, const size_t *plane_offset)
{
    (void) plane_offset; assert(plane_offset == NULL);
    struct priv *priv = interop->priv;

    picture_t *display_pic = priv->pbo.display_pics[priv->pbo.display_idx];
    picture_sys_t *p_sys = display_pic->p_sys;
    priv->pbo.display_idx = (priv->pbo.display_idx + 1) % PBO_DISPLAY_COUNT;

    for (int i = 0; i < pic->i_planes; i++)
    {
        GLsizeiptr size = pic->p[i].i_lines * pic->p[i].i_pitch;
        const GLvoid *data = pic->p[i].p_pixels;
        priv->gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER,
                           p_sys->buffers[i]);
        priv->gl.BufferSubData(GL_PIXEL_UNPACK_BUFFER, 0, size, data);

        priv->gl.ActiveTexture(GL_TEXTURE0 + i);
        priv->gl.BindTexture(interop->tex_target, textures[i]);

        //for YUV with interleaved UV, pixel pitch is reported as 1
        if (pic->i_planes == 2 && interop->tex_count == 2 && i == 1)
            priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, pic->p[i].i_pitch / (pic->p[i].i_pixel_pitch * 2));
        else
            priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, pic->p[i].i_pitch / pic->p[i].i_pixel_pitch);

        priv->gl.TexSubImage2D(interop->tex_target, 0, 0, 0, tex_width[i], tex_height[i],
                                   interop->texs[i].format, interop->texs[i].type, NULL);
        priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    }

    if (pic->i_planes == 1 && interop->tex_count == 2)
    {
        /* For YUV 4:2:2 formats, a single plane is uploaded into 2 textures */
        priv->gl.ActiveTexture(GL_TEXTURE1);
        priv->gl.BindTexture(interop->tex_target, textures[1]);
        priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, (pic->p[0].i_pitch / pic->p[0].i_pixel_pitch) >> 1); /* yuv[Y] */
        priv->gl.TexSubImage2D(interop->tex_target, 0, 0, 0, tex_width[1], tex_height[1],
                               interop->texs[1].format, interop->texs[1].type, NULL);
        priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    }
    GL_ASSERT_NOERROR(&priv->gl);

    /* turn off pbo */
    priv->gl.BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    return VLC_SUCCESS;
}

static int
tc_common_allocate_textures(const struct vlc_gl_interop *interop, uint32_t textures[],
                            const int32_t tex_width[], const int32_t tex_height[])
{
    const struct priv *priv = interop->priv;
    for (unsigned i = 0; i < interop->tex_count; i++)
    {
        priv->gl.BindTexture(interop->tex_target, textures[i]);
        priv->gl.TexImage2D(interop->tex_target, 0, interop->texs[i].internal,
                                tex_width[i], tex_height[i], 0, interop->texs[i].format,
                                interop->texs[i].type, NULL);

        /* Integer textures cannot be filtered using GL_LINEAR. They must use
         * GL_NEAREST. */
        if (priv->unsigned_sampler)
        {
            priv->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            priv->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        }
        GL_ASSERT_NOERROR(&priv->gl);
    }
    return VLC_SUCCESS;
}

static int
upload_plane(const struct vlc_gl_interop *interop, unsigned tex_idx,
             int32_t width, int32_t height, size_t pitch, size_t pixel_size,
             const void *pixels, unsigned pixel_pack)
{
    struct priv *priv = interop->priv;
    GLenum tex_format = interop->texs[tex_idx].format;
    GLenum tex_type = interop->texs[tex_idx].type;

    /* This unpack alignment is the default, but setting it just in case. */
    priv->gl.PixelStorei(GL_UNPACK_ALIGNMENT, 4);

    assert(height > 0);
    assert(width > 0);
    assert(pixel_size);
    assert(pitch % pixel_size == 0);
    assert((size_t) width * pixel_size <= pitch);

    size_t width_bytes = width * pixel_size * pixel_pack;

    if (!priv->has_unpack_subimage)
    {
        if (pitch != width_bytes)
        {
            size_t aligned_row_len = vlc_align(width_bytes, 4);
            size_t buf_size = aligned_row_len * height;
            const uint8_t *source = pixels;
            uint8_t *destination;
            if (priv->texture_temp_buf_size < buf_size)
            {
                priv->texture_temp_buf =
                    realloc_or_free(priv->texture_temp_buf, buf_size);
                if (priv->texture_temp_buf == NULL)
                {
                    priv->texture_temp_buf_size = 0;
                    return VLC_ENOMEM;
                }
                priv->texture_temp_buf_size = buf_size;
            }
            destination = priv->texture_temp_buf;

            for (GLsizei h = 0; h < height ; h++)
            {
                memcpy(destination, source, width_bytes);
                source += pitch;
                destination += aligned_row_len;
            }
            priv->gl.TexSubImage2D(interop->tex_target, 0, 0, 0, width, height,
                                   tex_format, tex_type, priv->texture_temp_buf);
        }
        else
        {
            priv->gl.TexSubImage2D(interop->tex_target, 0, 0, 0, width, height,
                                   tex_format, tex_type, pixels);
        }
    }
    else
    {
        priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, pitch / (pixel_size * pixel_pack ));

        priv->gl.TexSubImage2D(interop->tex_target, 0, 0, 0, width, height,
                               tex_format, tex_type, pixels);
        priv->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    }

    GL_ASSERT_NOERROR(&priv->gl);

    return VLC_SUCCESS;
}

static int
tc_common_update(const struct vlc_gl_interop *interop, uint32_t textures[],
                 const int32_t tex_width[], const int32_t tex_height[],
                 picture_t *pic, const size_t *plane_offset)
{
    const struct priv *priv = interop->priv;
    int ret = VLC_SUCCESS;
    for (int i = 0; i < pic->i_planes && ret == VLC_SUCCESS; i++)
    {
        assert(textures[i] != 0);
        priv->gl.ActiveTexture(GL_TEXTURE0 + i);
        priv->gl.BindTexture(interop->tex_target, textures[i]);
        const void *pixels = plane_offset != NULL ?
                             &pic->p[i].p_pixels[plane_offset[i]] :
                             pic->p[i].p_pixels;

        //are we uploading packed UV plane
        unsigned pixel_pack = (pic->i_planes == 2 && i == 1) ? 2 : 1;
        ret = upload_plane(interop, i, tex_width[i], tex_height[i],
                           pic->p[i].i_pitch, pic->p[i].i_pixel_pitch, pixels, pixel_pack);
    }

    if (pic->i_planes == 1 && interop->tex_count == 2)
    {
        /* For YUV 4:2:2 formats, a single plane is uploaded into 2 textures */
        assert(textures[1] != 0);
        priv->gl.ActiveTexture(GL_TEXTURE1);
        priv->gl.BindTexture(interop->tex_target, textures[1]);
        const void *pixels = plane_offset != NULL ?
                             &pic->p[0].p_pixels[plane_offset[0]] :
                             pic->p[0].p_pixels;

        ret = upload_plane(interop, 1, tex_width[1], tex_height[1],
                           pic->p[0].i_pitch, pic->p[0].i_pixel_pitch, pixels, 2);
    }

    return ret;
}

static inline void
DivideRationalByTwo(vlc_rational_t *r) {
    if (r->num % 2 == 0)
        r->num /= 2;
    else
        r->den *= 2;
}

static bool fixGLFormat(struct vlc_gl_interop *interop, unsigned pixel_size,
                        GLint* intfmt, GLint* fmt, GLint *type)
{
    struct priv *priv = interop->priv;
    if (*intfmt == 0)
        return true;

    bool gles2, gles3, gl2, gl3;
    if (priv->has_gl_3)
    {
        gl3 = interop->gl->api_type == VLC_OPENGL;
        gl2 = false;
        gles3 = interop->gl->api_type == VLC_OPENGL_ES2;
        gles2 = false;
    }
    else
    {
        gl3 = false;
        gl2 = interop->gl->api_type == VLC_OPENGL;
        gles3 = false;
        gles2 = interop->gl->api_type == VLC_OPENGL_ES2;
    }
    assert(gl3 || gl2 || gles3 || gles2);

    if (gles3)
    {
        switch (*intfmt) {
        case GL_R8:
        case GL_RG8:
            break;
        case GL_R16:
            if (priv->has_texture_integer)
            {
                msg_Dbg(interop->gl, "fallback to GL_R16UI");
                *intfmt = GL_R16UI;
                *fmt = GL_RED_INTEGER;
            }

            /* Android is unlikely to support GL_R16UI or GL_R16: */
            if (vlc_gl_interop_GetTexFormatSize(interop, GL_TEXTURE_2D, *fmt,
                                                *intfmt, *type) != 16)
            {
                /* XXX: Quality loss with 12 and 16bits since half float has
                 * 10bits of mantissa */
                msg_Dbg(interop->gl, "fallback to GL_R16F");
                *intfmt = GL_R16F;
                *fmt = GL_RED;
                *type = GL_HALF_FLOAT;
            }
            break;
        case GL_RG16:
            if (priv->has_texture_integer)
            {
                msg_Dbg(interop->gl, "fallback to GL_R1G6UI ?");
                *intfmt = GL_RG16UI;
                *fmt = GL_RED_INTEGER;
            }
            /* XXX Is GL_RG16F possible with semi planar ? */
            break;
        default:
            vlc_assert_unreachable();
        }
    }
    else if (gles2 && priv->has_texture_rg)
    {
        //for GLES2 GL_EXT_texture_rg we need to use GL_RED/GL_RG as internal format
        switch (*intfmt) {
        case GL_R8:
            msg_Dbg(interop->gl, "fallback to GL_GED");
            *intfmt = GL_RED;
            *fmt = GL_RED;
            break;
        case GL_RG8:
            msg_Dbg(interop->gl, "fallback to GL_RG8");
            *intfmt = GL_RG;
            *fmt = GL_RG;
            break;
        case GL_R16:
        case GL_RG16:
            return false;
        default:
            vlc_assert_unreachable();
        }
    }
    else if (gles2 || (gl2 && !priv->has_texture_rg))
    {
        //fallback to GL_LUMINANCE / GL_LUMINANCE_ALPHA
        switch (*intfmt) {
        case GL_R8:
            msg_Dbg(interop->gl, "fallback to GL_LUMINANCE");
            *intfmt = GL_LUMINANCE;
            *fmt = GL_LUMINANCE;
            break;
        case GL_R16:
            if (gles2)
                return false;

            msg_Dbg(interop->gl, "fallback to GL_LUMINANCE16");
            *intfmt = GL_LUMINANCE16;
            *fmt = GL_LUMINANCE;
            break;
        case GL_RG8:
            msg_Dbg(interop->gl, "fallback to GL_LUMINANCE_ALPHA");
            *intfmt = GL_LUMINANCE_ALPHA;
            *fmt = GL_LUMINANCE_ALPHA;
            break;
        case GL_RG16:
            if (gles2)
                return false;

            msg_Dbg(interop->gl, "fallback to GL_LUMINANCE16_ALPHA16");
            *intfmt = GL_LUMINANCE16_ALPHA16;
            *fmt = GL_LUMINANCE_ALPHA;
            break;
        default:
            vlc_assert_unreachable();
        }
    }
    else
    {
        //OpenGL 3.0 and OpenGL with GL_ARB_texture_rg
        //don't need transformations
        assert(gl3 || (gl2 && priv->has_texture_rg));
    }

    if (pixel_size == 2
     && vlc_gl_interop_GetTexFormatSize(interop, GL_TEXTURE_2D, *fmt,
                                        *intfmt, *type) != 16)
        return false;
    return true;
}

static int
interop_yuv_base_init(struct vlc_gl_interop *interop,
                      const vlc_chroma_description_t *desc)
{
    struct interop_formats
    {
        GLint intfmt;
        GLint fmt;
        GLint plane2_intfmt;
        GLint plane2_fmt;
        GLint type;
        GLint plane2_type;
    } formats[] = {
        // 3 and 4 planes
            // 8 bits pixels
            { GL_R8, GL_RED, 0, 0, GL_UNSIGNED_BYTE, 0 },
            // 16 bits pixels
            { GL_R16, GL_RED, 0, 0, GL_UNSIGNED_SHORT, 0 },
        // 2 planes
            // 8 bits pixels
            { GL_R8, GL_RED, GL_RG8, GL_RG, GL_UNSIGNED_BYTE, GL_UNSIGNED_BYTE },
            // 16 bits pixels
            { GL_R16, GL_RED, GL_RG16, GL_RG, GL_UNSIGNED_SHORT, GL_UNSIGNED_SHORT },
        // 1 plane is a special case that will be handled explicitly
    };

    if (desc->plane_count == 1)
    {
        if (desc->fcc == VLC_CODEC_VUYA)
        {
            interop->tex_count = 2;
            interop->texs[0] = (struct vlc_gl_tex_cfg) {
                { 1, 1 }, { 1, 1 },
                GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE
            };
            return VLC_SUCCESS;
        }
        else if (desc->pixel_size != 2)
        {
            msg_Warn(interop->gl, "unsupported chroma %.4s", (char*)&desc->fcc);
            return VLC_EGENERIC;
        }

        /* Only YUV 4:2:2 formats */
        /* The pictures have only 1 plane, but it is uploaded twice, once to
         * access the Y components, once to access the UV components. See
         * #26712. */
        GLint intfmt = GL_RG8;
        GLint fmt = GL_RG;
        GLint type = GL_UNSIGNED_BYTE;
        if (!fixGLFormat(interop, desc->pixel_size, &intfmt, &fmt, &type))
            return VLC_EGENERIC;

        interop->tex_count = 2;
        interop->texs[0] = (struct vlc_gl_tex_cfg) {
            { 1, 1 }, { 1, 1 },
            intfmt, fmt, type
        };
        interop->texs[1] = (struct vlc_gl_tex_cfg) {
            { 1, 2 }, { 1, 1 },
            GL_RGBA, GL_RGBA, type
        };
        return VLC_SUCCESS;
    }

    uint8_t format_index = 0;
    switch (desc->plane_count)
    {
    case 4:
    case 3:
        break;
    case 2:
        format_index += 2;
        break;
    default:
        vlc_assert_unreachable();
        break;
    }

    switch (desc->pixel_size)
    {
    case 1:
        break;
    case 2:
        format_index += 1;
        break;
    default:
        return VLC_EGENERIC;
    }

    assert(format_index < ARRAY_SIZE(formats));

    struct interop_formats* format = &formats[format_index];

    GLint plane1_intfmt = format->intfmt;
    GLint plane1_fmt = format->fmt;
    GLint plane1_type = format->type;
    if (!fixGLFormat(interop, desc->pixel_size, &plane1_intfmt, &plane1_fmt,
                     &plane1_type))
        return VLC_EGENERIC;

    GLint plane2_intfmt = format->plane2_intfmt;
    GLint plane2_fmt = format->plane2_fmt;
    GLint plane2_type = format->plane2_type;
    if (!fixGLFormat(interop, desc->pixel_size, &plane2_intfmt, &plane2_fmt,
                     &plane2_type))
        return VLC_EGENERIC;

    msg_Dbg(interop, "Using format at index %u", format_index);
    msg_Dbg(interop, "Plane1: fmt=%#x intfmt=%#x type=%#x", plane1_fmt,
            plane1_intfmt, plane1_type);
    msg_Dbg(interop, "Plane2: fmt=%#x intfmt=%#x type=%#x", plane2_fmt,
            plane2_intfmt, plane2_type);

    if (desc->plane_count >= 3)
    {
        interop->tex_count = desc->plane_count;
        for (unsigned i = 0; i < interop->tex_count; ++i )
        {
            interop->texs[i] = (struct vlc_gl_tex_cfg) {
                { desc->p[i].w.num, desc->p[i].w.den },
                { desc->p[i].h.num, desc->p[i].h.den },
                plane1_intfmt, plane1_fmt, plane1_type
            };
        }
    }
    else if (desc->plane_count == 2)
    {
        interop->tex_count = 2;

        interop->texs[0] = (struct vlc_gl_tex_cfg) {
            { desc->p[0].w.num, desc->p[0].w.den },
            { desc->p[0].h.num, desc->p[0].h.den },
            plane1_intfmt, plane1_fmt, plane1_type
        };
        interop->texs[1] = (struct vlc_gl_tex_cfg) {
            { desc->p[1].w.num, desc->p[1].w.den },
            { desc->p[1].h.num, desc->p[1].h.den },
            plane2_intfmt, plane2_fmt, plane2_type
        };

        /*
         * If plane_count == 2, then the chroma is semiplanar: the U and V
         * planes are packed in the second plane. As a consequence, the
         * horizontal scaling, as reported in the vlc_chroma_description_t, is
         * doubled.
         *
         * But once imported as an OpenGL texture, both components are stored
         * in a single texel (the two first components of the vec4).
         * Therefore, from OpenGL, the width is not doubled, so the horizontal
         * scaling must be divided by 2 to compensate.
         */
         DivideRationalByTwo(&interop->texs[1].w);
    }

    return VLC_SUCCESS;
}

static int
interop_rgb_base_init(struct vlc_gl_interop *interop, vlc_fourcc_t chroma)
{
    switch (chroma)
    {
        case VLC_CODEC_RGB24:
            interop->texs[0] = (struct vlc_gl_tex_cfg) {
                { 1, 1 }, { 1, 1 }, GL_RGB, GL_RGB, GL_UNSIGNED_BYTE
            };
            break;
#ifdef GL_BGR
        case VLC_CODEC_BGR24:
            interop->texs[0] = (struct vlc_gl_tex_cfg) {
                { 1, 1 }, { 1, 1 }, GL_RGB, GL_BGR, GL_UNSIGNED_BYTE
            };
            break;
#endif

        case VLC_CODEC_RGBA:
            interop->texs[0] = (struct vlc_gl_tex_cfg) {
                { 1, 1 }, { 1, 1 }, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE
            };
            break;
        case VLC_CODEC_BGRA: {
            if (vlc_gl_interop_GetTexFormatSize(interop, GL_TEXTURE_2D, GL_BGRA, GL_RGBA,
                                                GL_UNSIGNED_BYTE) != 32)
                return VLC_EGENERIC;
            interop->texs[0] = (struct vlc_gl_tex_cfg) {
                { 1, 1 }, { 1, 1 }, GL_RGBA, GL_BGRA, GL_UNSIGNED_BYTE
            };
            break;
        }
        default:
            return VLC_EGENERIC;
    }
    interop->tex_count = 1;
    return VLC_SUCCESS;
}

static void
interop_xyz12_init(struct vlc_gl_interop *interop)
{
    interop->tex_count = 1;
    interop->tex_target = GL_TEXTURE_2D;
    interop->texs[0] = (struct vlc_gl_tex_cfg) {
        { 1, 1 }, { 1, 1 }, GL_RGB, GL_RGB, GL_UNSIGNED_SHORT
    };
}

static int
opengl_interop_init(struct vlc_gl_interop *interop,
                    vlc_fourcc_t chroma, video_color_space_t yuv_space)
{
    const vlc_chroma_description_t *desc =
        vlc_fourcc_GetChromaDescription(chroma);
    if (!desc || desc->plane_count == 0)
        return VLC_EGENERIC;

    assert(!interop->fmt_out.p_palette);
    interop->fmt_out.i_chroma = chroma;
    interop->fmt_out.space = yuv_space;
    interop->tex_target = GL_TEXTURE_2D;

    if (chroma == VLC_CODEC_XYZ_12B||
        chroma == VLC_CODEC_XYZ_12L)
    {
        interop_xyz12_init(interop);
        return VLC_SUCCESS;
    }

    if (vlc_fourcc_IsYUV(chroma))
        return interop_yuv_base_init(interop, desc);

    return interop_rgb_base_init(interop, chroma);
}

static void
opengl_interop_generic_deinit(struct vlc_gl_interop *interop)
{
    struct priv *priv = interop->priv;
    for (size_t i = 0; i < PBO_DISPLAY_COUNT && priv->pbo.display_pics[i]; ++i)
        picture_Release(priv->pbo.display_pics[i]);
    free(priv->texture_temp_buf);
    free(priv);
}

static int
opengl_interop_generic_init(struct vlc_gl_interop *interop, bool allow_dr)
{

    struct priv *priv = calloc(1, sizeof(struct priv));
    if (unlikely(priv == NULL))
        return VLC_ENOMEM;

    interop->priv = priv;

#define LOAD_SYMBOL(type, name) \
    priv->gl.name = vlc_gl_GetProcAddress(interop->gl, "gl" # name); \
    assert(priv->gl.name != NULL);

    OPENGL_VTABLE_F(LOAD_SYMBOL);

    struct vlc_gl_extension_vt extension_vt;
    vlc_gl_LoadExtensionFunctions(interop->gl, &extension_vt);

    /* OpenGL or OpenGL ES2 with GL_EXT_unpack_subimage ext */
    priv->has_unpack_subimage = interop->gl->api_type == VLC_OPENGL
        || vlc_gl_HasExtension(&extension_vt, "GL_EXT_unpack_subimage");

    /* RG textures are available natively since OpenGL 3.0 and OpenGL ES 3.0 */
    priv->has_gl_3 = vlc_gl_GetVersionMajor(&extension_vt) >= 3;

    priv->has_texture_rg =
        (interop->gl->api_type == VLC_OPENGL
            && vlc_gl_HasExtension(&extension_vt, "GL_ARB_texture_rg"))
        || (interop->gl->api_type == VLC_OPENGL_ES2
            && vlc_gl_HasExtension(&extension_vt, "GL_EXT_texture_rg"));

    priv->has_texture_integer = priv->has_gl_3 &&
        (interop->gl->api_type == VLC_OPENGL
        || vlc_gl_HasExtension(&extension_vt, "GL_EXT_texture_integer"));

    video_color_space_t space;
    const bool is_yup = vlc_fourcc_IsYUV(interop->fmt_in.i_chroma);

    if (is_yup)
    {
        GLint max_texture_units = 0;
        priv->gl.GetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &max_texture_units);
        if (max_texture_units < 3)
            goto error;

        space = interop->fmt_in.space;
    }
    else
        space = COLOR_SPACE_UNDEF;

    /* The pictures are uploaded upside-down */
    video_format_TransformBy(&interop->fmt_out, TRANSFORM_VFLIP);

    /* Check whether the given chroma is translatable to OpenGL. */
    vlc_fourcc_t i_chroma = interop->fmt_in.i_chroma;
    int ret = opengl_interop_init(interop, i_chroma, space);
    if (ret == VLC_SUCCESS)
        goto interop_init;

    if (!is_yup)
    {
        i_chroma = VLC_CODEC_RGBA;
        ret = opengl_interop_init(interop, i_chroma, space);
        if (ret == VLC_SUCCESS)
            goto interop_init;
    }

    vlc_fourcc_t *list = NULL;
    if (is_yup)
        list = vlc_fourcc_GetYUVFallback(interop->fmt_in.i_chroma);
    else if (interop->fmt_in.i_chroma != VLC_CODEC_XYZ_12B)
        list = vlc_fourcc_GetRGBFallback(interop->fmt_in.i_chroma);

    if (list == NULL)
        goto error;

    /* Check whether any fallback for the chroma is translatable to OpenGL. */
    for (const vlc_fourcc_t *fcc = list; *fcc; fcc++)
    {
        ret = opengl_interop_init(interop, *fcc, space);
        if (ret == VLC_SUCCESS)
        {
            i_chroma = *fcc;
            msg_Warn(interop->gl, "direct rendering failed for %4.4s, "
                     "fallback to %4.4s",
                     (const char *)&interop->fmt_in.i_chroma,
                     (const char *)&i_chroma);
            goto interop_init;
        }
    }
    free(list);

    goto error;

interop_init:
    /* We found a chroma with matching parameters for OpenGL. The interop can
     * be created. */

    interop->fmt_in.i_chroma = i_chroma;

    static const struct vlc_gl_interop_ops ops = {
        .allocate_textures = tc_common_allocate_textures,
        .update_textures = tc_common_update,
        .close = opengl_interop_generic_deinit,
    };
    interop->ops = &ops;

    switch (interop->texs[0].format)
    {
        case GL_RED_INTEGER:
        case GL_RG_INTEGER:
            priv->unsigned_sampler = true;
            break;
        default:
            priv->unsigned_sampler = false;
            break;
    }

    if (allow_dr && priv->has_unpack_subimage)
    {
        /* Ensure we do direct rendering / PBO with OpenGL 3.0 or higher. */
        const unsigned char *ogl_version = priv->gl.GetString(GL_VERSION);
        const bool glver_ok = strverscmp((const char *)ogl_version, "3.0") >= 0;

        const bool has_pbo = glver_ok &&
            (vlc_gl_HasExtension(&extension_vt, "GL_ARB_pixel_buffer_object") ||
             vlc_gl_HasExtension(&extension_vt, "GL_EXT_pixel_buffer_object"));

        const bool supports_pbo = has_pbo && priv->gl.BufferData
            && priv->gl.BufferSubData;
        if (supports_pbo && pbo_pics_alloc(interop) == VLC_SUCCESS)
        {
            static const struct vlc_gl_interop_ops pbo_ops = {
                .allocate_textures = tc_common_allocate_textures,
                .update_textures = tc_pbo_update,
                .close = opengl_interop_generic_deinit,
            };
            interop->ops = &pbo_ops;
            msg_Dbg(interop->gl, "PBO support enabled");
        }
    }

    return VLC_SUCCESS;

error:
    free(priv);
    interop->priv = NULL;
    return VLC_EGENERIC;
}

static int OpenInteropSW(struct vlc_gl_interop *interop)
{
    return opengl_interop_generic_init(interop, false);
}

static int OpenInteropDirectRendering(struct vlc_gl_interop *interop)
{
    return opengl_interop_generic_init(interop, true);
}

vlc_module_begin ()
    set_description("Software OpenGL interop")
    set_capability("opengl sw interop", 1)
    set_callback(OpenInteropSW)
    set_subcategory(SUBCAT_VIDEO_VOUT)
    add_shortcut("sw")

    add_submodule()
    set_callback(OpenInteropDirectRendering)
    set_capability("opengl sw interop", 2)
    add_shortcut("pbo")
vlc_module_end ()
