[system/karton] /: Optimize VM viewer rendering through GL scanout

Derek Lin <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 9fd6b273e396c9d298483cdd9e4d5660d7e832c7 by Derek Lin.
Committed on 21/07/2026 at 01:10.
Pushed by kenoi into branch 'master'.

Optimize VM viewer rendering through GL scanout

- Rewrite to GL scanout rendering pipeline via gl-draw signal with EGL/OpenGL
- Added SPICE URI detection and connection handling logic
- Enabled 3d-accel in VM installer, unix socket connection (TCP still supported)
- Various style improvements

M  +2    -0    CMakeLists.txt
M  +3    -0    src/CMakeLists.txt
M  +31   -9    src/domaininstaller.cpp
M  +3    -0    src/domaininstaller.h
M  +335  -93   src/domainviewer.cpp
M  +24   -16   src/domainviewer.h
M  +2    -0    src/karton.cpp
M  +1    -1    src/karton.h
M  +3    -3    src/libvirtmonitor.cpp
M  +1    -0    src/osinfoconfig.cpp
M  +23   -6    src/qml/VMViewerWindow.qml
M  +7    -0    src/vmlistmodel.cpp

https://invent.kde.org/system/karton/-/commit/9fd6b273e396c9d298483cdd9e4d5660d7e832c7

