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
Коментарі, питання, здорова критика в коментарях вітається.

далі буде...

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.