Пользователь
0,0
рейтинг
9 января 2015 в 17:31

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

Недавний релиз 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 (Examples\Qt-5.4\quick\rendercontrol) — собственно по нему и проходили все разбирательства;


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


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 ( include\QtPlatformHeaders ), т.к. документация в этой части сильно не полна.

Далее можно использовать этот контекст (но при этом необходимо внимательно следить чтоб изменения состояния этого контекста не входили в конфликт с манипуляциями владельца), а можно сделать 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
Коментарии, вопросы, здоровая критика в комментариях — приветствуются.

Продолжение: Часть II: Загружаем QML, Часть III: Обработка пользовательского ввода
Радионов Сергей @RSATom
карма
15,0
рейтинг 0,0

Самое читаемое Разработка

Комментарии (12)

  • 0
    Поясните пожалуйста, чем обусловлена цифра 5 в renderTimer->setInterval( 5 );
    • 0
      Если быть честным — я не знаю точного ответа на данный вопрос. Число было взято из примера rendercontrol (window.cpp:100).
      Я предполагаю что при выборе данного интервала стоит ориентироваться на желатемый fps. Но лучше, как мне кажется, вовсе не использовать таймер, а привязываться вертикальной синхронизаци (VSync) экрана.
      • 0
        Желаемый fps для вас — 200?
        Да, вы правы, лучше не использовать таймер, хотелось бы и увидеть привязку к вертикальной синхронизации.
        • 0
          скорее не для меня а для автора rendercontrol :)
          • 0
            Скорее даже не понятно, почему сигналы не вызываются в момент когда действительно должна произойти отрисовка, и не пихают примитивы в контекст, а вместо этого нужно делать отдельный «пререндеринг» текстуры, которая в последствии будет отдана на отрисовку.
            • 0
              Я думаю что это может быть связано с тем фактом что при стандартном использовании QQuickWindow (т.е. без использования QQuickRenderControl) рендеринг осуществляется в отдельной нити (согласно документации и личного опыта).
              Кроме того, заранее сложно предположить как и где в дальнейшем пользователь будет использовать полученный FBO — как следствие необходимо предоставлять максимальные возможности, даже ценой усложнения использования.
              • 0
                Тут идет не только усложнение использования, но сильная потеря производительности. Я уже смотрел как происходит рендеринг при «стандартном использовании QQuickWindow», я просто не очень понял почему нельзя было сделать тот же механизм, со «сбросом» SGNode'ов, но в чужом контексте. Единственная адекватная причина — отсутствие «синхронных» нотификаций о входе в функцию рисования. Хотя с другой стороны не понятно как тогда работает сброс FBO. В общем надо смотреть имплементацию QQuickWindow и околостоящих классов при работе с «чужим» контекстом. Если у Вас будет желание и возможность, то было бы круто увидеть это в «Продолжение следует...» :)
                • 0
                  Я думаю что это просто напросто первая итерация реализации идеи, причем реализованная «на скорую руку» — Предполагаю что в одной из следующих версий мы увидим полностью переработанную концепцию — уж больно много «вкусностей» обещает данный подход.
                • +1
                  а по поводу «Продолжение следует...» — я бы рад, осталось только придумать как увеличить количество часов в сутках хотя бы до 26-ти ;)
            • 0
              Скажите, а как вы узнаете когда должна произойти отрисовка?
              • 0
                Чуть выше в ветке уже написал об этом, и тут же возникает вопрос, а как Qt сбрасывает FBO в таком случае, потому что на деле задача одна и та же. Хотя я могу ошибаться в чем-то. Надо бы смотреть код, чтобы разобраться.
  • 0
    За статью спасибо, в целом это выглядит очень печально. Появилось желание посмотреть как это работает внутри, потому что костыль(Со стороны Qt) какой-то уж больно топорный…

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.