diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -374,6 +374,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 ############### @@ -520,6 +535,12 @@ xwl/xwayland_interface.cpp ) +if(HAVE_PIPEWIRE_SUPPORT) + set(kwin_SRCS ${kwin_SRCS} + screencaststream.cpp + ) +endif() + if (CMAKE_SYSTEM_NAME MATCHES "Linux") set(kwin_SRCS ${kwin_SRCS} @@ -676,6 +697,11 @@ ) target_link_libraries(kwin ${kwinLibs}) + +if(HAVE_PIPEWIRE_SUPPORT) + target_link_libraries(kwin PkgConfig::PipeWire) +endif() + generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) target_link_libraries(kwin kwinglutils ${epoxy_LIBRARY}) diff --git a/abstract_client.h b/abstract_client.h --- a/abstract_client.h +++ b/abstract_client.h @@ -860,7 +860,7 @@ /** * Return window management interface */ - KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { + KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const override { return m_windowManagementInterface; } diff --git a/libinput/connection.cpp b/libinput/connection.cpp --- a/libinput/connection.cpp +++ b/libinput/connection.cpp @@ -656,6 +656,9 @@ void Connection::applyScreenToDevice(Device *device) { #ifndef KWIN_BUILD_TESTING + if (!screens()) + return; + QMutexLocker locker(&m_mutex); if (!device->isTouch()) { return; 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; @@ -61,6 +68,8 @@ bool initBufferConfigs(); bool initRenderingContext(); void initRemotePresent(); + + void streamingRequested(KWaylandServer::ScreencastingStreamInterface* stream, ::wl_resource *output); struct Output { DrmOutput *output = nullptr; DrmBuffer *buffer = nullptr; @@ -101,6 +110,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,11 +25,15 @@ #include "gbm_surface.h" #include "logging.h" #include "options.h" +#include "screencaststream.h" #include "screens.h" +#include "wayland_server.h" + // kwin libs #include // Qt #include +#include // system #include @@ -45,6 +49,8 @@ setSyncsToVBlank(true); connect(m_backend, &DrmBackend::outputAdded, this, &EglGbmBackend::createOutput); connect(m_backend, &DrmBackend::outputRemoved, this, &EglGbmBackend::removeOutput); + + connect(waylandServer()->screencasting(), &KWaylandServer::ScreencastingInterface::outputScreencastRequested, this, &EglGbmBackend::streamingRequested); } EglGbmBackend::~EglGbmBackend() @@ -251,6 +257,7 @@ if (it == m_outputs.end()) { return; } + cleanupOutput(*it); m_outputs.erase(it); } @@ -441,12 +448,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); @@ -565,6 +574,123 @@ return true; } +static void recordFrame(AbstractEglBackend* backend, ScreenCastStream* stream, gbm_bo *bo) +{ + struct pw_buffer *buffer; + struct spa_buffer *spa_buffer; + + auto pwStream = stream->pwStream; + if (!(buffer = pw_stream_dequeue_buffer(pwStream))) { + //qCWarning(KWIN_DRM) << "Failed to record frame: couldn't obtain PipeWire buffer"; + return; + } + + spa_buffer = buffer->buffer; + + uint8_t *data = (uint8_t *) spa_buffer->datas[0].data; + if (!data) { + qCWarning(KWIN_DRM) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + const quint32 height = gbm_bo_get_height(bo); + const quint32 stride = gbm_bo_get_stride(bo); + + const quint32 minStride = SPA_ROUND_UP_N(stream->videoFormat.size.width * ScreenCastStream::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 + quint32 streamStride; + if (srcSize <= destSize) { + streamStride = stride; + // Fallback to fixed minimum stride, which should be (width * bpp) + } else if (minSrcSize == destSize) { + streamStride = minStride; + } else { + qCWarning(KWIN_DRM) << "Failed to record frame: got buffer with higher stride than we can handle"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + // bind context to render thread + eglMakeCurrent(backend->eglDisplay(), EGL_NO_SURFACE, EGL_NO_SURFACE, backend->context()); + + // create EGL image from imported BO + EGLImageKHR image = eglCreateImageKHR(backend->eglDisplay(), nullptr, EGL_NATIVE_PIXMAP_KHR, bo, nullptr); + + if (image == EGL_NO_IMAGE_KHR) { + qCWarning(KWIN_DRM) << "Failed to record frame: Error creating EGLImageKHR - " << glGetError(); + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + // 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(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); +} + +void EglGbmBackend::streamingRequested(KWaylandServer::ScreencastingStreamInterface* waylandStream, ::wl_resource *outputResource) +{ + auto output = KWaylandServer::OutputInterface::get(outputResource); + if (!output) { + waylandStream->sendFailed(i18n("Invalid output")); + return; + } + + DrmOutput* drmOutput = nullptr; + for (auto o : qAsConst(m_outputs)) { + if (o.output->waylandOutput() == output) + drmOutput = o.output; + } + if (!drmOutput) { + waylandStream->sendFailed(i18n("Could not find output")); + return; + } + + auto stream = new ScreenCastStream(output->pixelSize(), this); + if (stream->init()) { + connect(waylandStream, &KWaylandServer::ScreencastingStreamInterface::stop, stream, &ScreenCastStream::stop); + QObject::connect(stream, &ScreenCastStream::streamReady, waylandStream, [output, waylandStream] (quint32 nodeId) { + waylandStream->sendCreated(nodeId, output->pixelSize()); + }); + connect(this, &EglGbmBackend::passBuffer, stream, [this, drmOutput, stream] (DrmOutput *output, DrmSurfaceBuffer* gbmbuf) { + if (drmOutput == output) { + auto bo = gbmbuf->getBo(); + recordFrame(this, stream, bo); + } + }); + connect(stream, &ScreenCastStream::stopStreaming, this, [waylandStream, stream] () { + waylandStream->sendClosed(); + delete stream; + }); + } else { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} + /************************************************ * EglTexture ************************************************/ diff --git a/plugins/scenes/opengl/scene_opengl.h b/plugins/scenes/opengl/scene_opengl.h --- a/plugins/scenes/opengl/scene_opengl.h +++ b/plugins/scenes/opengl/scene_opengl.h @@ -145,8 +145,9 @@ class OpenGLWindowPixmap; -class OpenGLWindow final : public Scene::Window +class OpenGLWindow final : public QObject, public Scene::Window { + Q_OBJECT public: enum Leaf { ShadowLeaf, DecorationLeaf, ContentLeaf, PreviousContentLeaf }; @@ -188,6 +189,11 @@ WindowPixmap *createWindowPixmap() override; void performPaint(int mask, const QRegion ®ion, const WindowPaintData &data) override; + void startStreaming(KWaylandServer::ScreencastingStreamInterface * stream) override; + +Q_SIGNALS: + void painted(); + private: QMatrix4x4 transformation(int mask, const WindowPaintData &data) const; GLTexture *getDecorationTexture() const; diff --git a/plugins/scenes/opengl/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp --- a/plugins/scenes/opengl/scene_opengl.cpp +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include @@ -72,6 +73,8 @@ #include #include +#include "screencaststream.h" + // HACK: workaround for libepoxy < 1.3 #ifndef GL_GUILTY_CONTEXT_RESET #define GL_GUILTY_CONTEXT_RESET 0x8253 @@ -1528,8 +1531,80 @@ ShaderManager::instance()->popShader(); endRenderWindow(); + + Q_EMIT painted(); } +static void recordFrame(OpenGLWindowPixmap* pixmap, ScreenCastStream* stream) +{ + auto pwStream = stream->pwStream; + struct pw_buffer *buffer = pw_stream_dequeue_buffer(pwStream); + + if (!buffer) { + qCDebug(KWIN_OPENGL) << "Failed to record frame: couldn't obtain PipeWire buffer"; + return; + } + + struct spa_buffer *spa_buffer = buffer->buffer; + struct spa_data *spa_data = spa_buffer->datas; + + const auto s = pixmap->texture()->size(); + + uint8_t *data = (uint8_t *) spa_data[0].data; + if (!data) { + qCWarning(KWIN_OPENGL) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + if (spa_buffer->datas[0].type != SPA_DATA_DmaBuf && !data) { + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + GLint streamStride; + glGetIntegerv(GL_PACK_ROW_LENGTH, &streamStride); + if (streamStride == 0) { + streamStride = SPA_ROUND_UP_N (s.width() * ScreenCastStream::BITS_PER_PIXEL, 4); + } + if (uint(streamStride * s.height()) > spa_data->maxsize) { + qCDebug(KWIN_OPENGL) << "Failed to record frame: window is too big"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + pixmap->texture()->bind(); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + pixmap->texture()->unbind(); + + spa_data[0].chunk->offset = 0; + spa_data[0].chunk->size = spa_data[0].maxsize; + spa_data[0].chunk->stride = streamStride; + + pw_stream_queue_buffer(pwStream, buffer); +} + +void OpenGLWindow::startStreaming(KWaylandServer::ScreencastingStreamInterface * waylandStream) +{ + auto stream = new ScreenCastStream(size(), m_scene); + if (stream->init()) { + QObject::connect(waylandStream, &KWaylandServer::ScreencastingStreamInterface::stop, stream, &ScreenCastStream::stop); + QObject::connect(stream, &ScreenCastStream::streamReady, waylandStream, [this, waylandStream] (quint32 nodeId) { + waylandStream->sendCreated(nodeId, size()); + }); + connect(this, &OpenGLWindow::painted, stream, [this, stream] () { + auto pixmap = windowPixmap(); + recordFrame(pixmap, stream); + }); + QObject::connect(stream, &ScreenCastStream::stopStreaming, waylandStream, [waylandStream, stream] () { + waylandStream->sendClosed(); + delete stream; + }); + } else { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} //**************************************** // OpenGLWindowPixmap diff --git a/scene.h b/scene.h --- a/scene.h +++ b/scene.h @@ -34,6 +34,7 @@ { class BufferInterface; class SubSurfaceInterface; +class ScreencastingStreamInterface; } namespace KWin @@ -347,6 +348,8 @@ void unreferencePreviousPixmap(); void invalidateQuadsCache(); void preprocess(); + virtual void startStreaming(KWaylandServer::ScreencastingStreamInterface* stream); + protected: WindowQuadList makeDecorationQuads(const QRect *rects, const QRegion ®ion, qreal textureScale = 1.0) const; WindowQuadList makeContentsQuads() const; diff --git a/scene.cpp b/scene.cpp --- a/scene.cpp +++ b/scene.cpp @@ -85,6 +85,9 @@ #include #include #include +#include +#include +#include namespace KWin { @@ -97,6 +100,25 @@ : QObject(parent) { last_time.invalidate(); // Initialize the timer + + if (waylandServer()) { + connect(waylandServer()->screencasting(), &KWaylandServer::ScreencastingInterface::windowScreencastRequested, this, [this] (KWaylandServer::ScreencastingStreamInterface* stream, quint32 winid) { + Scene::Window *window = nullptr; + for (auto it = m_windows.constBegin(), itEnd = m_windows.constEnd(); it != itEnd; ++it) { + if (it.key()->windowManagementInterface() && it.key()->windowManagementInterface()->internalId() == winid) { + window = it.value(); + break; + } + } + + if (!window) { + stream->sendFailed(i18n("Could not find window id %1", winid)); + return; + } + + window->startStreaming(stream); + }); + } } Scene::~Scene() @@ -457,9 +479,9 @@ void Scene::windowGeometryShapeChanged(Toplevel *c) { - if (!m_windows.contains(c)) // this is ok, shape is not valid by default + Window *w = m_windows.value(c); + if (!w) // this is ok, shape is not valid by default return; - Window *w = m_windows[ c ]; w->discardShape(); } @@ -1090,6 +1112,11 @@ } } +void Scene::Window::startStreaming(KWaylandServer::ScreencastingStreamInterface *stream) +{ + stream->sendFailed(i18n("Streaming not supported")); +} + //**************************************** // WindowPixmap //**************************************** diff --git a/toplevel.h b/toplevel.h --- a/toplevel.h +++ b/toplevel.h @@ -43,6 +43,7 @@ namespace KWaylandServer { class SurfaceInterface; +class PlasmaWindowInterface; } namespace KWin @@ -343,6 +344,7 @@ * Default implementation returns same as geometry. */ virtual QRect inputGeometry() const; + virtual KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { return nullptr; } QSize size() const; QPoint pos() const; QRect rect() const; 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 @@ KWaylandServer::PlasmaWindowManagementInterface *windowManagement() { return m_windowManagement; } + KWaylandServer::ScreencastingInterface *screencasting() { + return m_screencasting; + } KWaylandServer::ServerSideDecorationManagerInterface *decorationManager() const { return m_decorationManager; } @@ -267,6 +271,7 @@ KWaylandServer::XdgDecorationManagerInterface *m_xdgDecorationManager = nullptr; KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr; QSet m_linuxDmabufBuffers; + KWaylandServer::ScreencastingInterface *m_screencasting = nullptr; struct { KWaylandServer::ClientConnection *client = nullptr; QMetaObject::Connection destroyConnection; diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -234,7 +234,7 @@ return interfaces; } - QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate"}; + QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate", "zkde_screencast_unstable_v1"}; bool allowInterface(KWaylandServer::ClientConnection *client, const QByteArray &interfaceName) override { if (client->processId() == getpid()) { @@ -446,6 +446,8 @@ m_keyState = m_display->createKeyStateInterface(m_display); m_keyState->create(); + m_screencasting = m_display->createScreencastingInterface(m_display); + return true; }