QQuickRenderControl, или как подружить QML с чужим OpenGL контекстом. Часть I
Недавний релиз 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Коментарии, вопросы, здоровая критика в комментариях приветствуется.
Продолжение следует…