public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Steve Cossette <farchord@gmail.com>
To: git-commits@fedoraproject.org
Subject: [rpms/karton] rawhide: Update to latest git snapshot + added upstream patch
Date: Fri, 21 Aug 2026 21:33:02 GMT [thread overview]
Message-ID: <178734798271.1.10794625759998318274.rpms-karton-40cd9b0adc8d@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/karton
Branch : rawhide
Commit : 40cd9b0adc8debe1f348f1e9ea7e5acafe0720fa
Author : Steve Cossette <farchord@gmail.com>
Date : 2026-08-21T17:32:55-04:00
Stats : +995/-4 in 4 file(s)
URL : https://src.fedoraproject.org/rpms/karton/c/40cd9b0adc8debe1f348f1e9ea7e5acafe0720fa?branch=rawhide
Log:
Update to latest git snapshot + added upstream patch
---
diff --git a/.gitignore b/.gitignore
index 4a270ab..98af00e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/karton-b318dca300c7a39a80b4a21d979c45c869ceac76.tar.gz
/karton-07520e06f5a66e9cfe33a298a11e5482a2395aa0.tar.gz
/karton-a426132cf31da3b5a4c109cafba02cc87d0ad27a.tar.gz
+/karton-556458ecac0c03418ba36611bf7430957bf40afb.tar.gz
diff --git a/63.patch b/63.patch
new file mode 100644
index 0000000..5c5af2c
--- /dev/null
+++ b/63.patch
@@ -0,0 +1,982 @@
+From 95e5debedffb96cb31313b116ae9f7d6850e6f9f Mon Sep 17 00:00:00 2001
+From: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+Date: Tue, 18 Aug 2026 01:27:22 +0300
+Subject: [PATCH 1/4] Fix GL scanout texture handling
+
+Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+---
+ src/glscanoutrenderer.cpp | 148 +++++++++++++++++++++++++-------------
+ src/glscanoutrenderer.h | 15 ++++
+ 2 files changed, 115 insertions(+), 48 deletions(-)
+
+diff --git a/src/glscanoutrenderer.cpp b/src/glscanoutrenderer.cpp
+index ec1b66a..6a32c52 100644
+--- a/src/glscanoutrenderer.cpp
++++ b/src/glscanoutrenderer.cpp
+@@ -23,8 +23,14 @@ void GlScanoutRenderer::attach(SpiceDisplayChannel *channel)
+ {
+ detach();
+ m_channel = channel;
++ // gl-draw means a new frame in the existing buffer, gl-scanout means a new buffer
+ m_glDrawHandlerId = g_signal_connect(channel, "gl-draw", G_CALLBACK(gl_draw_callback), this);
++ m_glScanoutHandlerId = g_signal_connect(channel, "notify::gl-scanout", G_CALLBACK(gl_scanout_notify_callback), this);
+ // TODO: Might need to check if gl is enabled. Domains created by virt-manager are not accel3d by default.
++
++ if (auto scanout = spice_display_channel_get_gl_scanout(channel)) {
++ handleGlScanout(scanout);
++ }
+ }
+
+ void GlScanoutRenderer::detach()
+@@ -32,7 +38,11 @@ void GlScanoutRenderer::detach()
+ if (m_channel && m_glDrawHandlerId) {
+ g_signal_handler_disconnect(m_channel, m_glDrawHandlerId);
+ }
++ if (m_channel && m_glScanoutHandlerId) {
++ g_signal_handler_disconnect(m_channel, m_glScanoutHandlerId);
++ }
+ m_glDrawHandlerId = 0;
++ m_glScanoutHandlerId = 0;
+ m_channel = nullptr;
+
+ cleanupEGLResources();
+@@ -46,40 +56,55 @@ void GlScanoutRenderer::gl_draw_callback(SpiceDisplayChannel *channel, guint x,
+ Q_UNUSED(height);
+
+ auto *self = static_cast<GlScanoutRenderer *>(user_data);
+- auto scanout = spice_display_channel_get_gl_scanout(channel);
+- if (!scanout) {
+- return;
+- }
+- self->handleGlScanout(scanout);
++ Q_EMIT self->frameReady();
+ spice_display_channel_gl_draw_done(channel); // releases the GL resource
+ }
+
+-void GlScanoutRenderer::handleGlScanout(const SpiceGlScanout *scanout)
++void GlScanoutRenderer::gl_scanout_notify_callback(GObject *object, GParamSpec *pspec, gpointer user_data)
+ {
+- if (m_hasScanout && m_scanout.fd >= 0) {
+- close(m_scanout.fd);
+- m_scanout.fd = -1;
++ Q_UNUSED(pspec);
++
++ auto *self = static_cast<GlScanoutRenderer *>(user_data);
++ if (auto scanout = spice_display_channel_get_gl_scanout(SPICE_DISPLAY_CHANNEL(object))) {
++ self->handleGlScanout(scanout);
+ }
++}
+
+- cleanupEGLImage();
++void GlScanoutRenderer::handleGlScanout(const SpiceGlScanout *scanout)
++{
++ bool sizeChanged = false;
+
+- m_scanout = *scanout; // struct copy
++ {
++ QMutexLocker locker(&m_scanoutLock);
+
+- // 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;
++ if (m_hasScanout && m_scanout.fd >= 0) {
++ close(m_scanout.fd);
++ m_scanout.fd = -1;
+ }
+- }
+
+- m_imageHeight = scanout->height;
+- m_imageWidth = scanout->width;
+- m_hasScanout = true;
++ m_scanout = *scanout; // struct copy
++
++ // 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;
++ }
++ }
+
+- Q_EMIT frameSizeChanged();
++ m_scanoutDirty = true;
++
++ sizeChanged = m_imageWidth != static_cast<int>(scanout->width) || m_imageHeight != static_cast<int>(scanout->height);
++ m_imageHeight = scanout->height;
++ m_imageWidth = scanout->width;
++ m_hasScanout = true;
++ }
++
++ if (sizeChanged) {
++ Q_EMIT frameSizeChanged();
++ }
+ Q_EMIT frameReady();
+ }
+
+@@ -107,6 +132,9 @@ void GlScanoutRenderer::createTextureFromScanout(const SpiceGlScanout *scanout)
+ return;
+ }
+
++ // not in handleGlScanout(): no EGL display is current there, so the destroy is skipped
++ cleanupEGLImage();
++
+ // generate texture if empty
+ if (m_texId == 0) {
+ gl->glGenTextures(1, &m_texId);
+@@ -155,7 +183,7 @@ void GlScanoutRenderer::createTextureFromScanout(const SpiceGlScanout *scanout)
+
+ 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_MIN_FILTER, GL_LINEAR);
+ 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);
+@@ -182,8 +210,14 @@ void GlScanoutRenderer::createTextureFromScanout(const SpiceGlScanout *scanout)
+ QSGNode *GlScanoutRenderer::updatePaintNode(QQuickWindow *window, QSGNode *oldNode, const QRectF &bounds)
+ {
+ // create opengl texture from scanout data
+- if (m_hasScanout && m_texId == 0) {
+- createTextureFromScanout(&m_scanout);
++ QSize frameSize;
++ {
++ QMutexLocker locker(&m_scanoutLock);
++ if (m_hasScanout && (m_texId == 0 || m_scanoutDirty)) {
++ createTextureFromScanout(&m_scanout);
++ m_scanoutDirty = false;
++ }
++ frameSize = QSize(m_imageWidth, m_imageHeight);
+ }
+
+ if (!m_texId) {
+@@ -196,6 +230,8 @@ QSGNode *GlScanoutRenderer::updatePaintNode(QQuickWindow *window, QSGNode *oldNo
+ if (!textureNode) {
+ textureNode = new QSGSimpleTextureNode();
+ textureNode->setOwnsTexture(true);
++ // the node defaults to nearest, which aliases badly once the frame is scaled
++ textureNode->setFiltering(QSGTexture::Linear);
+ }
+
+ QOpenGLContext *context = QOpenGLContext::currentContext();
+@@ -207,7 +243,7 @@ QSGNode *GlScanoutRenderer::updatePaintNode(QQuickWindow *window, QSGNode *oldNo
+ 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_MIN_FILTER, GL_LINEAR);
+ gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->glBindTexture(GL_TEXTURE_2D, 0);
+ }
+@@ -220,32 +256,41 @@ QSGNode *GlScanoutRenderer::updatePaintNode(QQuickWindow *window, QSGNode *oldNo
+ 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;
+- }
++ // the GL texture id is stable, so only rebuild the wrappers when the frame size changes
++ if (!m_rhiTexture || m_rhiTextureSize != frameSize || !textureNode->texture()) {
++ QRhiTexture *rhiTexture = rhi->newTexture(QRhiTexture::RGBA8, frameSize);
++ 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;
++ // 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;
+- }
++ 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;
++ QSGTexture *texture = window->createTextureFromRhiTexture(rhiTexture, options);
++ if (!texture) {
++ qCDebug(KARTON_DEBUG) << "Failed to create QSG texture";
++ delete rhiTexture;
++ return textureNode;
++ }
++
++ // the node owns the QSG wrapper, the QRhiTexture underneath is ours to free
++ QRhiTexture *previous = m_rhiTexture;
++ textureNode->setTexture(texture);
++ delete previous;
++
++ m_rhiTexture = rhiTexture;
++ m_rhiTextureSize = frameSize;
+ }
+
+- textureNode->setTexture(texture);
+ textureNode->setRect(bounds);
+
+ qCDebug(KARTON_DEBUG) << "GlScanoutRenderer: Successfully updated canvas.";
+@@ -258,14 +303,21 @@ QSGNode *GlScanoutRenderer::updatePaintNode(QQuickWindow *window, QSGNode *oldNo
+ // cleans up scanout and egl image, textures
+ void GlScanoutRenderer::cleanupEGLResources()
+ {
++ QMutexLocker locker(&m_scanoutLock);
++
+ if (m_hasScanout && m_scanout.fd >= 0) {
+ close(m_scanout.fd);
+ m_scanout.fd = -1;
+ }
+ m_hasScanout = false;
++ m_scanoutDirty = false;
+
+ cleanupEGLImage();
+
++ delete m_rhiTexture;
++ m_rhiTexture = nullptr;
++ m_rhiTextureSize = QSize();
++
+ QOpenGLContext *context = QOpenGLContext::currentContext();
+ if (m_texId && context) {
+ QOpenGLFunctions *gl = context->functions();
+diff --git a/src/glscanoutrenderer.h b/src/glscanoutrenderer.h
+index 5484956..abc9529 100644
+--- a/src/glscanoutrenderer.h
++++ b/src/glscanoutrenderer.h
+@@ -10,6 +10,11 @@
+ #include <GLES2/gl2.h>
+ #include <GLES2/gl2ext.h>
+
++#include <QMutex>
++#include <QSize>
++
++#include <rhi/qrhi.h>
++
+ // hardware-accel renderer
+ class GlScanoutRenderer : public SpiceDisplayRenderer
+ {
+@@ -26,11 +31,13 @@ public:
+
+ QSize frameSize() const override
+ {
++ QMutexLocker locker(&m_scanoutLock);
+ return QSize(m_imageWidth, m_imageHeight);
+ }
+
+ private:
+ static void gl_draw_callback(SpiceDisplayChannel *channel, guint x, guint y, guint width, guint height, gpointer user_data);
++ static void gl_scanout_notify_callback(GObject *object, GParamSpec *pspec, gpointer user_data);
+ void handleGlScanout(const SpiceGlScanout *scanout);
+ void createTextureFromScanout(const SpiceGlScanout *scanout);
+ void cleanupEGLImage();
+@@ -38,14 +45,22 @@ private:
+
+ SpiceDisplayChannel *m_channel = nullptr;
+ gulong m_glDrawHandlerId = 0;
++ gulong m_glScanoutHandlerId = 0;
+
++ // guards the scanout against the render thread importing it while it is being replaced
++ mutable QMutex m_scanoutLock;
+ int m_imageWidth = 0;
+ int m_imageHeight = 0;
+ SpiceGlScanout m_scanout = {};
+ bool m_hasScanout = false;
++ bool m_scanoutDirty = false;
+ EGLImageKHR m_eglImage = EGL_NO_IMAGE_KHR;
+ PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR = nullptr;
+ PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES = nullptr;
+ PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR = nullptr;
+ GLuint m_texId = 0;
++
++ // cached across frames: rebuilding these per paint leaks GPU memory until the driver dies
++ QRhiTexture *m_rhiTexture = nullptr;
++ QSize m_rhiTextureSize;
+ };
+--
+GitLab
+
+
+From 8496ceeafa0d9ac665071406f61cdc734428a7b5 Mon Sep 17 00:00:00 2001
+From: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+Date: Tue, 18 Aug 2026 01:27:22 +0300
+Subject: [PATCH 2/4] Add a SPICE agent channel so guests can be resized
+
+Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+---
+ src/domainxmlbuilder.cpp | 59 ++++++++++++++++++++++++++++++++++++++++
+ src/domainxmlbuilder.h | 16 +++++++++++
+ src/karton.cpp | 35 ++++++++++++++++++++++++
+ src/karton.h | 1 +
+ 4 files changed, 111 insertions(+)
+
+diff --git a/src/domainxmlbuilder.cpp b/src/domainxmlbuilder.cpp
+index 851f298..b75cc2b 100644
+--- a/src/domainxmlbuilder.cpp
++++ b/src/domainxmlbuilder.cpp
+@@ -209,6 +209,14 @@ QString DomainXmlBuilder::generateXML(virConnectPtr conn, const DomainConfig *co
+
+ addConsoleDevices(document, devices, {.type = QStringLiteral("pty")});
+
++ // devices->controller and devices->channel elements
++ // virtio-serial port for spice-vdagent, required for the guest to follow the viewer size
++ addControllerDevices(document, devices, {.type = QStringLiteral("virtio-serial"), .index = QStringLiteral("0")});
++
++ addChannelDevices(document,
++ devices,
++ {.type = QStringLiteral("spicevmc"), .targetType = QStringLiteral("virtio"), .targetName = QStringLiteral("com.redhat.spice.0")});
++
+ // write to file
+ QString xmlString = document.toString(4);
+
+@@ -246,6 +254,37 @@ QString DomainXmlBuilder::generateDiskXML(const DiskDeviceConfig &config)
+ return document.toString(4);
+ }
+
++QString DomainXmlBuilder::generateSpiceAgentChannelXML()
++{
++ QDomDocument document;
++ QDomElement devices = document.createElement(QStringLiteral("devices"));
++ // libvirt adds the virtio-serial controller itself
++ addChannelDevices(document,
++ devices,
++ {.type = QStringLiteral("spicevmc"), .targetType = QStringLiteral("virtio"), .targetName = QStringLiteral("com.redhat.spice.0")});
++ document.appendChild(devices.firstChildElement());
++
++ return document.toString(4);
++}
++
++bool DomainXmlBuilder::hasSpiceAgentChannel(const QString &xmlDesc)
++{
++ QDomDocument document;
++ if (!document.setContent(xmlDesc)) {
++ return false;
++ }
++
++ const QDomNodeList channels = document.documentElement().elementsByTagName(QStringLiteral("channel"));
++ for (int i = 0; i < channels.count(); ++i) {
++ const QDomElement target = channels.at(i).toElement().firstChildElement(QStringLiteral("target"));
++ if (target.attribute(QStringLiteral("name")) == QStringLiteral("com.redhat.spice.0")) {
++ return true;
++ }
++ }
++
++ return false;
++}
++
+ void DomainXmlBuilder::addHardwareElements(QDomDocument &document, QDomElement &root, int maxRam, int cpus)
+ {
+ QMap<QString, QString> mem;
+@@ -395,6 +434,26 @@ void DomainXmlBuilder::addConsoleDevices(QDomDocument &document, QDomElement &pa
+ console.setAttribute(QStringLiteral("type"), config.type);
+ }
+
++void DomainXmlBuilder::addControllerDevices(QDomDocument &document, QDomElement &parent, const ControllerConfig &config)
++{
++ QDomElement controller = document.createElement(QStringLiteral("controller"));
++ parent.appendChild(controller);
++ controller.setAttribute(QStringLiteral("type"), config.type);
++ controller.setAttribute(QStringLiteral("index"), config.index);
++}
++
++void DomainXmlBuilder::addChannelDevices(QDomDocument &document, QDomElement &parent, const ChannelConfig &config)
++{
++ QDomElement channel = document.createElement(QStringLiteral("channel"));
++ parent.appendChild(channel);
++ channel.setAttribute(QStringLiteral("type"), config.type);
++
++ QMap<QString, QString> target;
++ target[QStringLiteral("type")] = config.targetType;
++ target[QStringLiteral("name")] = config.targetName;
++ addElementWithAttributes(document, channel, QStringLiteral("target"), QString(), target);
++}
++
+ // Temporarily: generate a random mac address (in unicast)...
+ // eventually generate a network domain.
+ QString DomainXmlBuilder::genMac()
+diff --git a/src/domainxmlbuilder.h b/src/domainxmlbuilder.h
+index 3532e62..e00b0e5 100644
+--- a/src/domainxmlbuilder.h
++++ b/src/domainxmlbuilder.h
+@@ -39,6 +39,9 @@ public:
+ QString updateXML(const QString &xmlDesc, const EditableConfig &config);
+ QString generateDiskXML(const DiskDeviceConfig &config);
+
++ QString generateSpiceAgentChannelXML();
++ static bool hasSpiceAgentChannel(const QString &xmlDesc);
++
+ private:
+ struct NetworkInterfaceConfig {
+ QString type;
+@@ -82,6 +85,17 @@ private:
+ QString type;
+ };
+
++ struct ControllerConfig {
++ QString type;
++ QString index;
++ };
++
++ struct ChannelConfig {
++ QString type;
++ QString targetType;
++ QString targetName;
++ };
++
+ void addDiskDevices(QDomDocument &document, QDomElement &parent, const DiskDeviceConfig &config);
+ void addNetworkInterfaceDevices(QDomDocument &document, QDomElement &parent, const NetworkInterfaceConfig &config);
+ void addGraphicsDevices(QDomDocument &document, QDomElement &parent, const GraphicsConfig &config);
+@@ -90,6 +104,8 @@ private:
+ void addVideoDevices(QDomDocument &document, QDomElement &parent, const VideoConfig &config);
+ void addInputDevices(QDomDocument &document, QDomElement &parent, const InputConfig &config);
+ void addConsoleDevices(QDomDocument &document, QDomElement &parent, const ConsoleConfig &config);
++ void addControllerDevices(QDomDocument &document, QDomElement &parent, const ControllerConfig &config);
++ void addChannelDevices(QDomDocument &document, QDomElement &parent, const ChannelConfig &config);
+ void addHardwareElements(QDomDocument &document, QDomElement &root, int maxRam, int cpus);
+
+ QString genMac();
+diff --git a/src/karton.cpp b/src/karton.cpp
+index 4137d49..0073002 100644
+--- a/src/karton.cpp
++++ b/src/karton.cpp
+@@ -4,6 +4,7 @@
+ #include "karton.h"
+
+ #include <libvirt/libvirt.h>
++#include <libvirt/virterror.h>
+
+ #include <KLocalizedString>
+ #include <QDir>
+@@ -243,6 +244,9 @@ QVector<Domain *> Karton::domains()
+ bool Karton::startDomain(const Domain *domain)
+ {
+ virDomainPtr domainPtr = domain->domainPtr();
++
++ ensureSpiceAgentChannel(domain);
++
+ int result = virDomainCreate(domainPtr);
+
+ if (result < 0) {
+@@ -255,6 +259,37 @@ bool Karton::startDomain(const Domain *domain)
+ return true;
+ }
+
++// attached while the domain is inactive, so it applies on the boot that follows
++void Karton::ensureSpiceAgentChannel(const Domain *domain)
++{
++ virDomainPtr domainPtr = domain->domainPtr();
++ if (virDomainIsActive(domainPtr) != 0) {
++ return;
++ }
++
++ char *xmlDesc = virDomainGetXMLDesc(domainPtr, VIR_DOMAIN_XML_INACTIVE);
++ if (!xmlDesc) {
++ qCWarning(KARTON_DEBUG) << "Could not read configuration to check for a SPICE agent channel:" << domain->config()->name();
++ return;
++ }
++ const QString desc = QString::fromUtf8(xmlDesc);
++ free(xmlDesc);
++
++ if (DomainXmlBuilder::hasSpiceAgentChannel(desc)) {
++ return;
++ }
++
++ DomainXmlBuilder builder;
++ const QString fragment = builder.generateSpiceAgentChannelXML();
++ // only warns: a missing agent channel must never stop a VM from starting
++ if (virDomainAttachDeviceFlags(domainPtr, fragment.toUtf8().constData(), VIR_DOMAIN_AFFECT_CONFIG) < 0) {
++ qCWarning(KARTON_DEBUG) << "Could not add a SPICE agent channel to" << domain->config()->name() << ":" << virGetLastErrorMessage();
++ return;
++ }
++
++ qCInfo(KARTON_DEBUG) << "Added a SPICE agent channel to" << domain->config()->name();
++}
++
+ bool Karton::stopDomain(const Domain *domain)
+ {
+ virDomainPtr domainPtr = domain->domainPtr();
+diff --git a/src/karton.h b/src/karton.h
+index 945f31a..22128f6 100644
+--- a/src/karton.h
++++ b/src/karton.h
+@@ -76,4 +76,5 @@ private:
+ CommandRunner *m_commandRunner;
+
+ bool init();
++ void ensureSpiceAgentChannel(const Domain *domain);
+ };
+--
+GitLab
+
+
+From 1af65834cfddd0bb81bab477b9524db6569af67a Mon Sep 17 00:00:00 2001
+From: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+Date: Tue, 18 Aug 2026 01:27:22 +0300
+Subject: [PATCH 3/4] Send the viewer size to the guest over the SPICE main
+ channel
+
+Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+---
+ src/domainviewer.cpp | 113 +++++++++++++++++++++++++++++++++++++++++--
+ src/domainviewer.h | 24 +++++++++
+ 2 files changed, 134 insertions(+), 3 deletions(-)
+
+diff --git a/src/domainviewer.cpp b/src/domainviewer.cpp
+index 0a4b3dd..851c449 100644
+--- a/src/domainviewer.cpp
++++ b/src/domainviewer.cpp
+@@ -10,6 +10,8 @@
+ #include <QString>
+ #include <QUrl>
+
++#include <algorithm>
++
+ #include "domain.h"
+ #include "glib.h"
+ #include "glscanoutrenderer.h"
+@@ -42,6 +44,12 @@ DomainViewer::DomainViewer(QQuickItem *parent)
+ setAcceptHoverEvents(true);
+ setFlag(ItemIsFocusScope, true);
+
++ // coalesce a resize drag into a single guest modeset
++ m_resizeDebounce = new QTimer(this);
++ m_resizeDebounce->setSingleShot(true);
++ m_resizeDebounce->setInterval(1000);
++ connect(m_resizeDebounce, &QTimer::timeout, this, &DomainViewer::sendGuestResize);
++
+ connect(m_commandRunner, &CommandRunner::commandFinished, this, &DomainViewer::handleHostPort);
+ }
+
+@@ -159,12 +167,24 @@ void DomainViewer::disconnectFromSpice()
+ m_renderer.reset();
+ }
+
++ m_resizeDebounce->stop();
++ m_lastRequestedGuestSize = QSize();
++
++ if (m_main_channel && m_agentNotifyId) {
++ g_signal_handler_disconnect(m_main_channel, m_agentNotifyId);
++ }
++ m_agentNotifyId = 0;
++ m_main_channel = nullptr;
++
++ m_agentConnected = false;
++
+ if (m_session) {
+ spice_session_disconnect(m_session);
+
+ g_object_unref(m_session);
+ m_session = nullptr;
+ m_display_channel = nullptr;
++ m_inputs_channel = nullptr;
+ m_audio = nullptr;
+ m_playback_channel = nullptr;
+ m_connected = false;
+@@ -178,7 +198,10 @@ void DomainViewer::channel_new_callback(SpiceSession *session, SpiceChannel *cha
+ DomainViewer *item = static_cast<DomainViewer *>(user_data);
+
+ item->checkChannelStatus(); // uncomment for channel debug msgs
+- if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
++ if (SPICE_IS_MAIN_CHANNEL(channel)) {
++ qCInfo(KARTON_DEBUG) << "SPICE: main channel connected";
++ item->attachMainChannel(channel);
++ } else if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
+ qCInfo(KARTON_DEBUG) << "SPICE display connected";
+ item->attachDisplayChannel(channel);
+ } else if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+@@ -194,14 +217,98 @@ void DomainViewer::channel_new_callback(SpiceSession *session, SpiceChannel *cha
+ g_signal_connect(channel, "playback-data", G_CALLBACK(playback_data_callback), item);
+ g_signal_connect(channel, "playback-stop", G_CALLBACK(playback_stop_callback), item);
+ } else {
+- qCWarning(KARTON_DEBUG) << "Unrecognised SPICE channel type";
++ qCDebug(KARTON_DEBUG) << "Unhandled SPICE channel type";
+ }
+ }
+
+ // ========================== Display rendering ========================
+
++void DomainViewer::attachMainChannel(SpiceChannel *channel)
++{
++ m_main_channel = SPICE_MAIN_CHANNEL(channel);
++ m_agentNotifyId = g_signal_connect(channel, "notify::agent-connected", G_CALLBACK(&DomainViewer::main_agent_connected_callback), this);
++
++ spice_channel_connect(channel);
++ updateAgentConnected();
++}
++
++void DomainViewer::main_agent_connected_callback(GObject *object, GParamSpec *pspec, gpointer user_data)
++{
++ Q_UNUSED(object);
++ Q_UNUSED(pspec);
++
++ static_cast<DomainViewer *>(user_data)->updateAgentConnected();
++}
++
++void DomainViewer::updateAgentConnected()
++{
++ if (!m_main_channel) {
++ return;
++ }
++
++ gboolean connected = FALSE;
++ g_object_get(m_main_channel, "agent-connected", &connected, nullptr);
++
++ if (static_cast<bool>(connected) == m_agentConnected) {
++ return;
++ }
++ m_agentConnected = connected;
++ qCInfo(KARTON_DEBUG) << "SPICE guest agent connected:" << m_agentConnected;
++
++ // forget the last request so the size is pushed again after a guest reboot
++ m_lastRequestedGuestSize = QSize();
++
++ if (m_agentConnected) {
++ sendGuestResize();
++ }
++}
++
++void DomainViewer::setAvailableArea(const QSizeF &area)
++{
++ if (m_availableArea == area) {
++ return;
++ }
++
++ m_availableArea = area;
++ Q_EMIT availableAreaChanged();
++
++ m_resizeDebounce->start();
++}
++
++void DomainViewer::sendGuestResize()
++{
++ if (!m_main_channel || !m_agentConnected || !m_connected) {
++ return;
++ }
++ if (!window() || m_availableArea.width() <= 0 || m_availableArea.height() <= 0) {
++ return;
++ }
++
++ // & ~1 keeps the size even, which the video encode path prefers
++ const qreal dpr = window()->devicePixelRatio();
++ const int width = std::max(static_cast<int>(qRound(m_availableArea.width() * dpr)) & ~1, minimumGuestSize.width());
++ const int height = std::max(static_cast<int>(qRound(m_availableArea.height() * dpr)) & ~1, minimumGuestSize.height());
++
++ const QSize requested(width, height);
++ if (requested == m_lastRequestedGuestSize) {
++ return;
++ }
++ m_lastRequestedGuestSize = requested;
++
++ qCInfo(KARTON_DEBUG) << "Requesting guest resolution" << requested;
++ // without the enable call the display stays undefined and nothing is sent at all
++ spice_main_channel_update_display_enabled(m_main_channel, m_displayId, TRUE, FALSE);
++ // sent by hand, letting spice-gtk send it costs another second
++ spice_main_channel_update_display(m_main_channel, m_displayId, 0, 0, width, height, FALSE);
++ spice_main_channel_send_monitor_config(m_main_channel);
++}
++
+ void DomainViewer::attachDisplayChannel(SpiceChannel *channel)
+ {
++ gint channelId = 0;
++ g_object_get(channel, "channel-id", &channelId, nullptr);
++ m_displayId = channelId;
++
+ spice_channel_connect(channel);
+ m_display_channel = channel;
+
+@@ -442,7 +549,7 @@ void DomainViewer::hoverMoveEvent(QHoverEvent *event)
+ {
+ static int hoverCounter = 0;
+ if (++hoverCounter % 20 == 0) {
+- qCInfo(KARTON_DEBUG) << "Mouse hover at (" << event->position().x() << "," << event->position().y() << ")";
++ qCDebug(KARTON_DEBUG) << "Mouse hover at (" << event->position().x() << "," << event->position().y() << ")";
+ }
+ if (m_inputs_channel && m_connected) {
+ qreal x = event->position().x();
+diff --git a/src/domainviewer.h b/src/domainviewer.h
+index 5f93400..3bf2b54 100644
+--- a/src/domainviewer.h
++++ b/src/domainviewer.h
+@@ -14,6 +14,7 @@
+ #include <QAudioFormat>
+ #include <QAudioSink>
+ #include <QIODevice>
++#include <QTimer>
+
+ #include <memory>
+
+@@ -28,6 +29,7 @@ class DomainViewer : public QQuickItem
+ Q_PROPERTY(Domain *domain READ domain WRITE setDomain NOTIFY domainChanged REQUIRED)
+ Q_PROPERTY(QString host MEMBER m_host NOTIFY hostChanged)
+ Q_PROPERTY(int port MEMBER m_port NOTIFY portChanged)
++ Q_PROPERTY(QSizeF availableArea READ availableArea WRITE setAvailableArea NOTIFY availableAreaChanged)
+
+ public:
+ explicit DomainViewer(QQuickItem *parent = nullptr);
+@@ -60,6 +62,12 @@ public:
+
+ Q_INVOKABLE void saveFrameToDomain();
+
++ QSizeF availableArea() const
++ {
++ return m_availableArea;
++ }
++ void setAvailableArea(const QSizeF &area);
++
+ QString host() const
+ {
+ return m_host;
+@@ -91,6 +99,7 @@ Q_SIGNALS:
+
+ void portChanged();
+ void hostChanged();
++ void availableAreaChanged();
+
+ private Q_SLOTS:
+ void handleHostPort(int exitCode, const QString &output);
+@@ -103,7 +112,11 @@ private:
+ };
+
+ static void channel_new_callback(SpiceSession *session, SpiceChannel *channel, gpointer user_data);
++ static void main_agent_connected_callback(GObject *object, GParamSpec *pspec, gpointer user_data);
+ void attachDisplayChannel(SpiceChannel *channel);
++ void attachMainChannel(SpiceChannel *channel);
++ void updateAgentConnected();
++ void sendGuestResize();
+ static uint8_t evdevToPcXt(uint32_t evdev_scancode);
+
+ static void playback_start_callback(SpicePlaybackChannel *channel, gint format, gint channels, gint rate, gpointer user_data);
+@@ -125,6 +138,17 @@ private:
+ SpiceChannel *m_display_channel = nullptr;
+ SpiceInputsChannel *m_inputs_channel = nullptr;
+ SpicePlaybackChannel *m_playback_channel;
++ SpiceMainChannel *m_main_channel = nullptr;
++
++ gulong m_agentNotifyId = 0;
++ bool m_agentConnected = false;
++ int m_displayId = 0;
++ QTimer *m_resizeDebounce = nullptr;
++ QSize m_lastRequestedGuestSize;
++ QSizeF m_availableArea;
++
++ // same floor virt-viewer uses; a zero size makes spice-gtk drop the config without a word
++ static constexpr QSize minimumGuestSize = {320, 200};
+
+ int m_current_button_mask = 0;
+
+--
+GitLab
+
+
+From d835d9b94ed03ab8c221f5c1f055dc3ad2662a5e Mon Sep 17 00:00:00 2001
+From: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+Date: Tue, 18 Aug 2026 01:27:22 +0300
+Subject: [PATCH 4/4] Scale the guest display to fit the viewer window
+
+Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
+---
+ src/primarysurfacerenderer.cpp | 17 +++---
+ src/qml/VMViewerWindow.qml | 95 +++++++++++++++++++++++-----------
+ 2 files changed, 75 insertions(+), 37 deletions(-)
+
+diff --git a/src/primarysurfacerenderer.cpp b/src/primarysurfacerenderer.cpp
+index 4ffc8cb..2ad5045 100644
+--- a/src/primarysurfacerenderer.cpp
++++ b/src/primarysurfacerenderer.cpp
+@@ -94,7 +94,7 @@ QSGNode *PrimarySurfaceRenderer::updatePaintNode(QQuickWindow *window, QSGNode *
+ {
+ QMutexLocker locker(&m_frameLock);
+
+- if (!m_frameUpdated || m_frame.isNull() || m_frame.width() <= 0 || m_frame.height() <= 0) {
++ if (m_frame.isNull() || m_frame.width() <= 0 || m_frame.height() <= 0) {
+ delete oldNode;
+ return nullptr;
+ }
+@@ -103,14 +103,19 @@ QSGNode *PrimarySurfaceRenderer::updatePaintNode(QQuickWindow *window, QSGNode *
+ if (!node) {
+ node = new QSGSimpleTextureNode();
+ node->setOwnsTexture(true);
++ // the node defaults to nearest, which aliases badly once the frame is scaled
++ node->setFiltering(QSGTexture::Linear);
+ }
+
+- QSGTexture *texture = window->createTextureFromImage(m_frame);
+- if (texture) {
+- node->setTexture(texture);
+- node->setRect(bounds);
+- m_frameUpdated = false;
++ // a resize repaints without a new frame, so keep showing the last one rather than blanking
++ if (m_frameUpdated || !node->texture()) {
++ if (QSGTexture *texture = window->createTextureFromImage(m_frame)) {
++ node->setTexture(texture);
++ m_frameUpdated = false;
++ }
+ }
+
++ node->setRect(bounds);
++
+ return node;
+ }
+diff --git a/src/qml/VMViewerWindow.qml b/src/qml/VMViewerWindow.qml
+index dac5add..c7404f0 100644
+--- a/src/qml/VMViewerWindow.qml
++++ b/src/qml/VMViewerWindow.qml
+@@ -3,12 +3,14 @@
+
+ import QtQuick
+ import QtQuick.Controls as Controls
++import QtQuick.Window
+ import org.kde.kirigami as Kirigami
+ import org.kde.karton
+
+ Kirigami.ApplicationWindow {
+ id: viewerWindow
+ required property Domain domain
++ property bool initialSizeApplied: false
+
+ title: domain ? i18nc("%1 is the name of the virtual machine", "VM Viewer - %1", domain.config.name) : i18n("VM Viewer")
+
+@@ -20,16 +22,28 @@ Kirigami.ApplicationWindow {
+ domainViewer.disconnectFromSpice();
+ }
+
++ function applyInitialSize() {
++ if (initialSizeApplied
++ || viewerWindow.visibility !== Window.Windowed
++ || domainViewer.implicitWidth <= 0
++ || domainViewer.implicitHeight <= 0) {
++ return
++ }
++ initialSizeApplied = true
++
++ const dpr = domainViewer.dprHelper.devicePixelRatio
++ viewerWindow.width = Math.min(Screen.desktopAvailableWidth, domainViewer.implicitWidth / dpr)
++ viewerWindow.height = Math.min(Screen.desktopAvailableHeight,
++ domainViewer.implicitHeight / dpr + pageStack.globalToolBar.height)
++ }
++
+ Connections {
+ target: domainViewer
+ function onImplicitWidthChanged() {
+- if (domainViewer.implicitWidth > 0)
+- viewerWindow.width = domainViewer.implicitWidth / domainViewer.dprHelper.devicePixelRatio
++ viewerWindow.applyInitialSize()
+ }
+ function onImplicitHeightChanged() {
+- if (domainViewer.implicitHeight > 0)
+- viewerWindow.height = domainViewer.implicitHeight / domainViewer.dprHelper.devicePixelRatio
+- + pageStack.globalToolBar.height
++ viewerWindow.applyInitialSize()
+ }
+ }
+
+@@ -39,7 +53,10 @@ Kirigami.ApplicationWindow {
+
+ actions: [
+ Kirigami.Action {
+- icon.name: "view-fullscreen"
++ text: viewerWindow.visibility === Window.FullScreen ? i18n("Exit Full Screen") : i18n("Full Screen")
++ icon.name: viewerWindow.visibility === Window.FullScreen ? "view-restore" : "view-fullscreen"
++ checkable: true
++ checked: viewerWindow.visibility === Window.FullScreen
+ onTriggered: {
+ if (viewerWindow.visibility === Window.FullScreen) {
+ viewerWindow.showNormal()
+@@ -50,35 +67,51 @@ Kirigami.ApplicationWindow {
+ }
+ ]
+
+- DomainViewer {
+- id: domainViewer
++ Rectangle {
++ id: viewerArea
+
+- property DevicePixelRatioHelper dprHelper: DevicePixelRatioHelper {
+- window: domainViewer.Window.window
+- }
++ anchors.fill: parent
++ color: "black"
+
+- // Pre-cancel out scaling, and show VM pixels at 1:1
+- // 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
++ DomainViewer {
++ id: domainViewer
+
+- domain: viewerWindow.domain
++ anchors.centerIn: parent
+
+- focus: true
+- activeFocusOnTab: true
+- onActiveFocusChanged: {
+- console.log("DomainViewer focus changed to:", activeFocus)
+- }
+- onFocusChanged: {
+- console.log("DomainViewer focus property changed to:", focus)
+- }
+- MouseArea {
+- anchors.fill: parent
+- onPressed: {
+- console.log("MouseArea click. giving focus to domainviewer")
+- parent.forceActiveFocus()
+- mouse.accepted = false
++ property DevicePixelRatioHelper dprHelper: DevicePixelRatioHelper {
++ window: domainViewer.Window.window
++ }
++
++ readonly property real nativeWidth: implicitWidth > 0 ? implicitWidth / dprHelper.devicePixelRatio : 0
++ readonly property real nativeHeight: implicitHeight > 0 ? implicitHeight / dprHelper.devicePixelRatio : 0
++ readonly property real fitScale: nativeWidth > 0 && nativeHeight > 0
++ ? Math.min(viewerArea.width / nativeWidth, viewerArea.height / nativeHeight)
++ : 1.0
++
++ // the container size, not the fitted size, or the guest never fills the window
++ availableArea: Qt.size(viewerArea.width, viewerArea.height)
++
++ // sized rather than scaled, the mouse mapping divides by width()
++ width: nativeWidth > 0 ? Math.round(nativeWidth * fitScale) : Kirigami.Units.gridUnit * 56.55
++ height: nativeHeight > 0 ? Math.round(nativeHeight * fitScale) : Kirigami.Units.gridUnit * 36
++
++ domain: viewerWindow.domain
++
++ focus: true
++ activeFocusOnTab: true
++ onActiveFocusChanged: {
++ console.log("DomainViewer focus changed to:", activeFocus)
++ }
++ onFocusChanged: {
++ console.log("DomainViewer focus property changed to:", focus)
++ }
++ MouseArea {
++ anchors.fill: parent
++ onPressed: (mouse) => {
++ console.log("MouseArea click. giving focus to domainviewer")
++ parent.forceActiveFocus()
++ mouse.accepted = false
++ }
+ }
+ }
+ }
+--
+GitLab
+
diff --git a/karton.spec b/karton.spec
index dd00e61..67a811d 100644
--- a/karton.spec
+++ b/karton.spec
@@ -1,5 +1,5 @@
-%global gitcommit a426132cf31da3b5a4c109cafba02cc87d0ad27a
-%global gitdate 20260805.143350
+%global gitcommit 556458ecac0c03418ba36611bf7430957bf40afb
+%global gitdate 20260818.104436
%global shortcommit %(c=%{gitcommit}; echo ${c:0:7})
Name: karton
@@ -9,12 +9,17 @@ Summary: A Libvirt-based Virtual Machine Manager for KDE
License: BSD-2-Clause AND CC-BY-SA-4.0 AND CC0-1.0 AND GPL-3.0-or-later
# It'll change at some point
-URL: https://invent.kde.org/sitter/%{name}
+URL: https://invent.kde.org/system/%{name}
Source0: %{url}/-/archive/%{gitcommit}/%{name}-%{gitcommit}.tar.gz
# Downstream Patches
Patch0: hwaccel-default-off.patch
+# Upstream Patches
+# Resize the guest display to follow the viewer window
+# https://invent.kde.org/system/karton/-/merge_requests/63
+Patch100: 63.patch
+
# qemu isn't available on 32-bit
ExcludeArch: %{ix86}
@@ -76,6 +81,9 @@ desktop-file-validate %{buildroot}/%{_datadir}/applications/org.kde.karton.deskt
%{_kf6_datadir}/qlogging-categories6/karton.categories
%changelog
+* Fri Aug 21 2026 Steve Cossette <farchord@gmail.com> - 0.1^20260818.104436.556458e-1
+- Latest git snapshot + upstream patch
+
* Mon Aug 17 2026 Steve Cossette <farchord@gmail.com> - 0.1^20260805.143350.a426132-1
- Updated to a newer git commit
diff --git a/sources b/sources
index db75602..f0a76c6 100644
--- a/sources
+++ b/sources
@@ -1 +1 @@
-SHA512 (karton-a426132cf31da3b5a4c109cafba02cc87d0ad27a.tar.gz) = be92ceec7097a6c2f4746e335e9023a250e095dc603713053b3321ae069ade2d5e9afe9e30e5926e5952a3d12ef5aca5af53692d5a428b6bf462627205fc8aed
+SHA512 (karton-556458ecac0c03418ba36611bf7430957bf40afb.tar.gz) = a6a2f18aefbbcfbd05a620f85cb189c8dc152e57306d16a1669e89c0bff816190c3e6b196cec06e0935defc5f5e8123474b95abf591b57ea43c57bd4a32eedae
reply other threads:[~2026-08-21 21:33 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178734798271.1.10794625759998318274.rpms-karton-40cd9b0adc8d@fedoraproject.org \
--to=farchord@gmail.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox