Porting Krita's OpenGL canvas to Qt6
Open, Needs TriagePublic

Description

Problem

Qt6 is going to drop ANGLE which will require us to use OpenGL on Windows which is known to be buggy.

Options

There are three main things which we can do to port our canvas to OpenGL.

Window Container based approach

Krita being a QWidget based app we can use QWidget::createWindowContainer to embed a QWindow (our canvas) in our Main window. The way this would work is we set the desired Surface type (e.g OpenGLSurface, Direct3DSurface ...) for our QWindow and create our QRhi instance, which will create an instance that is platform specific. We can then use the abstraction to carry out our canvas rendering.

Demo Link: https://github.com/sh-zam/qt6-demo-app/tree/rhi (NOTE: This is on a different branch)
QRhi docs: https://alpqr.github.io/qtrhi/qrhi.html

Issues
  • QWindow is a heavy class, having bunch of them can be expensive.
  • There are several known limitations for using window containers (as per the docs), the one applicable to us could be:
    • Using window container in QMdiArea will make everything that is its child a native window. Which as per the docs can cause performance issues.
  • QRhi is still a private API, so using it this way is probably something which Qt devs won't recommend. That being said, there is an app which already uses the API, in the way described here.

Porting to QML

QML is already capable of rendering via different graphics APIs (OpenGL, Metal..) on a QQuickWindow. Porting to QML would open two ways to render to the window.

  1. Using custom implementation for each platform (Metal, Direct3D, OpenGL).
  1. Using QRhi based implementation.

Demo: This is a well established approach, there are several examples in the Qt's tree.

Issues

Porting to QML is a huge project. QRhi is still a private API, this is something to keep in mind. There is already a task for what it would take for QML port: https://phabricator.kde.org/T13339

Custom backend for QWidget

In theory it should be possible to render using native APIs if the Qt's paintEngine is turned off (QT::WA_PaintOnScreen). However, this seems to be only supported on X11 (as per the docs) and we would have to render on the screen directly.

Issues

I looked at this very briefly. Mostly because this would be too hacky and I am not sure what or how the other widgets (other than canvas, like dockers) would be rendered.

I came across this approach through this: https://github.com/giladreich/QtDirect3D

Related Objects

StatusAssignedTask
OpenNone
OpenNone
sh-zam created this task.Mar 1 2021, 8:05 PM
rempt added a subscriber: rempt.Mar 2 2021, 10:08 AM

Some questions/remarks:

  • QMdiArea: I am strongly in favour of replacing that with KDDockWidgets, which supports both widgets and Qt Quick, not just for dockers, but also for the MDI gui.
  • Using QRhi: does QRhi provide an abstraction for everything we need for our canvas code?
  • With "port to QML" -- can we just port the canvas to a QQuickWindow and use that inside an application that's still mostly QWidgets? That used to be possible with Qt5 at least. How would we implement our canvas on top of QQuickWinodw?
  • How will we port our canvas decorations, which are painted using QPainter using the OpengGL QPainter engine? This is important for all three options.
  • There's a fourth option, which is using Angle directly as a Krita dependency for our opengl code. This won't help on macOS.
ognarb added a subscriber: ognarb.Mar 2 2021, 9:46 PM
emmetoneill added a comment.EditedMar 3 2021, 5:30 AM

Thanks for looking into this issue, Sharaf. I also have a couple of questions:

  • Despite being a lot of work, porting to QML sounds like it could be the more standard and forward-thinking path to take. I haven't really researched it myself, but what's the risk that we take on a bunch of "technical debt" with the window container approach, only to find out later that the performance impact is too severe to continue? Is this something that we could test out before making a big commitment?
  • Both the window container and QML approaches rely on QRhi, but QRhi as a private API we're going into kind of unsupported territory, right? What's the significance of that for us? Also what's the best option for us if QRhi turns out to be a dead end? Would it be possible to make use of something like Vulkan + MoltenVK for Mac sort or like how we currently use OpenGL + ANGLE for Windows? I've heard that Vulkan has a bit of initial setup, but I'd imagine it'd be more efficient than maintaining 3 different rendering backends.

I've heard that Vulkan has a bit of initial setup

