// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtOpenGL/QOpenGLFramebufferObject>
#include <QtOpenGL/QOpenGLPaintDevice>
#include <QtOpenGL/QOpenGLTexture>
#include <QtOpenGL/qopengltextureblitter.h>
#include <QtOpenGL/QOpenGLVertexArrayObject>
#include <QtOpenGL/QOpenGLBuffer>
#if !QT_CONFIG(opengles2)
#  include <QtOpenGL/QOpenGLFunctions_4_2_Core>
#endif
#include <QtOpenGL/QOpenGLVersionFunctionsFactory>
#include <QtGui/private/qopenglcontext_p.h>
#include <QtGui/QOpenGLFunctions>
#include <QtGui/QPainter>
#include <QtGui/QPainterPath>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtGui/QOffscreenSurface>
#include <QtGui/QGenericMatrix>
#include <QtGui/QMatrix4x4>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/private/qopenglextensions_p.h>
#include <qpa/qplatformintegration.h>
#include <qpa/qplatformnativeinterface.h>

#include <QTest>

#include <QSignalSpy>

#include <optional>

Q_DECLARE_METATYPE(QImage::Format)

class tst_QOpenGL : public QObject
{
Q_OBJECT

private slots:
    void initTestCase();
    void sharedResourceCleanup_data();
    void sharedResourceCleanup();
    void multiGroupSharedResourceCleanup_data();
    void multiGroupSharedResourceCleanup();
    void multiGroupSharedResourceCleanupCustom_data();
    void multiGroupSharedResourceCleanupCustom();
    void fboSimpleRendering_data();
    void fboSimpleRendering();
    void fboTextureOwnership_data();
    void fboTextureOwnership();
    void fboRendering_data();
    void fboRendering();
    void fboRenderingRGB30_data();
    void fboRenderingRGB30();
    void fboRenderingRGB64_data();
    void fboRenderingRGB64();
    void fboHandleNulledAfterContextDestroyed();
    void fboMRT();
    void fboMRT_differentFormats();
    void openGLPaintDevice_data();
    void openGLPaintDevice();
    void openGLPaintDeviceWithChangingContext();
    void aboutToBeDestroyed();
    void sizeLessWindow();
    void QTBUG15621_triangulatingStrokerDivZero();
    void textureblitterFullSourceRectTransform();
    void textureblitterPartOriginBottomLeftSourceRectTransform();
    void textureblitterPartOriginTopLeftSourceRectTransform();
    void textureblitterFullTargetRectTransform();
    void textureblitterPartTargetRectTransform();
    void defaultSurfaceFormat();
    void imageFormatPainting();
    void nullTextureInitializtion();
    void clipRect();

#ifdef USE_GLX
    void glxContextWrap();
#endif

#if defined(Q_OS_WIN32) && !QT_CONFIG(opengles2)
    void wglContextWrap();
#endif

    void vaoCreate();
    void bufferCreate();
    void bufferMapRange();
    void defaultQGLCurrentBuffer();
    void makeCurrentAfterResizeWithNonDefaultFBOBound();
#if QT_CONFIG(egl)
    void dontCrashOnInvalidContextThreadTeardown();
#endif
};

struct SharedResourceTracker
{
    SharedResourceTracker()
    {
        reset();
    }

    void reset()
    {
        invalidateResourceCalls = 0;
        freeResourceCalls = 0;
        destructorCalls = 0;
    }

    int invalidateResourceCalls;
    int freeResourceCalls;
    int destructorCalls;
};

struct SharedResource : public QOpenGLSharedResource
{
    SharedResource(SharedResourceTracker *t)
        : QOpenGLSharedResource(QOpenGLContextGroup::currentContextGroup())
        , resource(1)
        , tracker(t)
    {
    }

    SharedResource(QOpenGLContext *ctx)
        : QOpenGLSharedResource(ctx->shareGroup())
        , resource(1)
        , tracker(0)
    {
    }

    ~SharedResource()
    {
        if (tracker)
            tracker->destructorCalls++;
    }

    void invalidateResource() override
    {
        resource = 0;
        if (tracker)
            tracker->invalidateResourceCalls++;
    }

    void freeResource(QOpenGLContext *context) override
    {
        Q_ASSERT(context == QOpenGLContext::currentContext());
        Q_UNUSED(context);
        resource = 0;
        if (tracker)
            tracker->freeResourceCalls++;
    }

    int resource;
    SharedResourceTracker *tracker;
};

static QSurface *createSurface(int surfaceClass)
{
    if (surfaceClass == int(QSurface::Window)) {
        QWindow *window = new QWindow;
        window->setSurfaceType(QWindow::OpenGLSurface);
        window->setGeometry(0, 0, 10, 10);
        window->create();
        return window;
    } else if (surfaceClass == int(QSurface::Offscreen)) {
        // Create a window and get the format from that.  For example, if an EGL
        // implementation provides 565 and 888 configs for PBUFFER_BIT but only
        // 888 for WINDOW_BIT, we may end up with a pbuffer surface that is
        // incompatible with the context since it could choose the 565 while the
        // window and the context uses a config with 888.
        static QSurfaceFormat format;
        if (format.redBufferSize() == -1) {
            QWindow *window = new QWindow;
            window->setSurfaceType(QWindow::OpenGLSurface);
            window->setGeometry(0, 0, 10, 10);
            window->create();
            format = window->format();
            delete window;
        }
        QOffscreenSurface *offscreenSurface = new QOffscreenSurface;
        offscreenSurface->setFormat(format);
        offscreenSurface->create();
        return offscreenSurface;
    }
    return 0;
}

void tst_QOpenGL::initTestCase()
{
    if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
        QSKIP("OpenGL is not supported on this platform.");
}

static void common_data()
{
    QTest::addColumn<int>("surfaceClass");

    QTest::newRow("Using QWindow") << int(QSurface::Window);
    QTest::newRow("Using QOffscreenSurface") << int(QSurface::Offscreen);
}

void tst_QOpenGL::sharedResourceCleanup_data()
{
    common_data();
}

void tst_QOpenGL::sharedResourceCleanup()
{
    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext *ctx = new QOpenGLContext;
    QVERIFY(ctx->create());
    QVERIFY(ctx->makeCurrent(surface.data()));

    SharedResourceTracker tracker;
    SharedResource *resource = new SharedResource(&tracker);
    resource->free();

    QCOMPARE(tracker.invalidateResourceCalls, 0);
    QCOMPARE(tracker.freeResourceCalls, 1);
    QCOMPARE(tracker.destructorCalls, 1);

    tracker.reset();

    resource = new SharedResource(&tracker);

    QOpenGLContext *ctx2 = new QOpenGLContext;
    ctx2->setShareContext(ctx);
    QVERIFY(ctx2->create());

    delete ctx;

    resource->free();

    // no current context, freeResource() delayed
    QCOMPARE(tracker.invalidateResourceCalls, 0);
    QCOMPARE(tracker.freeResourceCalls, 0);
    QCOMPARE(tracker.destructorCalls, 0);

    ctx2->makeCurrent(surface.data());

    // freeResource() should now have been called
    QCOMPARE(tracker.invalidateResourceCalls, 0);
    QCOMPARE(tracker.freeResourceCalls, 1);
    QCOMPARE(tracker.destructorCalls, 1);

    tracker.reset();

    resource = new SharedResource(&tracker);

    // this should cause invalidateResource() to be called
    delete ctx2;

    QCOMPARE(tracker.invalidateResourceCalls, 1);
    QCOMPARE(tracker.freeResourceCalls, 0);
    QCOMPARE(tracker.destructorCalls, 0);

    // should have no effect other than destroying the resource,
    // as it has already been invalidated
    resource->free();

    QCOMPARE(tracker.invalidateResourceCalls, 1);
    QCOMPARE(tracker.freeResourceCalls, 0);
    QCOMPARE(tracker.destructorCalls, 1);
}

void tst_QOpenGL::multiGroupSharedResourceCleanup_data()
{
    common_data();
}

void tst_QOpenGL::multiGroupSharedResourceCleanup()
{
    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    for (int i = 0; i < 10; ++i) {
        QOpenGLContext *gl = new QOpenGLContext();
        QVERIFY(gl->create());
        gl->makeCurrent(surface.data());
        {
            // Cause QOpenGLMultiGroupSharedResource instantiation.
            QOpenGLFunctions func(gl);
        }
        delete gl;
        // Cause context group's deleteLater() to be processed.
        QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
    }
    // Shouldn't crash when application exits.
}

