QML version of Krita
Open, Needs TriagePublic

Description

Notes on how to make Krita's canvas embeddable into QML graph

General considerations

  1. QML can render arbitrary openGL/DirectX/Metal code below or under QML graph. See example here.
  2. There is also an option to make a full-featured QML object that would render its contents using GPU directly and choose rendering API on the fly. See another example here.
  3. Both options above are already available in Qt5.
  4. In Qt6 QML will be using some weird RHI interface that would abstract graphics API somehow. I didn't manage to understand what it is going to be and whether we would be able to use that.

Roadmap for porting Krita to QML

  1. We have a special class KisAbstractCanvasWidget that abstracts the graphical API. So we would need to make implmentations of it for openGL, DirectX and/or PHI.
  2. KisAbstractCanvasWidget::widget() will have to be refactored into some higher-level interface. It is used for three purposes actually.
    • issue repaint for QOpenGLWidget with canvas->widget()->update(rc)
    • setup event filter on it in KisInputManager
    • for scrolling in KoCanvasControllerWidget
  3. KoCanvasControllerWidget will have to be refactored for not using QScrollArea and use the QML version instead. It already inherits from an abstract KoCanvasController, so in theory it should be "not difficult". The problem is that after splitting from Calligra we didn't keep this abstraction clean (there was only one implementation), so the refactoring may become a bit painful. It might happen that the whole zooming infrastructure will have to be refactored (though it would be nice to do that even without porting to QML).
  4. KisViewManager::statusBar() will have to be refactored into some abstract interface, like KoProgressProxy plus something.
  5. KisViewManager::paintOpBox() will have to be removed and replaced with some abstract interface (and the whole KisControlFrame will have to be rewritten in QML).
  6. KisViewManager::mainWindow() will have to go. We use it in rather surprising contexts. E.g. in KisNodeManager::slotTryRestartIsolatedMode() we check if we are the active window and skip restarting for non-active windows.
  7. All the actions are now loaded by KXmlGuiWindow. We will have to extract this code somehow and create KActionCollection manually.
  8. Some zoom actions are implemented by accessing "widgets" instead of abstract controllers. Specifically, KisZoomAction uses KoZoomAction widget to switch between predefined zoom levels on wheel events (12.5%,25%,50%,100%). This functionality will have to be moved into a generic class, e.g. KoZoomController (I don't know why it is not there atm).

Questions to be answered/investigated

  1. Qt6 is going to drop support of ANGLE. Will RHI interface provide us any usable abstractions? Or we will have to implement our canvas code for three native interfaces: openGL, DirectX and Metal? [1]
  2. Qt6's LTS releases are going to be closed-source only (well, with 1 year delay), so we will have to maintain it somehow ourselves. How are we going to do that?
  3. Porting Krita to QML will be a huge investment of work. Given that we have plans for GUI redesign anyway, perhaps we could start thinking about some other options we have?

[1] - UPD: as far as I can tell, RHI is going to be an internal API and not available to the users. At least for a few first versions.

dkazakov created this task.Jun 25 2020, 8:34 PM
dkazakov updated the task description. (Show Details)Jun 26 2020, 8:06 AM
dkazakov updated the task description. (Show Details)
ognarb added a subscriber: ognarb.Jun 26 2020, 9:41 AM

Just a list of the ways how we depend on Qt:

  1. QVector, QMap and other "core" classes
  2. Basic GUI elements
  3. KF5 libraries provide us standard dialogs, font selectors, xml gui, shortcuts, translations framework
  4. QPainter engine is used for rendering of vector shapes
  5. QPainterPath is used for manipulating bezier curves (and render them via QPainter)
  6. PyQt interface for plugins
  7. QtMultimedia for audio playback
rempt added a subscriber: rempt.Mar 2 2021, 10:33 AM
  • QPainter is also used to render the canvas decorations
  • I think it's possible to just port the canvas to a Qt Quick window, and keep the rest of the interface implemented using QWidgets.
  1. QPainterPath is used for manipulating bezier curves (and render them via QPainter)

it's possible to render a QPainterPath in QML using the scene graphs. I did it in Pikasso using a Rust library called Lyon: https://carlschwan.eu/2021/01/25/pikasso-a-simple-drawing-application-in-qtquick-with-rust/

sh-zam added a subscriber: sh-zam.Mar 7 2021, 5:29 PM

Just gonna plonk this gpl3 official qtreeview.qml here: https://code.qt.io/cgit/qt-extensions/qttreeview.git/tree/src/TreeView.qml

(The default qt.quick 2.0 controls don't have these, but it does have list, grid and table views, and we'll need it for the layerdocker even if it'll be a few years until we need to port the layer docker).

woltherav added a comment.EditedMar 14 2021, 12:14 PM

So, I managed to port the small color selector to qml:

https://invent.kde.org/woltherav/krita/-/commit/f3437a73084b16fd015357cb5bb5f76920d74d4b (might have some minor build errors, I didn't rebuild it after putting the code into it's own branch)

It consists of a dockerplugin that carries a QML file. The QML file has two KisGLImageQuickItem, which contain a Renderer that handles the opengl stuff as per opengl under qml example. There's also a SmallColorSelectorManagerItem which handles drawing KisGLImageF16 and all the other logic handled by KisSmallColorSelectorWidget in the regular selector. The rest (cursor, alignment, nits-spinbox) are all QML shapes and Qtquick.controls that are inside the QML file.

Some notes:

OpenGL

When you have multiple items draw opengl under qml, the QQuickWindow is their shared canvas, which means that the opengl code should limit itself to it's own area.

I haven't fully figured out how to do this properly. Like the limiting of the drawing works fine, but if you look closely, the upper part of the Saturation Value square is cut off, and I suspect this is mostly because I don't know opengl and thus don't understand dmitry's shader.

Key here is also these lines:

glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);

For some reason the QML items wouldn't show up above the OpenGL without it. If you look closely, black doesn't seem to get reached, I suspect the lowest rect in the selector qml main file is interacting somehow with the opengl sections.

The QSurfaceFormat stuff now needs to be set on the QQuickWindow, and I cannot find a place to set the TexFormat like we do for QOpenGL widget. I have no idea if the docker works with HDR either.

The docker feels surprisingly smooth in usage, and I suspect this is because it's locked into 60FPS as we noticed before. (However, might also be because the drawing of the QML cursor is completely detached from the generating of the OpenGL texture)

QML and our docker plugin stuff conflict.

This one's a bit odd. But basically, if you want to create a QML component with a lot of C++ logic, you need to register it with a QQmlExtensionPlugin, which requires Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) to be set. Apparantly our docker plugin stuff also uses Q_PLUGIN_METADATA, and you're only allowed to have one per library at a time. I solved this for now by sticking all the files I needed into sketch plugin.

This is kind of annoying however, because it means we either need to do all our bussiness logic outside the QML file and stick in the information we need (by setting QVariant properties), or all our bussiness logic qml components goes to whatever place we designate for our qml components instead of the docker/plugin that they're intended for.

Other things

I had to put the KisGLImageF16.cpp in a QObject wrapper. There are indications that just setting a Q_DECLARE_METATYPE at the bottom of the header file should work to have it recognized as something QVariantable, but I couldn't get it to work.

The DisplayColorConverter, despite being a QObject, seems to get qobjectmetatype errors when I stick it into a QVariant for sending to the manager, so the manager doesn't have access to the displaycolorconverter right now.

When making QVariants that contain objects you need to use QVariant::fromValue(QObject), because somehow QVariant(QObject) is private.

And that's about it.

The QSurfaceFormat stuff now needs to be set on the QQuickWindow, and I cannot find a place to set the TexFormat like we do for QOpenGL widget. I have no idea if the docker works with HDR either.

Some portion of the texture format for QOpenGLWidget was done with that custom HDR patch we add to Qt, so that might be absent in QQuickWidget (can be added quite easily).

The docker feels surprisingly smooth in usage, and I suspect this is because it's locked into 60FPS as we noticed before.

I'm not sure 60 fps limit is active here, because QQuickWidget is painted using the normal QWidget painting hierarchy, not the QML one. Our canvas is rendered with the same rendering code inside Qt.

Hello,

I've ported the canvas (as suggested a week back in IRC) to use Kirigami's ApplicationWindow as a starter: https://invent.kde.org/szaman/krita/-/commits/krita-qml

I've moved the parts of KisOpenGLCanvas2 to KisCanvasRenderer, only the parts which do the initial texture uploading and rendering. I didn't port updating part because there is no easy incremental way to test it since a lot of it depends on QWidget hierarchy. The parts I ported seem to work fine and as suggested by Halla, I've not created any dependence on QWindow, which can make it easy to reuse KisCanvasRenderer for both QWidget and QML applications as long as have a way of getting OpenGL context reliably.

The only graphics problem I had is already mentioned by Wolthera.

Concerning the opengl depth test stuff, there's some mention of it in the qt6 docs: https://doc.qt.io/qt-6/qquickgraphicsconfiguration.html
That class doesn't exist in 5, the only mention I can find is that the depth buffers got to be reset: https://doc.qt.io/qt-5/qquickwindow.html#resetOpenGLState

I guess that means I could get rid of the blending mode stuff? I'll test on thursday.

Concerning the opengl depth test stuff, there's some mention of it in the qt6 docs: https://doc.qt.io/qt-6/qquickgraphicsconfiguration.html
That class doesn't exist in 5, the only mention I can find is that the depth buffers got to be reset: https://doc.qt.io/qt-5/qquickwindow.html#resetOpenGLState

I guess that means I could get rid of the blending mode stuff? I'll test on thursday.

I did see this class and the function. I am not sure whether we can use it if there are multiple items on the screen because scene graph (see Warning) expects the state to be reverted back to the way it was, which experimentally was different from the default OpenGL state.

Small update:

Got the texture to align properly, figured out how to match the background to the style and handled the displaycolorconverter being set.

It's now virtually identical to the small color selector, except for the HDR parts :)

Nice small update :3

woltherav added a comment.EditedMar 26 2021, 3:47 PM

So, we had a meeting. As far as I can tell, the following should happen:

  1. We figure out what is necessary for the canvas. There's a laundrylist in the description of this task already.
  2. We need to figure out what the standard use of QML is going to look like in Krita. We can embed QML scenes into qquickwidgets for Krita desktop, but that still leaves many questions.
    1. Style guide and where Krita-wide QML widgets will live. (We could just say 'they'll live in libqml and will have the org.krita.components 2.0 module name', but that's still a decision we need to make...)
    2. How to handle dockers and other plugins: https://phabricator.kde.org/T13339#251319
    3. In which order UI elements will be ported from widgets to QML.
    4. How are other programs doing this, like KDENLive?
    5. Specific designs such as a file handling framework that fits more on mobile devices where people have no concept of how saving and loading works.

Until these questions are all answered, I am hesitant requiring people to use QML. People who are working on QWidgets right this instance should in any case keep working on those for now.

In the past two weeks I did a bit of experimentation with QtQuick2 and the canvas which resulted in two MR's:

What I found with the first MR is that if we want to mix QML with QWidget, QQuickWidget is practically unusable because it can reduce the canvas performance by a lot. Whenever the QtQuick2 scene requested an update, it delays actually updating by 5 ms in an attempt to batch updates. In a typical action like painting or panning the canvas, the following is roughly what happens:

  1. KisInputManager receives input events
  2. The canvas controller gets updated
  3. Redraw is requested for the canvas via KisCanvas2::updateCanvas, which is compressed by the max FPS setting (but the first update is fired immediately)
  4. KisSignalCompressor fires the actual QWidget::update event
    • If we have QML stuff to update, we would also call QQuickItem::update, or if we need to update the underlay, QQuickWindow::update()
    • The QQuickRenderControl that QQuickWidget uses will get renderRequested or sceneChanged
    • QQuickWidget then schedules an update in 5 ms, if not already scheduled
  5. QWidget repaint does not wait 5 ms, so the the existing FBO of the Quick Scene is composited onto the window, along with any other updates in the rest of the UI (e.g. ruler, scrollbars).
    • This means the rest of the QWidget UI is updated, but the canvas is now outdated
    • This also seem to wait for vsync on ANGLE
  6. After 5 ms (or after vsync), the QQuickWidget gets rendered to the offscreen FBO, then QWidget::update() is called
  7. The new content of the QQuickWidget, i.e. the updated canvas, is finally composited onto the window.
  8. Wait for vsync again on ANGLE

This shows that it takes 2 (!) window compositions to get the canvas updated.

When QQuickWidget is used for other dockers that is not the canvas, this is less problematic because dockers usually don't get updated every frame. It may pose an issue for a colour selector though (e.g. you can hold Ctrl and drag around the canvas, which rapidly updates the current colour...)


So, what I did in the second MR is to take explicit control of how the QtQuick2 scene is updated and rendered using QQuickRenderControl:

  • Any update requests from the Quick scene goes through KisCanvas2::updateCanvas, just like other canvas update requests.
  • Syncing and rendering of the QQuickRenderControl is done inside paintGL, in sync with the rest of QWidget on the same window.
  • Also, by using QQuickRenderControl we can render the Quick scene directly onto a QOpenGLWidget, keeping it closer to the existing behaviour of KisOpenGLCanavs2.

The downside with this is that we need to reimplement half of QQuickWidget, to forward input events to the offscreen QQuickWindow. (But there is no rush, we are still using KisInputManager directly on the widget for now.)

The actual canvas is still rendered directly in OpenGL, but at least we now have a way to add a QtQuick2 scene on top of it to slowly move things over. For the next step, I'm thinking of trying to port some canvas decorations to QtQuick2 (not sure how much QML, but we'll see). Probably starting with KisFpsDecoration since it is so simple.

QML and our docker plugin stuff conflict.

This one's a bit odd. But basically, if you want to create a QML component with a lot of C++ logic, you need to register it with a QQmlExtensionPlugin, which requires Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) to be set. Apparantly our docker plugin stuff also uses Q_PLUGIN_METADATA, and you're only allowed to have one per library at a time. I solved this for now by sticking all the files I needed into sketch plugin.

This is kind of annoying however, because it means we either need to do all our bussiness logic outside the QML file and stick in the information we need (by setting QVariant properties), or all our bussiness logic qml components goes to whatever place we designate for our qml components instead of the docker/plugin that they're intended for.

I think we don't need to use QQmlExtensionPlugin. We can just put the calls to registerQmlType and friends in the constructor of the class registered with K_PLUGIN_FACTORY_WITH_JSON.