QQuickRenderControl, or how to make friends QML with someone else's OpenGL context. Part I
- Tutorial
On the other hand, while working on one of my projects, I was faced with the need to draw a QML scene on CALayer (Mac OS X), without the slightest opportunity to access the parent window. A week-long search for possible solutions to the problem showed that the most appropriate solution would be to use QQuickRenderControl from Qt 5.4, due to a successful match, which received release status simultaneously with the occurrence of the aforementioned task.
Initially, I suggested that the task is trifling, and will be solved within a couple of evenings, but how much I was mistaken - the task took about a crescent for research, and another half a month for implementation (which is still far from ideal).
A few points
- QQuickRenderControl is just an additional interface to the QQuickWindow implementation for receiving notifications about changes to the QML scene, as well as sending commands in the opposite direction (that is, in fact, a “crutch”);
- The rendering result will be obtained in the form of QOpenGLFramebufferObject (hereinafter FBO) , which can later be used as a texture;
- You will have to work directly with QuickWindow, so the QML loading service provided by QQuickView will be unavailable, and you will have to implement it yourself;
- Since no window is actually created, it becomes necessary to artificially transfer mouse and keyboard events to QQuickWindow. It is also necessary to manually control the size of the window;
- I managed to find an example of using QQuickRenderControl only one, in Qt 5.4 (Examples \ Qt-5.4 \ quick \ rendercontrol) - actually all the proceedings went through it;
What needs to be done to solve the original problem?
1) Implement the QQuickWindow setting for rendering in FBO and controlling this process through QQuickRenderControl;
2) Implement loading Qml and attaching the result to QQuickWindow;
3) Implement the transfer of mouse and keyboard events;
4) Draw FBO (for the sake of which everything was started);
In this article I will allow myself to dwell only on paragraph 1), the remaining paragraphs in the following parts (if you find this interesting).
Customize QQuickWindow
External QOpenGLContext
The starting point is the OpenGL context in which the FBO will ultimately be drawn. But since, with a high degree of probability, it is necessary to work with a context that initially has nothing to do with Qt, it is necessary to convert the context from the format of the operating system to an instance of QOpenGLContext. To do this, use the QOpenGLContext :: setNativeHandle method .
NSOpenGLContext-based usage example:
NSOpenGLContext* nativeContext = [super openGLContextForPixelFormat: pixelFormat];
QOpenGLContext* extContext = new QOpenGLContext;
extContext->setNativeHandle( QVariant::fromValue( QCocoaNativeContext( nativeContext ) ) );
extContext->create();
The list of available Native Contexts is best viewed directly in the Qt header files (include \ QtPlatformHeaders), because the documentation in this part is not very complete.
Further, you can use this context (but at the same time, you need to carefully monitor that changes in the state of this context do not conflict with owner manipulations), but you can make a shared context:
QSurfaceFormat format;
format.setDepthBufferSize( 16 );
format.setStencilBufferSize( 8 );
context = new QOpenGLContext;
context->setFormat( format );
context->setShareContext( extContext );
context->create();
An important nuance for using OpenGL context with QML is the presence of configured Depth Buffer and Stencil Buffer in it, so if you cannot influence the parameters of the original context, you need to use the shared context with the set “Depth Buffer Size” and “Stencil Buffer Size”.
Creating QQuickWindow
When creating a QQuickWindow, a QQuickRenderControl is first created and passed to the constructor:
QQuickRenderControl* renderControl = new QQuickRenderControl();
QQuickWindow* quickWindow = new QQuickWindow( renderControl );
quickWindow->setGeometry( 0, 0, 640, 480 );
In addition, it is important to specify the size of the window, for the further successful creation of FBO.
Initializing QQuickRenderControl and QOpenGLFramebufferObject
Before calling QQuickRenderControl :: initialize, it is important to make the context current, as during the call, the sceneGraphInitialized signal will be generated, and this is a good point for creating an FBO (which, in turn, requires an exposed current context).
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();
Rendering
Rendering needs to be done as a reaction to the signals QQuickRenderControl :: renderRequested and QQuickRenderControl :: sceneChanged. The difference in these two cases is that in the second case, you must additionally call QQuickRenderControl :: polishItems and QQuickRenderControl :: sync. The second important feature is that it is strongly recommended not to render directly in the handlers of the signals mentioned above. Therefore, a timer with a small interval is used. Well, the last subtlety is that, in the case of using the shared OpenGL context, after rendering, you need to call glFlush - otherwise the primary context does not see changes in 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();
}
);
Well, that's basically it, the first part of the task is completed.
A class implementing the above concept is available on GitHub: FboQuickWindow.h , FboQuickWindow.cpp
Comments, questions, healthy criticism in the comments are welcome.
Continued: Part II: Downloading QML , Part III: Processing User Input