diff --git a/CMakeLists.txt b/CMakeLists.txt
index fb9d472..7cce03a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ find_package(Qt6 REQUIRED COMPONENTS
     Quick
     Test
     Gui
+    GuiPrivate
     QuickControls2
     Widgets
     Xml
@@ -38,6 +39,7 @@ find_package(Libvirt REQUIRED)
 find_package(Libosinfo REQUIRED)
 find_package(Glib REQUIRED)
 find_package(Spiceglib REQUIRED)
+find_package(OpenGL REQUIRED COMPONENTS OpenGL EGL)
 
 find_package(KF6 REQUIRED COMPONENTS
     Kirigami
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f3d03e1..b59fd6d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -68,6 +68,7 @@ target_link_libraries(karton
     Qt6::Widgets
     Qt6::Xml
     Qt6::Multimedia
+    Qt6::GuiPrivate
     KF6::I18n
     KF6::I18nQml
     KF6::CoreAddons
@@ -77,6 +78,8 @@ target_link_libraries(karton
     Spiceglib::Spiceglib
     Libosinfo::Libosinfo
     Glib::Glib
+    OpenGL::EGL
+    OpenGL::GL
 )
 
 install(TARGETS karton ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/src/domaininstaller.cpp b/src/domaininstaller.cpp
index fc87f3a..ed40ba8 100644
--- a/src/domaininstaller.cpp
+++ b/src/domaininstaller.cpp
@@ -46,7 +46,7 @@ QString DomainInstaller::generateXML(virConnectPtr conn, const DomainConfig *con
 
     QDomDocument document;
     QDomElement root = document.createElement(QStringLiteral("domain"));
-    root.setAttribute(QStringLiteral("type"), QStringLiteral("kvm")); // parameterize libos
+    root.setAttribute(QStringLiteral("type"), QStringLiteral("kvm")); // TODO: parameterize libos
 
     virDomainPtr *domains = nullptr;
     int numDomains = virConnectListAllDomains(conn, &domains, 0);
@@ -193,7 +193,13 @@ QString DomainInstaller::generateXML(virConnectPtr conn, const DomainConfig *con
                                {.type = QStringLiteral("user"), .mac = genMac(), .source = QString(), .linkState = true, .model = QStringLiteral("virtio")});
 
     // devices->graphics element
-    addGraphicsDevices(document, devices, {.type = QStringLiteral("spice"), .autoport = QStringLiteral("yes"), .listen = QStringLiteral("address")});
+    addGraphicsDevices(document,
+                       devices,
+                       {.type = QStringLiteral("spice"),
+                        .autoport = QStringLiteral("yes"),
+                        .listen = QStringLiteral("socket"),
+                        .glEnable = QStringLiteral("yes"),
+                        .uuid = uuidString});
 
     // devices->sound element
     addSoundDevices(document, devices, {.model = QStringLiteral("ich9"), .id = QStringLiteral("1")});
@@ -202,7 +208,10 @@ QString DomainInstaller::generateXML(virConnectPtr conn, const DomainConfig *con
     addAudioDevices(document, devices, {.id = QStringLiteral("1"), .type = QStringLiteral("spice")});
 
     // devices->video element
-    addVideoDevices(document, devices, {.model = QStringLiteral("virtio"), .heads = QStringLiteral("1"), .primary = QStringLiteral("yes")});
+    addVideoDevices(
+        document,
+        devices,
+        {.model = QStringLiteral("virtio"), .heads = QStringLiteral("1"), .primary = QStringLiteral("yes"), .enableAccel3d = QStringLiteral("yes")});
 
     addInputDevices(document, devices, {.type = QStringLiteral("tablet"), .bus = QStringLiteral("usb")});
 
@@ -281,11 +290,18 @@ void DomainInstaller::addGraphicsDevices(QDomDocument &document, QDomElement &pa
     QDomElement graphics = document.createElement(QStringLiteral("graphics"));
     parent.appendChild(graphics);
     graphics.setAttribute(QStringLiteral("type"), config.type);
-    graphics.setAttribute(QStringLiteral("autoport"), config.autoport);
+    // graphics.setAttribute(QStringLiteral("autoport"), config.autoport);
+    graphics.setAttribute(QStringLiteral("socket"), QStringLiteral("/tmp/spice%1.sock").arg(config.uuid));
 
     QMap<QString, QString> listen;
     listen[QStringLiteral("type")] = config.listen;
+    listen[QStringLiteral("socket")] = QStringLiteral("/tmp/spice%1.sock").arg(config.uuid);
+
+    QMap<QString, QString> gl;
+    gl[QStringLiteral("enable")] = config.glEnable;
+
     addElementWithAttributes(document, graphics, QStringLiteral("listen"), QString(), listen);
+    addElementWithAttributes(document, graphics, QStringLiteral("gl"), QString(), gl);
 }
 
 void DomainInstaller::addSoundDevices(QDomDocument &document, QDomElement &parent, const SoundConfig &config)
@@ -311,11 +327,17 @@ void DomainInstaller::addVideoDevices(QDomDocument &document, QDomElement &paren
 {
     QDomElement video = document.createElement(QStringLiteral("video"));
     parent.appendChild(video);
-    QMap<QString, QString> model;
-    model[QStringLiteral("type")] = config.model;
-    model[QStringLiteral("heads")] = config.heads;
-    model[QStringLiteral("primary")] = config.primary;
-    addElementWithAttributes(document, video, QStringLiteral("model"), QString(), model);
+
+    QDomElement model = document.createElement(QStringLiteral("model"));
+    video.appendChild(model);
+    model.setAttribute(QStringLiteral("type"), config.model);
+    model.setAttribute(QStringLiteral("heads"), config.heads);
+    model.setAttribute(QStringLiteral("primary"), config.primary);
+
+    QMap<QString, QString> acceleration;
+    acceleration[QStringLiteral("accel3d")] = config.enableAccel3d;
+
+    addElementWithAttributes(document, model, QStringLiteral("acceleration"), QString(), acceleration);
 }
 
 void DomainInstaller::addInputDevices(QDomDocument &document, QDomElement &parent, const InputConfig &config)
diff --git a/src/domaininstaller.h b/src/domaininstaller.h
index a9467f6..4c29cf8 100644
--- a/src/domaininstaller.h
+++ b/src/domaininstaller.h
@@ -44,6 +44,8 @@ private:
         QString type;
         QString autoport;
         QString listen;
+        QString glEnable;
+        QString uuid;
     };
 
     struct SoundConfig {
@@ -60,6 +62,7 @@ private:
         QString model;
         QString heads;
         QString primary;
+        QString enableAccel3d;
     };
 
     struct InputConfig {
diff --git a/src/domainviewer.cpp b/src/domainviewer.cpp
index 2c94974..c2d3d83 100644
--- a/src/domainviewer.cpp
+++ b/src/domainviewer.cpp
@@ -3,11 +3,15 @@
 
 #include "domainviewer.h"
 
+#include <QOpenGLContext>
+#include <QOpenGLFunctions>
+#include <QSGSimpleTextureNode>
+
+#include <rhi/qrhi.h> // need
 #include <spice-client.h>
 
 #include <QGuiApplication>
 #include <QQuickWindow>
-#include <QSGSimpleTextureNode>
 #include <QString>
 #include <QUrl>
 
@@ -21,14 +25,26 @@ DomainViewer::DomainViewer(QQuickItem *parent)
     : QQuickItem(parent)
     , m_commandRunner(new CommandRunner(this))
     , m_domain(nullptr)
+    , m_spiceUri(QString())
     , m_host(QStringLiteral("localhost"))
     , m_port(5900)
+    , m_password(QString())
     , m_connected(false)
-    , m_frameUpdated(false)
-    , m_audio(nullptr)
+    , m_imageWidth(0)
+    , m_imageHeight(0)
+    , m_scanout({})
+    , m_hasScanout(false)
+    , m_eglImage(EGL_NO_IMAGE_KHR)
+    , m_texId(0)
+    , m_session(nullptr)
+    , m_display_channel(nullptr)
+    , m_inputs_channel(nullptr)
     , m_playback_channel(nullptr)
+    , m_current_button_mask(0)
+    , m_audio(nullptr)
     , m_audioSink(nullptr)
     , m_audioDevice(nullptr)
+    , m_audioFormat()
 {
     setFlag(ItemHasContents, true);
     setAcceptedMouseButtons(Qt::AllButtons);
@@ -36,7 +52,6 @@ DomainViewer::DomainViewer(QQuickItem *parent)
     setFlag(ItemIsFocusScope, true);
 
     connect(m_commandRunner, &CommandRunner::commandFinished, this, &DomainViewer::handleHostPort);
-    qCDebug(KARTON_DEBUG) << "DomainViewer constructor - setting default host:" << m_host << "port:" << m_port;
 }
 
 DomainViewer::~DomainViewer()
@@ -67,59 +82,41 @@ void DomainViewer::setDomain(Domain *domain)
 
 void DomainViewer::componentComplete()
 {
-    qCCritical(KARTON_DEBUG) << "run?!";
     QQuickItem::componentComplete();
     if (m_domain) {
         setupSpiceSession();
     }
 }
 
-QSGNode *DomainViewer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
+void DomainViewer::handleHostPort(int exitCode, const QString &output)
 {
-    QMutexLocker locker(&m_frameLock);
-
-    // prevent render if not updated or valid
-    if (!m_frameUpdated || m_frame.isNull() || m_frame.width() <= 0 || m_frame.height() <= 0) {
-        delete oldNode;
-        return nullptr;
+    if (exitCode != 0 || output.isEmpty()) {
+        qCCritical(KARTON_DEBUG) << "handleHostPort: virsh domdisplay call failed";
+        return;
     }
 
-    auto node = static_cast<QSGSimpleTextureNode *>(oldNode);
+    QString trimmedOutput = output.trimmed();
+    QUrl url(output.trimmed());
 
-    if (!node) {
-        node = new QSGSimpleTextureNode();
-        node->setOwnsTexture(true);
+    if (!url.isValid()) {
+        qCCritical(KARTON_DEBUG) << "Invalid SPICE URI given to virsh domdisplay";
+        return;
     }
+    m_host = url.host();
+    m_port = url.port(); // defaults to -1 if not found
+    m_spiceUri = url.toString();
 
-    QSGTexture *texture = window()->createTextureFromImage(m_frame);
-    if (texture) {
-        node->setTexture(texture);
-        node->setRect(boundingRect());
-        m_frameUpdated = false;
+    if (url.toString().startsWith(QStringLiteral("spice+unix:///tmp/spice"))) {
+        qCInfo(KARTON_DEBUG) << "Detected UNIX socket. Connection parameters set.";
+    } else {
+        qCInfo(KARTON_DEBUG) << "Detected network TCP socket. Connection parameters set.";
     }
 
-    return node;
-}
-
-void DomainViewer::handleHostPort(int exitCode, const QString &output)
-{
-    qCInfo(KARTON_DEBUG) << "finished running command hostport";
-    if (exitCode == 0 && !output.isEmpty()) {
-        QUrl url(output.trimmed());
-
-        if (url.isValid()) {
-            QString host = url.host();
-            int port = url.port();
-
-            m_host = host;
-            m_port = port;
-            qCInfo(KARTON_DEBUG) << "setting host-port to " << host << ", " << port;
-            if (!connectToSpice()) {
-                qCCritical(KARTON_DEBUG) << "Failed to connect to SPICE";
-            }
-        }
+    if (!connectToSpice()) {
+        qCCritical(KARTON_DEBUG) << "Failed to connect to SPICE";
     }
 }
+
 bool DomainViewer::setupSpiceSession()
 {
     // TODO: replace virsh CLI, use libvirt API: https://libvirt.org/html/libvirt-libvirt-domain.html#VIR_MIGRATE_PARAM_GRAPHICS_URI
@@ -127,25 +124,26 @@ bool DomainViewer::setupSpiceSession()
     bool commandStarted = m_commandRunner->runCommand(QStringLiteral("virsh domdisplay %1").arg(m_domain->config()->name()));
     return commandStarted;
 }
+
 bool DomainViewer::connectToSpice()
 {
-    qCCritical(KARTON_DEBUG) << "Running connection to spice! host - " << m_host << ", Port is:" << m_port;
+    qCInfo(KARTON_DEBUG) << "Connecting to SPICE...";
+    qCInfo(KARTON_DEBUG) << "   SPICE URI:" << m_spiceUri;
+    qCInfo(KARTON_DEBUG) << "   host: " << m_host;
+    qCInfo(KARTON_DEBUG) << "   post" << m_port;
+
     if (!m_domain) {
         qCCritical(KARTON_DEBUG) << "connectToSpice() called but domain is null!";
         return false;
     }
 
     disconnectFromSpice();
-
     m_session = spice_session_new();
 
-    QString uri = QString::fromUtf8("spice://%1:%2").arg(m_host).arg(m_port);
-
-    qCInfo(KARTON_DEBUG) << "Connecting to URI! -" << uri;
-    g_object_set(m_session, "uri", uri.toUtf8().constData(), NULL);
-    // could use SpiceURI directly also
+    qCInfo(KARTON_DEBUG) << "Connecting to URI! -" << m_spiceUri;
+    g_object_set(m_session, "uri", m_spiceUri.toUtf8().constData(), NULL);
 
-    g_signal_connect(m_session, "channel-new", G_CALLBACK(DomainViewer::channel_new_cb), this);
+    g_signal_connect(m_session, "channel-new", G_CALLBACK(DomainViewer::channel_new_callback), this);
 
     if (!spice_session_connect(m_session)) {
         g_object_unref(m_session);
@@ -153,7 +151,7 @@ bool DomainViewer::connectToSpice()
         m_audio = nullptr;
         return false;
     }
-    qCInfo(KARTON_DEBUG) << "yay! connected to " << domain()->config()->name();
+    qCInfo(KARTON_DEBUG) << "Established connection! connected to " << domain()->config()->name();
     m_connected = true;
 
     return true;
@@ -175,19 +173,22 @@ void DomainViewer::disconnectFromSpice()
     }
 }
 
-void DomainViewer::channel_new_cb(SpiceSession *session, SpiceChannel *channel, gpointer user_data)
+void DomainViewer::channel_new_callback(SpiceSession *session, SpiceChannel *channel, gpointer user_data)
 {
+    Q_UNUSED(session);
+
     DomainViewer *item = static_cast<DomainViewer *>(user_data);
 
-    // checkChannelStatus(); // channel debug msgs
+    item->checkChannelStatus(); // uncomment for channel debug msgs
     if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
         qCInfo(KARTON_DEBUG) << "SPICE display connected";
 
         spice_channel_connect(channel);
         item->m_display_channel = channel;
 
-        g_signal_connect(channel, "display-primary-create", G_CALLBACK(display_primary_create_callback), item);
-        g_signal_connect(channel, "display-invalidate", G_CALLBACK(display_invalidate_callback), item);
+        g_signal_connect(channel, "gl-draw", G_CALLBACK(gl_draw_callback), item);
+        // TODO: Might need to check if gl is enabled. Domains created by virt-manager are not accel3d by default.
+
     } else if (SPICE_IS_INPUTS_CHANNEL(channel)) {
         qCInfo(KARTON_DEBUG) << "SPICE: Inputs connected";
         spice_channel_connect(channel);
@@ -204,57 +205,297 @@ void DomainViewer::channel_new_cb(SpiceSession *session, SpiceChannel *channel,
         qCWarning(KARTON_DEBUG) << "Unrecognised SPICE channel type";
     }
 }
-void DomainViewer::display_primary_create_callback(SpiceChannel *channel,
-                                                   gint format,
-                                                   gint width,
-                                                   gint height,
-                                                   gint stride,
-                                                   gint shmid,
-                                                   gpointer imgdata,
-                                                   gpointer user_data)
+
+// ========================== Display rendering  ========================
+
+void DomainViewer::gl_draw_callback(SpiceDisplayChannel *channel, guint x, guint y, guint width, guint height, gpointer user_data)
 {
+    Q_UNUSED(x);
+    Q_UNUSED(y);
+    Q_UNUSED(width);
+    Q_UNUSED(height);
+
     DomainViewer *item = static_cast<DomainViewer *>(user_data);
-    qCInfo(KARTON_DEBUG) << "SPICE: primary framebuffer received! size:" << width << "x" << height;
-    qCInfo(KARTON_DEBUG) << "SPICE: format is:" << format;
-    QMutexLocker locker(&item->m_frameLock);
+    auto scanout = spice_display_get_gl_scanout(channel);
+    if (!scanout)
+        return;
+    item->handleGlScanout(scanout);
+    spice_display_gl_draw_done(channel); // releases the GL resource
+}
+void DomainViewer::handleGlScanout(const SpiceGlScanout *scanout)
+{
+    if (m_hasScanout && m_scanout.fd >= 0) {
+        close(m_scanout.fd);
+        m_scanout.fd = -1;
+    }
 
-    item->m_frameBuffer = static_cast<uchar *>(imgdata);
-    item->m_imageWidth = width;
-    item->m_imageHeight = height;
-    // TODO: map incoming format.
-    item->m_frame = QImage(item->m_frameBuffer, width, height, stride, QImage::Format_RGB32);
+    cleanupEGLImage();
 
-    item->m_frameUpdated = true;
-    QMetaObject::invokeMethod(item, "frameUpdated", Qt::QueuedConnection); // could also do queued
-    QMetaObject::invokeMethod(item, "update", Qt::QueuedConnection);
+    m_scanout = *scanout; // struct copy
 
-    item->updateImplicitDimensions();
-}
+    // duplicate the file descriptor if exists
+    // we will be using the duplicate, and the original is freed by SPICE (gl_draw_done).
+    if (scanout->fd >= 0) {
+        m_scanout.fd = dup(scanout->fd);
+        if (m_scanout.fd < 0) {
+            qCWarning(KARTON_DEBUG) << "Failed to duplicate scanout FD";
+            return;
+        }
+    }
 
-void DomainViewer::updateImplicitDimensions()
-{
+    m_imageHeight = scanout->height;
+    m_imageWidth = scanout->width;
     setImplicitWidth(m_imageWidth);
     setImplicitHeight(m_imageHeight);
+    m_hasScanout = true;
+
+    update();
 }
 
-void DomainViewer::display_invalidate_callback(SpiceDisplayChannel *channel, gint x, gint y, gint width, gint height, gpointer user_data)
+void DomainViewer::createTextureFromScanout(const SpiceGlScanout *scanout)
 {
-    DomainViewer *item = static_cast<DomainViewer *>(user_data);
-    item->m_frameUpdated = true;
+    qCDebug(KARTON_DEBUG) << "=== createTextureFromScanout()";
+    qCDebug(KARTON_DEBUG) << "FD:" << scanout->fd;
+    qCDebug(KARTON_DEBUG) << "Size:" << scanout->width << "x" << scanout->height;
+    qCDebug(KARTON_DEBUG) << "Format:" << QStringLiteral("0x%1").arg(scanout->format, 0, 16);
+    qCDebug(KARTON_DEBUG) << "Stride:" << scanout->stride;
+
+    if (scanout->fd == -1) {
+        return;
+    }
+
+    QOpenGLContext *context = QOpenGLContext::currentContext();
+    if (!context) {
+        qCDebug(KARTON_DEBUG) << "No current OpenGL context";
+        return;
+    }
+
+    QOpenGLFunctions *gl = context->functions();
+    if (!gl) {
+        qCDebug(KARTON_DEBUG) << "Failed to get OpenGL functions";
+        return;
+    }
+
+    // generate texture if empty
+    if (m_texId == 0) {
+        gl->glGenTextures(1, &m_texId);
+        if (m_texId == 0) { // glGenTextures modifies m_texId
+            qCDebug(KARTON_DEBUG) << "Failed to generate texture";
+            return;
+        }
+    }
+
+    // setup EGL display
+    EGLDisplay display = eglGetCurrentDisplay();
+    if (display == EGL_NO_DISPLAY) {
+        qCDebug(KARTON_DEBUG) << "Failed to get EGL display";
+        return;
+    }
+
+    EGLint attrs[] = {EGL_DMA_BUF_PLANE0_FD_EXT,
+                      scanout->fd,
+                      EGL_DMA_BUF_PLANE0_PITCH_EXT,
+                      static_cast<EGLint>(scanout->stride),
+                      EGL_DMA_BUF_PLANE0_OFFSET_EXT,
+                      0,
+                      EGL_WIDTH,
+                      static_cast<EGLint>(scanout->width),
+                      EGL_HEIGHT,
+                      static_cast<EGLint>(scanout->height),
+                      EGL_LINUX_DRM_FOURCC_EXT,
+                      static_cast<EGLint>(scanout->format),
+                      EGL_NONE};
+
+    // create egl image
+    if (!m_eglCreateImageKHR) {
+        m_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+        if (!m_eglCreateImageKHR) {
+            qCDebug(KARTON_DEBUG) << "eglCreateImageKHR not available";
+            return;
+        }
+    }
+    m_eglImage = m_eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs);
+
+    if (m_eglImage == EGL_NO_IMAGE_KHR) {
+        EGLint error = eglGetError();
+        qCDebug(KARTON_DEBUG) << "Failed to create EGL image, error:" << QStringLiteral("0x%1").arg(error, 0, 16);
+        return;
+    }
+
+    gl->glBindTexture(GL_TEXTURE_2D, m_texId);
+
+    gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    // set image texture
+    if (!m_glEGLImageTargetTexture2DOES) {
+        m_glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+        if (!m_glEGLImageTargetTexture2DOES) {
+            qCDebug(KARTON_DEBUG) << "glEGLImageTargetTexture2DOES not supported";
+            return;
+        }
+    }
+    m_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)m_eglImage);
+
+    GLenum glError = gl->glGetError();
+    if (glError != GL_NO_ERROR) {
+        qCDebug(KARTON_DEBUG) << "OpenGL error after glEGLImageTargetTexture2DOES:" << QStringLiteral("0x%1").arg(glError, 0, 16);
+    }
+
+    gl->glBindTexture(GL_TEXTURE_2D, 0); // unbind texture
+}
+
+// triggered by update()
+QSGNode *DomainViewer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
+{
+    // create opengl texture from scanout data
+    if (m_hasScanout && m_texId == 0) {
+        createTextureFromScanout(&m_scanout);
+    }
+
+    if (!m_texId) {
+        qCDebug(KARTON_DEBUG) << "No texture available yet";
+        delete oldNode;
+        return nullptr;
+    }
+
+    auto *textureNode = static_cast<QSGSimpleTextureNode *>(oldNode);
+    if (!textureNode) {
+        textureNode = new QSGSimpleTextureNode();
+        textureNode->setOwnsTexture(true);
+    }
+
+    QOpenGLContext *context = QOpenGLContext::currentContext();
+    if (!context) {
+        qCDebug(KARTON_DEBUG) << "No current OpenGL context in updatePaintNode";
+        return textureNode;
+    }
+
+    QOpenGLFunctions *gl = context->functions();
+    if (gl) {
+        gl->glBindTexture(GL_TEXTURE_2D, m_texId);
+        gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+        gl->glBindTexture(GL_TEXTURE_2D, 0);
+    }
+
+    // loading the gl image texture onto the QSG
+    QQuickWindow::CreateTextureOptions options;
+    QRhi *rhi = window()->rhi();
+    if (!rhi) {
+        qCDebug(KARTON_DEBUG) << "No RHI available";
+        return textureNode;
+    }
+
+    QRhiTexture::Format rhiFormat = QRhiTexture::RGBA8;
+    QRhiTexture *rhiTexture = rhi->newTexture(rhiFormat, QSize(m_imageWidth, m_imageHeight));
+    if (!rhiTexture) {
+        qCDebug(KARTON_DEBUG) << "Failed to create RHI texture";
+        return textureNode;
+    }
+
+    // create native texture to be contained in RHI
+    QRhiTexture::NativeTexture nativeTex;
+    nativeTex.object = m_texId;
+    nativeTex.layout = 0;
+
+    if (!rhiTexture->createFrom(nativeTex)) {
+        qCDebug(KARTON_DEBUG) << "Failed to create RHI texture from native";
+        delete rhiTexture;
+        return textureNode;
+    }
+
+    QSGTexture *texture = window()->createTextureFromRhiTexture(rhiTexture, options);
+    if (!texture) {
+        qCDebug(KARTON_DEBUG) << "Failed to create QSG texture";
+        delete rhiTexture;
+        return textureNode;
+    }
+
+    textureNode->setTexture(texture);
+    textureNode->setRect(boundingRect());
+
+    qCDebug(KARTON_DEBUG) << m_domain->config()->name() << ": Successfully updated canvas.";
+    qCDebug(KARTON_DEBUG) << "  SPICE Graphics URI: " << m_spiceUri;
+    qCDebug(KARTON_DEBUG) << "  Texture ID:" << m_texId;
+    qCDebug(KARTON_DEBUG) << "  Size:" << m_imageWidth << "x" << m_imageHeight;
+
+    return textureNode;
+}
 
-    // Copy from spice-glib framebuffer to the QImage to render - inefficient, might want to switch to another approach (partial render?)
-    uint *source = reinterpret_cast<uint *>(item->m_frameBuffer);
-    for (int i = y; i < y + height; ++i) {
-        for (int j = x; j < x + width; ++j) {
-            item->m_frame.setPixel(j, i, source[item->m_imageWidth * i + j]);
+// cleans up scanout and egl image, textures
+void DomainViewer::cleanupEGLResources()
+{
+    if (m_hasScanout && m_scanout.fd >= 0) {
+        close(m_scanout.fd);
+        m_scanout.fd = -1;
+    }
+    m_hasScanout = false;
+
+    cleanupEGLImage();
+
+    QOpenGLContext *context = QOpenGLContext::currentContext();
+    if (m_texId && context) {
+        QOpenGLFunctions *gl = context->functions();
+        if (gl) {
+            gl->glDeleteTextures(1, &m_texId);
+        }
+        m_texId = 0;
+    }
+}
+
+// for cleanup of duplicate
+void DomainViewer::cleanupEGLImage()
+{
+    if (m_eglImage != EGL_NO_IMAGE_KHR) {
+        EGLDisplay display = eglGetCurrentDisplay();
+        if (display != EGL_NO_DISPLAY) {
+            if (!m_eglDestroyImageKHR) {
+                m_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+            }
+            if (m_eglDestroyImageKHR) {
+                m_eglDestroyImageKHR(display, m_eglImage);
+            }
         }
+        m_eglImage = EGL_NO_IMAGE_KHR;
+    }
+}
+
+void DomainViewer::saveFrameToDomain()
+{
+    if (!window()) {
+        qCDebug(KARTON_DEBUG) << "saveFrameToDomain: No window available";
+        return;
+    }
+
+    if (!isVisible() || width() <= 0 || height() <= 0) {
+        qCDebug(KARTON_DEBUG) << "saveFrameToDomain: Item not ready for grabbing";
+        return;
     }
 
-    QMetaObject::invokeMethod(item, "update", Qt::QueuedConnection);
+    // returns the image of the full window
+    QImage fullImage = window()->grabWindow();
+
+    if (!fullImage.isNull()) {
+        // crop it to the actual QQI
+        QRectF itemBounds = mapRectToScene(boundingRect());
+        QRect cropRect = itemBounds.toRect().intersected(fullImage.rect());
+        QImage finalImage = cropRect.isEmpty() ? fullImage : fullImage.copy(cropRect);
+
+        qCDebug(KARTON_DEBUG) << "Sending QImage frame to save... - " << finalImage.size() << ", " << m_domain->config()->name();
+        m_domain->savePreviewFrame(finalImage.convertToFormat(QImage::Format_RGB32));
+    } else {
+        qCDebug(KARTON_DEBUG) << "saveFrameToDomain: grabWindow returned null image";
+    }
 }
+// ========================== Audio callbacks =====================
 
 void DomainViewer::playback_start_callback(SpicePlaybackChannel *channel, gint format, gint channels, gint rate, gpointer user_data)
 {
+    Q_UNUSED(channel);
+
     DomainViewer *item = static_cast<DomainViewer *>(user_data);
     qCInfo(KARTON_DEBUG) << "Audio playback starting - Format:" << format << "Channels:" << channels << "Rate:" << rate;
 
@@ -272,8 +513,9 @@ void DomainViewer::playback_start_callback(SpicePlaybackChannel *channel, gint f
 
 void DomainViewer::playback_data_callback(SpicePlaybackChannel *channel, gpointer data, gint size, gpointer user_data)
 {
-    DomainViewer *item = static_cast<DomainViewer *>(user_data);
+    Q_UNUSED(channel);
 
+    DomainViewer *item = static_cast<DomainViewer *>(user_data);
     if (item->m_audioDevice) {
         item->m_audioDevice->write(static_cast<const char *>(data), size);
     }
@@ -281,6 +523,8 @@ void DomainViewer::playback_data_callback(SpicePlaybackChannel *channel, gpointe
 
 void DomainViewer::playback_stop_callback(SpicePlaybackChannel *channel, gpointer user_data)
 {
+    Q_UNUSED(channel);
+
     DomainViewer *item = static_cast<DomainViewer *>(user_data);
     qCInfo(KARTON_DEBUG) << "Audio playback stopping";
     item->stopAudio();
@@ -297,6 +541,8 @@ void DomainViewer::stopAudio()
     m_audioDevice = nullptr;
 }
 
+// ========================== Input handling =============================
+
 // maps qt provided scancode to pcxt
 uint8_t DomainViewer::evdevToPcXt(uint32_t evdev_scancode)
 {
@@ -319,13 +565,13 @@ void DomainViewer::keyPressEvent(QKeyEvent *event)
 {
     event->accept();
     quint32 evdev_scancode;
-    if (QGuiApplication::platformName() == QStringLiteral("xcb")) { // check if x11
+    if (QGuiApplication::platformName() == QStringLiteral("xcb")) { // check if X11
         evdev_scancode = event->nativeScanCode();
     } else { // wayland probably
         evdev_scancode = event->nativeScanCode() - x11_wayland_evdev_offset;
     }
 
-    uint8_t pcxt_scancode = DomainViewer::evdevToPcXt(evdev_scancode); // spice accepts PC XT: see inputs channel docs
+    uint8_t pcxt_scancode = DomainViewer::evdevToPcXt(evdev_scancode); // SPICE accepts PC XT: see inputs channel docs
     qCDebug(KARTON_DEBUG) << "key press: " << event->text() << evdev_scancode << pcxt_scancode;
 
     if (m_inputs_channel && m_connected && pcxt_scancode != 0) {
@@ -521,7 +767,3 @@ void DomainViewer::checkChannelStatus()
     }
 }
 
-void DomainViewer::saveFrameToDomain()
-{
-    m_domain->savePreviewFrame(std::move(m_frame.copy()));
-}
diff --git a/src/domainviewer.h b/src/domainviewer.h
index 22c430b..bb66c58 100644
--- a/src/domainviewer.h
+++ b/src/domainviewer.h
@@ -3,6 +3,10 @@
 
 #pragma once
 
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
 #include <linux/input-event-codes.h>
 #include <spice-client.h>
 
@@ -44,7 +48,7 @@ public:
     QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
     bool setupSpiceSession();
     bool connectToSpice();
-    void disconnectFromSpice();
+    Q_INVOKABLE void disconnectFromSpice();
 
     void stopAudio();
 
@@ -86,8 +90,6 @@ public:
         }
     }
 
-    Q_INVOKABLE void updateImplicitDimensions();
-
 Q_SIGNALS:
     void domainChanged();
     void frameUpdated();
@@ -100,10 +102,13 @@ private Q_SLOTS:
     void handleHostPort(int exitCode, const QString &output);
 
 private:
-    static void channel_new_cb(SpiceSession *session, SpiceChannel *channel, gpointer user_data);
-    static void
-    display_primary_create_callback(SpiceChannel *channel, gint format, gint width, gint height, gint stride, gint shmid, gpointer imgdata, gpointer user_data);
-    static void display_invalidate_callback(SpiceDisplayChannel *channel, gint x, gint y, gint width, gint height, gpointer user_data);
+    static void channel_new_callback(SpiceSession *session, SpiceChannel *channel, gpointer user_data);
+    static void gl_draw_callback(SpiceDisplayChannel *channel, guint x, guint y, guint width, guint height, gpointer user_data);
+    void handleGlScanout(const SpiceGlScanout *scanout);
+    void createTextureFromScanout(const SpiceGlScanout *scanout);
+    void cleanupEGLImage();
+    void cleanupEGLResources();
+    void captureFromSceneGraph();
     static uint8_t evdevToPcXt(uint32_t evdev_scancode);
 
     static void playback_start_callback(SpicePlaybackChannel *channel, gint format, gint channels, gint rate, gpointer user_data);
@@ -111,28 +116,31 @@ private:
     static void playback_stop_callback(SpicePlaybackChannel *channel, gpointer user_data);
 
     CommandRunner *m_commandRunner;
-    QColor m_color;
     Domain *m_domain;
+    QString m_spiceUri;
     QString m_host;
-    int m_port = 0;
+    int m_port;
     QString m_password;
     bool m_connected = false;
 
-    uint m_imageWidth = 0;
-    uint m_imageHeight = 0;
-    QImage m_frame;
-    uchar *m_frameBuffer = nullptr;
-    QMutex m_frameLock;
-    bool m_frameUpdated = false;
+    int m_imageWidth;
+    int m_imageHeight;
+    SpiceGlScanout m_scanout;
+    bool m_hasScanout;
+    EGLImageKHR m_eglImage = EGL_NO_IMAGE_KHR;
+    PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR = nullptr;
+    PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES = nullptr;
+    PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR = nullptr;
+    GLuint m_texId;
 
     SpiceSession *m_session = nullptr;
     SpiceChannel *m_display_channel = nullptr;
     SpiceInputsChannel *m_inputs_channel = nullptr;
+    SpicePlaybackChannel *m_playback_channel;
 
     int m_current_button_mask = 0;
 
     SpiceAudio *m_audio;
-    SpicePlaybackChannel *m_playback_channel;
     QAudioSink *m_audioSink;
     QIODevice *m_audioDevice;
     QAudioFormat m_audioFormat;
diff --git a/src/karton.cpp b/src/karton.cpp
index 7856122..9a2f7b2 100644
--- a/src/karton.cpp
+++ b/src/karton.cpp
@@ -23,6 +23,7 @@
 Karton::Karton(QObject *parent)
     : QObject(parent)
     , m_conn(nullptr)
+    , m_domains(QVector<Domain *>())
     , m_monitor(nullptr)
     , m_domainViewer(nullptr)
     , m_currentDomain(nullptr)
@@ -49,6 +50,7 @@ Karton *Karton::self()
 
 Karton *Karton::create(QQmlEngine *qmlEngine, QJSEngine *)
 {
+    Q_UNUSED(qmlEngine);
     return Karton::self();
 }
 
diff --git a/src/karton.h b/src/karton.h
index 4106583..58bbe06 100644
--- a/src/karton.h
+++ b/src/karton.h
@@ -68,12 +68,12 @@ private Q_SLOTS:
 
 private:
     virConnectPtr m_conn;
-    CommandRunner *m_commandRunner;
     QVector<Domain *> m_domains;
     LibvirtMonitor *m_monitor;
 
     DomainViewer *m_domainViewer = nullptr;
     Domain *m_currentDomain = nullptr;
+    CommandRunner *m_commandRunner;
 
     bool init();
 };
diff --git a/src/libvirtmonitor.cpp b/src/libvirtmonitor.cpp
index 448a54e..ecad1f2 100644
--- a/src/libvirtmonitor.cpp
+++ b/src/libvirtmonitor.cpp
@@ -51,10 +51,10 @@ LibvirtMonitor::~LibvirtMonitor()
 
 int LibvirtMonitor::domainEventCallback(virConnectPtr conn, virDomainPtr dom, int event, int detail, void *opaque)
 {
-    qCInfo(KARTON_DEBUG) << "event callback!";
-    auto monitor = static_cast<LibvirtMonitor *>(opaque);
-    // const char *name = virDomainGetName(dom);
+    Q_UNUSED(conn);
 
+    qCInfo(KARTON_DEBUG) << "Libvirt event received.";
+    auto monitor = static_cast<LibvirtMonitor *>(opaque);
     Q_EMIT monitor->domainStateChanged(dom, event, detail);
 
     return 0;
diff --git a/src/osinfoconfig.cpp b/src/osinfoconfig.cpp
index 905cb1c..47083bc 100644
--- a/src/osinfoconfig.cpp
+++ b/src/osinfoconfig.cpp
@@ -44,6 +44,7 @@ OsinfoConfig *OsinfoConfig::self()
 
 OsinfoConfig *OsinfoConfig::create(QQmlEngine *qmlEngine, QJSEngine *)
 {
+    Q_UNUSED(qmlEngine);
     return OsinfoConfig::self();
 }
 
diff --git a/src/qml/VMViewerWindow.qml b/src/qml/VMViewerWindow.qml
index 603eff5..dac5add 100644
--- a/src/qml/VMViewerWindow.qml
+++ b/src/qml/VMViewerWindow.qml
@@ -9,19 +9,33 @@ import org.kde.karton
 Kirigami.ApplicationWindow {
     id: viewerWindow
     required property Domain domain
-
+    
     title: domain ? i18nc("%1 is the name of the virtual machine", "VM Viewer - %1", domain.config.name) : i18n("VM Viewer")
-
+    
     width: Kirigami.Units.gridUnit * 53
     height: Kirigami.Units.gridUnit * 36
 
     onClosing: {
         domainViewer.saveFrameToDomain();
+        domainViewer.disconnectFromSpice();
+    }
+
+    Connections {
+        target: domainViewer
+        function onImplicitWidthChanged() {
+            if (domainViewer.implicitWidth > 0)
+                viewerWindow.width = domainViewer.implicitWidth / domainViewer.dprHelper.devicePixelRatio
+        }
+        function onImplicitHeightChanged() {
+            if (domainViewer.implicitHeight > 0)
+                viewerWindow.height = domainViewer.implicitHeight / domainViewer.dprHelper.devicePixelRatio
+                                      + pageStack.globalToolBar.height
+        }
     }
 
     pageStack.initialPage: Kirigami.Page {
         title: viewerWindow.title
-        padding: 0
+        padding: 0 
 
         actions: [
             Kirigami.Action {
@@ -44,10 +58,13 @@ Kirigami.ApplicationWindow {
             }
 
             // Pre-cancel out scaling, and show VM pixels at 1:1
-            width: implicitWidth / dprHelper.devicePixelRatio
-            height: implicitHeight / dprHelper.devicePixelRatio
+            // falls back to a default size (hardcoded) until the first GL scanout sets implicitWidth/Height.
+            // fixes 0 width/height bug.
+            width: implicitWidth > 0 ? implicitWidth / dprHelper.devicePixelRatio : Kirigami.Units.gridUnit * 56.55
+            height: implicitHeight > 0 ? implicitHeight / dprHelper.devicePixelRatio : Kirigami.Units.gridUnit * 36
 
             domain: viewerWindow.domain
+
             focus: true
             activeFocusOnTab: true
             onActiveFocusChanged: {
@@ -66,4 +83,4 @@ Kirigami.ApplicationWindow {
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/vmlistmodel.cpp b/src/vmlistmodel.cpp
index eb2f8af..0e1c1ea 100644
--- a/src/vmlistmodel.cpp
+++ b/src/vmlistmodel.cpp
@@ -20,6 +20,8 @@ VMModel::VMModel(QObject *parent)
 }
 int VMModel::rowCount(const QModelIndex &parent) const
 {
+    Q_UNUSED(parent);
+
     return m_datas.size();
 }
 
@@ -36,6 +38,8 @@ VMModel *VMModel::self()
 
 VMModel *VMModel::create(QQmlEngine *qmlEngine, QJSEngine *)
 {
+    Q_UNUSED(qmlEngine);
+
     return VMModel::self();
 }
 
@@ -59,6 +63,9 @@ QHash<int, QByteArray> VMModel::roleNames() const
 
 void VMModel::onDomainsChanged(const virDomainPtr domainPtr, int event, int detail)
 {
+    Q_UNUSED(event);
+    Q_UNUSED(detail);
+
     if (domainPtr) {
         updateDomains(domainPtr);
     }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.