diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,6 +368,21 @@ HAVE_SCHED_RESET_ON_FORK "Required for running kwin_wayland with real-time scheduling") + +option(ENABLE_PIPEWIRE "Disable PipeWire support. PipeWire is needed for screen sharing and remote desktop" ON) +if(ENABLE_PIPEWIRE) + set(HAVE_PIPEWIRE_SUPPORT 1) +else() + message(STATUS "Disabling PipeWire support") + set(HAVE_PIPEWIRE_SUPPORT 0) +endif() +add_definitions(-DHAVE_PIPEWIRE_SUPPORT=${HAVE_PIPEWIRE_SUPPORT}) + +if(HAVE_PIPEWIRE_SUPPORT) + pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3) + add_feature_info(PipeWire PipeWire_FOUND "Required for screencast portal") +endif() + configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) ########### global ############### diff --git a/plugins/platforms/drm/CMakeLists.txt b/plugins/platforms/drm/CMakeLists.txt --- a/plugins/platforms/drm/CMakeLists.txt +++ b/plugins/platforms/drm/CMakeLists.txt @@ -22,6 +22,13 @@ ) endif() +if(HAVE_PIPEWIRE_SUPPORT) + set(DRM_SOURCES ${DRM_SOURCES} + screencaststream.cpp + ) +endif() + + if (HAVE_EGL_STREAMS) set(DRM_SOURCES ${DRM_SOURCES} egl_stream_backend.cpp @@ -38,6 +45,10 @@ target_link_libraries(KWinWaylandDrmBackend gbm::gbm) endif() +if(HAVE_PIPEWIRE_SUPPORT) + target_link_libraries(KWinWaylandDrmBackend PkgConfig::PipeWire) +endif() + install( TARGETS KWinWaylandDrmBackend diff --git a/plugins/platforms/drm/drm_backend.cpp b/plugins/platforms/drm/drm_backend.cpp --- a/plugins/platforms/drm/drm_backend.cpp +++ b/plugins/platforms/drm/drm_backend.cpp @@ -321,6 +321,16 @@ } initCursor(); + DrmScopedPointer resources(drmModeGetResources(m_fd)); + for (int i = 0; resources && i < resources->count_crtcs; ++i) { + const uint32_t currentCrtc = resources->crtcs[i]; + auto c = new DrmCrtc(currentCrtc, this, i); + if (m_atomicModeSetting && !c->atomicInit()) { + delete c; + continue; + } + m_crtcs << c; + } if (!updateOutputs()) return; @@ -384,28 +394,9 @@ } } - auto oldCrtcs = m_crtcs; - for (int i = 0; i < resources->count_crtcs; ++i) { - const uint32_t currentCrtc = resources->crtcs[i]; - auto it = std::find_if(m_crtcs.constBegin(), m_crtcs.constEnd(), [currentCrtc] (DrmCrtc *c) { return c->id() == currentCrtc; }); - if (it == m_crtcs.constEnd()) { - auto c = new DrmCrtc(currentCrtc, this, i); - if (m_atomicModeSetting && !c->atomicInit()) { - delete c; - continue; - } - m_crtcs << c; - } else { - oldCrtcs.removeOne(*it); - } - } - for (auto c : qAsConst(oldConnectors)) { m_connectors.removeOne(c); } - for (auto c : qAsConst(oldCrtcs)) { - m_crtcs.removeOne(c); - } QVector connectedOutputs; QVector pendingConnectors; @@ -519,7 +510,6 @@ } qDeleteAll(oldConnectors); - qDeleteAll(oldCrtcs); return true; } diff --git a/plugins/platforms/drm/egl_gbm_backend.h b/plugins/platforms/drm/egl_gbm_backend.h --- a/plugins/platforms/drm/egl_gbm_backend.h +++ b/plugins/platforms/drm/egl_gbm_backend.h @@ -21,15 +21,19 @@ #define KWIN_EGL_GBM_BACKEND_H #include "abstract_egl_backend.h" #include "remoteaccess_manager.h" +#include +#include #include struct gbm_surface; +class ScreenCastStream; namespace KWin { class DrmBackend; class DrmBuffer; +class DrmSurfaceBuffer; class DrmOutput; class GbmSurface; @@ -52,6 +56,9 @@ QRegion prepareRenderingForScreen(int screenId) override; void init() override; +Q_SIGNALS: + void passBuffer(DrmOutput *output, DrmSurfaceBuffer* gbmbuf); + protected: void present() override; void cleanupSurfaces() override; @@ -71,6 +78,7 @@ * @brief The damage history for the past 10 frames. */ QList damageHistory; + KWayland::Server::ScreencastingSource source; struct { GLuint framebuffer = 0; @@ -101,6 +109,7 @@ DrmBackend *m_backend; QVector m_outputs; + QVector m_streams; QScopedPointer m_remoteaccessManager; friend class EglGbmTexture; }; diff --git a/plugins/platforms/drm/egl_gbm_backend.cpp b/plugins/platforms/drm/egl_gbm_backend.cpp --- a/plugins/platforms/drm/egl_gbm_backend.cpp +++ b/plugins/platforms/drm/egl_gbm_backend.cpp @@ -25,7 +25,10 @@ #include "gbm_surface.h" #include "logging.h" #include "options.h" +#include "screencaststream.h" #include "screens.h" +#include "wayland_server.h" + // kwin libs #include // Qt @@ -193,6 +196,9 @@ bool EglGbmBackend::resetOutput(Output &output, DrmOutput *drmOutput) { + if (output.output) { + waylandServer()->screencasting()->removeSource(output.source); + } output.output = drmOutput; const QSize size = drmOutput->hardwareTransforms() ? drmOutput->pixelSize() : drmOutput->modeSize(); @@ -216,6 +222,34 @@ output.eglSurface = eglSurface; output.gbmSurface = gbmSurface; + auto f = [this, drmOutput] (const KWayland::Server::ScreencastingSource &videoSource, wl_resource *r) { + auto stream = new ScreenCastStream(videoSource.geometry().size(), this, this); + if (stream->init()) { + connect(stream, &ScreenCastStream::streamReady, this, [videoSource, r, stream] (int nodeid) { + waylandServer()->screencasting()->sendCreated(r, nodeid, videoSource.sourceId()); + connect(waylandServer()->screencasting(), &KWayland::Server::ScreencastingInterface::stop, stream, [stream] (uint nodeid) { + if (nodeid == stream->nodeId()) + stream->stop(); + }); + }); + connect(this, &EglGbmBackend::passBuffer, stream, [drmOutput, stream] (DrmOutput *output, DrmSurfaceBuffer* gbmbuf) { + if (drmOutput == output) { + auto bo = gbmbuf->getBo(); + stream->recordFrame(bo, gbm_bo_get_width(bo), gbm_bo_get_height(bo), gbm_bo_get_stride(bo)); + } + }); + connect(stream, &ScreenCastStream::stopStreaming, this, [videoSource, r] (int nodeid) { + waylandServer()->screencasting()->sendClosed(r, nodeid); + }); + } else { + waylandServer()->screencasting()->sendFailed(r, videoSource.sourceId(), stream->error()); + delete stream; + } + }; + const KWayland::Server::ScreencastingSource videoSource = {0, output.output->name(), QStringLiteral("video-display"), true, output.output->geometry(), f}; + output.source = videoSource; + waylandServer()->screencasting()->addSource(output.source); + resetFramebuffer(output); return true; } @@ -251,6 +285,9 @@ if (it == m_outputs.end()) { return; } + + waylandServer()->screencasting()->removeSource(it->source); + cleanupOutput(*it); m_outputs.erase(it); } @@ -441,12 +478,14 @@ void EglGbmBackend::presentOnOutput(Output &output) { eglSwapBuffers(eglDisplay(), output.eglSurface); - output.buffer = m_backend->createBuffer(output.gbmSurface); + auto surfaceBuffer = m_backend->createBuffer(output.gbmSurface); + output.buffer = surfaceBuffer; - if(m_remoteaccessManager && gbm_surface_has_free_buffers(output.gbmSurface->surface())) { + if (gbm_surface_has_free_buffers(output.gbmSurface->surface())) { // GBM surface is released on page flip so // we should pass the buffer before it's presented. - m_remoteaccessManager->passBuffer(output.output, output.buffer); + + Q_EMIT passBuffer(output.output, surfaceBuffer); } m_backend->present(output.buffer, output.output); diff --git a/plugins/platforms/drm/screencaststream.h b/plugins/platforms/drm/screencaststream.h new file mode 100644 --- /dev/null +++ b/plugins/platforms/drm/screencaststream.h @@ -0,0 +1,139 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#ifndef SCREEN_CAST_STREAM_H +#define SCREEN_CAST_STREAM_H + +#include +#include + +#include +#include +#include +#include +#include "config-kwin.h" + +#if PW_CHECK_VERSION(0, 2, 90) +#include +#endif + +#if !PW_CHECK_VERSION(0, 2, 90) +class PwType { +public: + spa_type_media_type media_type; + spa_type_media_subtype media_subtype; + spa_type_format_video format_video; + spa_type_video_format video_format; +}; +#endif + +namespace KWin +{ + class AbstractEglBackend; + class DrmOutput; + class DrmSurfaceBuffer; +} + +class ScreenCastStream : public QObject +{ + Q_OBJECT +public: + explicit ScreenCastStream(const QSize &resolution, KWin::AbstractEglBackend* backend, QObject *parent = nullptr); + ~ScreenCastStream(); + +#if PW_CHECK_VERSION(0, 2, 90) + static void onCoreError(void *data, uint32_t id, int seq, int res, const char *message); + static void onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format); +#else + static void onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error); + static void onStreamFormatChanged(void *data, const struct spa_pod *format); +#endif + static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); + static void onStreamProcess(void *data); + + // Public + bool init(); + uint framerate(); + uint nodeId(); + QString error() const { + return m_error; + } + + void removeStream(); + +#if HAVE_GBM + bool recordFrame(struct gbm_bo *bo, quint32 width, quint32 height, quint32 stride); +#endif + + void stop(); + +Q_SIGNALS: + void streamReady(uint nodeId); + void startStreaming(); + void stopStreaming(uint nodeId); + +private: + pw_stream *createStream(); +#if !PW_CHECK_VERSION(0, 2, 90) + void initializePwTypes(); +#endif + +public: +#if PW_CHECK_VERSION(0, 2, 90) + struct pw_context *pwContext = nullptr; + struct pw_core *pwCore = nullptr; + struct pw_stream *pwStream = nullptr; + struct pw_thread_loop *pwMainLoop = nullptr; + + spa_hook coreListener; + spa_hook streamListener; + + // event handlers + pw_core_events pwCoreEvents = {}; + pw_stream_events pwStreamEvents = {}; + + uint32_t pwNodeId = 0; +#else + pw_core *pwCore = nullptr; + pw_loop *pwLoop = nullptr; + pw_thread_loop *pwMainLoop = nullptr; + pw_stream *pwStream = nullptr; + pw_remote *pwRemote = nullptr; + pw_type *pwCoreType = nullptr; + PwType *pwType = nullptr; + + spa_hook remoteListener; + spa_hook streamListener; + + // event handlers + pw_remote_events pwRemoteEvents = {}; + pw_stream_events pwStreamEvents = {}; +#endif + + QSize resolution; + bool m_stopped = false; + + spa_video_info_raw videoFormat; + KWin::AbstractEglBackend* const m_backend; + QString m_error; + +}; + +#endif // SCREEN_CAST_STREAM_H diff --git a/plugins/platforms/drm/screencaststream.cpp b/plugins/platforms/drm/screencaststream.cpp new file mode 100644 --- /dev/null +++ b/plugins/platforms/drm/screencaststream.cpp @@ -0,0 +1,615 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "screencaststream.h" + +#include +#include +#include +#include + +#include +#include "drm_buffer_gbm.h" + +#include + +#include +#include +#include + +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCastStream, "xdp-kde-screencast-stream") + +class PwFraction { +public: + int num; + int denom; +}; + +// Stolen from mutter + +#define MAX_TERMS 30 +#define MIN_DIVISOR 1.0e-10 +#define MAX_ERROR 1.0e-20 + +#define PROP_RANGE(min, max) 2, (min), (max) + +#define BITS_PER_PIXEL 4 + +static int greatestCommonDivisor(int a, int b) +{ + while (b != 0) { + int temp = a; + + a = b; + b = temp % b; + } + + return qAbs(a); +} + +static PwFraction pipewireFractionFromDouble(double src) +{ + double V, F; /* double being converted */ + int N, D; /* will contain the result */ + int A; /* current term in continued fraction */ + int64_t N1, D1; /* numerator, denominator of last approx */ + int64_t N2, D2; /* numerator, denominator of previous approx */ + int i; + int gcd; + bool negative = false; + + /* initialize fraction being converted */ + F = src; + if (F < 0.0) { + F = -F; + negative = true; + } + + V = F; + /* initialize fractions with 1/0, 0/1 */ + N1 = 1; + D1 = 0; + N2 = 0; + D2 = 1; + N = 1; + D = 1; + + for (i = 0; i < MAX_TERMS; ++i) { + /* get next term */ + A = (int) F; /* no floor() needed, F is always >= 0 */ + /* get new divisor */ + F = F - A; + + /* calculate new fraction in temp */ + N2 = N1 * A + N2; + D2 = D1 * A + D2; + + /* guard against overflow */ + if (N2 > INT_MAX || D2 > INT_MAX) + break; + + N = N2; + D = D2; + + /* save last two fractions */ + N2 = N1; + D2 = D1; + N1 = N; + D1 = D; + + /* quit if dividing by zero or close enough to target */ + if (F < MIN_DIVISOR || fabs (V - ((double) N) / D) < MAX_ERROR) + break; + + /* Take reciprocal */ + F = 1 / F; + } + + /* fix for overflow */ + if (D == 0) { + N = INT_MAX; + D = 1; + } + + /* fix for negative */ + if (negative) + N = -N; + + /* simplify */ + gcd = greatestCommonDivisor(N, D); + if (gcd) { + N /= gcd; + D /= gcd; + } + + PwFraction fraction; + fraction.num = N; + fraction.denom = D; + + return fraction; +} + +void ScreenCastStream::stop() +{ + m_stopped = true; + delete this; +} + +#if PW_CHECK_VERSION(0, 2, 90) +void ScreenCastStream::onCoreError(void *data, uint32_t id, int seq, int res, const char *message) +{ + Q_UNUSED(seq) + ScreenCastStream *pw = static_cast(data); + + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "PipeWire remote error: " << message; + + if (id == PW_ID_CORE) { + if (res == -EPIPE) { + Q_EMIT pw->stopStreaming(pw->nodeId()); + } + } +} +#else +void ScreenCastStream::onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error) +{ + Q_UNUSED(old); + + ScreenCastStream *pw = static_cast(data); + + switch (state) { + case PW_REMOTE_STATE_ERROR: + // TODO notify error + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Remote error: " << error; + break; + case PW_REMOTE_STATE_CONNECTED: + // TODO notify error + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); + pw->pwStream = pw->createStream(); + if (!pw->pwStream) { + Q_EMIT pw->stopStreaming(pw->nodeId()); + } + break; + default: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); + break; + } +} +#endif + +void ScreenCastStream::onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) +{ + Q_UNUSED(old) + + ScreenCastStream *pw = static_cast(data); + +#if PW_CHECK_VERSION(0, 2, 90) + switch (state) { + case PW_STREAM_STATE_ERROR: + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_PAUSED: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + if (pw->nodeId() == 0 && pw->pwStream) { + pw->pwNodeId = pw_stream_get_node_id(pw->pwStream); + Q_EMIT pw->streamReady(pw->nodeId()); + } + if (pw->m_stopped) { + Q_EMIT pw->stopStreaming(pw->nodeId()); + } + break; + case PW_STREAM_STATE_STREAMING: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->startStreaming(); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + break; + } +#else + switch (state) { + case PW_STREAM_STATE_ERROR: + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_CONFIGURE: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->streamReady((uint)pw_stream_get_node_id(pw->pwStream)); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + case PW_STREAM_STATE_READY: + case PW_STREAM_STATE_PAUSED: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->stopStreaming(); + break; + case PW_STREAM_STATE_STREAMING: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->startStreaming(); + break; + } +#endif +} + +#if PW_CHECK_VERSION(0, 2, 90) +void ScreenCastStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format) +#else +void ScreenCastStream::onStreamFormatChanged(void *data, const struct spa_pod *format) +#endif +{ + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream format changed"; + + ScreenCastStream *pw = static_cast(data); + + uint8_t paramsBuffer[1024]; + int32_t width, height, stride, size; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + +#if PW_CHECK_VERSION(0, 2, 90) + if (!format || id != SPA_PARAM_Format) { +#else + if (!format) { + pw_stream_finish_format(pw->pwStream, 0, nullptr, 0); +#endif + return; + } + +#if PW_CHECK_VERSION(0, 2, 90) + spa_format_video_raw_parse (format, &pw->videoFormat); +#else + spa_format_video_raw_parse (format, &pw->videoFormat, &pw->pwType->format_video); +#endif + + width = pw->videoFormat.size.width; + height =pw->videoFormat.size.height; + stride = SPA_ROUND_UP_N (width * BITS_PER_PIXEL, 4); + size = height * stride; + + pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); + +#if PW_CHECK_VERSION(0, 2, 90) + params[0] = (spa_pod*) spa_pod_builder_add_object(&pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), + SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(stride, stride, INT32_MAX), + SPA_PARAM_BUFFERS_align, SPA_POD_Int(16)); + pw_stream_update_params(pw->pwStream, params, 1); +#else + params[0] = (spa_pod*) spa_pod_builder_object(&pod_builder, + pw->pwCoreType->param.idBuffers, pw->pwCoreType->param_buffers.Buffers, + ":", pw->pwCoreType->param_buffers.size, "i", size, + ":", pw->pwCoreType->param_buffers.stride, "i", stride, + ":", pw->pwCoreType->param_buffers.buffers, "iru", 16, PROP_RANGE (2, 16), + ":", pw->pwCoreType->param_buffers.align, "i", 16); + pw_stream_finish_format (pw->pwStream, 0, params, 1); +#endif +} + +ScreenCastStream::ScreenCastStream(const QSize &resolution, KWin::AbstractEglBackend* backend, QObject *parent) + : QObject(parent) + , resolution(resolution) + , m_backend(backend) +{ +#if PW_CHECK_VERSION(0, 2, 90) + pwCoreEvents.version = PW_VERSION_CORE_EVENTS; + pwCoreEvents.error = &onCoreError; + + pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + pwStreamEvents.state_changed = &onStreamStateChanged; + pwStreamEvents.param_changed = &onStreamParamChanged; +#else + // initialize event handlers, remote end and stream-related + pwRemoteEvents.version = PW_VERSION_REMOTE_EVENTS; + pwRemoteEvents.state_changed = &onStateChanged; + + pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + pwStreamEvents.state_changed = &onStreamStateChanged; + pwStreamEvents.format_changed = &onStreamFormatChanged; +#endif +} + +ScreenCastStream::~ScreenCastStream() +{ + if (pwMainLoop) { + pw_thread_loop_stop(pwMainLoop); + } + +#if !PW_CHECK_VERSION(0, 2, 90) + if (pwType) { + delete pwType; + } +#endif + + if (pwStream) { + pw_stream_destroy(pwStream); + } + +#if !PW_CHECK_VERSION(0, 2, 90) + if (pwRemote) { + pw_remote_destroy(pwRemote); + } +#endif + +#if PW_CHECK_VERSION(0, 2, 90) + if (pwCore) { + pw_core_disconnect(pwCore); + } + + if (pwContext) { + pw_context_destroy(pwContext); + } +#else + if (pwCore) { + pw_core_destroy(pwCore); + } +#endif + + if (pwMainLoop) { + pw_thread_loop_destroy(pwMainLoop); + } + +#if !PW_CHECK_VERSION(0, 2, 90) + if (pwLoop) { + pw_loop_leave(pwLoop); + pw_loop_destroy(pwLoop); + } +#endif +} + +bool ScreenCastStream::init() +{ + pw_init(nullptr, nullptr); + +#if PW_CHECK_VERSION(0, 2, 90) + pwMainLoop = pw_thread_loop_new("pipewire-main-loop", nullptr); + pwContext = pw_context_new(pw_thread_loop_get_loop(pwMainLoop), nullptr, 0); + if (!pwContext) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to create PipeWire context"; + m_error = i18n("Failed to create PipeWire context"); + return false; + } + + pwCore = pw_context_connect(pwContext, nullptr, 0); + if (!pwCore) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to connect PipeWire context"; + m_error = i18n("Failed to connect PipeWire context"); + return false; + } + + pw_core_add_listener(pwCore, &coreListener, &pwCoreEvents, this); + + pwStream = createStream(); + if (!pwStream) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to create PipeWire stream"; + m_error = i18n("Failed to create PipeWire stream"); + return false; + } +#else + pwLoop = pw_loop_new(nullptr); + pwMainLoop = pw_thread_loop_new(pwLoop, "pipewire-main-loop"); + pwCore = pw_core_new(pwLoop, nullptr); + pwCoreType = pw_core_get_type(pwCore); + pwRemote = pw_remote_new(pwCore, nullptr, 0); + + initializePwTypes(); + + pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); + pw_remote_connect(pwRemote); +#endif + + + if (pw_thread_loop_start(pwMainLoop) < 0) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to start main PipeWire loop"; + m_error = i18n("Failed to start main PipeWire loop"); + return false; + } + return true; +} + +uint ScreenCastStream::framerate() +{ + if (pwStream) { + return videoFormat.max_framerate.num / videoFormat.max_framerate.denom; + } + + return 0; +} + +uint ScreenCastStream::nodeId() +{ +#if PW_CHECK_VERSION(0, 2, 90) + return pwNodeId; +#else + if (pwStream) { + return (uint)pw_stream_get_node_id(pwStream); + } + + return 0; +#endif +} + +pw_stream *ScreenCastStream::createStream() +{ +#if !PW_CHECK_VERSION(0, 2, 90) + if (pw_remote_get_state(pwRemote, nullptr) != PW_REMOTE_STATE_CONNECTED) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Cannot create pipewire stream"; + return nullptr; + } +#endif + + uint8_t buffer[1024]; + spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + const float frameRate = 25; + + spa_fraction maxFramerate; + spa_fraction minFramerate; + const spa_pod *params[1]; + +#if PW_CHECK_VERSION(0, 2, 90) + auto stream = pw_stream_new(pwCore, "kwin-screen-cast", nullptr); +#else + auto stream = pw_stream_new(pwRemote, "kwin-screen-cast", nullptr); +#endif + + PwFraction fraction = pipewireFractionFromDouble(frameRate); + + minFramerate = SPA_FRACTION(1, 1); + maxFramerate = SPA_FRACTION((uint32_t)fraction.num, (uint32_t)fraction.denom); + + spa_rectangle minResolution = SPA_RECTANGLE(1, 1); + spa_rectangle maxResolution = SPA_RECTANGLE((uint32_t)resolution.width(), (uint32_t)resolution.height()); + + spa_fraction paramFraction = SPA_FRACTION(0, 1); + +#if PW_CHECK_VERSION(0, 2, 90) + params[0] = (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), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGBx), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&maxResolution, &minResolution, &maxResolution), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(¶mFraction), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&maxFramerate, &minFramerate, &maxFramerate)); +#else + params[0] = (spa_pod*)spa_pod_builder_object(&podBuilder, + pwCoreType->param.idEnumFormat, pwCoreType->spa_format, + "I", pwType->media_type.video, + "I", pwType->media_subtype.raw, + ":", pwType->format_video.format, "I", pwType->video_format.RGBx, + ":", pwType->format_video.size, "Rru", &maxResolution, SPA_POD_PROP_MIN_MAX(&minResolution, &maxResolution), + ":", pwType->format_video.framerate, "F", ¶mFraction, + ":", pwType->format_video.max_framerate, "Fru", &maxFramerate, PROP_RANGE (&minFramerate, &maxFramerate)); +#endif + + pw_stream_add_listener(stream, &streamListener, &pwStreamEvents, this); + + auto flags = static_cast(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_MAP_BUFFERS); + +#if PW_CHECK_VERSION(0, 2, 90) + if (pw_stream_connect(stream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, params, 1) != 0) { +#else + if (pw_stream_connect(stream, PW_DIRECTION_OUTPUT, nullptr, flags, params, 1) != 0) { +#endif + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Could not connect to stream"; + return nullptr; + } + + return stream; +} + +bool ScreenCastStream::recordFrame(gbm_bo *bo, quint32 width, quint32 height, quint32 stride) +{ + struct pw_buffer *buffer; + struct spa_buffer *spa_buffer; + uint8_t *data = nullptr; + + if (!(buffer = pw_stream_dequeue_buffer(pwStream))) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to record frame: couldn't obtain PipeWire buffer"; + return false; + } + + spa_buffer = buffer->buffer; + + if (!(data = (uint8_t *) spa_buffer->datas[0].data)) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return false; + } + + quint32 streamStride; + const quint32 minStride = SPA_ROUND_UP_N(videoFormat.size.width * BITS_PER_PIXEL, 4); + const quint32 minSrcSize = height * minStride; + + const quint32 srcSize = height * stride; + const quint32 destSize = spa_buffer->datas[0].maxsize; + + // If we can fit source into the pipewire buffer, we can use stride we got from gbm_bo as the client + // should be able to handle it + if (srcSize <= destSize) { + streamStride = stride; + // Fallback to fixed minimum stride, which should be (width * bpp) + } else if (minSrcSize == destSize) { + streamStride = minStride; + } else { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to record frame: got buffer with higher stride than we can handle"; + pw_stream_queue_buffer(pwStream, buffer); + return false; + } + + // bind context to render thread + eglMakeCurrent(m_backend->eglDisplay(), EGL_NO_SURFACE, EGL_NO_SURFACE, m_backend->context()); + + // create EGL image from imported BO + EGLImageKHR image = eglCreateImageKHR(m_backend->eglDisplay(), nullptr, EGL_NATIVE_PIXMAP_KHR, bo, nullptr); + + if (image == EGL_NO_IMAGE_KHR) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to record frame: Error creating EGLImageKHR - " << glGetError(); + pw_stream_queue_buffer(pwStream, buffer); + return false; + } + + // create GL 2D texture for framebuffer + GLuint texture; + glGenTextures(1, &texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, texture); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); + + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + + glDeleteTextures(1, &texture); + eglDestroyImageKHR(m_backend->eglDisplay(), image); + + spa_buffer->datas[0].chunk->offset = 0; + spa_buffer->datas[0].chunk->size = spa_buffer->datas[0].maxsize; + spa_buffer->datas[0].chunk->stride = streamStride; + + pw_stream_queue_buffer(pwStream, buffer); + return true; +} + +void ScreenCastStream::removeStream() +{ + // FIXME destroying streams seems to be crashing, Mutter also doesn't remove them, maybe Pipewire does this automatically + // pw_stream_destroy(pwStream); + // pwStream = nullptr; + pw_stream_disconnect(pwStream); +} + +#if !PW_CHECK_VERSION(0, 2, 90) +void ScreenCastStream::initializePwTypes() +{ + // raw C-like ScreenCastStream type map + auto map = pwCoreType->map; + + pwType = new PwType(); + + spa_type_media_type_map(map, &pwType->media_type); + spa_type_media_subtype_map(map, &pwType->media_subtype); + spa_type_format_video_map (map, &pwType->format_video); + spa_type_video_format_map (map, &pwType->video_format); +} +#endif diff --git a/scene.cpp b/scene.cpp --- a/scene.cpp +++ b/scene.cpp @@ -552,7 +552,7 @@ thumbMask |= PAINT_WINDOW_TRANSLUCENT; } QRegion clippingRegion = region; - clippingRegion &= QRegion(wImpl->x(), wImpl->y(), wImpl->width(), wImpl->height()); + clippingRegion &= QRegion(wImpl->frameGeometry()); adjustClipRegion(item, clippingRegion); effects->drawWindow(thumb, thumbMask, clippingRegion, thumbData); } @@ -585,7 +585,7 @@ const qreal y = point.y() + w->y() + (item->height() - size.height()) / 2; const QRect region = QRect(x, y, item->width(), item->height()); QRegion clippingRegion = region; - clippingRegion &= QRegion(wImpl->x(), wImpl->y(), wImpl->width(), wImpl->height()); + clippingRegion &= QRegion(wImpl->frameGeometry()); adjustClipRegion(item, clippingRegion); data += QPointF(x, y); const int desktopMask = PAINT_SCREEN_TRANSFORMED | PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST; diff --git a/toplevel.cpp b/toplevel.cpp --- a/toplevel.cpp +++ b/toplevel.cpp @@ -273,7 +273,7 @@ xcb_damage_create(connection(), damage_handle, frameId(), XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); } - damage_region = QRegion(0, 0, width(), height()); + damage_region = QRegion(rect()); effect_window = new EffectWindowImpl(this); Compositor::self()->scene()->addToplevel(this); diff --git a/wayland_server.h b/wayland_server.h --- a/wayland_server.h +++ b/wayland_server.h @@ -70,6 +70,7 @@ class LinuxDmabufUnstableV1Interface; class LinuxDmabufUnstableV1Buffer; class TabletManagerInterface; +class ScreencastingInterface; } } @@ -119,6 +120,9 @@ KWayland::Server::PlasmaWindowManagementInterface *windowManagement() { return m_windowManagement; } + KWayland::Server::ScreencastingInterface *screencasting() { + return m_screencasting; + } KWayland::Server::ServerSideDecorationManagerInterface *decorationManager() const { return m_decorationManager; } @@ -258,6 +262,7 @@ KWayland::Server::PlasmaShellInterface *m_plasmaShell = nullptr; KWayland::Server::PlasmaWindowManagementInterface *m_windowManagement = nullptr; KWayland::Server::PlasmaVirtualDesktopManagementInterface *m_virtualDesktopManagement = nullptr; + KWayland::Server::ScreencastingInterface *m_screencasting = nullptr; KWayland::Server::ServerSideDecorationManagerInterface *m_decorationManager = nullptr; KWayland::Server::OutputManagementInterface *m_outputManagement = nullptr; KWayland::Server::AppMenuManagerInterface *m_appMenuManager = nullptr; diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -448,6 +448,8 @@ m_keyState = m_display->createKeyStateInterface(m_display); m_keyState->create(); + m_screencasting = m_display->createScreencastingInterface(m_display); + return true; }