[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);
}