From c898f96df3e3a2d418ca511fdb6610cf087ca464 Mon Sep 17 00:00:00 2001 From: Erik Kurzinger Date: Mon, 15 Apr 2019 07:26:22 -0700 Subject: [PATCH] [platforms/drm] EGLStream DRM Backend Initial Implementation Summary: This is the initial implementation of a DRM backend based on the EGLDevice, EGLOutput, and EGLStream extensions, supporting NVIDIA graphics hardware using their proprietary driver. The new backend will be used if the environment variable KWIN_DRM_USE_EGL_STREAMS is set. On initialization, it will attempt to create an EGLDevice based on the DRM device currently in use and create EGLOutputs and EGLStreams for any attached displays. These are used to control presentation of the final composited frame. Additionally, it will register the wl_eglstream_controller Wayland interface so that native EGL windows created by clients can be attached to an EGLStream allowing buffer contents to be shared with the compositor as a GL texture. At this time there are two known bugs in the NVIDIA driver's EGL implementation affecting desktop functionality. The first can result in tooltip windows drawn by plasmashell to contain incorrect contents. The second prevents KWayland from being able to query the format of EGLStream-backed buffers which interferes with the blur effect. Fixes for both of these are currently in development and should appear in an upcoming NVIDIA driver release. Additionally, hardware cursors are currently not supported with this backend. Enabling them causes the desktop to intermittently hang for several seconds. This is also likely a bug in the NVIDIA DRM-KMS implementation but the root cause is still under investigation. Test Plan: On a system with an NVIDIA graphics card running a recent release of their proprietary driver * Ensure the nvidia_drm kernel module is loaded with the option "modeset=1" ("# cat /sys/module/nvidia_drm/parameters/modeset" should print "Y") * Ensure EGL external platform support is installed https://github.com/NVIDIA/eglexternalplatform * Ensure KWin was build with the CMake option KWIN_BUILD_EGL_STREAM_BACKEND=ON (this is the default) * Start a plasma wayland session with the environment variable KWIN_DRM_USE_EGL_STREAMS set * Ensure output from KWin OpenGL initialization indicates the NVIDIA EGL driver is in use (as opposed to Mesa / llvmpipe). * Desktop should be fully functional and perform smoothly. Reviewers: #kwin, romangg, davidedmundson Reviewed By: #kwin, romangg, davidedmundson Subscribers: kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D18570 --- CMakeLists.txt | 5 + config-kwin.h.cmake | 1 + plugins/platforms/drm/CMakeLists.txt | 6 + plugins/platforms/drm/drm_backend.cpp | 42 +- plugins/platforms/drm/drm_backend.h | 16 +- plugins/platforms/drm/drm_output.cpp | 17 +- plugins/platforms/drm/drm_output.h | 7 + plugins/platforms/drm/egl_stream_backend.cpp | 692 +++++++++++++++++++ plugins/platforms/drm/egl_stream_backend.h | 116 ++++ workspace.cpp | 6 + 10 files changed, 902 insertions(+), 6 deletions(-) create mode 100644 plugins/platforms/drm/egl_stream_backend.cpp create mode 100644 plugins/platforms/drm/egl_stream_backend.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d0a0ace190..0e788a88d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,11 @@ if(HAVE_DRM AND gbm_FOUND) set(HAVE_GBM TRUE) endif() +option(KWIN_BUILD_EGL_STREAM_BACKEND "Enable building of EGLStream based DRM backend" ON) +if(HAVE_DRM AND KWIN_BUILD_EGL_STREAM_BACKEND) + set(HAVE_EGL_STREAMS TRUE) +endif() + find_package(libhybris) set_package_properties(libhybris PROPERTIES TYPE OPTIONAL PURPOSE "Required for libhybris backend") set(HAVE_LIBHYBRIS ${libhybris_FOUND}) diff --git a/config-kwin.h.cmake b/config-kwin.h.cmake index 8f680800f0..90efb3e58f 100644 --- a/config-kwin.h.cmake +++ b/config-kwin.h.cmake @@ -13,6 +13,7 @@ #cmakedefine01 HAVE_X11_XINPUT #cmakedefine01 HAVE_DRM #cmakedefine01 HAVE_GBM +#cmakedefine01 HAVE_EGL_STREAMS #cmakedefine01 HAVE_LIBHYBRIS #cmakedefine01 HAVE_WAYLAND_EGL #cmakedefine01 HAVE_SYS_PRCTL_H diff --git a/plugins/platforms/drm/CMakeLists.txt b/plugins/platforms/drm/CMakeLists.txt index 28cd0c71f8..54ff8b60a5 100644 --- a/plugins/platforms/drm/CMakeLists.txt +++ b/plugins/platforms/drm/CMakeLists.txt @@ -21,6 +21,12 @@ if(HAVE_GBM) ) endif() +if(HAVE_EGL_STREAMS) + set(DRM_SOURCES ${DRM_SOURCES} + egl_stream_backend.cpp + ) +endif() + include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) add_library(KWinWaylandDrmBackend MODULE ${DRM_SOURCES}) diff --git a/plugins/platforms/drm/drm_backend.cpp b/plugins/platforms/drm/drm_backend.cpp index 4f07d8c4cb..10f1fef091 100644 --- a/plugins/platforms/drm/drm_backend.cpp +++ b/plugins/platforms/drm/drm_backend.cpp @@ -35,6 +35,9 @@ along with this program. If not, see . #include "egl_gbm_backend.h" #include #endif +#if HAVE_EGL_STREAMS +#include "egl_stream_backend.h" +#endif // KWayland #include #include @@ -74,6 +77,11 @@ DrmBackend::DrmBackend(QObject *parent) , m_udevMonitor(m_udev->monitor()) , m_dpmsFilter() { +#if HAVE_EGL_STREAMS + if (qEnvironmentVariableIsSet("KWIN_DRM_USE_EGL_STREAMS")) { + m_useEglStreams = true; + } +#endif setSupportsGammaControl(true); handleOutputs(); } @@ -250,9 +258,10 @@ void DrmBackend::openDrm() qCWarning(KWIN_DRM) << "Did not find a GPU"; return; } - int fd = LogindIntegration::self()->takeDevice(device->devNode()); + m_devNode = device->devNode(); + int fd = LogindIntegration::self()->takeDevice(m_devNode.constData()); if (fd < 0) { - qCWarning(KWIN_DRM) << "failed to open drm device at" << device->devNode(); + qCWarning(KWIN_DRM) << "failed to open drm device at" << m_devNode; return; } m_fd = fd; @@ -605,13 +614,13 @@ DrmOutput *DrmBackend::findOutput(const QByteArray &uuid) return nullptr; } -void DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) +bool DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) { if (!buffer || buffer->bufferId() == 0) { if (m_deleteBufferAfterPageFlip) { delete buffer; } - return; + return false; } if (output->present(buffer)) { @@ -619,13 +628,24 @@ void DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) if (m_pageFlipsPending == 1 && Compositor::self()) { Compositor::self()->aboutToSwapBuffers(); } + return true; } else if (m_deleteBufferAfterPageFlip) { delete buffer; } + return false; } void DrmBackend::initCursor() { + +#if HAVE_EGL_STREAMS + // Hardware cursors aren't currently supported with EGLStream backend, + // possibly an NVIDIA driver bug + if (m_useEglStreams) { + setSoftWareCursor(true); + } +#endif + m_cursorEnabled = waylandServer()->seat()->hasPointer(); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::hasPointerChanged, this, [this] { @@ -733,6 +753,13 @@ QPainterBackend *DrmBackend::createQPainterBackend() OpenGLBackend *DrmBackend::createOpenGLBackend() { +#if HAVE_EGL_STREAMS + if (m_useEglStreams) { + m_deleteBufferAfterPageFlip = false; + return new EglStreamBackend(this); + } +#endif + #if HAVE_GBM m_deleteBufferAfterPageFlip = true; return new EglGbmBackend(this); @@ -774,6 +801,10 @@ QVector DrmBackend::supportedCompositors() const } #if HAVE_GBM return QVector{OpenGLCompositing, QPainterCompositing}; +#elif HAVE_EGL_STREAMS + return m_useEglStreams ? + QVector{OpenGLCompositing, QPainterCompositing} : + QVector{QPainterCompositing}; #else return QVector{QPainterCompositing}; #endif @@ -787,6 +818,9 @@ QString DrmBackend::supportInformation() const s << "Name: " << "DRM" << endl; s << "Active: " << m_active << endl; s << "Atomic Mode Setting: " << m_atomicModeSetting << endl; +#if HAVE_EGL_STREAMS + s << "Using EGL Streams: " << m_useEglStreams << endl; +#endif return supportInfo; } diff --git a/plugins/platforms/drm/drm_backend.h b/plugins/platforms/drm/drm_backend.h index 45e37e11b0..cd90926abf 100644 --- a/plugins/platforms/drm/drm_backend.h +++ b/plugins/platforms/drm/drm_backend.h @@ -85,7 +85,7 @@ public: #if HAVE_GBM DrmSurfaceBuffer *createBuffer(const std::shared_ptr &surface); #endif - void present(DrmBuffer *buffer, DrmOutput *output); + bool present(DrmBuffer *buffer, DrmOutput *output); int fd() const { return m_fd; @@ -125,6 +125,16 @@ public: return m_gbmDevice; } + QByteArray devNode() const { + return m_devNode; + } + +#if HAVE_EGL_STREAMS + bool useEglStreams() const { + return m_useEglStreams; + } +#endif + QVector supportedCompositors() const override; QString supportInformation() const override; @@ -182,6 +192,10 @@ private: QSize m_cursorSize; int m_pageFlipsPending = 0; bool m_active = false; + QByteArray m_devNode; +#if HAVE_EGL_STREAMS + bool m_useEglStreams = false; +#endif // all available planes: primarys, cursors and overlays QVector m_planes; QVector m_overlayPlanes; diff --git a/plugins/platforms/drm/drm_output.cpp b/plugins/platforms/drm/drm_output.cpp index 5c049ce5d5..8432f3c1c1 100644 --- a/plugins/platforms/drm/drm_output.cpp +++ b/plugins/platforms/drm/drm_output.cpp @@ -909,6 +909,15 @@ bool DrmOutput::presentAtomically(DrmBuffer *buffer) return false; } +#if HAVE_EGL_STREAMS + if (m_backend->useEglStreams() && !m_modesetRequested) { + // EglStreamBackend queues normal page flips through EGL, + // modesets are still performed through DRM-KMS + m_pageFlipPending = true; + return true; + } +#endif + m_primaryPlane->setNext(buffer); m_nextPlanesFlipList << m_primaryPlane; @@ -1052,7 +1061,13 @@ bool DrmOutput::doAtomicCommit(AtomicCommitMode mode) // TODO: Evaluating this condition should only be necessary, as long as we expect older kernels than 4.10. flags |= DRM_MODE_ATOMIC_NONBLOCK; } - flags |= DRM_MODE_PAGE_FLIP_EVENT; + +#if HAVE_EGL_STREAMS + if (!m_backend->useEglStreams()) + // EglStreamBackend uses the NV_output_drm_flip_event EGL extension + // to register the flip event through eglStreamConsumerAcquireAttribNV +#endif + flags |= DRM_MODE_PAGE_FLIP_EVENT; } } else { flags |= DRM_MODE_ATOMIC_TEST_ONLY; diff --git a/plugins/platforms/drm/drm_output.h b/plugins/platforms/drm/drm_output.h index de3e75699f..0c8ea71b44 100644 --- a/plugins/platforms/drm/drm_output.h +++ b/plugins/platforms/drm/drm_output.h @@ -83,6 +83,13 @@ public: return m_uuid; } + const DrmCrtc *crtc() const { + return m_crtc; + } + const DrmPlane *primaryPlane() const { + return m_primaryPlane; + } + bool initCursor(const QSize &cursorSize); bool supportsTransformations() const; diff --git a/plugins/platforms/drm/egl_stream_backend.cpp b/plugins/platforms/drm/egl_stream_backend.cpp new file mode 100644 index 0000000000..374ab42bbb --- /dev/null +++ b/plugins/platforms/drm/egl_stream_backend.cpp @@ -0,0 +1,692 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2019 NVIDIA Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "egl_stream_backend.h" +#include "composite.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "drm_object_crtc.h" +#include "drm_object_plane.h" +#include "logging.h" +#include "logind.h" +#include "options.h" +#include "scene.h" +#include "screens.h" +#include "wayland_server.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +typedef EGLStreamKHR (*PFNEGLCREATESTREAMATTRIBNV)(EGLDisplay, EGLAttrib *); +typedef EGLBoolean (*PFNEGLGETOUTPUTLAYERSEXT)(EGLDisplay, EGLAttrib *, EGLOutputLayerEXT *, EGLint, EGLint *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMEROUTPUTEXT)(EGLDisplay, EGLStreamKHR, EGLOutputLayerEXT); +typedef EGLSurface (*PFNEGLCREATESTREAMPRODUCERSURFACEKHR)(EGLDisplay, EGLConfig, EGLStreamKHR, EGLint *); +typedef EGLBoolean (*PFNEGLDESTROYSTREAMKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERACQUIREATTRIBNV)(EGLDisplay, EGLStreamKHR, EGLAttrib *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLQUERYSTREAMATTRIBNV)(EGLDisplay, EGLStreamKHR, EGLenum, EGLAttrib *); +typedef EGLBoolean (*PFNEGLSTREAMCONSUMERRELEASEKHR)(EGLDisplay, EGLStreamKHR); +typedef EGLBoolean (*PFNEGLQUERYWAYLANDBUFFERWL)(EGLDisplay, wl_resource *, EGLint, EGLint *); +PFNEGLCREATESTREAMATTRIBNV pEglCreateStreamAttribNV = nullptr; +PFNEGLGETOUTPUTLAYERSEXT pEglGetOutputLayersEXT = nullptr; +PFNEGLSTREAMCONSUMEROUTPUTEXT pEglStreamConsumerOutputEXT = nullptr; +PFNEGLCREATESTREAMPRODUCERSURFACEKHR pEglCreateStreamProducerSurfaceKHR = nullptr; +PFNEGLDESTROYSTREAMKHR pEglDestroyStreamKHR = nullptr; +PFNEGLSTREAMCONSUMERACQUIREATTRIBNV pEglStreamConsumerAcquireAttribNV = nullptr; +PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR pEglStreamConsumerGLTextureExternalKHR = nullptr; +PFNEGLQUERYSTREAMATTRIBNV pEglQueryStreamAttribNV = nullptr; +PFNEGLSTREAMCONSUMERRELEASEKHR pEglStreamConsumerReleaseKHR = nullptr; +PFNEGLQUERYWAYLANDBUFFERWL pEglQueryWaylandBufferWL = nullptr; + +#ifndef EGL_CONSUMER_AUTO_ACQUIRE_EXT +#define EGL_CONSUMER_AUTO_ACQUIRE_EXT 0x332B +#endif + +#ifndef EGL_DRM_MASTER_FD_EXT +#define EGL_DRM_MASTER_FD_EXT 0x333C +#endif + +#ifndef EGL_DRM_FLIP_EVENT_DATA_NV +#define EGL_DRM_FLIP_EVENT_DATA_NV 0x333E +#endif + +#ifndef EGL_WAYLAND_EGLSTREAM_WL +#define EGL_WAYLAND_EGLSTREAM_WL 0x334B +#endif + +#ifndef EGL_WAYLAND_Y_INVERTED_WL +#define EGL_WAYLAND_Y_INVERTED_WL 0x31DB +#endif + +EglStreamBackend::EglStreamBackend(DrmBackend *b) + : AbstractEglBackend(), m_backend(b) +{ + setIsDirectRendering(true); + setSyncsToVBlank(true); + connect(m_backend, &DrmBackend::outputAdded, this, &EglStreamBackend::createOutput); + connect(m_backend, &DrmBackend::outputRemoved, this, + [this] (DrmOutput *output) { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [output] (const Output &o) { + return o.output == output; + }); + if (it == m_outputs.end()) { + return; + } + cleanupOutput(*it); + m_outputs.erase(it); + }); +} + +EglStreamBackend::~EglStreamBackend() +{ + cleanup(); +} + +void EglStreamBackend::cleanupSurfaces() +{ + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + cleanupOutput(*it); + } + m_outputs.clear(); +} + +void EglStreamBackend::cleanupOutput(const Output &o) +{ + if (o.buffer != nullptr) { + delete o.buffer; + } + if (o.eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(eglDisplay(), o.eglSurface); + } + if (o.eglStream != EGL_NO_STREAM_KHR) { + pEglDestroyStreamKHR(eglDisplay(), o.eglStream); + } +} + +bool EglStreamBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_backend->sceneEglDisplay(); + if (display == EGL_NO_DISPLAY) { + if (!hasClientExtension(QByteArrayLiteral("EGL_EXT_device_base")) && + !(hasClientExtension(QByteArrayLiteral("EGL_EXT_device_query")) && + hasClientExtension(QByteArrayLiteral("EGL_EXT_device_enumeration")))) { + setFailed("Missing required EGL client extension: " + "EGL_EXT_device_base or " + "EGL_EXT_device_query and EGL_EXT_device_enumeration"); + return false; + } + + // Try to find the EGLDevice corresponding to our DRM device file + int numDevices; + eglQueryDevicesEXT(0, nullptr, &numDevices); + QVector devices(numDevices); + eglQueryDevicesEXT(numDevices, devices.data(), &numDevices); + for (EGLDeviceEXT device : devices) { + const char *drmDeviceFile = eglQueryDeviceStringEXT(device, EGL_DRM_DEVICE_FILE_EXT); + if (m_backend->devNode() != drmDeviceFile) { + continue; + } + + const char *deviceExtensionCString = eglQueryDeviceStringEXT(device, EGL_EXTENSIONS); + QByteArray deviceExtensions = QByteArray::fromRawData(deviceExtensionCString, + qstrlen(deviceExtensionCString)); + if (!deviceExtensions.split(' ').contains(QByteArrayLiteral("EGL_EXT_device_drm"))) { + continue; + } + + EGLint platformAttribs[] = { + EGL_DRM_MASTER_FD_EXT, m_backend->fd(), + EGL_NONE + }; + display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, device, platformAttribs); + break; + } + } + + if (display == EGL_NO_DISPLAY) { + setFailed("No suitable EGL device found"); + return false; + } + + setEglDisplay(display); + if (!initEglAPI()) { + return false; + } + + const QVector requiredExtensions = { + QByteArrayLiteral("EGL_EXT_output_base"), + QByteArrayLiteral("EGL_EXT_output_drm"), + QByteArrayLiteral("EGL_KHR_stream"), + QByteArrayLiteral("EGL_KHR_stream_producer_eglsurface"), + QByteArrayLiteral("EGL_EXT_stream_consumer_egloutput"), + QByteArrayLiteral("EGL_NV_stream_attrib"), + QByteArrayLiteral("EGL_EXT_stream_acquire_mode"), + QByteArrayLiteral("EGL_KHR_stream_consumer_gltexture"), + QByteArrayLiteral("EGL_WL_wayland_eglstream") + }; + for (const QByteArray &ext : requiredExtensions) { + if (!hasExtension(ext)) { + setFailed(QStringLiteral("Missing required EGL extension: ") + ext); + return false; + } + } + + pEglCreateStreamAttribNV = (PFNEGLCREATESTREAMATTRIBNV)eglGetProcAddress("eglCreateStreamAttribNV"); + pEglGetOutputLayersEXT = (PFNEGLGETOUTPUTLAYERSEXT)eglGetProcAddress("eglGetOutputLayersEXT"); + pEglStreamConsumerOutputEXT = (PFNEGLSTREAMCONSUMEROUTPUTEXT)eglGetProcAddress("eglStreamConsumerOutputEXT"); + pEglCreateStreamProducerSurfaceKHR = (PFNEGLCREATESTREAMPRODUCERSURFACEKHR)eglGetProcAddress("eglCreateStreamProducerSurfaceKHR"); + pEglDestroyStreamKHR = (PFNEGLDESTROYSTREAMKHR)eglGetProcAddress("eglDestroyStreamKHR"); + pEglStreamConsumerAcquireAttribNV = (PFNEGLSTREAMCONSUMERACQUIREATTRIBNV)eglGetProcAddress("eglStreamConsumerAcquireAttribNV"); + pEglStreamConsumerGLTextureExternalKHR = (PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHR)eglGetProcAddress("eglStreamConsumerGLTextureExternalKHR"); + pEglQueryStreamAttribNV = (PFNEGLQUERYSTREAMATTRIBNV)eglGetProcAddress("eglQueryStreamAttribNV"); + pEglStreamConsumerReleaseKHR = (PFNEGLSTREAMCONSUMERRELEASEKHR)eglGetProcAddress("eglStreamConsumerReleaseKHR"); + pEglQueryWaylandBufferWL = (PFNEGLQUERYWAYLANDBUFFERWL)eglGetProcAddress("eglQueryWaylandBufferWL"); + return true; +} + +EglStreamBackend::StreamTexture *EglStreamBackend::lookupStreamTexture(KWayland::Server::SurfaceInterface *surface) +{ + auto it = m_streamTextures.find(surface); + return it != m_streamTextures.end() ? + &it.value() : + nullptr; +} + +void EglStreamBackend::attachStreamConsumer(KWayland::Server::SurfaceInterface *surface, + void *eglStream, + wl_array *attribs) +{ + QVector streamAttribs; + streamAttribs << EGL_WAYLAND_EGLSTREAM_WL << (EGLAttrib)eglStream; + EGLAttrib *attribArray = (EGLAttrib *)attribs->data; + for (unsigned int i = 0; i < attribs->size; ++i) { + streamAttribs << attribArray[i]; + } + streamAttribs << EGL_NONE; + + EGLStreamKHR stream = pEglCreateStreamAttribNV(eglDisplay(), streamAttribs.data()); + if (stream == EGL_NO_STREAM_KHR) { + qCWarning(KWIN_DRM) << "Failed to create EGL stream"; + return; + } + + GLuint texture; + StreamTexture *st = lookupStreamTexture(surface); + if (st != nullptr) { + pEglDestroyStreamKHR(eglDisplay(), st->stream); + st->stream = stream; + texture = st->texture; + } else { + StreamTexture newSt = { stream, 0 }; + glGenTextures(1, &newSt.texture); + m_streamTextures.insert(surface, newSt); + texture = newSt.texture; + + connect(surface, &KWayland::Server::Resource::unbound, this, + [surface, this]() { + const StreamTexture &st = m_streamTextures.take(surface); + pEglDestroyStreamKHR(eglDisplay(), st.stream); + glDeleteTextures(1, &st.texture); + }); + } + + glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture); + if (!pEglStreamConsumerGLTextureExternalKHR(eglDisplay(), stream)) { + qCWarning(KWIN_DRM) << "Failed to bind EGL stream to texture"; + } + glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); +} + +void EglStreamBackend::init() +{ + if (!m_backend->atomicModeSetting()) { + setFailed("EGLStream backend requires atomic modesetting"); + return; + } + + if (!initializeEgl()) { + setFailed("Failed to initialize EGL api"); + return; + } + if (!initRenderingContext()) { + setFailed("Failed to initialize rendering context"); + return; + } + + initKWinGL(); + setSupportsBufferAge(false); + initWayland(); + + using namespace KWayland::Server; + m_eglStreamControllerInterface = waylandServer()->display()->createEglStreamControllerInterface(); + connect(m_eglStreamControllerInterface, &EglStreamControllerInterface::streamConsumerAttached, this, + &EglStreamBackend::attachStreamConsumer); + m_eglStreamControllerInterface->create(); + if (!m_eglStreamControllerInterface->isValid()) { + setFailed("failed to initialize wayland-eglstream-controller interface"); + } +} + +bool EglStreamBackend::initRenderingContext() +{ + initBufferConfigs(); + + if (!createContext()) { + return false; + } + + const auto outputs = m_backend->drmOutputs(); + for (DrmOutput *drmOutput : outputs) { + createOutput(drmOutput); + } + if (m_outputs.isEmpty()) { + qCCritical(KWIN_DRM) << "Failed to create output surface"; + return false; + } + // set our first surface as the one for the abstract backend + setSurface(m_outputs.first().eglSurface); + + return makeContextCurrent(m_outputs.first()); +} + +bool EglStreamBackend::resetOutput(Output &o, DrmOutput *drmOutput) +{ + o.output = drmOutput; + if (o.buffer != nullptr) { + delete o.buffer; + } + // dumb buffer used for modesetting + o.buffer = m_backend->createBuffer(drmOutput->pixelSize()); + + EGLAttrib streamAttribs[] = { + EGL_STREAM_FIFO_LENGTH_KHR, 0, // mailbox mode + EGL_CONSUMER_AUTO_ACQUIRE_EXT, EGL_FALSE, + EGL_NONE + }; + EGLStreamKHR stream = pEglCreateStreamAttribNV(eglDisplay(), streamAttribs); + if (stream == EGL_NO_STREAM_KHR) { + qCCritical(KWIN_DRM) << "Failed to create EGL stream for output"; + return false; + } + + EGLAttrib outputAttribs[3]; + if (drmOutput->primaryPlane()) { + outputAttribs[0] = EGL_DRM_PLANE_EXT; + outputAttribs[1] = drmOutput->primaryPlane()->id(); + } else { + outputAttribs[0] = EGL_DRM_CRTC_EXT; + outputAttribs[1] = drmOutput->crtc()->id(); + } + outputAttribs[2] = EGL_NONE; + EGLint numLayers; + EGLOutputLayerEXT outputLayer; + pEglGetOutputLayersEXT(eglDisplay(), outputAttribs, &outputLayer, 1, &numLayers); + if (numLayers == 0) { + qCCritical(KWIN_DRM) << "No EGL output layers found"; + return false; + } + + pEglStreamConsumerOutputEXT(eglDisplay(), stream, outputLayer); + EGLint streamProducerAttribs[] = { + EGL_WIDTH, drmOutput->pixelSize().width(), + EGL_HEIGHT, drmOutput->pixelSize().height(), + EGL_NONE + }; + EGLSurface eglSurface = pEglCreateStreamProducerSurfaceKHR(eglDisplay(), config(), stream, + streamProducerAttribs); + if (eglSurface == EGL_NO_SURFACE) { + qCCritical(KWIN_DRM) << "Failed to create EGL surface for output"; + return false; + } + + if (o.eglSurface != EGL_NO_SURFACE) { + if (surface() == o.eglSurface) { + setSurface(eglSurface); + } + eglDestroySurface(eglDisplay(), o.eglSurface); + } + + if (o.eglStream != EGL_NO_STREAM_KHR) { + pEglDestroyStreamKHR(eglDisplay(), o.eglStream); + } + + o.eglStream = stream; + o.eglSurface = eglSurface; + return true; +} + +void EglStreamBackend::createOutput(DrmOutput *drmOutput) +{ + Output o; + if (!resetOutput(o, drmOutput)) { + return; + } + + connect(drmOutput, &DrmOutput::modeChanged, this, + [drmOutput, this] { + auto it = std::find_if(m_outputs.begin(), m_outputs.end(), + [drmOutput] (const auto &o) { + return o.output == drmOutput; + } + ); + if (it == m_outputs.end()) { + return; + } + resetOutput(*it, drmOutput); + } + ); + m_outputs << o; +} + +bool EglStreamBackend::makeContextCurrent(const Output &output) +{ + const EGLSurface surface = output.eglSurface; + if (surface == EGL_NO_SURFACE) { + return false; + } + + if (eglMakeCurrent(eglDisplay(), surface, surface, context()) == EGL_FALSE) { + qCCritical(KWIN_DRM) << "Failed to make EGL context current"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_DRM) << "Error occurred while making EGL context current" << error; + return false; + } + + const QSize &overall = screens()->size(); + const QRect &v = output.output->geometry(); + qreal scale = output.output->scale(); + glViewport(-v.x() * scale, (v.height() - overall.height() + v.y()) * scale, + overall.width() * scale, overall.height() * scale); + return true; +} + +bool EglStreamBackend::initBufferConfigs() +{ + const EGLint configAttribs[] = { + EGL_SURFACE_TYPE, EGL_STREAM_BIT_KHR, + EGL_RED_SIZE, 1, + EGL_GREEN_SIZE, 1, + EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }; + EGLint count; + EGLConfig config; + if (!eglChooseConfig(eglDisplay(), configAttribs, &config, 1, &count)) { + qCCritical(KWIN_DRM) << "Failed to query available EGL configs"; + return false; + } + if (count == 0) { + qCCritical(KWIN_DRM) << "No suitable EGL config found"; + return false; + } + + setConfig(config); + return true; +} + +void EglStreamBackend::present() +{ + for (auto &o : m_outputs) { + makeContextCurrent(o); + presentOnOutput(o); + } +} + +void EglStreamBackend::presentOnOutput(EglStreamBackend::Output &o) +{ + eglSwapBuffers(eglDisplay(), o.eglSurface); + if (!m_backend->present(o.buffer, o.output)) { + return; + } + + EGLAttrib acquireAttribs[] = { + EGL_DRM_FLIP_EVENT_DATA_NV, (EGLAttrib)o.output, + EGL_NONE, + }; + if (!pEglStreamConsumerAcquireAttribNV(eglDisplay(), o.eglStream, acquireAttribs)) { + qCWarning(KWIN_DRM) << "Failed to acquire output EGL stream frame"; + } +} + +void EglStreamBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) +} + +SceneOpenGLTexturePrivate *EglStreamBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglStreamTexture(texture, this); +} + +QRegion EglStreamBackend::prepareRenderingFrame() +{ + startRenderTimer(); + return QRegion(); +} + +QRegion EglStreamBackend::prepareRenderingForScreen(int screenId) +{ + const Output &o = m_outputs.at(screenId); + makeContextCurrent(o); + return o.output->geometry(); +} + +void EglStreamBackend::endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion) + Q_UNUSED(damagedRegion) +} + +void EglStreamBackend::endRenderingFrameForScreen(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) +{ + Q_UNUSED(renderedRegion); + Q_UNUSED(damagedRegion); + Output &o = m_outputs[screenId]; + presentOnOutput(o); +} + +bool EglStreamBackend::usesOverlayWindow() const +{ + return false; +} + +bool EglStreamBackend::perScreenRendering() const +{ + return true; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglStreamTexture::EglStreamTexture(SceneOpenGLTexture *texture, EglStreamBackend *backend) + : AbstractEglTexture(texture, backend), m_backend(backend), m_fbo(0), m_rbo(0) +{ +} + +EglStreamTexture::~EglStreamTexture() +{ + glDeleteRenderbuffers(1, &m_rbo); + glDeleteFramebuffers(1, &m_fbo); +} + +bool EglStreamTexture::acquireStreamFrame(EGLStreamKHR stream) +{ + EGLAttrib streamState; + if (!pEglQueryStreamAttribNV(m_backend->eglDisplay(), stream, + EGL_STREAM_STATE_KHR, &streamState)) { + qCWarning(KWIN_DRM) << "Failed to query EGL stream state"; + return false; + } + + if (streamState == EGL_STREAM_STATE_NEW_FRAME_AVAILABLE_KHR) { + if (pEglStreamConsumerAcquireAttribNV(m_backend->eglDisplay(), stream, nullptr)) { + return true; + } else { + qCWarning(KWIN_DRM) << "Failed to acquire EGL stream frame"; + } + } + + // Re-use previous texture contents if no new frame is available + // or if acquisition fails for some reason + return false; +} + +void EglStreamTexture::createFbo() +{ + glDeleteRenderbuffers(1, &m_rbo); + glDeleteFramebuffers(1, &m_fbo); + + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glGenRenderbuffers(1, &m_rbo); + glBindRenderbuffer(GL_RENDERBUFFER, m_rbo); + glRenderbufferStorage(GL_RENDERBUFFER, m_format, m_size.width(), m_size.height()); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_rbo); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +// Renders the contents of the given EXTERNAL_OES texture +// to the scratch framebuffer, then copies this to m_texture +void EglStreamTexture::copyExternalTexture(GLuint tex) +{ + GLint oldViewport[4], oldProgram; + glGetIntegerv(GL_VIEWPORT, oldViewport); + glViewport(0, 0, m_size.width(), m_size.height()); + glGetIntegerv(GL_CURRENT_PROGRAM, &oldProgram); + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glBindRenderbuffer(GL_RENDERBUFFER, m_rbo); + glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex); + glEnable(GL_TEXTURE_EXTERNAL_OES); + + GLfloat yTop = texture()->isYInverted() ? 0 : 1; + glBegin(GL_QUADS); + glTexCoord2f(0, yTop); + glVertex2f(-1, 1); + glTexCoord2f(0, 1 - yTop); + glVertex2f(-1, -1); + glTexCoord2f(1, 1 - yTop); + glVertex2f(1, -1); + glTexCoord2f(1, yTop); + glVertex2f(1, 1); + glEnd(); + + texture()->bind(); + glCopyTexImage2D(m_target, 0, m_format, 0, 0, m_size.width(), m_size.height(), 0); + texture()->unbind(); + + glDisable(GL_TEXTURE_EXTERNAL_OES); + glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glUseProgram(oldProgram); + glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); +} + +bool EglStreamTexture::attachBuffer(KWayland::Server::BufferInterface *buffer) +{ + QSize oldSize = m_size; + m_size = buffer->size(); + GLenum oldFormat = m_format; + m_format = buffer->hasAlphaChannel() ? GL_RGBA : GL_RGB; + + EGLint yInverted, wasYInverted = texture()->isYInverted(); + if (!pEglQueryWaylandBufferWL(m_backend->eglDisplay(), buffer->resource(), EGL_WAYLAND_Y_INVERTED_WL, &yInverted)) { + yInverted = EGL_TRUE; + } + texture()->setYInverted(yInverted); + updateMatrix(); + + return oldSize != m_size || + oldFormat != m_format || + wasYInverted != texture()->isYInverted(); +} + +bool EglStreamTexture::loadTexture(WindowPixmap *pixmap) +{ + using namespace KWayland::Server; + SurfaceInterface *surface = pixmap->surface(); + const EglStreamBackend::StreamTexture *st = m_backend->lookupStreamTexture(surface); + if (!pixmap->buffer().isNull() && st != nullptr) { + + glGenTextures(1, &m_texture); + texture()->setWrapMode(GL_CLAMP_TO_EDGE); + texture()->setFilter(GL_LINEAR); + + attachBuffer(surface->buffer()); + createFbo(); + surface->resetTrackedDamage(); + + if (acquireStreamFrame(st->stream)) { + copyExternalTexture(st->texture); + if (!pEglStreamConsumerReleaseKHR(m_backend->eglDisplay(), st->stream)) { + qCWarning(KWIN_DRM) << "Failed to release EGL stream"; + } + } + return true; + } else { + // Not an EGLStream surface + return AbstractEglTexture::loadTexture(pixmap); + } +} + +void EglStreamTexture::updateTexture(WindowPixmap *pixmap) +{ + using namespace KWayland::Server; + SurfaceInterface *surface = pixmap->surface(); + const EglStreamBackend::StreamTexture *st = m_backend->lookupStreamTexture(surface); + if (!pixmap->buffer().isNull() && st != nullptr) { + + if (attachBuffer(surface->buffer())) { + createFbo(); + } + surface->resetTrackedDamage(); + + if (acquireStreamFrame(st->stream)) { + copyExternalTexture(st->texture); + if (!pEglStreamConsumerReleaseKHR(m_backend->eglDisplay(), st->stream)) { + qCWarning(KWIN_DRM) << "Failed to release EGL stream"; + } + } + } else { + // Not an EGLStream surface + AbstractEglTexture::updateTexture(pixmap); + } +} + +} // namespace diff --git a/plugins/platforms/drm/egl_stream_backend.h b/plugins/platforms/drm/egl_stream_backend.h new file mode 100644 index 0000000000..6bae8e76c4 --- /dev/null +++ b/plugins/platforms/drm/egl_stream_backend.h @@ -0,0 +1,116 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2019 NVIDIA Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#ifndef KWIN_EGL_STREAM_BACKEND_H +#define KWIN_EGL_STREAM_BACKEND_H +#include "abstract_egl_backend.h" +#include +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmOutput; +class DrmBuffer; +class ShellClient; + +/** + * @brief OpenGL Backend using Egl with an EGLDevice. + **/ +class EglStreamBackend : public AbstractEglBackend +{ + Q_OBJECT +public: + EglStreamBackend(DrmBackend *b); + virtual ~EglStreamBackend(); + void screenGeometryChanged(const QSize &size) override; + SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + QRegion prepareRenderingFrame() override; + void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion) override; + void endRenderingFrameForScreen(int screenId, const QRegion &damage, const QRegion &damagedRegion) override; + bool usesOverlayWindow() const override; + bool perScreenRendering() const override; + QRegion prepareRenderingForScreen(int screenId) override; + void init() override; + +protected: + void present() override; + void cleanupSurfaces() override; + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + struct StreamTexture + { + EGLStreamKHR stream; + GLuint texture; + }; + StreamTexture *lookupStreamTexture(KWayland::Server::SurfaceInterface *surface); + void attachStreamConsumer(KWayland::Server::SurfaceInterface *surface, + void *eglStream, + wl_array *attribs); + struct Output + { + DrmOutput *output = nullptr; + DrmBuffer *buffer = nullptr; + EGLSurface eglSurface = EGL_NO_SURFACE; + EGLStreamKHR eglStream = EGL_NO_STREAM_KHR; + }; + bool resetOutput(Output &output, DrmOutput *drmOutput); + bool makeContextCurrent(const Output &output); + void presentOnOutput(Output &output); + void cleanupOutput(const Output &output); + void createOutput(DrmOutput *output); + + DrmBackend *m_backend; + QVector m_outputs; + KWayland::Server::EglStreamControllerInterface *m_eglStreamControllerInterface; + QHash m_streamTextures; + + friend class EglStreamTexture; +}; + +/** + * @brief External texture bound to an EGLStreamKHR. + **/ +class EglStreamTexture : public AbstractEglTexture +{ +public: + virtual ~EglStreamTexture(); + bool loadTexture(WindowPixmap *pixmap) override; + void updateTexture(WindowPixmap *pixmap) override; + +private: + EglStreamTexture(SceneOpenGLTexture *texture, EglStreamBackend *backend); + bool acquireStreamFrame(EGLStreamKHR stream); + void createFbo(); + void copyExternalTexture(GLuint tex); + bool attachBuffer(KWayland::Server::BufferInterface *buffer); + EglStreamBackend *m_backend; + GLuint m_fbo, m_rbo; + GLenum m_format; + friend class EglStreamBackend; +}; + +} // namespace + +#endif diff --git a/workspace.cpp b/workspace.cpp index e18f4bf192..1645ed9769 100644 --- a/workspace.cpp +++ b/workspace.cpp @@ -1399,6 +1399,12 @@ QString Workspace::supportInformation() const support.append(yes); #else support.append(no); +#endif + support.append(QStringLiteral("HAVE_EGL_STREAMS: ")); +#if HAVE_EGL_STREAMS + support.append(yes); +#else + support.append(no); #endif support.append(QStringLiteral("HAVE_X11_XCB: ")); #if HAVE_X11_XCB