void tst_QOpenGL::multiGroupSharedResourceCleanupCustom_data()
{
    common_data();
}

void tst_QOpenGL::multiGroupSharedResourceCleanupCustom()
{
    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext *ctx = new QOpenGLContext();
    QVERIFY(ctx->create());
    QVERIFY(ctx->makeCurrent(surface.data()));

    QOpenGLMultiGroupSharedResource multiGroupSharedResource;
    SharedResource *resource = multiGroupSharedResource.value<SharedResource>(ctx);
    SharedResourceTracker tracker;
    resource->tracker = &tracker;

    delete ctx;

    QCOMPARE(tracker.invalidateResourceCalls, 1);
    QCOMPARE(tracker.freeResourceCalls, 0);
    QCOMPARE(tracker.destructorCalls, 1);
}

static bool fuzzyComparePixels(const QRgb testPixel, const QRgb refPixel, const char* file, int line, int x = -1, int y = -1)
{
    static int maxFuzz = 1;
    static bool maxFuzzSet = false;

    // On 16 bpp systems, we need to allow for more fuzz:
    if (!maxFuzzSet) {
        maxFuzzSet = true;
        if (QGuiApplication::primaryScreen()->depth() < 24)
            maxFuzz = 32;
    }

    int redFuzz = qAbs(qRed(testPixel) - qRed(refPixel));
    int greenFuzz = qAbs(qGreen(testPixel) - qGreen(refPixel));
    int blueFuzz = qAbs(qBlue(testPixel) - qBlue(refPixel));
    int alphaFuzz = qAbs(qAlpha(testPixel) - qAlpha(refPixel));

    if (refPixel != 0 && testPixel == 0) {
        QString msg;
        if (x >= 0) {
            msg = QString("Test pixel [%1, %2] is null (black) when it should be (%3,%4,%5,%6)")
                            .arg(x).arg(y)
                            .arg(qRed(refPixel)).arg(qGreen(refPixel)).arg(qBlue(refPixel)).arg(qAlpha(refPixel));
        } else {
            msg = QString("Test pixel is null (black) when it should be (%2,%3,%4,%5)")
                            .arg(qRed(refPixel)).arg(qGreen(refPixel)).arg(qBlue(refPixel)).arg(qAlpha(refPixel));
        }

        QTest::qFail(msg.toLatin1(), file, line);
        return false;
    }

    if (redFuzz > maxFuzz || greenFuzz > maxFuzz || blueFuzz > maxFuzz || alphaFuzz > maxFuzz) {
        QString msg;

        if (x >= 0)
            msg = QString("Pixel [%1,%2]: ").arg(x).arg(y);
        else
            msg = QString("Pixel ");

        msg += QString("Max fuzz (%1) exceeded: (%2,%3,%4,%5) vs (%6,%7,%8,%9)")
                      .arg(maxFuzz)
                      .arg(qRed(testPixel)).arg(qGreen(testPixel)).arg(qBlue(testPixel)).arg(qAlpha(testPixel))
                      .arg(qRed(refPixel)).arg(qGreen(refPixel)).arg(qBlue(refPixel)).arg(qAlpha(refPixel));
        QTest::qFail(msg.toLatin1(), file, line);
        return false;
    }
    return true;
}

static void fuzzyCompareImages(const QImage &testImage, const QImage &referenceImage, const char* file, int line)
{
    QCOMPARE(testImage.devicePixelRatio(), referenceImage.devicePixelRatio());
    QCOMPARE(testImage.width(), referenceImage.width());
    QCOMPARE(testImage.height(), referenceImage.height());

    for (int y = 0; y < testImage.height(); y++) {
        for (int x = 0; x < testImage.width(); x++) {
            if (!fuzzyComparePixels(testImage.pixel(x, y), referenceImage.pixel(x, y), file, line, x, y)) {
                // Might as well save the images for easier debugging:
                referenceImage.save("referenceImage.png");
                testImage.save("testImage.png");
                return;
            }
        }
    }
}

#define QFUZZY_COMPARE_IMAGES(A,B) \
            fuzzyCompareImages(A, B, __FILE__, __LINE__)

#define QFUZZY_COMPARE_PIXELS(A,B) \
            fuzzyComparePixels(A, B, __FILE__, __LINE__)

void qt_opengl_draw_test_pattern(QPainter* painter, int width, int height)
{
    QPainterPath intersectingPath;
    intersectingPath.moveTo(0, 0);
    intersectingPath.lineTo(100, 0);
    intersectingPath.lineTo(0, 100);
    intersectingPath.lineTo(100, 100);
    intersectingPath.closeSubpath();

    QPainterPath trianglePath;
    trianglePath.moveTo(50, 0);
    trianglePath.lineTo(100, 100);
    trianglePath.lineTo(0, 100);
    trianglePath.closeSubpath();

    painter->setTransform(QTransform()); // reset xform
    painter->fillRect(-1, -1, width+2, height+2, Qt::red); // Background
    painter->translate(14, 14);
    painter->fillPath(intersectingPath, Qt::blue); // Test stencil buffer works
    painter->translate(128, 0);
    painter->setClipPath(trianglePath); // Test depth buffer works
    painter->setTransform(QTransform()); // reset xform ready for fill
    painter->fillRect(-1, -1, width+2, height+2, Qt::green);
}

void qt_opengl_check_test_pattern(const QImage& img)
{
    // As we're doing more than trivial painting, we can't just compare to
    // an image rendered with raster. Instead, we sample at well-defined
    // test-points:
    QVERIFY(!img.isNull());
    QVERIFY2(img.width() > 217, QByteArray::number(img.width()));
    QVERIFY2(img.height() > 90, QByteArray::number(img.height()));

    QFUZZY_COMPARE_PIXELS(img.pixel(39, 64), QColor(Qt::red).rgb());
    QFUZZY_COMPARE_PIXELS(img.pixel(89, 64), QColor(Qt::red).rgb());
    QFUZZY_COMPARE_PIXELS(img.pixel(64, 39), QColor(Qt::blue).rgb());
    QFUZZY_COMPARE_PIXELS(img.pixel(64, 89), QColor(Qt::blue).rgb());

    QFUZZY_COMPARE_PIXELS(img.pixel(167, 39), QColor(Qt::red).rgb());
    QFUZZY_COMPARE_PIXELS(img.pixel(217, 39), QColor(Qt::red).rgb());
    QFUZZY_COMPARE_PIXELS(img.pixel(192, 64), QColor(Qt::green).rgb());
}

void tst_QOpenGL::fboSimpleRendering_data()
{
    common_data();
}

void tst_QOpenGL::fboSimpleRendering()
{
    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    // No multisample with combined depth/stencil attachment:
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment);

    const QSize size(200, 100);
    QScopedPointer<QOpenGLFramebufferObject> fbo(new QOpenGLFramebufferObject(size, fboFormat));

    QVERIFY(fbo->bind());

    ctx.functions()->glClearColor(1.0, 0.0, 0.0, 1.0);
    ctx.functions()->glClear(GL_COLOR_BUFFER_BIT);
    ctx.functions()->glFinish();

    const QImage fb = fbo->toImage().convertToFormat(QImage::Format_RGB32);
    QCOMPARE(fb.size(), size);
    QImage reference(size, QImage::Format_RGB32);
    reference.fill(0xffff0000);

    QFUZZY_COMPARE_IMAGES(fb, reference);
}

void tst_QOpenGL::fboTextureOwnership_data()
{
    common_data();
}

void tst_QOpenGL::fboTextureOwnership()
{
    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    ctx.makeCurrent(surface.data());

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment);

    const auto fbo = std::make_unique<QOpenGLFramebufferObject>(200, 100, fboFormat);
    QVERIFY(fbo->texture() != 0);
    fbo->bind();

    // pull out the texture
    GLuint texture = fbo->takeTexture();
    QVERIFY(texture != 0);
    QCOMPARE(fbo->texture(), GLuint(0));

    // verify that the next bind() creates a new texture
    fbo->bind();
    QVERIFY(fbo->texture() != 0 && fbo->texture() != texture);

    ctx.functions()->glClearColor(1.0, 0.0, 0.0, 1.0);
    ctx.functions()->glClear(GL_COLOR_BUFFER_BIT);
    ctx.functions()->glFinish();

    QImage fb = fbo->toImage().convertToFormat(QImage::Format_RGB32);
    QImage reference(fb.size(), QImage::Format_RGB32);
    reference.fill(0xffff0000);

    QFUZZY_COMPARE_IMAGES(fb, reference);

    ctx.functions()->glDeleteTextures(1, &texture);
}

void tst_QOpenGL::fboRendering_data()
{
    common_data();
}

// NOTE: This tests that CombinedDepthStencil attachment works by assuming the
//       GL2 engine is being used and is implemented the same way as it was when
//       this autotest was written. If this is not the case, there may be some
//       false-positives: I.e. The test passes when either the depth or stencil
//       buffer is actually missing. But that's probably ok anyway.
void tst_QOpenGL::fboRendering()
{
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    // No multisample with combined depth/stencil attachment:
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    // Uncomplicate things by using POT:
    const QSize size(256, 128);
    QOpenGLFramebufferObject fbo(size, fboFormat);

    if (fbo.attachment() != QOpenGLFramebufferObject::CombinedDepthStencil)
        QSKIP("FBOs missing combined depth~stencil support");

    QVERIFY(fbo.bind());

    QPainter fboPainter;
    QOpenGLPaintDevice device(fbo.width(), fbo.height());
    bool painterBegun = fboPainter.begin(&device);
    QVERIFY(painterBegun);

    qt_opengl_draw_test_pattern(&fboPainter, fbo.width(), fbo.height());

    fboPainter.end();

    const QImage fb = fbo.toImage().convertToFormat(QImage::Format_RGB32);
    QCOMPARE(fb.size(), size);

    qt_opengl_check_test_pattern(fb);
}

void tst_QOpenGL::fboRenderingRGB30_data()
{
    common_data();
}

#ifndef GL_RGB5_A1
#define GL_RGB5_A1                        0x8057
#endif

#ifndef GL_RGBA8
#define GL_RGBA8                          0x8058
#endif

#ifndef GL_RGB10_A2
#define GL_RGB10_A2                       0x8059
#endif

#ifndef GL_RGBA16
#define GL_RGBA16                         0x805B
#endif

#ifndef GL_FRAMEBUFFER_RENDERABLE
#define GL_FRAMEBUFFER_RENDERABLE         0x8289
#endif

#ifndef GL_FULL_SUPPORT
#define GL_FULL_SUPPORT                   0x82B7
#endif

static bool supportsInternalFboFormat(QOpenGLContext *ctx, int glFormat)
{
    if (ctx->format().majorVersion() < 3)
        return false;
#if !QT_CONFIG(opengles2)
    if (!ctx->isOpenGLES() && ctx->format().majorVersion() >= 4) {
        GLint value = -1;
        auto *vFuncs = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_4_2_Core>(ctx);
        if (vFuncs && vFuncs->initializeOpenGLFunctions()) {
            vFuncs->glGetInternalformativ(GL_TEXTURE_2D, glFormat, GL_FRAMEBUFFER_RENDERABLE, 1, &value);
            if (value != GL_FULL_SUPPORT)
                return false;
        }
    }
#else
    Q_UNUSED(glFormat);
#endif
    return true;
}

void tst_QOpenGL::fboRenderingRGB30()
{
#ifdef Q_OS_ANDROID
    if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31)
        QSKIP("Fails on Android 12 (QTBUG-105738)");
#endif
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    if (!supportsInternalFboFormat(&ctx, GL_RGB10_A2))
        QSKIP("An internal RGB30_A2 format is not guaranteed on this platform");

    // No multisample with combined depth/stencil attachment:
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    fboFormat.setInternalTextureFormat(GL_RGB10_A2);

    // Uncomplicate things by using POT:
    const QSize size(256, 128);
    QOpenGLFramebufferObject fbo(size, fboFormat);

    if (fbo.attachment() != QOpenGLFramebufferObject::CombinedDepthStencil)
        QSKIP("FBOs missing combined depth~stencil support");

    QVERIFY(fbo.bind());

    QPainter fboPainter;
    QOpenGLPaintDevice device(fbo.width(), fbo.height());
    bool painterBegun = fboPainter.begin(&device);
    QVERIFY(painterBegun);

    qt_opengl_draw_test_pattern(&fboPainter, fbo.width(), fbo.height());

    fboPainter.end();

    QImage fb = fbo.toImage();
    QCOMPARE(fb.format(), QImage::Format_A2BGR30_Premultiplied);
    QCOMPARE(fb.size(), size);

    qt_opengl_check_test_pattern(fb);

    // Check rendering can handle color values below 1/256.
    fboPainter.begin(&device);
    fboPainter.fillRect(QRect(0, 0, 256, 128), QColor::fromRgbF(1.0/512, 1.0/512, 1.0/512));
    fboPainter.end();
    fb = fbo.toImage();
    uint pixel = ((uint*)fb.bits())[0];
    QVERIFY((pixel & 0x3f) > 0);
    QVERIFY(((pixel >> 10) & 0x3f) > 0);
    QVERIFY(((pixel >> 20) & 0x3f) > 0);

    pixel = (3U << 30) | (2U << 20) | (2U << 10) | 2U;
    fb.fill(pixel);

    fboPainter.begin(&device);
    fboPainter.setCompositionMode(QPainter::CompositionMode_Source);
    fboPainter.drawImage(0, 0, fb);
    fboPainter.end();
    fb = fbo.toImage();
    pixel = ((uint*)fb.bits())[0];
    QVERIFY((pixel & 0x3f) > 0);
    QVERIFY(((pixel >> 10) & 0x3f) > 0);
    QVERIFY(((pixel >> 20) & 0x3f) > 0);
}

void tst_QOpenGL::fboRenderingRGB64_data()
{
    common_data();
}

void tst_QOpenGL::fboRenderingRGB64()
{
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    QFETCH(int, surfaceClass);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    if (!supportsInternalFboFormat(&ctx, GL_RGBA16))
        QSKIP("An internal RGBA16 format is not guaranteed on this platform");

    // No multisample with combined depth/stencil attachment:
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    fboFormat.setInternalTextureFormat(GL_RGBA16);

    // Uncomplicate things by using POT:
    const QSize size(256, 128);
    QOpenGLFramebufferObject fbo(size, fboFormat);

    if (fbo.attachment() != QOpenGLFramebufferObject::CombinedDepthStencil)
        QSKIP("FBOs missing combined depth~stencil support");

    QVERIFY(fbo.bind());

    QPainter fboPainter;
    QOpenGLPaintDevice device(fbo.width(), fbo.height());
    bool painterBegun = fboPainter.begin(&device);
    QVERIFY(painterBegun);

    qt_opengl_draw_test_pattern(&fboPainter, fbo.width(), fbo.height());

    fboPainter.end();

    QImage fb = fbo.toImage();
    QCOMPARE(fb.format(), QImage::Format_RGBA64_Premultiplied);
    QCOMPARE(fb.size(), size);

    qt_opengl_check_test_pattern(fb);

    // Check rendering can handle precise 16 bit color values.
    fboPainter.begin(&device);
    fboPainter.fillRect(QRect(0, 0, 256, 128), QColor::fromRgba64(5, 1002, 8001, 65535));
    fboPainter.end();
    fb = fbo.toImage();
    QRgba64 pixel = ((QRgba64*)fb.bits())[0];
    QCOMPARE(pixel.red(), 5);
    QCOMPARE(pixel.green(), 1002);
    QCOMPARE(pixel.blue(), 8001);
}

void tst_QOpenGL::fboHandleNulledAfterContextDestroyed()
{
    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setGeometry(0, 0, 10, 10);
    window.create();

    std::optional<QOpenGLFramebufferObject> fbo;

    {
        QOpenGLContext ctx;
        QVERIFY(ctx.create());

        ctx.makeCurrent(&window);

        if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
            QSKIP("QOpenGLFramebufferObject not supported on this platform");

        fbo.emplace(128, 128);

        QVERIFY(fbo->handle() != 0);
    }

    QVERIFY(fbo);
    QCOMPARE(fbo->handle(), 0U);
}

void tst_QOpenGL::fboMRT()
{
    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setGeometry(0, 0, 10, 10);
    window.create();

    QOpenGLContext ctx;
    QVERIFY(ctx.create());
    ctx.makeCurrent(&window);

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    if (!ctx.functions()->hasOpenGLFeature(QOpenGLFunctions::MultipleRenderTargets))
        QSKIP("Multiple render targets not supported on this platform");

    QOpenGLExtraFunctions *ef = ctx.extraFunctions();

    {
        // 3 color attachments, different sizes, same internal format, no depth/stencil.
        QList<QSize> sizes;
        sizes << QSize(128, 128) << QSize(192, 128) << QSize(432, 123);
        QOpenGLFramebufferObject fbo(sizes[0]);
        fbo.addColorAttachment(sizes[1]);
        fbo.addColorAttachment(sizes[2]);
        QVERIFY(fbo.bind());
        QCOMPARE(fbo.attachment(), QOpenGLFramebufferObject::NoAttachment);
        QCOMPARE(sizes, fbo.sizes());
        QCOMPARE(sizes[0], fbo.size());
        // Clear the three buffers to red, green and blue.
        GLenum drawBuf = GL_COLOR_ATTACHMENT0;
        ef->glDrawBuffers(1, &drawBuf);
        ef->glClearColor(1, 0, 0, 1);
        ef->glClear(GL_COLOR_BUFFER_BIT);
        drawBuf = GL_COLOR_ATTACHMENT0 + 1;
        ef->glDrawBuffers(1, &drawBuf);
        ef->glClearColor(0, 1, 0, 1);
        ef->glClear(GL_COLOR_BUFFER_BIT);
        drawBuf = GL_COLOR_ATTACHMENT0 + 2;
        ef->glDrawBuffers(1, &drawBuf);
        ef->glClearColor(0, 0, 1, 1);
        ef->glClear(GL_COLOR_BUFFER_BIT);
        // Verify, keeping in mind that only a 128x123 area is touched in the buffers.
        // Some drivers do not get this right, unfortunately, so do not rely on it.
        const char *vendor = (const char *) ef->glGetString(GL_VENDOR);
        bool hasCorrectMRT = false;
        if (vendor && strstr(vendor, "NVIDIA")) // maybe others too
            hasCorrectMRT = true;
        QImage img = fbo.toImage(false, 0);
        QCOMPARE(img.size(), sizes[0]);
        QCOMPARE(img.pixel(0, 0), qRgb(255, 0, 0));
        if (hasCorrectMRT)
            QCOMPARE(img.pixel(127, 122), qRgb(255, 0, 0));
        img = fbo.toImage(false, 1);
        QCOMPARE(img.size(), sizes[1]);
        QCOMPARE(img.pixel(0, 0), qRgb(0, 255, 0));
        if (hasCorrectMRT)
            QCOMPARE(img.pixel(127, 122), qRgb(0, 255, 0));
        img = fbo.toImage(false, 2);
        QCOMPARE(img.size(), sizes[2]);
        QCOMPARE(img.pixel(0, 0), qRgb(0, 0, 255));
        if (hasCorrectMRT)
            QCOMPARE(img.pixel(127, 122), qRgb(0, 0, 255));
        fbo.release();
    }

    {
        // 2 color attachments, same size, same internal format, depth/stencil.
        QList<QSize> sizes;
        sizes.fill(QSize(128, 128), 2);
        QOpenGLFramebufferObject fbo(sizes[0], QOpenGLFramebufferObject::CombinedDepthStencil);
        fbo.addColorAttachment(sizes[1]);
        QVERIFY(fbo.bind());
        QCOMPARE(fbo.attachment(), QOpenGLFramebufferObject::CombinedDepthStencil);
        QCOMPARE(sizes, fbo.sizes());
        QCOMPARE(sizes[0], fbo.size());
        ef->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        ef->glFinish();
        fbo.release();
    }
}

void tst_QOpenGL::fboMRT_differentFormats()
{
    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setGeometry(0, 0, 10, 10);
    window.create();

    QOpenGLContext ctx;
    QVERIFY(ctx.create());
    ctx.makeCurrent(&window);

    QOpenGLFunctions *f = ctx.functions();
    const char * vendor = (const char *) f->glGetString(GL_VENDOR);
    if (vendor && strstr(vendor, "VMware, Inc."))
        QSKIP("The tested formats may not be supported on this platform");

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    if (!f->hasOpenGLFeature(QOpenGLFunctions::MultipleRenderTargets))
        QSKIP("Multiple render targets not supported on this platform");

    if (!supportsInternalFboFormat(&ctx, GL_RGB10_A2))
        QSKIP("RGB10_A2 not supported on this platform");

    // 3 color attachments, same size, different internal format, depth/stencil.
    QList<QSize> sizes;
    sizes.fill(QSize(128, 128), 3);
    QOpenGLFramebufferObjectFormat format;
    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    QList<GLenum> internalFormats;
    internalFormats << GL_RGBA8 << GL_RGB10_A2 << GL_RGB5_A1;
    format.setInternalTextureFormat(internalFormats[0]);
    QOpenGLFramebufferObject fbo(sizes[0], format);
    fbo.addColorAttachment(sizes[1], internalFormats[1]);
    fbo.addColorAttachment(sizes[2], internalFormats[2]);

    QVERIFY(fbo.bind());
    QCOMPARE(fbo.attachment(), QOpenGLFramebufferObject::CombinedDepthStencil);
    QCOMPARE(sizes, fbo.sizes());
    QCOMPARE(sizes[0], fbo.size());

    QOpenGLExtraFunctions *ef = ctx.extraFunctions();
    QVERIFY(ef->glGetError() == 0);
    ef->glClearColor(1, 0, 0, 1);
    GLenum drawBuf[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT0 + 1, GL_COLOR_ATTACHMENT0 + 2 };
    ef->glDrawBuffers(3, drawBuf);
    ef->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    QVERIFY(ef->glGetError() == 0);

    QImage img = fbo.toImage(true, 0);
    QCOMPARE(img.size(), sizes[0]);
    QCOMPARE(img.pixel(0, 0), qRgb(255, 0, 0));
    img = fbo.toImage(true, 1);
    QCOMPARE(img.size(), sizes[1]);
    QCOMPARE(img.format(), QImage::Format_A2BGR30_Premultiplied);
    QCOMPARE(img.pixel(0, 0), qRgb(255, 0, 0));

    fbo.release();
}

void tst_QOpenGL::imageFormatPainting()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    const QSize size(128, 128);
    QOpenGLFramebufferObject fbo(size, fboFormat);

    if (fbo.attachment() != QOpenGLFramebufferObject::CombinedDepthStencil)
        QSKIP("FBOs missing combined depth~stencil support");

    QVERIFY(fbo.bind());

    QImage alpha(128, 128, QImage::Format_Alpha8);
    alpha.fill(127);

    QPainter fboPainter;
    QOpenGLPaintDevice device(fbo.width(), fbo.height());

    QVERIFY(fboPainter.begin(&device));
    fboPainter.fillRect(0, 0, 128, 128, qRgb(255, 0, 255));
    fboPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
    fboPainter.drawImage(0, 0, alpha);
    fboPainter.end();

    QImage fb = fbo.toImage();
    QCOMPARE(fb.pixel(0, 0), qRgba(127, 0, 127, 127));

    QImage grayscale(128, 128, QImage::Format_Grayscale8);
    grayscale.fill(128);

    QVERIFY(fboPainter.begin(&device));
    fboPainter.setCompositionMode(QPainter::CompositionMode_Plus);
    fboPainter.drawImage(0, 0, grayscale);
    fboPainter.end();

    fb = fbo.toImage();
    QCOMPARE(fb.pixel(0, 0), qRgb(255, 128, 255));

    QImage argb(128, 128, QImage::Format_ARGB32);
    argb.fill(qRgba(255, 255, 255, 128));

    QVERIFY(fboPainter.begin(&device));
    fboPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
    fboPainter.drawImage(0, 0, argb);
    fboPainter.end();

    fb = fbo.toImage();
    QCOMPARE(fb.pixel(0, 0), qRgb(255, 192, 255));

}

void tst_QOpenGL::openGLPaintDevice_data()
{
    QTest::addColumn<int>("surfaceClass");
    QTest::addColumn<QImage::Format>("imageFormat");

    QTest::newRow("Using QWindow - RGB32") << int(QSurface::Window) << QImage::Format_RGB32;
    QTest::newRow("Using QOffscreenSurface - RGB32") << int(QSurface::Offscreen) << QImage::Format_RGB32;
    QTest::newRow("Using QOffscreenSurface - RGBx8888") << int(QSurface::Offscreen) << QImage::Format_RGBX8888;
    QTest::newRow("Using QOffscreenSurface - RGB888") << int(QSurface::Offscreen) << QImage::Format_RGB888;
    QTest::newRow("Using QOffscreenSurface - RGB16") << int(QSurface::Offscreen) << QImage::Format_RGB16;
}

static void drawColoredRects(QPainter *p, const QSize &size)
{
    p->fillRect(0, 0, size.width() / 2, size.height() / 2, Qt::red);
    p->fillRect(size.width() / 2, 0, size.width() / 2, size.height() / 2, Qt::green);
    p->fillRect(size.width() / 2, size.height() / 2, size.width() / 2, size.height() / 2, Qt::blue);
    p->fillRect(0, size.height() / 2, size.width() / 2, size.height() / 2, Qt::white);
}

void tst_QOpenGL::openGLPaintDevice()
{
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    QFETCH(int, surfaceClass);
    QFETCH(QImage::Format, imageFormat);
    QScopedPointer<QSurface> surface(createSurface(surfaceClass));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QSurfaceFormat format = ctx.format();
    if (format.majorVersion() < 2)
        QSKIP("This test requires at least OpenGL 2.0");
    QVERIFY(ctx.makeCurrent(surface.data()));

    const QSize size(128, 128);

    QImage image(size, imageFormat);
    QPainter p(&image);
    drawColoredRects(&p, image.size());
    p.end();

    QOpenGLFramebufferObject fbo(size);
    QVERIFY(fbo.bind());

    QOpenGLPaintDevice device(size);
    QVERIFY(p.begin(&device));
    drawColoredRects(&p, image.size());
    p.end();

    QImage actual = fbo.toImage().convertToFormat(imageFormat);
    QCOMPARE(image.size(), actual.size());
    QCOMPARE(image, actual);

    QVERIFY(p.begin(&device));
    p.fillRect(0, 0, image.width(), image.height(), Qt::black);
    p.drawImage(0, 0, image);
    p.end();

    actual = fbo.toImage().convertToFormat(imageFormat);
    QCOMPARE(image.size(), actual.size());
    QCOMPARE(image, actual);

    QVERIFY(p.begin(&device));
    p.fillRect(0, 0, image.width(), image.height(), Qt::black);
    p.fillRect(0, 0, image.width(), image.height(), QBrush(image));
    p.end();

    actual = fbo.toImage().convertToFormat(imageFormat);
    QCOMPARE(image.size(), actual.size());
    QCOMPARE(image, actual);
}

void tst_QOpenGL::openGLPaintDeviceWithChangingContext()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    const QSize size(512, 512);

    // QOpenGLPaintDevice has a thread-local paint engine. Therefore render
    // twice, with a different context and device. Under the hood it will
    // still use the same paint engine!

    QOpenGLContext ctx;
    QVERIFY(ctx.create());
    QVERIFY(ctx.makeCurrent(surface.data()));

    QOpenGLFramebufferObject fbo(size);
    QVERIFY(fbo.bind());

    QOpenGLPaintDevice device(size);

    QPainter p;
    QVERIFY(p.begin(&device));
    drawColoredRects(&p, size);
    p.end();

    QImage img1 = fbo.toImage();

    QOpenGLContext ctx2;
    // When supported, test the special case, where the second context is
    // totally incompatible due to being a core profile one.
    QSurfaceFormat coreFormat;
    coreFormat.setVersion(3, 2);
    coreFormat.setProfile(QSurfaceFormat::CoreProfile);
    ctx2.setFormat(coreFormat);
    if (!ctx2.create() || !ctx2.makeCurrent(surface.data())) {
        ctx2.setFormat(QSurfaceFormat());
        QVERIFY(ctx2.create());
    }

    QVERIFY(ctx2.makeCurrent(surface.data()));

    QOpenGLFramebufferObject fbo2(size);
    QVERIFY(fbo2.bind());

    QOpenGLPaintDevice device2(size);

    QVERIFY(p.begin(&device2));
    drawColoredRects(&p, size);
    p.end();

    QImage img2 = fbo2.toImage();

    QFUZZY_COMPARE_IMAGES(img1, img2);
}

void tst_QOpenGL::aboutToBeDestroyed()
{
    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setGeometry(0, 0, 128, 128);
    window.create();

    QOpenGLContext *context = new QOpenGLContext;
    QSignalSpy spy(context, SIGNAL(aboutToBeDestroyed()));

    QVERIFY(context->create());
    QVERIFY(context->makeCurrent(&window));

    QCOMPARE(spy.size(), 0);

    delete context;

    QCOMPARE(spy.size(), 1);
}

// Verify that QOpenGLContext works with QWindows that do
// not have an explicit size set.
void tst_QOpenGL::sizeLessWindow()
{
    // top-level window
    {
        QWindow window;
        window.setSurfaceType(QWindow::OpenGLSurface);

        QOpenGLContext context;
        QVERIFY(context.create());

        window.show();
        QVERIFY(context.makeCurrent(&window));
        QVERIFY(QOpenGLContext::currentContext());
    }

    QVERIFY(!QOpenGLContext::currentContext());

    // child window
    {
        QWindow parent;
        parent.setSurfaceType(QWindow::OpenGLSurface);
        QWindow window(&parent);
        window.setSurfaceType(QWindow::OpenGLSurface);

        QOpenGLContext context;
        QVERIFY(context.create());

        parent.show();
        window.show();
        QVERIFY(context.makeCurrent(&window));
        QVERIFY(QOpenGLContext::currentContext());
    }

    QVERIFY(!QOpenGLContext::currentContext());
}

void tst_QOpenGL::QTBUG15621_triangulatingStrokerDivZero()
{
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    const QSize size(128, 128);

    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setGeometry(QRect(QPoint(0, 0), size));
    window.create();

    QOpenGLContext ctx;
    QVERIFY(ctx.create());
    QVERIFY(ctx.makeCurrent(&window));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    QOpenGLFramebufferObject fbo(size);
    QVERIFY(fbo.bind());

    QOpenGLPaintDevice device(size);

    // QTBUG-15621 is only a problem when qreal is double, but do the test anyway.
    qreal delta = sizeof(qreal) == sizeof(float) ? 1e-4 : 1e-8;
    QVERIFY(128 != 128 + delta);

    QPainterPath path;
    path.moveTo(16 + delta, 16);
    path.moveTo(16, 16);

    path.lineTo(16 + delta, 16); // Short lines to check for division by zero.
    path.lineTo(112 - delta, 16);
    path.lineTo(112, 16);

    path.quadTo(112, 16, 112, 16 + delta);
    path.quadTo(112, 64, 112, 112 - delta);
    path.quadTo(112, 112, 112, 112);

    path.cubicTo(112, 112, 112, 112, 112 - delta, 112);
    path.cubicTo(80, 112, 48, 112, 16 + delta, 112);
    path.cubicTo(16 + delta, 112, 16 + delta, 112, 16, 112);

    path.closeSubpath();

    QPen pen(Qt::red, 28, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin);

    QPainter p(&device);
    p.fillRect(QRect(QPoint(0, 0), size), Qt::blue);
    p.strokePath(path, pen);
    p.end();
    const QImage image = fbo.toImage().convertToFormat(QImage::Format_RGB32);
    QCOMPARE(image.size(), size);

    const QRgb red = 0xffff0000;
    const QRgb blue = 0xff0000ff;

    QCOMPARE(image.pixel(8, 8), red);
    QCOMPARE(image.pixel(119, 8), red);
    QCOMPARE(image.pixel(8, 119), red);
    QCOMPARE(image.pixel(119, 119), red);

    QCOMPARE(image.pixel(0, 0), blue);
    QCOMPARE(image.pixel(127, 0), blue);
    QCOMPARE(image.pixel(0, 127), blue);
    QCOMPARE(image.pixel(127, 127), blue);

    QCOMPARE(image.pixel(32, 32), blue);
    QCOMPARE(image.pixel(95, 32), blue);
    QCOMPARE(image.pixel(32, 95), blue);
    QCOMPARE(image.pixel(95, 95), blue);
}

typedef QGenericMatrix<1, 3, float> TestVertex3D;
static const float uv_top_left[] = {0.f, 1.f, 1.f};
static const float uv_bottom_left[] = {0.f, 0.f, 1.f};
static const float uv_top_right[] = {1.f, 1.f, 1.f};
static const float uv_bottom_right[] = {1.f, 0.f, 1.f};

bool q_fuzzy_compare(const TestVertex3D &left, const TestVertex3D &right) {
    return qFuzzyCompare(left(0,0), right(0,0)) &&
           qFuzzyCompare(left(1,0), right(1,0)) &&
           qFuzzyCompare(left(2,0), right(2,0));
}

void tst_QOpenGL::textureblitterFullSourceRectTransform()
{
    TestVertex3D topLeft(uv_top_left);
    TestVertex3D bottomLeft(uv_bottom_left);
    TestVertex3D topRight(uv_top_right);
    TestVertex3D bottomRight(uv_bottom_right);

    QRectF rect(0,0,1,1);
    QMatrix3x3 flippedMatrix = QOpenGLTextureBlitter::sourceTransform(rect, rect.size().toSize(), QOpenGLTextureBlitter::OriginTopLeft);

    TestVertex3D flippedTopLeft = flippedMatrix * topLeft;
    QCOMPARE(flippedTopLeft, bottomLeft);

    TestVertex3D flippedBottomLeft = flippedMatrix * bottomLeft;
    QCOMPARE(flippedBottomLeft, topLeft);

    TestVertex3D flippedTopRight = flippedMatrix * topRight;
    QCOMPARE(flippedTopRight, bottomRight);

    TestVertex3D flippedBottomRight = flippedMatrix * bottomRight;
    QCOMPARE(flippedBottomRight, topRight);

    QMatrix3x3 identityMatrix = QOpenGLTextureBlitter::sourceTransform(rect, rect.size().toSize(), QOpenGLTextureBlitter::OriginBottomLeft);

    TestVertex3D notFlippedTopLeft = identityMatrix * topLeft;
    QCOMPARE(notFlippedTopLeft, topLeft);

    TestVertex3D notFlippedBottomLeft = identityMatrix * bottomLeft;
    QCOMPARE(notFlippedBottomLeft, bottomLeft);

    TestVertex3D notFlippedTopRight = identityMatrix * topRight;
    QCOMPARE(notFlippedTopRight, topRight);

    TestVertex3D notFlippedBottomRight = identityMatrix * bottomRight;
    QCOMPARE(notFlippedBottomRight, bottomRight);
}

void tst_QOpenGL::textureblitterPartOriginBottomLeftSourceRectTransform()
{
    TestVertex3D topLeft(uv_top_left);
    TestVertex3D bottomLeft(uv_bottom_left);
    TestVertex3D topRight(uv_top_right);
    TestVertex3D bottomRight(uv_bottom_right);

    QRectF sourceRect(50,200,200,200);
    QSize textureSize(400,400);

    QMatrix3x3 sourceMatrix = QOpenGLTextureBlitter::sourceTransform(sourceRect, textureSize, QOpenGLTextureBlitter::OriginBottomLeft);

    const float x_point_ratio = sourceRect.topLeft().x() / textureSize.width();
    const float y_point_ratio = sourceRect.topLeft().y() / textureSize.height();
    const float width_ratio = sourceRect.width() / textureSize.width();
    const float height_ratio = sourceRect.height() / textureSize.height();

    TestVertex3D uvTopLeft = sourceMatrix * topLeft;
    const float expected_top_left[] = { x_point_ratio, y_point_ratio + height_ratio, 1 };
    TestVertex3D expectedTopLeft(expected_top_left);
    QCOMPARE(uvTopLeft, expectedTopLeft);

    TestVertex3D uvBottomLeft = sourceMatrix * bottomLeft;
    const float expected_bottom_left[] = { x_point_ratio, y_point_ratio, 1 };
    TestVertex3D expectedBottomLeft(expected_bottom_left);
    QCOMPARE(uvBottomLeft, expectedBottomLeft);

    TestVertex3D uvTopRight = sourceMatrix * topRight;
    const float expected_top_right[] = { x_point_ratio + width_ratio, y_point_ratio + height_ratio, 1 };
    TestVertex3D expectedTopRight(expected_top_right);
    QCOMPARE(uvTopRight, expectedTopRight);

    TestVertex3D uvBottomRight = sourceMatrix * bottomRight;
    const float expected_bottom_right[] = { x_point_ratio + width_ratio, y_point_ratio, 1 };
    TestVertex3D expectedBottomRight(expected_bottom_right);
    QCOMPARE(uvBottomRight, expectedBottomRight);
}

void tst_QOpenGL::textureblitterPartOriginTopLeftSourceRectTransform()
{
    TestVertex3D topLeft(uv_top_left);
    TestVertex3D bottomLeft(uv_bottom_left);
    TestVertex3D topRight(uv_top_right);
    TestVertex3D bottomRight(uv_bottom_right);

    QRectF sourceRect(50,190,170,170);
    QSize textureSize(400,400);

    QMatrix3x3 sourceMatrix = QOpenGLTextureBlitter::sourceTransform(sourceRect, textureSize, QOpenGLTextureBlitter::OriginTopLeft);

    const float x_point_ratio = sourceRect.topLeft().x() / textureSize.width();
    const float y_point_ratio = sourceRect.topLeft().y() / textureSize.height();
    const float width_ratio = sourceRect.width() / textureSize.width();
    const float height_ratio = sourceRect.height() / textureSize.height();

    TestVertex3D uvTopLeft = sourceMatrix * topLeft;
    const float expected_top_left[] = { x_point_ratio, 1 - y_point_ratio - height_ratio, 1 };
    TestVertex3D expectedTopLeft(expected_top_left);
    QVERIFY(q_fuzzy_compare(uvTopLeft, expectedTopLeft));

    TestVertex3D uvBottomLeft = sourceMatrix * bottomLeft;
    const float expected_bottom_left[] = { x_point_ratio, 1 - y_point_ratio, 1 };
    TestVertex3D expectedBottomLeft(expected_bottom_left);
    QVERIFY(q_fuzzy_compare(uvBottomLeft, expectedBottomLeft));

    TestVertex3D uvTopRight = sourceMatrix * topRight;
    const float expected_top_right[] = { x_point_ratio + width_ratio, 1 - y_point_ratio - height_ratio, 1 };
    TestVertex3D expectedTopRight(expected_top_right);
    QVERIFY(q_fuzzy_compare(uvTopRight, expectedTopRight));

    TestVertex3D uvBottomRight = sourceMatrix * bottomRight;
    const float expected_bottom_right[] = { x_point_ratio + width_ratio, 1 - y_point_ratio, 1 };
    TestVertex3D expectedBottomRight(expected_bottom_right);
    QVERIFY(q_fuzzy_compare(uvBottomRight, expectedBottomRight));
}

void tst_QOpenGL::textureblitterFullTargetRectTransform()
{
    QVector4D topLeft(-1.f, 1.f, 0.f, 1.f);
    QVector4D bottomLeft(-1.f, -1.f, 0.f, 1.f);
    QVector4D topRight(1.f, 1.f, 0.f, 1.f);
    QVector4D bottomRight(1.f, -1.f, 0.f, 1.f);

    QRectF rect(0,0,200,200);
    QMatrix4x4 targetMatrix = QOpenGLTextureBlitter::targetTransform(rect,rect.toRect());

    QVector4D translatedTopLeft = targetMatrix * topLeft;
    QCOMPARE(translatedTopLeft, topLeft);

    QVector4D translatedBottomLeft = targetMatrix * bottomLeft;
    QCOMPARE(translatedBottomLeft, bottomLeft);

    QVector4D translatedTopRight = targetMatrix * topRight;
    QCOMPARE(translatedTopRight, topRight);

    QVector4D translatedBottomRight = targetMatrix * bottomRight;
    QCOMPARE(translatedBottomRight, bottomRight);
}

