pipewirestream.cpp 19.4 KB
Newer Older
1
/*
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
2
3
4
5
6
7
    SPDX-FileCopyrightText: 2018-2020 Red Hat Inc
    SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
    SPDX-FileContributor: Jan Grulich <jgrulich@redhat.com>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/
8
9
10

#include "pipewirestream.h"
#include "cursor.h"
11
#include "dmabuftexture.h"
12
#include "eglnativefence.h"
13
14
#include "kwingltexture.h"
#include "kwinglutils.h"
15
#include "kwinscreencast_logging.h"
16
#include "main.h"
17
#include "pipewirecore.h"
18
#include "platform.h"
19
#include "utils.h"
20
21
22

#include <KLocalizedString>

23
24
25
26
27
28
29
30
31
32
33
34
#include <QLoggingCategory>
#include <QPainter>

#include <spa/buffer/meta.h>

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

namespace KWin
{

35
36
37
void PipeWireStream::onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message)
{
    PipeWireStream *pw = static_cast<PipeWireStream*>(data);
38
    qCDebug(KWIN_SCREENCAST) << "state changed"<< pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state) << error_message;
39
40
41

    switch (state) {
    case PW_STREAM_STATE_ERROR:
42
        qCWarning(KWIN_SCREENCAST) << "Stream error: " << error_message;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        break;
    case PW_STREAM_STATE_PAUSED:
        if (pw->nodeId() == 0 && pw->pwStream) {
            pw->pwNodeId = pw_stream_get_node_id(pw->pwStream);
            Q_EMIT pw->streamReady(pw->nodeId());
        }
        break;
    case PW_STREAM_STATE_STREAMING:
        Q_EMIT pw->startStreaming();
        break;
    case PW_STREAM_STATE_CONNECTING:
        break;
    case PW_STREAM_STATE_UNCONNECTED:
        if (!pw->m_stopped) {
            Q_EMIT pw->stopStreaming();
        }
        break;
    }
}

#define CURSOR_BPP	4
#define CURSOR_META_SIZE(w,h)	(sizeof(struct spa_meta_cursor) + \
				 sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)

void PipeWireStream::newStreamParams()
{
    const int bpp = videoFormat.format == SPA_VIDEO_FORMAT_RGB || videoFormat.format == SPA_VIDEO_FORMAT_BGR ? 3 : 4;
    auto stride = SPA_ROUND_UP_N (m_resolution.width() * bpp, 4);

    uint8_t paramsBuffer[1024];
    spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer));

    spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height()));
76
    const int cursorSize = Cursors::self()->currentCursor()->themeSize() * m_cursor.scale;
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
    const spa_pod *params[] = {
        (spa_pod*) spa_pod_builder_add_object(&pod_builder,
                                              SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
                                              SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution),
                                              SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16),
                                              SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1),
                                              SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride),
                                              SPA_PARAM_BUFFERS_size, SPA_POD_Int(stride * m_resolution.height()),
                                              SPA_PARAM_BUFFERS_align, SPA_POD_Int(16)),
        (spa_pod*) spa_pod_builder_add_object (&pod_builder,
                                               SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
                                               SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor),
                                               SPA_PARAM_META_size, SPA_POD_Int (CURSOR_META_SIZE (cursorSize, cursorSize)))
    };
    pw_stream_update_params(pwStream, params, 2);
}

void PipeWireStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format)
{
    if (!format || id != SPA_PARAM_Format) {
        return;
    }

    PipeWireStream *pw = static_cast<PipeWireStream *>(data);
    spa_format_video_raw_parse (format, &pw->videoFormat);
102
    qCDebug(KWIN_SCREENCAST) << "Stream format changed" << pw << pw->videoFormat.format;
103
104
105
106
107
108
109
110
111
112
113
    pw->newStreamParams();
}

void PipeWireStream::onStreamAddBuffer(void *data, pw_buffer *buffer)
{
    PipeWireStream *stream = static_cast<PipeWireStream *>(data);
    struct spa_data *spa_data = buffer->buffer->datas;

    spa_data->mapoffset = 0;
    spa_data->flags = SPA_DATA_FLAG_READWRITE;

114
    QSharedPointer<DmaBufTexture> dmabuf(kwinApp()->platform()->createDmaBufTexture(stream->m_resolution));
115
116
117
    if (dmabuf) {
      spa_data->type = SPA_DATA_DmaBuf;
      spa_data->fd = dmabuf->fd();
118
      spa_data->data = nullptr;
119
120
121
      spa_data->maxsize = dmabuf->stride() * stream->m_resolution.height();

      stream->m_dmabufDataForPwBuffer.insert(buffer, dmabuf);
122
#ifdef F_SEAL_SEAL //Disable memfd on systems that don't have it, like BSD < 12
123
124
125
126
127
128
129
    } else {
        const int bytesPerPixel = stream->m_hasAlpha ? 4 : 3;
        const int stride = SPA_ROUND_UP_N (stream->m_resolution.width() * bytesPerPixel, 4);
        spa_data->maxsize = stride * stream->m_resolution.height();
        spa_data->type = SPA_DATA_MemFd;
        spa_data->fd = memfd_create("kwin-screencast-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
        if (spa_data->fd == -1) {
130
            qCCritical(KWIN_SCREENCAST) << "memfd: Can't create memfd";
131
132
133
134
135
            return;
        }
        spa_data->mapoffset = 0;

        if (ftruncate (spa_data->fd, spa_data->maxsize) < 0) {
136
            qCCritical(KWIN_SCREENCAST) << "memfd: Can't truncate to" << spa_data->maxsize;
137
138
139
140
141
            return;
        }

        unsigned int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
        if (fcntl(spa_data->fd, F_ADD_SEALS, seals) == -1)
142
            qCWarning(KWIN_SCREENCAST) << "memfd: Failed to add seals";
143

144
        spa_data->data = mmap(nullptr,
145
146
147
148
149
150
                              spa_data->maxsize,
                              PROT_READ | PROT_WRITE,
                              MAP_SHARED,
                              spa_data->fd,
                              spa_data->mapoffset);
        if (spa_data->data == MAP_FAILED)
151
            qCCritical(KWIN_SCREENCAST) << "memfd: Failed to mmap memory";
152
        else
153
            qCDebug(KWIN_SCREENCAST) << "memfd: created successfully" << spa_data->data << spa_data->maxsize;
154
#endif
155
156
157
158
159
160
    }
}

void PipeWireStream::onStreamRemoveBuffer(void *data, pw_buffer *buffer)
{
    PipeWireStream *stream = static_cast<PipeWireStream *>(data);
161
    stream->m_dmabufDataForPwBuffer.remove(buffer);
162
163
164

    struct spa_buffer *spa_buffer = buffer->buffer;
    struct spa_data *spa_data = spa_buffer->datas;
165
    if (spa_data && spa_data->type == SPA_DATA_MemFd) {
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
        munmap (spa_data->data, spa_data->maxsize);
        close (spa_data->fd);
    }
}

PipeWireStream::PipeWireStream(bool hasAlpha, const QSize &resolution, QObject *parent)
    : QObject(parent)
    , m_resolution(resolution)
    , m_hasAlpha(hasAlpha)
{
    pwStreamEvents.version = PW_VERSION_STREAM_EVENTS;
    pwStreamEvents.add_buffer = &PipeWireStream::onStreamAddBuffer;
    pwStreamEvents.remove_buffer = &PipeWireStream::onStreamRemoveBuffer;
    pwStreamEvents.state_changed = &PipeWireStream::onStreamStateChanged;
    pwStreamEvents.param_changed = &PipeWireStream::onStreamParamChanged;
}

PipeWireStream::~PipeWireStream()
{
    m_stopped = true;
    if (pwStream) {
        pw_stream_destroy(pwStream);
    }
}

bool PipeWireStream::init()
{
    pwCore = PipeWireCore::self();
    if (!pwCore->m_error.isEmpty()) {
        m_error = pwCore->m_error;
        return false;
    }

    connect(pwCore.data(), &PipeWireCore::pipewireFailed, this, &PipeWireStream::coreFailed);

    if (!createStream()) {
202
        qCWarning(KWIN_SCREENCAST) << "Failed to create PipeWire stream";
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
        m_error = i18n("Failed to create PipeWire stream");
        return false;
    }

    return true;
}

uint PipeWireStream::framerate()
{
    if (pwStream) {
        return videoFormat.max_framerate.num / videoFormat.max_framerate.denom;
    }

    return 0;
}

uint PipeWireStream::nodeId()
{
    return pwNodeId;
}

bool PipeWireStream::createStream()
{
    const QByteArray objname = "kwin-screencast-" + objectName().toUtf8();
    pwStream = pw_stream_new(pwCore->pwCore, objname, nullptr);

    uint8_t buffer[1024];
    spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
    spa_fraction minFramerate = SPA_FRACTION(1, 1);
    spa_fraction maxFramerate = SPA_FRACTION(25, 1);
    spa_fraction defaultFramerate = SPA_FRACTION(0, 1);

    spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height()));

237
238
239
240
    auto canCreateDmaBuf = [this] () -> bool {
        return QSharedPointer<DmaBufTexture>(kwinApp()->platform()->createDmaBufTexture(m_resolution));
    };
    const auto format = m_hasAlpha || canCreateDmaBuf() ? SPA_VIDEO_FORMAT_BGRA : SPA_VIDEO_FORMAT_BGR;
241
242
243
244
245

    const spa_pod *param = (spa_pod*)spa_pod_builder_add_object(&podBuilder,
                                        SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
                                        SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
                                        SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
246
                                        SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(format == SPA_VIDEO_FORMAT_BGRA ? 3 : 2,
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
247
                                                                                        format, format, format == SPA_VIDEO_FORMAT_BGRA ? SPA_VIDEO_FORMAT_BGRx : SPA_VIDEO_FORMAT_UNKNOWN),
248
249
250
251
252
253
254
255
                                        SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution),
                                        SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&defaultFramerate),
                                        SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&maxFramerate, &minFramerate, &maxFramerate));

    pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this);
    auto flags = pw_stream_flags(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS);

    if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, &param, 1) != 0) {
256
        qCWarning(KWIN_SCREENCAST) << "Could not connect to stream";
257
        pw_stream_destroy(pwStream);
258
        pwStream = nullptr;
259
260
261
        return false;
    }

262
    if (m_cursor.mode == KWaylandServer::ScreencastV1Interface::Embedded) {
263
        connect(Cursors::self(), &Cursors::positionChanged, this, [this] {
264
265
            if (m_cursor.lastFrameTexture) {
                m_repainting = true;
266
                recordFrame(m_cursor.lastFrameTexture.data(), QRegion{m_cursor.lastRect} | cursorGeometry(Cursors::self()->currentCursor()));
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
                m_repainting = false;
            }
        });
    }

    return true;
}
void PipeWireStream::coreFailed(const QString &errorMessage)
{
    m_error = errorMessage;
    Q_EMIT stopStreaming();
}

void PipeWireStream::stop()
{
    m_stopped = true;
    delete this;
}

286
static GLTexture *copyTexture(GLTexture *texture)
287
{
288
    GLTexture *copy = new GLTexture(texture->internalFormat(), texture->size());
289
290
291
292
293
294
295
296
297
298
299
    copy->setFilter(GL_LINEAR);
    copy->setWrapMode(GL_CLAMP_TO_EDGE);

    const QRect r({}, texture->size());

    copy->bind();
    glCopyTextureSubImage2D(copy->texture(), 0, 0, 0, 0, 0, r.width(), r.height());
    copy->unbind();
    return copy;
}

300
void PipeWireStream::recordFrame(GLTexture *frameTexture, const QRegion &damagedRegion)
301
302
303
304
{
    Q_ASSERT(!m_stopped);
    Q_ASSERT(frameTexture);

305
306
307
308
309
    if (m_pendingBuffer) {
        qCWarning(KWIN_SCREENCAST) << "Dropping a screencast frame because the compositor is slow";
        return;
    }

310
311
312
313
314
315
316
317
318
319
    if (frameTexture->size() != m_resolution) {
        m_resolution = frameTexture->size();
        newStreamParams();
        return;
    }

    const char *error = "";
    auto state = pw_stream_get_state(pwStream, &error);
    if (state != PW_STREAM_STATE_STREAMING) {
        if (error) {
320
            qCWarning(KWIN_SCREENCAST) << "Failed to record frame: stream is not active" << error;
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
        }
        return;
    }

    struct pw_buffer *buffer = pw_stream_dequeue_buffer(pwStream);

    if (!buffer) {
        return;
    }

    struct spa_buffer *spa_buffer = buffer->buffer;
    struct spa_data *spa_data = spa_buffer->datas;

    uint8_t *data = (uint8_t *) spa_data->data;
    if (!data && spa_buffer->datas->type != SPA_DATA_DmaBuf) {
336
        qCWarning(KWIN_SCREENCAST) << "Failed to record frame: invalid buffer data";
337
338
339
340
341
342
343
344
345
346
347
348
        pw_stream_queue_buffer(pwStream, buffer);
        return;
    }

    const auto size = frameTexture->size();
    spa_data->chunk->offset = 0;
    if (data) {
        const int bpp = data && !m_hasAlpha ? 3 : 4;
        const uint stride = SPA_ROUND_UP_N (size.width() * bpp, 4);
        const uint bufferSize = stride * size.height();

        if (bufferSize > spa_data->maxsize) {
349
            qCDebug(KWIN_SCREENCAST) << "Failed to record frame: frame is too big";
350
351
352
353
354
355
356
357
358
            pw_stream_queue_buffer(pwStream, buffer);
            return;
        }

        spa_data->chunk->size = bufferSize;
        spa_data->chunk->stride = stride;

        frameTexture->bind();
        glGetTextureImage(frameTexture->texture(), 0, m_hasAlpha ? GL_BGRA : GL_BGR, GL_UNSIGNED_BYTE, bufferSize, data);
359
        auto cursor = Cursors::self()->currentCursor();
360
        if (m_cursor.mode == KWaylandServer::ScreencastV1Interface::Embedded && m_cursor.viewport.contains(cursor->pos())) {
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
            QImage dest(data, size.width(), size.height(), QImage::Format_RGBA8888_Premultiplied);
            QPainter painter(&dest);
            const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale;
            painter.drawImage(QRect{position, cursor->image().size()}, cursor->image());
        }
    } else {
        auto &buf = m_dmabufDataForPwBuffer[buffer];

        spa_data->chunk->stride = buf->stride();
        spa_data->chunk->size = spa_data->maxsize;

        GLRenderTarget::pushRenderTarget(buf->framebuffer());
        frameTexture->bind();

        QRect r(QPoint(), size);
        auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture);

        QMatrix4x4 mvp;
        mvp.ortho(r);
        shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp);

        QRegion dr = damagedRegion;
        if (m_cursor.texture) {
            dr |= m_cursor.lastRect;
        }

        frameTexture->render(damagedRegion, r, true);

389
        auto cursor = Cursors::self()->currentCursor();
390
        if (m_cursor.mode == KWaylandServer::ScreencastV1Interface::Embedded && m_cursor.viewport.contains(cursor->pos())) {
391
392
393
394
            if (!m_repainting) //We need to copy the last version of the stream to render the moved cursor on top
                m_cursor.lastFrameTexture.reset(copyTexture(frameTexture));

            if (!m_cursor.texture || m_cursor.lastKey != cursor->image().cacheKey())
395
                m_cursor.texture.reset(new GLTexture(cursor->image()));
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415

            m_cursor.texture->setYInverted(false);
            m_cursor.texture->bind();
            const auto cursorRect = cursorGeometry(cursor);
            mvp.translate(cursorRect.left(), r.height() - cursorRect.top() - cursor->image().height() * m_cursor.scale);
            shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp);

            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            m_cursor.texture->render(cursorRect, cursorRect, true);
            glDisable(GL_BLEND);
            m_cursor.texture->unbind();
            m_cursor.lastRect = cursorRect;
        }
        ShaderManager::instance()->popShader();

        GLRenderTarget::popRenderTarget();
    }
    frameTexture->unbind();

416
    if (m_cursor.mode == KWaylandServer::ScreencastV1Interface::Metadata) {
417
        sendCursorData(Cursors::self()->currentCursor(),
418
419
420
                        (spa_meta_cursor *) spa_buffer_find_meta_data (spa_buffer, SPA_META_Cursor, sizeof (spa_meta_cursor)));
    }

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    tryEnqueue(buffer);
}

void PipeWireStream::tryEnqueue(pw_buffer *buffer)
{
    m_pendingBuffer = buffer;

    // The GPU doesn't necessarily process draw commands as soon as they are issued. Thus,
    // we need to insert a fence into the command stream and enqueue the pipewire buffer
    // only after the fence is signaled; otherwise stream consumers will most likely see
    // a corrupted buffer.
    if (kwinApp()->platform()->supportsNativeFence()) {
        Q_ASSERT_X(eglGetCurrentContext(), "tryEnqueue", "no current context");
        m_pendingFence = new EGLNativeFence(kwinApp()->platform()->sceneEglDisplay());
        if (!m_pendingFence->isValid()) {
            qCWarning(KWIN_SCREENCAST) << "Failed to create a native EGL fence";
            glFinish();
            enqueue();
        } else {
            m_pendingNotifier = new QSocketNotifier(m_pendingFence->fileDescriptor(),
                                                    QSocketNotifier::Read, this);
            connect(m_pendingNotifier, &QSocketNotifier::activated, this, &PipeWireStream::enqueue);
        }
    } else {
        // The compositing backend doesn't support native fences. We don't have any other choice
        // but stall the graphics pipeline. Otherwise stream consumers may see an incomplete buffer.
        glFinish();
        enqueue();
    }
}

void PipeWireStream::enqueue()
{
    Q_ASSERT_X(m_pendingBuffer, "enqueue", "pending buffer must be valid");

    delete m_pendingFence;
    delete m_pendingNotifier;

    pw_stream_queue_buffer(pwStream, m_pendingBuffer);

    m_pendingBuffer = nullptr;
    m_pendingFence = nullptr;
    m_pendingNotifier = nullptr;
464
465
}

466
QRect PipeWireStream::cursorGeometry(Cursor *cursor) const
467
468
469
470
471
{
    const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale;
    return QRect{position, m_cursor.texture->size()};
}

472
void PipeWireStream::sendCursorData(Cursor *cursor, spa_meta_cursor *spa_meta_cursor)
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
{
    if (!cursor || !spa_meta_cursor) {
        return;
    }

    const auto position = (cursor->pos() - m_cursor.viewport.topLeft()) * m_cursor.scale;

    spa_meta_cursor->id = 1;
    spa_meta_cursor->position.x = position.x();
    spa_meta_cursor->position.y = position.y();
    spa_meta_cursor->hotspot.x = cursor->hotspot().x() * m_cursor.scale;
    spa_meta_cursor->hotspot.y = cursor->hotspot().y() * m_cursor.scale;
    spa_meta_cursor->bitmap_offset = 0;

    const QImage image = cursor->image();
    if (image.cacheKey() == m_cursor.lastKey) {
        return;
    }

    m_cursor.lastKey = image.cacheKey();
    spa_meta_cursor->bitmap_offset = sizeof (struct spa_meta_cursor);

    struct spa_meta_bitmap *spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor,
                                                          spa_meta_cursor->bitmap_offset,
                                                          struct spa_meta_bitmap);
    spa_meta_bitmap->format = SPA_VIDEO_FORMAT_RGBA;
    spa_meta_bitmap->offset = sizeof (struct spa_meta_bitmap);

    uint8_t *bitmap_data = SPA_MEMBER (spa_meta_bitmap, spa_meta_bitmap->offset, uint8_t);
502
503
504
505
    const int bufferSideSize = Cursors::self()->currentCursor()->themeSize() * m_cursor.scale;
    QImage dest(bitmap_data, std::min(bufferSideSize, image.width()), std::min(bufferSideSize, image.height()), QImage::Format_RGBA8888_Premultiplied);
    spa_meta_bitmap->size.width = dest.width();
    spa_meta_bitmap->size.height = dest.height();
506
507
    spa_meta_bitmap->stride = dest.bytesPerLine();

508
509
510
511
512
    if (image.isNull()) {
        return;
    }

    dest.fill(Qt::transparent);
513
514
515
516
    QPainter painter(&dest);
    painter.drawImage(QPoint(), image);
}

517
void PipeWireStream::setCursorMode(KWaylandServer::ScreencastV1Interface::CursorMode mode, qreal scale, const QRect &viewport)
518
519
520
521
522
{
    m_cursor.mode = mode;
    m_cursor.scale = scale;
    m_cursor.viewport = viewport;
}
523
524

} // namespace KWin