public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/karton] rawhide: Update to latest git snapshot + added upstream patch
@ 2026-08-21 21:33 Steve Cossette
  0 siblings, 0 replies; only message in thread
From: Steve Cossette @ 2026-08-21 21:33 UTC (permalink / raw)
  To: git-commits

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

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-08-21 21:33 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-21 21:33 [rpms/karton] rawhide: Update to latest git snapshot + added upstream patch Steve Cossette

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox