QQuickRenderControl, или как подружить QML с чужим OpenGL контекстом. Часть I

в 14:31, , рубрики: OpenGL, QML, qt, Qt Quick 2, qt5

Недавний релиз Qt 5.4, помимо прочего, предоставил в распоряжение разработчиков один, на мой взгляд, очень любопытный инструмент. А именно, разработчики Qt сделали QQuickRenderControl частью публичного API. Занятность данного класса заключается в том, что теперь появилась возможность использовать Qml в связке с любым другим фреймворком, если он предоставляет возможность получить (или задать) указатель на используемый OpenGL контекст.
С другой стороны, в процессе работы над одним из своих проектов, я столкнулся с необходимостью отрисовывать QML сцену на CALayer (Mac OS X), без малейшей возможности получить доступ к родительскому окну. Недельный поиск возможных вариантов решения проблемы показал, что самым адекватным решением будет как раз использование QQuickRenderControl из Qt 5.4, благодаря удачному совпадению, получившего статус релиза одновременно с возникновением вышеупомянутой задачи.
Изначально я предположил что задача плевая, и будет решена в течении пары вечеров, но как же я сильно заблуждался — задача заняла порядка полумесяца на исследования, и еще пол месяца на реализацию (которая все еще далека от идеала).

Несколько тезисов

  • QQuickRenderControl это всего навсего дополнительный интерфейс к реализации QQuickWindow для получения нотификаций об изменении QML сцены, а так же передачи команд в обратном направлении (т.е. фактически «костыль»);
  • Результат рендеринга будет получен в виде QOpenGLFramebufferObject (далее FBO), который в дальнейшем может быть использован в качестве текстуры;
  • Работать придется непосредственно с QuickWindow, соответственно сервис по загрузке QML предоставляемый QQuickView будет недоступен, и придется его реализовывать самостоятельно;
  • Поскольку никакого окна на самом деле не создается, возникает необходимость искуственно передавать события мыши и клавиатуры в QQuickWindow. Так же необходимо вручную управлять размером окна;
  • Пример использования QQuickRenderControl я сумел найти только один, в Qt 5.4 (ExamplesQt-5.4quickrendercontrol) — собственно по нему и проходили все разбирательства;

Что же нужно сделать для решения исходной задачи?

1) Реализовать настройку QQuickWindow для рендеринга в FBO и управления этим процессом через QQuickRenderControl;
2) Реализовать загрузку Qml и присоединение результата к QQuickWindow;
3) Реализовать передачу событий мыши и клавиатуры;
4) Отрисовать FBO (ради чего все и затевалось);

В данной статье я позволю себе остановится только на пункте 1), остальные пункты в последющих частях (если вы сочтете это интересным).

Настраиваем QQuickWindow

Внешний QOpenGLContext

Отправной точкой является OpenGL контекст в котором в конечном итоге и будет отрисовываться FBO. Но поскольку, с большой долей вероятности, работать необходимо с контекстом изначально не имеющим никакого отношения к Qt, то необходимо провести конвертацию контекста из формата операционной системы в экземпляр QOpenGLContext. Для этого необходимо использовать метод QOpenGLContext::​setNativeHandle.
Пример использования на основе NSOpenGLContext:

    NSOpenGLContext* nativeContext = [super openGLContextForPixelFormat: pixelFormat];

    QOpenGLContext* extContext = new QOpenGLContext;
    extContext->setNativeHandle( QVariant::fromValue( QCocoaNativeContext( nativeContext ) ) );
    extContext->create();

Список доступных Native Context лучше смотреть непосредственно в заголовочных файлах Qt ( includeQtPlatformHeaders ), т.к. документация в этой части сильно не полна.

Далее можно использовать этот контекст (но при этом необходимо внимательно следить чтоб изменения состояния этого контекста не входили в конфликт с манипуляциями владельца), а можно сделать shared контекст:

    QSurfaceFormat format;
    format.setDepthBufferSize( 16 );
    format.setStencilBufferSize( 8 );

    context = new QOpenGLContext;
    context->setFormat( format );
    context->setShareContext( extContext );
    context->create();

Важным ньюансом для использования OpenGL контекста с QML является наличие в нем настроенных Depth Buffer и Stencil Buffer, поэтому если у вас нет возможности влиять на параметры исходного контекста, нужно использовать shared контекст с установленными «Depth Buffer Size» и «Stencil Buffer Size».

Создание QQuickWindow

При создании QQuickWindow предварительно создается QQuickRenderControl и передается в конструктор:

    QQuickRenderControl* renderControl = new QQuickRenderControl();
    QQuickWindow* quickWindow = new QQuickWindow( renderControl );
    quickWindow->setGeometry( 0, 0, 640, 480 );

Кроме того важно задать размер окна, для дальнейшего успешного создания FBO.

Инициализация QQuickRenderControl и QOpenGLFramebufferObject

Перед вызовом QQuickRenderControl::initialize важно сделать контекст текущим, т.к. в процессе вызова будет сгенерирован сигнал sceneGraphInitialized, а это хорошая точка для создания FBO (который, в свою очередь, требует выставленного текущего контекста).

    QOpenGLFramebufferObject* fbo = nullptr;
    connect( quickWindow, &QQuickWindow::sceneGraphInitialized,
        [&] () {
            fbo = new QOpenGLFramebufferObject( quickWindow->size(), QOpenGLFramebufferObject::CombinedDepthStencil );
            quickWindow->setRenderTarget( fbo );
        }
    );

    offscreenSurface = new QOffscreenSurface();
    offscreenSurface->setFormat( context->format() );
    offscreenSurface->create();

    context->makeCurrent( offscreenSurface );
    renderControl->initialize( context );
    context->doneCurrent();
Рендеринг

Рендеринг необходимо осуществлять как реакцию на сигналы QQuickRenderControl::renderRequested и QQuickRenderControl::sceneChanged. Разница в этих двух случаях заключается в том что во втором случае необходимо дополнительно вызывать QQuickRenderControl::polishItems и QQuickRenderControl::sync. Второй важной особенностью является то что настойчиво не рекомендуется отсуществлять рендеринг непосредственно в обработчиках упомянутых выше сигналов. Поэтому используется таймер с небольшим интервалом. Ну и последней токостью является то, что, в случае использования shared OpenGL контекста, после рендеринга, требуется вызывать glFlush — в противном случае первичный контекст не видит изменений в FBO.

    bool* needSyncAndPolish = new bool;
    *needSyncAndPolish = true;
 
    QTimer* renderTimer = new QTimer;
    renderTimer->setSingleShot( true );
    renderTimer->setInterval( 5 );
    connect( renderTimer, &QTimer::timeout,
        [&] () {
            if( context->makeCurrent( offscreenSurface ) ) {
                if( *needPolishAndSync ) {
                    *needPolishAndSync = false;
                    renderControl->polishItems();
                    renderControl->sync();
                }
                renderControl->render();
                quickWindow->resetOpenGLState();
                context->functions()->glFlush();
                context->doneCurrent();
            }
    );
 
    connect( renderControl, &QQuickRenderControl::renderRequested,
        [&] () {
            if( !renderTimer->isActive() )
                renderTimer->start();
        }
    );

    connect( renderControl, &QQuickRenderControl::sceneChanged,
        [&] () {
            *needPolishAndSync = true;
            if( !renderTimer->isActive() )
                renderTimer->start();
        }
    );

Ну вот в общем то и все, первая часть задачи выполнена.

Класс реализующий вышеприведенную концепцию доступен на GitHub: FboQuickWindow.h, FboQuickWindow.cpp
Коментарии, вопросы, здоровая критика в комментариях приветствуется.

Продолжение следует...

Автор: RSATom

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js