void tst_QOpenGL::textureblitterPartTargetRectTransform()
{
    QVector4D topLeft(-1.f, 1.f, 0.f, 1.f);
    QVector4D bottomLeft(-1.f, -1.f, 0.f, 1.f);
    QVector4D topRight(1.f, 1.f, 0.f, 1.f);
    QVector4D bottomRight(1.f, -1.f, 0.f, 1.f);

    QRectF targetRect(50,50,200,200);
    QRect viewport(0,0,400,400);

    //multiply by 2 since coordinate system goes from -1 -> 1;
    qreal x_point_ratio = (50. / 400.) * 2;
    qreal y_point_ratio = (50. / 400.) * 2;
    qreal width_ratio = (200. / 400.) * 2;
    qreal height_ratio = (200. / 400.) * 2;

    QMatrix4x4 targetMatrix = QOpenGLTextureBlitter::targetTransform(targetRect, viewport);

    QVector4D targetTopLeft = targetMatrix * topLeft;
    QVector4D expectedTopLeft(-1 + x_point_ratio, 1 - y_point_ratio, .0, 1.0);
    QCOMPARE(targetTopLeft, expectedTopLeft);

    QVector4D targetBottomLeft = targetMatrix * bottomLeft;
    QVector4D expectedBottomLeft(-1 + x_point_ratio, 1 - y_point_ratio  - height_ratio, 0.0, 1.0);
    QCOMPARE(targetBottomLeft, expectedBottomLeft);

    QVector4D targetTopRight = targetMatrix * topRight;
    QVector4D expectedTopRight(-1 + x_point_ratio + width_ratio, 1 - y_point_ratio, 0.0, 1.0);
    QCOMPARE(targetTopRight, expectedTopRight);

    QVector4D targetBottomRight = targetMatrix * bottomRight;
    QVector4D expectedBottomRight(-1 + x_point_ratio + width_ratio, 1 - y_point_ratio - height_ratio, 0.0, 1.0);
    QCOMPARE(targetBottomRight, expectedBottomRight);
}

void tst_QOpenGL::defaultSurfaceFormat()
{
    QSurfaceFormat fmt;
    QCOMPARE(QSurfaceFormat::defaultFormat(), fmt);

    fmt.setDepthBufferSize(16);
    QSurfaceFormat::setDefaultFormat(fmt);
    QCOMPARE(QSurfaceFormat::defaultFormat(), fmt);
    QCOMPARE(QSurfaceFormat::defaultFormat().depthBufferSize(), 16);

    QScopedPointer<QWindow> window(new QWindow);
    QCOMPARE(window->requestedFormat(), fmt);

    QScopedPointer<QOpenGLContext> context(new QOpenGLContext);
    QCOMPARE(context->format(), fmt);
}

using namespace QNativeInterface;

#ifdef USE_GLX
void tst_QOpenGL::glxContextWrap()
{
    if (QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive))
        QSKIP("Offscreen: This fails.");

    if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive))
        QSKIP("Fails on Wayland.");

    QWindow *window = new QWindow;
    window->setSurfaceType(QWindow::OpenGLSurface);
    window->setGeometry(0, 0, 10, 10);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window));

    QPlatformNativeInterface *nativeIf = QGuiApplicationPrivate::instance()->platformIntegration()->nativeInterface();
    QVERIFY(nativeIf);

    // Fetch a GLXContext.
    QOpenGLContext *ctx0 = new QOpenGLContext;
    ctx0->setFormat(window->format());
    QVERIFY(ctx0->create());
    auto *glxContextIf = ctx0->nativeInterface<QGLXContext>();
    QVERIFY(glxContextIf);
    GLXContext context = glxContextIf->nativeContext();
    QVERIFY(context);

    // Then create another QOpenGLContext wrapping it.
    QOpenGLContext *ctx = QGLXContext::fromNative(context);
    QVERIFY(ctx);
    QVERIFY(nativeIf->nativeResourceForContext(QByteArrayLiteral("glxcontext"), ctx) == (void *) context);

    QVERIFY(ctx->makeCurrent(window));
    ctx->doneCurrent();

    delete ctx;
    delete ctx0;

    delete window;
}
#endif // USE_GLX

#if defined(Q_OS_WIN32) && !QT_CONFIG(opengles2)
void tst_QOpenGL::wglContextWrap()
{
    QScopedPointer<QOpenGLContext> ctx(new QOpenGLContext);
    QVERIFY(ctx->create());
    if (ctx->isOpenGLES())
        QSKIP("Not applicable to EGL");

    QScopedPointer<QWindow> window(new QWindow);
    window->setSurfaceType(QWindow::OpenGLSurface);
    window->setGeometry(0, 0, 256, 256);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    auto *wglContext = ctx->nativeInterface<QWGLContext>();
    QVERIFY(wglContext);
    QVERIFY(wglContext->nativeContext());

    // Now do a makeCurrent() do make sure the pixel format on the native
    // window (the HWND we are going to retrieve below) is set.
    QVERIFY(ctx->makeCurrent(window.data()));
    ctx->doneCurrent();

    HWND wnd = (HWND) qGuiApp->platformNativeInterface()->nativeResourceForWindow(QByteArrayLiteral("handle"), window.data());
    QVERIFY(wnd);

    QScopedPointer<QOpenGLContext> adopted(QWGLContext::fromNative(wglContext->nativeContext(), wnd));
    QVERIFY(!adopted.isNull());
    QVERIFY(adopted->isValid());

    // This tests two things: that a regular, non-adopted QOpenGLContext is
    // able to return a QSurfaceFormat containing the real values after
    // create(), and that the adopted context got the correct pixel format from
    // window and was able to update its format accordingly.
    QCOMPARE(adopted->format().version(), ctx->format().version());
    QCOMPARE(adopted->format().profile(), ctx->format().profile());
    QVERIFY(ctx->format().redBufferSize() > 0);
    QCOMPARE(adopted->format().redBufferSize(), ctx->format().redBufferSize());
    QVERIFY(ctx->format().greenBufferSize() > 0);
    QCOMPARE(adopted->format().greenBufferSize(), ctx->format().greenBufferSize());
    QVERIFY(ctx->format().blueBufferSize() > 0);
    QCOMPARE(adopted->format().blueBufferSize(), ctx->format().blueBufferSize());
    QVERIFY(ctx->format().depthBufferSize() > 0);
    QCOMPARE(adopted->format().depthBufferSize(), ctx->format().depthBufferSize());
    QVERIFY(ctx->format().stencilBufferSize() > 0);
    QCOMPARE(adopted->format().stencilBufferSize(), ctx->format().stencilBufferSize());

    // This must work since we are using the exact same window.
    QVERIFY(adopted->makeCurrent(window.data()));
    adopted->doneCurrent();
}
#endif // Q_OS_WIN32 && !QT_CONFIG(opengles2)

void tst_QOpenGL::vaoCreate()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    QOpenGLContext ctx;
    ctx.create();
    ctx.makeCurrent(surface.data());

    QOpenGLVertexArrayObject vao;
    bool success = vao.create();
    if (ctx.isOpenGLES()) {
        if (ctx.format().majorVersion() >= 3 || ctx.hasExtension(QByteArrayLiteral("GL_OES_vertex_array_object")))
            QVERIFY(success);
    } else {
        if (ctx.format().majorVersion() >= 3 || ctx.hasExtension(QByteArrayLiteral("GL_ARB_vertex_array_object")))
            QVERIFY(success);
    }

    vao.destroy();
    ctx.doneCurrent();
}

void tst_QOpenGL::bufferCreate()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    QOpenGLContext ctx;
    ctx.create();
    ctx.makeCurrent(surface.data());

    QOpenGLBuffer buf;

    QVERIFY(!buf.isCreated());

    QVERIFY(buf.create());
    QVERIFY(buf.isCreated());

    QCOMPARE(buf.type(), QOpenGLBuffer::VertexBuffer);

    buf.bind();
    buf.allocate(128);
    QCOMPARE(buf.size(), 128);

    {
        QOpenGLBuffer moved = std::move(buf);
        QCOMPARE_EQ(moved.isCreated(), true);
        QCOMPARE_EQ(moved.size(), 128);
        buf = std::move(moved);
    }

    buf.release();

    buf.destroy();
    QVERIFY(!buf.isCreated());

    ctx.doneCurrent();
}

