Making OpenGL and threading work nicely together: how to?
-
Hello!
I'm Arisotura, author of melonDS - a popular DS emulator. The desktop version of melonDS uses Qt for its user interface.
The main melonDS interface is a window that has a menubar and a ScreenPanel widget that receives the emulated DS's video output, handles the screen layout calculations, forwards touchscreen input to the emulator core, ... The ScreenPanel widget may be of two types: a native panel that uses Qt's API to draw the DS video output, or an OpenGL-enabled panel that uses OpenGL to do its drawing (and enables the emulator core to use OpenGL too).
However, OpenGL integration has always been an issue.
The main issue is that when creating a QOpenGLWidget, Qt controls the underlying OpenGL context and decides when to redraw stuff. However, in melonDS, the actual DS emulation runs on a separate thread (EmuThread). Since the emulator core includes an OpenGL-based renderer, we need to be able to use OpenGL in the EmuThread.
My initial solution to this problem was to create a separate OpenGL context that is shared with the GL widget's context, and move that context to the EmuThread. However, this solution has been retired, I think for performance reasons: while context sharing may be fine for doing some work on a separate thread (loading textures or whatever), it isn't really suited to actual rendering work, and results in poor performance.
Since then, a different solution has been adopted: a native child window is created inside the ScreenPanel widget -- platform-specific code is used to create that window and obtain an OpenGL context for it. The advantage is that we have full control of the context, we get to decide when and where to do rendering work. No need to rely on shared contexts.
However, it is apparent that it's causing issues on Wayland/KDE. Example: https://github.com/melonDS-emu/melonDS/issues/2470
The Qt documentation recommends using QWidget::createWindowContainer() with a QWindow. I'm considering this, but I think there's the same issue with controlling the OpenGL context - it seems that Qt will automatically acquire the context whenever it decides to redraw stuff, which isn't ideal. I don't expect shuffling an OpenGL context around between threads to be performant...
So, what would be the best course of action for this use case?
-
I'm not entirely sure what the best way is to approach this problem. However, it seems plausible that you need to use more lower-level (OS-specific) functions to get good performance.
Here is another idea (not sure if it is better): You could render offscreen into an image and let Qt just show that image. With the simplest approach you might get screen tearing (the render thread writing to the image while Qt is reading it). You have to implement double buffering for the offscreen image yourself. This also means that you need to introduce synchronization between the threads which could hurt performance. Also, double buffering introduces a short delay. Maybe you could at least try to circumvent double buffering in Qt as well (so that you don't have a double delay).
-
I second @SimonSchroeder comment about OSspecific functions. You probly should not use Qt for a presentation layer on a project that expects high refresh rates. Qt is more suited for classical productivityGUIs rather than gaming.