You end up having to basically write your own graphics driver. Godot has a 6k lines file for this, but admittedly, their requirements are a lot higher than ours.

Would it be possible to make use of something like Vulkan + [[ https://github.com/KhronosGroup/MoltenVK | MoltenVK for Mac]] sort or like how we currently use OpenGL + ANGLE for Windows? I've heard that Vulkan has a bit of initial setup, but I'd imagine it'd be more efficient than maintaining 3 different rendering backends.

I think using a higher level abstraction like wgpu, which is a cross-platform implementation of WebGPU (with an API similar to Metal) may make more sense than using Vulkan...

But given that the existing OpenGL code works fine, what about using MetalANGLE, which is a fork of ANGLE with slightly better support for macOS?

This still leaves the question of whether any of these can be easily incorporated with Qt6...

Window Container based approach

There will be one more problem of this approach. The outer window will still be rendered by Qt and will use opengl.dll. It will cause two troubles:

  1. On Windows, where openGL driver is broken, we might have black-screen problems or something like that
  2. On OSX, openGL driver will still block main GUI thread for the VSync (or whatever it waits on OSX), which will still cause "bended lines" problems. Though speaking truly, I'm not sure that other approaches will help us avoid that.

Technically, if we create a a native QWindow, then we are not obliged to use QRhi. We can use anything, Angle, Vulkan or MoltenVK. I have a feeling that Angle (or MetalAngle) would be the best approach.

Porting to QML

Well, we should be a bit more clear here: we should remove all usages of QWidget and (probably) QPainter-based painting on screen inside Krita. All the tools, widgets and dialogs should be ported to QML+QRhi.

If we keep at least one QWidget inside Krita (say hi to seexpr!) then openGL-based subsystem will still be engaged and we will end-up with "window container approach" again with multiple drivers/APIs used by the same app.

Custom backend for QWidget

I've seen this code inside Qt's openGL renderer code. Theoretically, it is possible to do (or rewrite in Qt). But that also leads to multiple APIs used by the same app, with the drawbacks of the first approach.

Actually, we have another approach, which might even be simpler than the others, though it would mean quite a lot of maintenance work...

"Fork"[0] Qt and return Angle into it

The main benefit of that, we can also start using Angle on OSX, which might solve some rendering issues on those devices. Theoretically, it can also fix the bended lines problem. Theoretically, we can also use Angle on Linux as well, just to make the backend API more homogeneous.

I vaguely remember the code that switched with openGL engine in Qt and it wasn't that much (actaully, two classes). The main problem here is that some QWidgets may accidentally use openGL features that are outside openGLES. That will be the main maintenance burden we'll have to do.

[0] - well, "fork" might sound as a bit clickbait tern in the current context, but we do already customize Qt's rendering code in few places and maintain about 70 patches for it.

Speaking truly, I don't see why we should even try to use QRhi unless we port entire Krita to QML...

rempt added a comment.Mar 6 2021, 10:02 AM

The point of QML is that we can use it to create a better tablet UI for Krita, so we have a good story on Android.

I know switching to QML is going to be a big task, but it does seem to be better maintained. If you ever listen to any talks with Qt developers, all they seem to talk about is QML. I am guessing that is where they are putting all their resources for improvements going forward.

For example on mobile, QRadioButton and QCheckbox do not have word wrap for the UI widgets...but do in QML. There has been an open request for years to add this, but I imagine the Qt company is focusing their energy on QML... https://bugreports.qt.io/browse/QTBUG-5370

This is one instance of why the UI is going to be less flexible if we stick to using ui widgets longterm. If we could slowly port things to QML that would be best...instead of having to do everything in one release. I know Qt modified their property syntax a bit with how QML is hooked up, so we might want to wait until we switch to Qt6 before we start moving UI files over to avoid duplicate work.

rempt added a comment.Mar 6 2021, 3:14 PM

yes, but like kdenlive, our gui will probably remain a mixture for quite time; the important thing is that that mixture must be possible while still allowing a native gpu accelerated canvas that isn't limited to a fixed 60 fps refreshrate.

I know switching to QML is going to be a big task, but it does seem to be better maintained.

Well, my main concern is the all or nothing approach. I cannot understand how we can switch to QML gradually. Basically, we need some kind of roadmap, where we port widgets into QMLs bu groups. E.g. in one release we port all the filters and generators GUI to QML. In the next release, we port all the menus and dialogs in menus. Then, in the next release we port dockers and tool options. Then port the canvas. And so on. And right now I cannot understand how to do that. QML is just too different beast, so you need to take all or nothing. That is what worries me the most.

rempt added a comment.Mar 6 2021, 6:28 PM

No, that's not the case. You can just convert one component to QML, and use that, right inside a QWidgets application, like we do with the touch docker. And we can just stick with the components where we get most out of the new capabilities of QML compared to QWidgets.

ognarb added a comment.Mar 6 2021, 7:09 PM

I know switching to QML is going to be a big task, but it does seem to be better maintained.

Well, my main concern is the all or nothing approach. I cannot understand how we can switch to QML gradually. Basically, we need some kind of roadmap, where we port widgets into QMLs bu groups. E.g. in one release we port all the filters and generators GUI to QML. In the next release, we port all the menus and dialogs in menus. Then, in the next release we port dockers and tool options. Then port the canvas. And so on. And right now I cannot understand how to do that. QML is just too different beast, so you need to take all or nothing. That is what worries me the most.

In the example of Kdenlive, the timeline UI is completely in QML, same with a few dockers but the rest is in QtWidgets. Another example is plasma system settings, that are currently half ported to QML and the other one are still using QWidgets. The problem I see is that it's using a QQuickWidgets and those are using internally an OpenGL framebuffer.

No, that's not the case. You can just convert one component to QML, and use that, right inside a QWidgets application, like we do with the touch docker.

Well, if we do that, that these QML objects will still use QWidget rendering hierarchy, where no QRhi available, only pure openGL. So we cannot do that unless we recover Angle in Qt. Or make every(!) ported QML widget a native window, which is a bad idea because of frame synchronization issues.

In my concern by QML vs QWidget I meant not the widgets themselves, but the whole rendering hierarchy.

I think we need a BBB talk for that. There is too much confusion in writing.

rempt added a comment.Mar 7 2021, 4:22 PM

Yes, a meeting would be good.

But: we do know that it is possible to render using QRhi in a QWindow in a QWidget application, so that might still be a route for our canvas. In this case QRhi uses the platform specific API's.

If we then gradually rewrite our UI using QML, it's not a problem that the QML is rendered using QQuickWidgets until the last QWidget is gone. Those parts are not performance critical.

sh-zam added a comment.Mar 7 2021, 5:02 PM

Window Container based approach

There will be one more problem of this approach. The outer window will still be rendered by Qt and will use opengl.dll.

I am not sure about this. From the docs, OpenGL based rendering isn't the default it is enabled when a child-widget is QOpenGLWidget. So, theoretically if we use a QWindow in a window container with some other rendering API, the outer windows may be painted using a software renderer.

Technically, if we create a a native QWindow, then we are not obliged to use QRhi. We can use anything, Angle, Vulkan or MoltenVK. I have a feeling that Angle (or MetalAngle) would be the best approach.

Yes. I am going to emphasize this using QRhi is not necessary in any case. On other hand using window container while porting to QML may be, that's a topic for meeting, though.

rempt added a comment.Mar 7 2021, 5:06 PM

I've also mailed the qt-interest mailing list asking for a bit more information.

This is also starting to sound like we should just get someone to just sit down and write a test program within a week (just basically a window with the aforementioned combos of qtquickwidgets, regular widget, opengl or qrhi widgets, etc) and just see what that uses on the different platforms? I think it might prove time saving?

sh-zam added a comment.Mar 7 2021, 5:24 PM

This is also starting to sound like we should just get someone to just sit down and write a test program within a week (just basically a window with the aforementioned combos of qtquickwidgets, regular widget, opengl or qrhi widgets, etc) and just see what that uses on the different platforms? I think it might prove time saving?

There is an app above which uses QQuickWidget, QWindow (in a window container) and a normal QWidget. It worked fine on Linux. I can test it on Windows, MacOS may be for @rempt / amyspark.

... but what's the risk that we take on a bunch of "technical debt" with the window container approach, only to find out later that the performance impact is too severe to continue? Is this something that we could test out before making a big commitment?

I've started looking into this. I ported our canvas to QOpenGLWindow (QWindow hierarchy) which is pretty much same the API as QOpenGLWidget and render it in main window inside a window container. Rendering does seem to work fine, however there seem to issues with focus, though I haven't looked at fixing this, yet :)

Utilising Vulkan would make for a more performant implementation as well as being able to utilise threading in the rendering, unlike OpenGL which is single threaded.

woltherav added a comment.EditedMar 8 2021, 12:02 PM

Utilising Vulkan would make for a more performant implementation as well as being able to utilise threading in the rendering, unlike OpenGL which is single threaded.

Considering Krita uses graphics acceleration only to display the image, these things are not as much as a concern compared to the huge amount of time it'd take to port to it. We'd much rather use an abstraction layer like Angle or QRhi that take opengl calls and convert them to Vulkan (or OpenGL, or DirectX, or Metal), written by people who are graphics driver experts, as the maintainance burden might be too much.

sh-zam added a comment.Mar 8 2021, 6:25 PM

This is also starting to sound like we should just get someone to just sit down and write a test program within a week (just basically a window with the aforementioned combos of qtquickwidgets, regular widget, opengl or qrhi widgets, etc) and just see what that uses on the different platforms? I think it might prove time saving?

There is an app above which uses QQuickWidget, QWindow (in a window container) and a normal QWidget. It worked fine on Linux. I can test it on Windows, MacOS may be for @rempt / amyspark.

I tested the demo app. When a QWindow (uses QRhi) and QWidget (as a docker) is used with Direct3d as a backend things work fine, the application only uses Direct3d for rendering. When we add QQuickWidget, it brings OpenGL into the game again. Things still work fine, but because it is using OpenGL it brings us back to square one.

woltherav added a comment.EditedMar 8 2021, 6:32 PM

So... if we would want to port, then the filters, settings, new image dialog, impex-dialogs and other things we keep in separate windows can be done first, but dockers will all need to be ported in one go?

Alright, then the next question. Halla is thinking about using KDDockWidget. If we'd use that here, will that lead to the same issue?

EDIT: A quick search suggests that it doesn't use qtquickwidgets for it's dual qml-widget support, so it might not have the same issue.

EDIT2: Qtquick support is apparantly 'experimental'. :/

sh-zam added a comment.Mar 8 2021, 7:14 PM

So... if we would want to port, then the filters, settings, new image dialog, impex-dialogs and other things we keep in separate windows can be done first, but dockers will all need to be ported in one go?

Alright, then the next question. Halla is thinking about using KDDockWidget. If we'd use that here, will that lead to the same issue?

Yes, it will lead to the same issue. The problem here is not because of QDockWidget but QQuickWidget. So, if our canvas is ported to some other API the OpenGL problems will continue to exist with QQuickWidget while we port them gradually. So, it has to happen in a go or we have to make ANGLE work. (This is also one of the concerns which Dmitry had from the start but with QWidgets).

Mind if I try to summarize the problem and options as I understand them?
(If nothing else, I think it'll help me understand a bit better. I'm no expert so please feel free to correct me if I'm wrong about anything here.)

Present / Qt5

  • Qt5 internally uses OpenGL for its own rendering needs. While OpenGL works well on Linux and OSX, it is famously subpar on Windows.
  • Qt5 also ships with the option of using ANGLE on Windows, a shim/layer which converts all OpenGL calls into Direct3D calls for better compatibility and performance on Windows.
  • For the most part (aside from the Canvas and the Touch Docker), Krita uses the QtWidgets for our GUI. (Qt renders this for us on top of OpenGL or a software renderer?)
  • Our canvas has special needs that require us to write custom OpenGL code. Right now Qt uses ANGLE on Windows to interpret this automatically for us, just like it does internally.

In effect, any OpenGL that either we or Qt write is interpreted by ANGLE in a cross-platform way that suits our current needs. We all speak OpenGL.

Soon / Qt6

  • Qt6 has created their own rendering abstraction layer, QRhi, which they now use internally instead of OpenGL. As such, they no longer need ANGLE for Windows compatibility, so they no longer ship it. Qt6 now, for the most part, speaks QRhi.
  • Because Qt6 no longer ships ANGLE, we can no longer rely on it to interpret our Canvas' OpenGL calls. This is a problem because it has major performance and stability implications for our Canvas on Windows. Krita still speaks OpenGL.

Solutions

Fundamentally, we have 2 options:

  1. We can modify our Canvas to work better with Qt6, by moving away from OpenGL calls in favor of QRhi. To do this likely means porting, at the very least, our Canvas to QML.
  2. We can modify Qt6 to work better with our current Canvas, by patching ANGLE back in at some point so that our custom OpenGL calls are interpreted as Direct3D calls on Windows.

There may be a lot of room for variation within these two options, but as I understand it, this is really the choice that we're being faced with here.
Let me know if there's anything I'm missing or misunderstanding here, because it could be that I'm not the only one who is. :)

Yes, but while thinking through the docker stuff, I just realized. We need ANGLE to not go nuts when handling the HDR functionality of our current canvas. I mean, that part is fragile anyway, but that was several months of work...

Yes, @woltherav is right, if we go to QRhi approach, then we'll have to implement our HDR code from scratch. Not that it is too complicated, but it will still demand a bit of work.

dkazakov added a comment.EditedMar 11 2021, 12:19 PM

Hi, @sh-zam!

I am not sure about this. From the docs, OpenGL based rendering isn't the default it is enabled when a child-widget is QOpenGLWidget. So, theoretically if we use a QWindow in a window container with some other rendering API, the outer windows may be painted using a software renderer.

Are you sure that openGL is not used for rendering normal widgets on Windows? Can you find the code path that is used for them? I'm looking at the code of QWindowsWindow and I don't see any code path that creates QWindowsDirect2DBackingStore, which should be used for the widgets rendering...

In the current Qt5 implementation, Qt uses QWidgetBackingStore (renamed into QWidgetRepaintManager in Qt6) to render widgets. It uses internal pointer to QBackingStore, which forwards all the calls to an abstract QPlatformBackingStore. And there are two options for the platform backing store, QOpenGLCompositorBackingStore and QWindowsDirect2DBackingStore. I cannot find the code that selects them. The DirectX backing store was also present in Qt5, but it was never used. So I don't understand why it should change in Qt6...

UPD:
What I can see in QGuiApplicationPrivate::createPlatformIntegration(), the platform integration is created only once on loading, I cannot see any code how it can be switched on the runtime

UPD2:
What I can see from the code, we can explicitly select DirectX backing store for the widgets. Though I don't know how it would work, because this code path was never used by us on Windows.

UPD3:
I was wrong about QOpenGLCompositorBackingStore. It is never used. Instead, the default platform plugin is QWindowsGdiIntegration, which doesn't use any DirectX capabilities. Instead it uses GDI methods for painting. And if there is at least one widget in the hierarchy with "renderToTexture" support (QOpenGLWidget is an example of such widget), then QPlatformBackingStore (note, the root class, not a specific platform implementation) triggers initialization of the openGL engine and uses it for composing the widgets (see QPlatformBackingStore::composeAndFlush). That is, whatever platform plugin you select, Qt will still force openGL rendering if there is at least one widget in the hierarchy with support of that.

So, theoretically, we can try to avoid using widgets triggering openGL engine (how?), then Qt will use GDI for normal rendering and we will somehow paint the canvas using DXGI calls on a native window. Though cannot fully understand that atm.

UPD4:
I have checked the code in QQuickWidget. It can either use software renderer or openGL. It cannot use anything else, like QRhi.

I think some Qt developers will be a the KF6 sprints, would it make sense to try to get a few Krita developers to sprint and ask them a few questions about how Krita should move forwards? and what change in Qt will be required? Krita is probably not the only application affected by Angle dropping out of Qt and there are afaik many commercial application using OpenGL + Qt in the wild that will face the same difficulties.

We're having a meeting about this issue today. I'll try to attend the Qt company slot on Sunday and see what I can learn :-)

Vcpkg seems to have a cmake build config for a somewhat recent version of ANGLE (at least way more recent than the one inside Qt 5.12), if needed we may try to use it: https://github.com/microsoft/vcpkg/tree/master/ports/angle

bam added a subscriber: bam.Jul 6 2023, 9:29 PM