void tst_QOpenGL::bufferMapRange()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    QOpenGLContext ctx;
    ctx.create();
    ctx.makeCurrent(surface.data());

    QOpenGLExtensions funcs(&ctx);
    if (!funcs.hasOpenGLExtension(QOpenGLExtensions::MapBufferRange))
        QSKIP("glMapBufferRange not supported");

    QOpenGLBuffer buf;
    QVERIFY(buf.create());
    buf.bind();
    const char data[] = "some data";
    buf.allocate(data, sizeof(data));

    char *p = (char *) buf.mapRange(0, sizeof(data), QOpenGLBuffer::RangeRead | QOpenGLBuffer::RangeWrite);
    QVERIFY(p);
    QVERIFY(!strcmp(p, data));
    p[1] = 'O';
    buf.unmap();

    p = (char *) buf.mapRange(1, 2, QOpenGLBuffer::RangeWrite);
    QVERIFY(p);
    p[1] = 'M';
    buf.unmap();

    p = (char *) buf.mapRange(0, sizeof(data), QOpenGLBuffer::RangeRead);
    QVERIFY(!strcmp(p, "sOMe data"));
    buf.unmap();

    buf.destroy();
    ctx.doneCurrent();
}

void tst_QOpenGL::defaultQGLCurrentBuffer()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    QScopedPointer<QOpenGLContext> ctx(new QOpenGLContext);
    ctx->create();
    ctx->makeCurrent(surface.data());

    // Bind default FBO on the current context, and record what's the current QGL FBO. It should
    // be nullptr because the default platform OpenGL FBO is not backed by a
    // QOpenGLFramebufferObject.
    QOpenGLFramebufferObject::bindDefault();
    QOpenGLFramebufferObject *defaultQFBO = QOpenGLContextPrivate::get(ctx.data())->qgl_current_fbo;

    // Create new FBO, bind it, and see that the QGL FBO points to the newly created FBO.
    QScopedPointer<QOpenGLFramebufferObject> obj(new QOpenGLFramebufferObject(128, 128));
    obj->bind();
    QOpenGLFramebufferObject *customQFBO = QOpenGLContextPrivate::get(ctx.data())->qgl_current_fbo;
    QVERIFY(defaultQFBO != customQFBO);

    // Bind the default FBO, and check that the QGL FBO points to the original FBO object.
    QOpenGLFramebufferObject::bindDefault();
    QOpenGLFramebufferObject *finalQFBO = QOpenGLContextPrivate::get(ctx.data())->qgl_current_fbo;
    QCOMPARE(defaultQFBO, finalQFBO);

    ctx->doneCurrent();
}

void tst_QOpenGL::nullTextureInitializtion()
{
    QScopedPointer<QSurface> surface(createSurface(QSurface::Window));
    QOpenGLContext ctx;
    ctx.create();
    ctx.makeCurrent(surface.data());

    QImage i;
    QOpenGLTexture t(i);
    QVERIFY(!t.isCreated());
}

/*
    Verify that the clipping works correctly.
    Just like fillRect, cliprect should snap rightwards and downwards in case of .5 coordinates.
    The red outline should be covered by the blue rect on top,
    while it should be clipped on the other edges and thus the red outline be visible

    See: QTBUG-83229, modified by QTBUG-100329
*/
void tst_QOpenGL::clipRect()
{
#if defined(Q_OS_LINUX) && defined(Q_CC_GNU) && !defined(__x86_64__)
    QSKIP("QTBUG-22617");
#endif

    QScopedPointer<QSurface> surface(createSurface(int(QSurface::Window)));

    QOpenGLContext ctx;
    QVERIFY(ctx.create());

    QVERIFY(ctx.makeCurrent(surface.data()));

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    // No multisample with combined depth/stencil attachment:
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    // Uncomplicate things by using POT:
    const QSize size(654, 480);
    const QRect rect(QPoint(0, 0), size);
    QOpenGLFramebufferObject fbo(size, fboFormat);

    if (fbo.attachment() != QOpenGLFramebufferObject::CombinedDepthStencil)
        QSKIP("FBOs missing combined depth~stencil support");

    QVERIFY(fbo.bind());

    QPainter fboPainter;
    QOpenGLPaintDevice device(fbo.width(), fbo.height());
    bool painterBegun = fboPainter.begin(&device);
    QVERIFY(painterBegun);

    qreal halfWidth = size.width() / 2.0;
    qreal halfHeight = size.height() / 2.0;

    QRectF clipRect = QRectF(halfWidth - halfWidth / 2.0, halfHeight - halfHeight / 2.0,
                             halfWidth / 2.0, halfHeight / 2.0);

    fboPainter.fillRect(rect, Qt::white);
    fboPainter.setPen(Qt::red);
    fboPainter.drawRect(clipRect);

    fboPainter.setClipRect(clipRect, Qt::ReplaceClip);
    fboPainter.fillRect(rect, Qt::blue);

    fboPainter.end();

    const QImage fb = fbo.toImage().convertToFormat(QImage::Format_RGB32);
    QCOMPARE(fb.size(), size);

    QCOMPARE(fb.pixelColor(clipRect.left() + 1, clipRect.top()), QColor(Qt::blue));
    QCOMPARE(fb.pixelColor(clipRect.left(), clipRect.top() + 1), QColor(Qt::red));
    QCOMPARE(fb.pixelColor(clipRect.left() + 1, clipRect.bottom()), QColor(Qt::red));

    // Enable this once QTBUG-85286 is fixed
    //QCOMPARE(fb.pixelColor(clipRect.right(), clipRect.top() + 1), QColor(Qt::red));
}

#if QT_CONFIG(egl)
void tst_QOpenGL::dontCrashOnInvalidContextThreadTeardown()
{
    class Thread : public QThread
    {
        void run() override
        {
            auto context = std::make_unique<QOpenGLContext>();
            QVERIFY(context->create());
            QScopedPointer<QSurface> surface(createSurface(int(QSurface::Window)));
            QVERIFY(context->makeCurrent(surface.data()));
            auto eglContext = context->nativeInterface<QNativeInterface::QEGLContext>();
            if (!eglContext) {
                QSKIP("Need an egl context for this test");
            }
            eglContext->invalidateContext();
            context->doneCurrent();
        }
    };
    Thread thread;
    thread.start();
    thread.wait();
}
#endif

// QTBUG-143779: On macOS, when a window has been resized, QOpenGLContext::makeCurrent()
// does a lazy update of the context, which recreates the window's default FBO and any
// associcated structures. If client code has a different FBO bound at this point, the
// update logic fails to set up these internal structures, and we end up crashing when
// we later swap. This test checks that a makeCurrent after a resize doesn't crash, and
// that the user's FBO is still bound.
void tst_QOpenGL::makeCurrentAfterResizeWithNonDefaultFBOBound()
{
    if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive))
        QSKIP("Wayland: fails has no empty window is shown on Wayland.");

    class FBOWindow : public QWindow
    {
    public:
        FBOWindow() {
            QSurfaceFormat format;
            format.setSamples(4);

            setSurfaceType(QWindow::OpenGLSurface);
            setFormat(format);
            create();

            context.reset(new QOpenGLContext);
            context->setFormat(format);
            QVERIFY(context->create());
            QVERIFY(context->makeCurrent(this));
            fbo.reset(new QOpenGLFramebufferObject(64, 64));
            fbo->bind();
        }

        void exposeEvent(QExposeEvent *) override
        {
            if (!fbo)
                return; // macOS 26.0-26.4 might send expose during makeCurrent

            ++paintCount;

            // If FBO was bound in a previous frame we shouldn't crash
            QVERIFY(context->makeCurrent(this));

            // makeCurrent shouldn't affect the bound FBO
            QVERIFY(fbo->isBound());

            // Temporarily bind the default FBO, and render to it
            auto *f = context->functions();
            fbo->release();
            f->glClearColor(1.0, 0.0, 0.0, 1.0);
            f->glClear(GL_COLOR_BUFFER_BIT);
            context->swapBuffers(this);

            // Then rebind the non-default FBO bound for next frame
            fbo->bind();
        }

        QScopedPointer<QOpenGLContext> context;
        QScopedPointer<QOpenGLFramebufferObject> fbo;
        int paintCount = 0;
    };

    FBOWindow window;

    if (!QOpenGLFramebufferObject::hasOpenGLFramebufferObjects())
        QSKIP("QOpenGLFramebufferObject not supported on this platform");

    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    for (int i = 0; i < 10; ++i) {
        int paintCount = window.paintCount;
        window.resize(window.width() + 1, 600);
        QTRY_VERIFY(window.paintCount > paintCount);
    }
}

QTEST_MAIN(tst_QOpenGL)

#include "tst_qopengl.moc"
