diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ec6d8c..a67e5f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,7 @@ jobs: qt6-shadertools \ wayland-protocols \ wayland \ + libdrm \ libxcb \ libpipewire \ cli11 \ diff --git a/BUILD.md b/BUILD.md index cf6b3a0..3172dbe 100644 --- a/BUILD.md +++ b/BUILD.md @@ -130,6 +130,24 @@ which allows quickshell to be used as a session lock under compatible wayland co To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` +#### Screencopy +Enables streaming video from monitors and toplevel windows through various protocols. + +To disable: `-DSCREENCOPY=OFF` + +Dependencies: +- `libdrm` +- `libgbm` + +Specific protocols can also be disabled: +- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] +- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1] +- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1] + +[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1 +[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1 +[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1 + ### X11 This feature enables x11 support. Currently this implements panel windows for X11 similarly to the wlroots layershell above. diff --git a/CMakeLists.txt b/CMakeLists.txt index a491995..846a280 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,10 @@ boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND) +boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND) +boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND) +boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND) +boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND) boption(X11 "X11" ON) boption(I3 "I3/Sway" ON) boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) @@ -70,7 +74,7 @@ boption(SERVICE_NOTIFICATIONS "Notifications" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) -add_compile_options(-Wall -Wextra) +add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) # pipewire defines this, breaking PCH add_compile_definitions(_REENTRANT) diff --git a/default.nix b/default.nix index fab038a..79c9b7a 100644 --- a/default.nix +++ b/default.nix @@ -14,6 +14,8 @@ jemalloc, wayland, wayland-protocols, + libdrm, + libgbm ? null, xorg, pipewire, pam, @@ -64,7 +66,7 @@ ++ lib.optional withCrashReporter breakpad ++ lib.optional withJemalloc jemalloc ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optionals withWayland [ qt6.qtwayland wayland ] + ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else [])) ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire; @@ -79,6 +81,7 @@ (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) (lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "WAYLAND" withWayland) + (lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "HYPRLAND" withHyprland) diff --git a/src/core/stacklist.hpp b/src/core/stacklist.hpp new file mode 100644 index 0000000..7e9ee78 --- /dev/null +++ b/src/core/stacklist.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +template +class StackList { +public: + T& operator[](size_t i) { + if (i < N) { + return this->array[i]; + } else { + return this->vec[i - N]; + } + } + + const T& operator[](size_t i) const { + return const_cast*>(this)->operator[](i); // NOLINT + } + + void push(const T& value) { + if (this->size < N) { + this->array[this->size] = value; + } else { + this->vec.push_back(value); + } + + ++this->size; + } + + [[nodiscard]] size_t length() const { return this->size; } + [[nodiscard]] bool isEmpty() const { return this->size == 0; } + + [[nodiscard]] bool operator==(const StackList& other) const { + if (other.size != this->size) return false; + + for (size_t i = 0; i < this->size; ++i) { + if (this->operator[](i) != other[i]) return false; + } + + return true; + } + + template + struct BaseIterator { + using iterator_category = std::bidirectional_iterator_tag; + using difference_type = int64_t; + using value_type = IT; + using pointer = IT*; + using reference = IT&; + + BaseIterator() = default; + explicit BaseIterator(ListPtr list, size_t i): list(list), i(i) {} + + reference operator*() const { return this->list->operator[](this->i); } + pointer operator->() const { return &**this; } + + Self& operator++() { + ++this->i; + return *static_cast(this); + } + Self& operator--() { + --this->i; + return *static_cast(this); + } + + Self operator++(int) { + auto v = *this; + this->operator++(); + return v; + } + Self operator--(int) { + auto v = *this; + this->operator--(); + return v; + } + + difference_type operator-(const Self& other) { + return static_cast(this->i) - static_cast(other.i); + } + + Self& operator+(difference_type offset) { + return Self(this->list, static_cast(this->i) + offset); + } + + [[nodiscard]] bool operator==(const Self& other) const { + return this->list == other.list && this->i == other.i; + } + + [[nodiscard]] bool operator!=(const Self& other) const { return !(*this == other); } + + private: + ListPtr list = nullptr; + size_t i = 0; + }; + + struct Iterator: public BaseIterator*, T> { + Iterator() = default; + Iterator(StackList* list, size_t i) + : BaseIterator*, T>(list, i) {} + }; + + struct ConstIterator: public BaseIterator*, const T> { + ConstIterator() = default; + ConstIterator(const StackList* list, size_t i) + : BaseIterator*, const T>(list, i) {} + }; + + [[nodiscard]] Iterator begin() { return Iterator(this, 0); } + [[nodiscard]] Iterator end() { return Iterator(this, this->size); } + + [[nodiscard]] ConstIterator begin() const { return ConstIterator(this, 0); } + [[nodiscard]] ConstIterator end() const { return ConstIterator(this, this->size); } + + [[nodiscard]] bool isContiguous() const { return this->vec.empty(); } + [[nodiscard]] const T* pArray() const { return this->array.data(); } + [[nodiscard]] size_t dataLength() const { return this->size * sizeof(T); } + + const T* populateAlloc(void* alloc) const { + auto arraylen = std::min(this->size, N) * sizeof(T); + memcpy(alloc, this->array.data(), arraylen); + + if (!this->vec.empty()) { + memcpy( + static_cast(alloc) + arraylen, // NOLINT + this->vec.data(), + this->vec.size() * sizeof(T) + ); + } + + return static_cast(alloc); + } + +private: + std::array array {}; + std::vector vec; + size_t size = 0; +}; + +// might be incorrectly aligned depending on type +// #define STACKLIST_ALLOCA_VIEW(list) ((list).isContiguous() ? (list).pArray() : (list).populateAlloc(alloca((list).dataLength()))) + +// NOLINTBEGIN +#define STACKLIST_VLA_VIEW(type, list, var) \ + const type* var; \ + type var##Data[(list).length()]; \ + if ((list).isContiguous()) { \ + (var) = (list).pArray(); \ + } else { \ + (list).populateAlloc(var##Data); \ + (var) = var##Data; \ + } +// NOLINTEND diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 54bb59b..3b3d08a 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -103,6 +103,13 @@ if (WAYLAND_TOPLEVEL_MANAGEMENT) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) endif() +if (SCREENCOPY) + add_subdirectory(buffer) + add_subdirectory(screencopy) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._Screencopy) +endif() + + if (HYPRLAND) add_subdirectory(hyprland) endif() diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt new file mode 100644 index 0000000..f80c53a --- /dev/null +++ b/src/wayland/buffer/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl) + +qt_add_library(quickshell-wayland-buffer STATIC + manager.cpp + dmabuf.cpp + shm.cpp +) + +wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") + +target_link_libraries(quickshell-wayland-buffer PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + PkgConfig::dmabuf-deps + wlp-linux-dmabuf +) + +qs_pch(quickshell-wayland-buffer SET large) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp new file mode 100644 index 0000000..4716702 --- /dev/null +++ b/src/wayland/buffer/dmabuf.cpp @@ -0,0 +1,659 @@ +#include "dmabuf.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/stacklist.hpp" +#include "manager.hpp" +#include "manager_p.hpp" + +namespace qs::wayland::buffer::dmabuf { + +namespace { + +Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); + +LinuxDmabufManager* MANAGER = nullptr; // NOLINT + +class FourCCStr { +public: + explicit FourCCStr(uint32_t code) + : chars( + {static_cast(code >> 0 & 0xff), + static_cast(code >> 8 & 0xff), + static_cast(code >> 16 & 0xff), + static_cast(code >> 24 & 0xff), + '\0'} + ) { + for (auto i = 3; i != 0; i--) { + if (chars[i] == ' ') chars[i] = '\0'; + else break; + } + } + + [[nodiscard]] const char* cStr() const { return this->chars.data(); } + +private: + std::array chars {}; +}; + +class FourCCModStr { +public: + explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {} + ~FourCCModStr() { + if (this->drmStr) drmFree(this->drmStr); + } + + Q_DISABLE_COPY_MOVE(FourCCModStr); + + [[nodiscard]] const char* cStr() const { return this->drmStr; } + +private: + char* drmStr; +}; + +QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { + debug << fourcc.cStr(); + return debug; +} + +QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc) { + debug << fourcc.cStr(); + return debug; +} + +} // namespace + +QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { + auto saver = QDebugStateSaver(debug); + debug.nospace(); + + if (buffer) { + debug << "WlDmaBuffer(" << static_cast(buffer) << ", size=" << buffer->width << 'x' + << buffer->height << ", format=" << FourCCStr(buffer->format) << ", modifier=`" + << FourCCModStr(buffer->modifier) << "`)"; + } else { + debug << "WlDmaBuffer(0x0)"; + } + + return debug; +} + +GbmDeviceHandle::~GbmDeviceHandle() { + if (device) { + MANAGER->unrefGbmDevice(this->device); + } +} + +// This will definitely backfire later +void LinuxDmabufFormatSelection::ensureSorted() { + if (this->sorted) return; + auto beginIter = this->formats.begin(); + + auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_XRGB8888; + }); + + if (xrgbIter != this->formats.end()) { + std::swap(*beginIter, *xrgbIter); + ++beginIter; + } + + auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_ARGB8888; + }); + + if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); + + this->sorted = true; +} + +LinuxDmabufFeedback::LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback) + : zwp_linux_dmabuf_feedback_v1(feedback) {} + +LinuxDmabufFeedback::~LinuxDmabufFeedback() { this->destroy(); } + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) { + this->formatTableSize = size; + + this->formatTable = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + + if (this->formatTable == MAP_FAILED) { + this->formatTable = nullptr; + qCFatal(logDmabuf) << "Failed to mmap format table."; + } + + qCDebug(logDmabuf) << "Got format table"; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) { + if (device->size != sizeof(dev_t)) { + qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " + "Try recompiling both."; + } + + this->mainDevice = *reinterpret_cast(device->data); + qCDebug(logDmabuf) << "Got main device id" << this->mainDevice; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) { + if (device->size != sizeof(dev_t)) { + qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " + "Try recompiling both."; + } + + auto& tranche = this->tranches.emplaceBack(); + tranche.device = *reinterpret_cast(device->data); + qCDebug(logDmabuf) << "Got target device id" << this->mainDevice; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) { + this->tranches.back().flags = flags; + qCDebug(logDmabuf) << "Got target device flags" << flags; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) { + struct FormatTableEntry { + uint32_t format; + uint32_t padding; + uint64_t modifier; + }; + + static_assert(sizeof(FormatTableEntry) == 16, "Format table entry was not packed to 16 bytes."); + + if (this->formatTable == nullptr) { + qCFatal(logDmabuf) << "Received tranche formats before format table."; + } + + auto& tranche = this->tranches.back(); + + auto* table = reinterpret_cast(this->formatTable); + auto* indexTable = reinterpret_cast(indices->data); + auto indexTableLength = indices->size / sizeof(uint16_t); + + uint32_t lastFormat = 0; + LinuxDmabufModifiers* lastModifiers = nullptr; + LinuxDmabufModifiers* modifiers = nullptr; + + for (uint16_t ti = 0; ti != indexTableLength; ++ti) { + auto i = indexTable[ti]; // NOLINT + const auto& entry = table[i]; // NOLINT + + // Compositors usually send a single format's modifiers as a block. + if (!modifiers || entry.format != lastFormat) { + // We can often share modifier lists between formats + if (lastModifiers && modifiers->modifiers == lastModifiers->modifiers) { + // avoids storing a second list + modifiers->modifiers = lastModifiers->modifiers; + } + + lastFormat = entry.format; + lastModifiers = modifiers; + + auto modifiersIter = std::ranges::find_if(tranche.formats.formats, [&](const auto& pair) { + return pair.first == entry.format; + }); + + if (modifiersIter == tranche.formats.formats.end()) { + tranche.formats.formats.push(qMakePair(entry.format, LinuxDmabufModifiers())); + modifiers = &(--tranche.formats.formats.end())->second; + } else { + modifiers = &modifiersIter->second; + } + } + + if (entry.modifier == DRM_FORMAT_MOD_INVALID) { + modifiers->implicit = true; + } else { + modifiers->modifiers.push(entry.modifier); + } + } + + if (lastModifiers && modifiers && modifiers->modifiers == lastModifiers->modifiers) { + modifiers->modifiers = lastModifiers->modifiers; + } +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_done() { + qCDebug(logDmabuf) << "Got tranche end."; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_done() { + qCDebug(logDmabuf) << "Got feedback done."; + + if (this->formatTable) { + munmap(this->formatTable, this->formatTableSize); + this->formatTable = nullptr; + } + + if (logDmabuf().isDebugEnabled()) { + qCDebug(logDmabuf) << "Dmabuf tranches:"; + + for (auto& tranche: this->tranches) { + qCDebug(logDmabuf) << " Tranche on device" << tranche.device; + + // will be sorted on first use otherwise + tranche.formats.ensureSorted(); + + for (auto& [format, modifiers]: tranche.formats.formats) { + qCDebug(logDmabuf) << " Format" << FourCCStr(format); + + if (modifiers.implicit) { + qCDebug(logDmabuf) << " Implicit Modifier"; + } + + for (const auto& modifier: modifiers.modifiers) { + qCDebug(logDmabuf) << " Explicit Modifier" << FourCCModStr(modifier); + } + } + } + } + + // Copy tranches to the manager. If the compositor ever updates + // our tranches, we'll start from a clean slate. + MANAGER->tranches = this->tranches; + this->tranches.clear(); + + MANAGER->feedbackDone(); +} + +LinuxDmabufManager::LinuxDmabufManager(WlBufferManagerPrivate* manager) + : QWaylandClientExtensionTemplate(5) + , manager(manager) { + MANAGER = this; + this->initialize(); + + if (this->isActive()) { + qCDebug(logDmabuf) << "Requesting default dmabuf feedback..."; + new LinuxDmabufFeedback(this->get_default_feedback()); + } +} + +void LinuxDmabufManager::feedbackDone() { this->manager->dmabufReady(); } + +GbmDeviceHandle LinuxDmabufManager::getGbmDevice(dev_t handle) { + struct DrmFree { + static void cleanup(drmDevice* d) { drmFreeDevice(&d); } + }; + + std::string renderNodeStorage; + std::string* renderNode = nullptr; + + auto sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { + return d.handle == handle; + }); + + if (sharedDevice != this->gbmDevices.end()) { + renderNode = &sharedDevice->renderNode; + } else { + drmDevice* drmDevPtr = nullptr; + if (auto error = drmGetDeviceFromDevId(handle, 0, &drmDevPtr); error != 0) { + qCWarning(logDmabuf) << "Failed to get drm device information from handle:" + << qt_error_string(error); + return nullptr; + } + + auto drmDev = QScopedPointer(drmDevPtr); + + if (!(drmDev->available_nodes & (1 << DRM_NODE_RENDER))) { + qCDebug(logDmabuf) << "Cannot create GBM device: DRM device does not have render node."; + return nullptr; + } + + renderNodeStorage = drmDev->nodes[DRM_NODE_RENDER]; // NOLINT + renderNode = &renderNodeStorage; + sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { + return d.renderNode == renderNodeStorage; + }); + } + + if (sharedDevice != this->gbmDevices.end()) { + qCDebug(logDmabuf) << "Used existing GBM device on render node" << *renderNode; + ++sharedDevice->refcount; + return sharedDevice->device; + } else { + auto fd = open(renderNode->c_str(), O_RDWR | O_CLOEXEC); + if (fd < 0) { + qCDebug(logDmabuf) << "Could not open render node" << *renderNode << ":" + << qt_error_string(fd); + return nullptr; + } + + auto* device = gbm_create_device(fd); + + if (!device) { + qCDebug(logDmabuf) << "Failed to create GBM device from render node" << *renderNode; + close(fd); + return nullptr; + } + + qCDebug(logDmabuf) << "Created GBM device on render node" << *renderNode; + + this->gbmDevices.push_back({ + .handle = handle, + .renderNode = std::move(renderNodeStorage), + .device = device, + .refcount = 1, + }); + + return device; + } +} + +void LinuxDmabufManager::unrefGbmDevice(gbm_device* device) { + auto iter = std::ranges::find_if(this->gbmDevices, [device](const SharedGbmDevice& d) { + return d.device == device; + }); + if (iter == this->gbmDevices.end()) return; + + qCDebug(logDmabuf) << "Lost reference to GBM device" << device; + + if (--iter->refcount == 0) { + auto fd = gbm_device_get_fd(iter->device); + gbm_device_destroy(iter->device); + close(fd); + + this->gbmDevices.erase(iter); + qCDebug(logDmabuf) << "Destroyed GBM device" << device; + } +} + +GbmDeviceHandle LinuxDmabufManager::dupHandle(const GbmDeviceHandle& handle) { + if (!handle) return GbmDeviceHandle(); + + auto iter = std::ranges::find_if(this->gbmDevices, [&handle](const SharedGbmDevice& d) { + return d.device == *handle; + }); + if (iter == this->gbmDevices.end()) return GbmDeviceHandle(); + + qCDebug(logDmabuf) << "Duplicated GBM device handle" << *handle; + ++iter->refcount; + return GbmDeviceHandle(*handle); +} + +WlBuffer* LinuxDmabufManager::createDmabuf(const WlBufferRequest& request) { + for (auto& tranche: this->tranches) { + if (request.dmabuf.device != 0 && tranche.device != request.dmabuf.device) { + continue; + } + + LinuxDmabufFormatSelection formats; + for (const auto& format: request.dmabuf.formats) { + if (!format.modifiers.isEmpty()) { + formats.formats.push( + qMakePair(format.format, LinuxDmabufModifiers {.modifiers = format.modifiers}) + ); + } else { + for (const auto& trancheFormat: tranche.formats.formats) { + if (trancheFormat.first == format.format) { + formats.formats.push(trancheFormat); + } + } + } + } + + if (formats.formats.isEmpty()) continue; + formats.ensureSorted(); + + auto gbmDevice = this->getGbmDevice(tranche.device); + + if (!gbmDevice) { + qCWarning(logDmabuf) << "Hit unusable tranche device while trying to create dmabuf."; + continue; + } + + for (const auto& [format, modifiers]: formats.formats) { + if (auto* buf = + this->createDmabuf(gbmDevice, format, modifiers, request.width, request.height)) + { + return buf; + } + } + } + + qCWarning(logDmabuf) << "Unable to create dmabuf for request: No matching formats."; + return nullptr; +} + +WlBuffer* LinuxDmabufManager::createDmabuf( + GbmDeviceHandle& device, + uint32_t format, + const LinuxDmabufModifiers& modifiers, + uint32_t width, + uint32_t height +) { + auto buffer = std::unique_ptr(new WlDmaBuffer()); + auto& bo = buffer->bo; + + const uint32_t flags = GBM_BO_USE_RENDERING; + + if (modifiers.modifiers.isEmpty()) { + if (!modifiers.implicit) { + qCritical(logDmabuf + ) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers."; + return nullptr; + } + + qCDebug(logDmabuf) << "Creating gbm_bo without modifiers..."; + bo = gbm_bo_create(*device, width, height, format, flags); + } else { + qCDebug(logDmabuf) << "Creating gbm_bo with modifiers..."; + + STACKLIST_VLA_VIEW(uint64_t, modifiers.modifiers, modifiersData); + + bo = gbm_bo_create_with_modifiers2( + *device, + width, + height, + format, + modifiersData, + modifiers.modifiers.length(), + flags + ); + } + + if (!bo) { + qCritical(logDmabuf) << "Failed to create gbm_bo."; + return nullptr; + } + + buffer->planeCount = gbm_bo_get_plane_count(bo); + buffer->planes = new WlDmaBuffer::Plane[buffer->planeCount](); + buffer->modifier = gbm_bo_get_modifier(bo); + + auto params = QtWayland::zwp_linux_buffer_params_v1(this->create_params()); + + for (auto i = 0; i < buffer->planeCount; ++i) { + auto& plane = buffer->planes[i]; // NOLINT + plane.fd = gbm_bo_get_fd_for_plane(bo, i); + + if (plane.fd < 0) { + qCritical(logDmabuf) << "Failed to get gbm_bo fd for plane" << i << qt_error_string(plane.fd); + params.destroy(); + gbm_bo_destroy(bo); + return nullptr; + } + + plane.stride = gbm_bo_get_stride_for_plane(bo, i); + plane.offset = gbm_bo_get_offset(bo, i); + + params.add( + plane.fd, + i, + plane.offset, + plane.stride, + buffer->modifier >> 32, + buffer->modifier & 0xffffffff + ); + } + + buffer->mBuffer = + params.create_immed(static_cast(width), static_cast(height), format, 0); + params.destroy(); + + buffer->device = this->dupHandle(device); + buffer->width = width; + buffer->height = height; + buffer->format = format; + + qCDebug(logDmabuf) << "Created dmabuf" << buffer.get(); + return buffer.release(); +} + +WlDmaBuffer::WlDmaBuffer(WlDmaBuffer&& other) noexcept + : device(std::move(other.device)) + , bo(other.bo) + , mBuffer(other.mBuffer) + , planes(other.planes) { + other.mBuffer = nullptr; + other.bo = nullptr; + other.planeCount = 0; +} + +WlDmaBuffer& WlDmaBuffer::operator=(WlDmaBuffer&& other) noexcept { + this->~WlDmaBuffer(); + new (this) WlDmaBuffer(std::move(other)); + return *this; +} + +WlDmaBuffer::~WlDmaBuffer() { + if (this->mBuffer) { + wl_buffer_destroy(this->mBuffer); + } + + if (this->bo) { + gbm_bo_destroy(this->bo); + qCDebug(logDmabuf) << "Destroyed" << this << "freeing bo" << this->bo; + } + + for (auto i = 0; i < this->planeCount; ++i) { + const auto& plane = this->planes[i]; // NOLINT + if (plane.fd) close(plane.fd); + } + + delete[] this->planes; +} + +bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { + if (request.width != this->width || request.height != this->height) return false; + + auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { + return format.format == this->format + && (format.modifiers.isEmpty() + || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); + }); + + return matchingFormat != request.dmabuf.formats.end(); +} + +WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { + static auto* glEGLImageTargetTexture2DOES = []() { + auto* fn = reinterpret_cast( + eglGetProcAddress("glEGLImageTargetTexture2DOES") + ); + + if (!fn) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: " + "glEGLImageTargetTexture2DOES is missing."; + } + + return fn; + }(); + + auto* context = QOpenGLContext::currentContext(); + if (!context) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No GL context."; + } + + auto* qEglContext = context->nativeInterface(); + if (!qEglContext) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No EGL context."; + } + + auto* display = qEglContext->display(); + + // clang-format off + auto attribs = std::array { + EGL_WIDTH, this->width, + EGL_HEIGHT, this->height, + EGL_LINUX_DRM_FOURCC_EXT, this->format, + EGL_DMA_BUF_PLANE0_FD_EXT, this->planes[0].fd, // NOLINT + EGL_DMA_BUF_PLANE0_OFFSET_EXT, this->planes[0].offset, // NOLINT + EGL_DMA_BUF_PLANE0_PITCH_EXT, this->planes[0].stride, // NOLINT + EGL_NONE + }; + // clang-format on + + auto* eglImage = + eglCreateImage(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()); + + if (eglImage == EGL_NO_IMAGE) { + qFatal() << "failed to make egl image" << eglGetError(); + return nullptr; + } + + window->beginExternalCommands(); + GLuint glTexture = 0; + glGenTextures(1, &glTexture); + + glBindTexture(GL_TEXTURE_2D, glTexture); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage); + glBindTexture(GL_TEXTURE_2D, 0); + window->endExternalCommands(); + + auto* qsgTexture = QNativeInterface::QSGOpenGLTexture::fromNative( + glTexture, + window, + QSize(static_cast(this->width), static_cast(this->height)) + ); + + auto* tex = new WlDmaBufferQSGTexture(eglImage, glTexture, qsgTexture); + qCDebug(logDmabuf) << "Created WlDmaBufferQSGTexture" << tex << "from" << this; + return tex; +} + +WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { + auto* context = QOpenGLContext::currentContext(); + auto* display = context->nativeInterface()->display(); + + if (this->glTexture) glDeleteTextures(1, &this->glTexture); + if (this->eglImage) eglDestroyImage(display, this->eglImage); + delete this->qsgTexture; + + qCDebug(logDmabuf) << "WlDmaBufferQSGTexture" << this << "destroyed."; +} + +} // namespace qs::wayland::buffer::dmabuf diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp new file mode 100644 index 0000000..97b5576 --- /dev/null +++ b/src/wayland/buffer/dmabuf.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qsg.hpp" + +namespace qs::wayland::buffer { +class WlBufferManagerPrivate; +} + +namespace qs::wayland::buffer::dmabuf { + +class LinuxDmabufManager; + +class GbmDeviceHandle { +public: + GbmDeviceHandle() = default; + GbmDeviceHandle(gbm_device* device): device(device) {} + + GbmDeviceHandle(GbmDeviceHandle&& other) noexcept: device(other.device) { + other.device = nullptr; + } + + ~GbmDeviceHandle(); + Q_DISABLE_COPY(GbmDeviceHandle); + + GbmDeviceHandle& operator=(GbmDeviceHandle&& other) noexcept { + this->device = other.device; + other.device = nullptr; + return *this; + } + + [[nodiscard]] gbm_device* operator*() const { return this->device; } + [[nodiscard]] operator bool() const { return this->device; } + +private: + gbm_device* device = nullptr; +}; + +class WlDmaBufferQSGTexture: public WlBufferQSGTexture { +public: + ~WlDmaBufferQSGTexture() override; + Q_DISABLE_COPY_MOVE(WlDmaBufferQSGTexture); + + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; } + +private: + WlDmaBufferQSGTexture(EGLImage eglImage, GLuint glTexture, QSGTexture* qsgTexture) + : eglImage(eglImage) + , glTexture(glTexture) + , qsgTexture(qsgTexture) {} + + EGLImage eglImage; + GLuint glTexture; + QSGTexture* qsgTexture; + + friend class WlDmaBuffer; +}; + +class WlDmaBuffer: public WlBuffer { +public: + ~WlDmaBuffer() override; + Q_DISABLE_COPY(WlDmaBuffer); + WlDmaBuffer(WlDmaBuffer&& other) noexcept; + WlDmaBuffer& operator=(WlDmaBuffer&& other) noexcept; + + [[nodiscard]] wl_buffer* buffer() const override { return this->mBuffer; } + + [[nodiscard]] QSize size() const override { + return QSize(static_cast(this->width), static_cast(this->height)); + } + + [[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override; + [[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override; + +private: + WlDmaBuffer() noexcept = default; + + struct Plane { + int fd = 0; + uint32_t offset = 0; + uint32_t stride = 0; + }; + + GbmDeviceHandle device; + gbm_bo* bo = nullptr; + wl_buffer* mBuffer = nullptr; + int planeCount = 0; + Plane* planes = nullptr; + uint32_t format = 0; + uint64_t modifier = 0; + uint32_t width = 0; + uint32_t height = 0; + + friend class LinuxDmabufManager; + friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); +}; + +QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); + +struct LinuxDmabufModifiers { + StackList modifiers; + bool implicit = false; +}; + +struct LinuxDmabufFormatSelection { + bool sorted = false; + StackList, 2> formats; + void ensureSorted(); +}; + +struct LinuxDmabufTranche { + dev_t device = 0; + uint32_t flags = 0; + LinuxDmabufFormatSelection formats; +}; + +class LinuxDmabufFeedback: public QtWayland::zwp_linux_dmabuf_feedback_v1 { +public: + explicit LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback); + ~LinuxDmabufFeedback() override; + Q_DISABLE_COPY_MOVE(LinuxDmabufFeedback); + +protected: + void zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) override; + void zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) override; + void zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) override; + void zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) override; + void zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) override; + void zwp_linux_dmabuf_feedback_v1_tranche_done() override; + void zwp_linux_dmabuf_feedback_v1_done() override; + +private: + dev_t mainDevice = 0; + QList tranches; + void* formatTable = nullptr; + uint32_t formatTableSize = 0; +}; + +class LinuxDmabufManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_linux_dmabuf_v1 { +public: + explicit LinuxDmabufManager(WlBufferManagerPrivate* manager); + + [[nodiscard]] WlBuffer* createDmabuf(const WlBufferRequest& request); + + [[nodiscard]] WlBuffer* createDmabuf( + GbmDeviceHandle& device, + uint32_t format, + const LinuxDmabufModifiers& modifiers, + uint32_t width, + uint32_t height + ); + +private: + struct SharedGbmDevice { + dev_t handle = 0; + std::string renderNode; + gbm_device* device = nullptr; + qsizetype refcount = 0; + }; + + void feedbackDone(); + + GbmDeviceHandle getGbmDevice(dev_t handle); + void unrefGbmDevice(gbm_device* device); + GbmDeviceHandle dupHandle(const GbmDeviceHandle& handle); + + QList tranches; + QList gbmDevices; + WlBufferManagerPrivate* manager; + + friend class LinuxDmabufFeedback; + friend class GbmDeviceHandle; +}; + +} // namespace qs::wayland::buffer::dmabuf diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp new file mode 100644 index 0000000..dde71a8 --- /dev/null +++ b/src/wayland/buffer/manager.cpp @@ -0,0 +1,114 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dmabuf.hpp" +#include "manager_p.hpp" +#include "qsg.hpp" +#include "shm.hpp" + +namespace qs::wayland::buffer { + +WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { + auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2; + + if (!buffer || !buffer->isCompatible(request)) { + buffer.reset(WlBufferManager::instance()->createBuffer(request)); + if (newBuffer) *newBuffer = true; + } + + return buffer.get(); +} + +WlBufferManager::WlBufferManager(): p(new WlBufferManagerPrivate(this)) {} + +WlBufferManager::~WlBufferManager() { delete this->p; } + +WlBufferManager* WlBufferManager::instance() { + static auto* instance = new WlBufferManager(); + return instance; +} + +bool WlBufferManager::isReady() const { return this->p->mReady; } + +[[nodiscard]] WlBuffer* WlBufferManager::createBuffer(const WlBufferRequest& request) { + static const bool dmabufDisabled = qEnvironmentVariableIsSet("QS_DISABLE_DMABUF"); + + if (!dmabufDisabled) { + if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf; + qCWarning(shm::logShm) << "DMA buffer creation failed, falling back to SHM."; + } + + return shm::ShmbufManager::createShmbuf(request); +} + +WlBufferManagerPrivate::WlBufferManagerPrivate(WlBufferManager* manager) + : manager(manager) + , dmabuf(this) {} + +void WlBufferManagerPrivate::dmabufReady() { + this->mReady = true; + emit this->manager->ready(); +} + +WlBufferQSGDisplayNode::WlBufferQSGDisplayNode(QQuickWindow* window) + : window(window) + , imageNode(window->createImageNode()) { + this->appendChildNode(this->imageNode); +} + +void WlBufferQSGDisplayNode::setRect(const QRectF& rect) { + const auto* buffer = (this->presentSecondBuffer ? this->buffer2 : this->buffer1).first; + if (!buffer) return; + + auto matrix = QMatrix4x4(); + auto center = rect.center(); + auto centerX = static_cast(center.x()); + auto centerY = static_cast(center.y()); + matrix.translate(centerX, centerY); + buffer->transform.apply(matrix); + matrix.translate(-centerX, -centerY); + + auto viewRect = matrix.mapRect(rect); + auto bufferSize = buffer->size().toSizeF(); + + bufferSize.scale(viewRect.width(), viewRect.height(), Qt::KeepAspectRatio); + this->imageNode->setRect( + viewRect.x() + viewRect.width() / 2 - bufferSize.width() / 2, + viewRect.y() + viewRect.height() / 2 - bufferSize.height() / 2, + bufferSize.width(), + bufferSize.height() + ); + + this->setMatrix(matrix); +} + +void WlBufferQSGDisplayNode::syncSwapchain(const WlBufferSwapchain& swapchain) { + auto* buffer = swapchain.frontbuffer(); + auto& texture = swapchain.presentSecondBuffer ? this->buffer2 : this->buffer1; + + if (swapchain.presentSecondBuffer == this->presentSecondBuffer && texture.first == buffer) { + return; + } + + this->presentSecondBuffer = swapchain.presentSecondBuffer; + + if (texture.first == buffer) { + texture.second->sync(texture.first, this->window); + } else { + texture.first = buffer; + texture.second.reset(buffer->createQsgTexture(this->window)); + } + + this->imageNode->setTexture(texture.second->texture()); +} + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/manager.hpp b/src/wayland/buffer/manager.hpp new file mode 100644 index 0000000..c3f62a0 --- /dev/null +++ b/src/wayland/buffer/manager.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/stacklist.hpp" + +class QQuickWindow; + +namespace qs::wayland::buffer { + +class WlBufferManagerPrivate; +class WlBufferQSGTexture; + +struct WlBufferTransform { + enum Transform : uint8_t { + Normal0 = 0, + Normal90 = 1, + Normal180 = 2, + Normal270 = 3, + Flipped0 = 4, + Flipped90 = 5, + Flipped180 = 6, + Flipped270 = 7, + } transform = Normal0; + + WlBufferTransform() = default; + WlBufferTransform(uint8_t transform): transform(static_cast(transform)) {} + + [[nodiscard]] int degrees() const { return 90 * (this->transform & 0b11111011); } + [[nodiscard]] bool flip() const { return this->transform & 0b00000100; } + + void apply(QMatrix4x4& matrix) const { + matrix.rotate(this->flip() ? 180 : 0, 0, 1, 0); + matrix.rotate(static_cast(this->degrees()), 0, 0, 1); + } +}; + +struct WlBufferRequest { + uint32_t width = 0; + uint32_t height = 0; + + struct DmaFormat { + DmaFormat() = default; + DmaFormat(uint32_t format): format(format) {} + + uint32_t format = 0; + StackList modifiers; + }; + + struct { + StackList formats; + } shm; + + struct { + dev_t device = 0; + StackList formats; + } dmabuf; +}; + +class WlBuffer { +public: + virtual ~WlBuffer() = default; + Q_DISABLE_COPY_MOVE(WlBuffer); + + [[nodiscard]] virtual wl_buffer* buffer() const = 0; + [[nodiscard]] virtual QSize size() const = 0; + [[nodiscard]] virtual bool isCompatible(const WlBufferRequest& request) const = 0; + [[nodiscard]] operator bool() const { return this->buffer(); } + + // Must be called from render thread. + [[nodiscard]] virtual WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const = 0; + + WlBufferTransform transform; + +protected: + explicit WlBuffer() = default; +}; + +class WlBufferSwapchain { +public: + [[nodiscard]] WlBuffer* + createBackbuffer(const WlBufferRequest& request, bool* newBuffer = nullptr); + + void swapBuffers() { this->presentSecondBuffer = !this->presentSecondBuffer; } + + [[nodiscard]] WlBuffer* backbuffer() const { + return this->presentSecondBuffer ? this->buffer1.get() : this->buffer2.get(); + } + + [[nodiscard]] WlBuffer* frontbuffer() const { + return this->presentSecondBuffer ? this->buffer2.get() : this->buffer1.get(); + } + +private: + std::unique_ptr buffer1; + std::unique_ptr buffer2; + bool presentSecondBuffer = false; + + friend class WlBufferQSGDisplayNode; +}; + +class WlBufferManager: public QObject { + Q_OBJECT; + +public: + ~WlBufferManager() override; + Q_DISABLE_COPY_MOVE(WlBufferManager); + + static WlBufferManager* instance(); + + [[nodiscard]] bool isReady() const; + [[nodiscard]] WlBuffer* createBuffer(const WlBufferRequest& request); + +signals: + void ready(); + +private: + explicit WlBufferManager(); + + WlBufferManagerPrivate* p; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/manager_p.hpp b/src/wayland/buffer/manager_p.hpp new file mode 100644 index 0000000..55f5e66 --- /dev/null +++ b/src/wayland/buffer/manager_p.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "dmabuf.hpp" +#include "manager.hpp" + +namespace qs::wayland::buffer { + +class WlBufferManagerPrivate { +public: + explicit WlBufferManagerPrivate(WlBufferManager* manager); + + void dmabufReady(); + + WlBufferManager* manager; + dmabuf::LinuxDmabufManager dmabuf; + + bool mReady = false; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/qsg.hpp b/src/wayland/buffer/qsg.hpp new file mode 100644 index 0000000..c230cfe --- /dev/null +++ b/src/wayland/buffer/qsg.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::buffer { + +// Interact only from QSG thread. +class WlBufferQSGTexture { +public: + virtual ~WlBufferQSGTexture() = default; + Q_DISABLE_COPY_MOVE(WlBufferQSGTexture); + + [[nodiscard]] virtual QSGTexture* texture() const = 0; + virtual void sync(const WlBuffer* /*buffer*/, QQuickWindow* /*window*/) {} + +protected: + WlBufferQSGTexture() = default; +}; + +// Interact only from QSG thread. +class WlBufferQSGDisplayNode: public QSGTransformNode { +public: + explicit WlBufferQSGDisplayNode(QQuickWindow* window); + + void syncSwapchain(const WlBufferSwapchain& swapchain); + void setRect(const QRectF& rect); + +private: + QQuickWindow* window; + QSGImageNode* imageNode; + QPair> buffer1; + QPair> buffer2; + bool presentSecondBuffer = false; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/shm.cpp b/src/wayland/buffer/shm.cpp new file mode 100644 index 0000000..8973cdf --- /dev/null +++ b/src/wayland/buffer/shm.cpp @@ -0,0 +1,91 @@ +#include "shm.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::buffer::shm { + +Q_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg); + +bool WlShmBuffer::isCompatible(const WlBufferRequest& request) const { + if (QSize(static_cast(request.width), static_cast(request.height)) != this->size()) { + return false; + } + + auto matchingFormat = std::ranges::find(request.shm.formats, this->format); + return matchingFormat != request.shm.formats.end(); +} + +QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer) { + auto saver = QDebugStateSaver(debug); + debug.nospace(); + + if (buffer) { + auto fmt = QtWaylandClient::QWaylandShm::formatFrom( + static_cast<::wl_shm_format>(buffer->format) // NOLINT + ); + + debug << "WlShmBuffer(" << static_cast(buffer) << ", size=" << buffer->size() + << ", format=" << fmt << ')'; + } else { + debug << "WlShmBuffer(0x0)"; + } + + return debug; +} + +WlShmBuffer::~WlShmBuffer() { qCDebug(logShm) << "Destroyed" << this; } + +WlBufferQSGTexture* WlShmBuffer::createQsgTexture(QQuickWindow* window) const { + auto* texture = new WlShmBufferQSGTexture(); + + // If the QWaylandShmBuffer is destroyed before the QSGTexture, we'll hit a UAF + // in the render thread. + texture->shmBuffer = this->shmBuffer; + + texture->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image())); + texture->sync(this, window); + return texture; +} + +void WlShmBufferQSGTexture::sync(const WlBuffer* /*unused*/, QQuickWindow* window) { + // This is both dumb and expensive. We should use an RHI texture and render images into + // it more intelligently, but shm buffers are already a horribly slow fallback path, + // to the point where it barely matters. + this->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image())); +} + +WlBuffer* ShmbufManager::createShmbuf(const WlBufferRequest& request) { + if (request.shm.formats.isEmpty()) return nullptr; + + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + // Its probably fine... + auto format = request.shm.formats[0]; + + auto* buffer = new WlShmBuffer( + new QtWaylandClient::QWaylandShmBuffer( + display, + QSize(static_cast(request.width), static_cast(request.height)), + QtWaylandClient::QWaylandShm::formatFrom(static_cast<::wl_shm_format>(format)) // NOLINT + ), + format + ); + + qCDebug(logShm) << "Created shmbuf" << buffer; + return buffer; +} +} // namespace qs::wayland::buffer::shm diff --git a/src/wayland/buffer/shm.hpp b/src/wayland/buffer/shm.hpp new file mode 100644 index 0000000..12af26e --- /dev/null +++ b/src/wayland/buffer/shm.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qsg.hpp" + +namespace qs::wayland::buffer::shm { + +Q_DECLARE_LOGGING_CATEGORY(logShm); + +class WlShmBuffer: public WlBuffer { +public: + ~WlShmBuffer() override; + Q_DISABLE_COPY_MOVE(WlShmBuffer); + + [[nodiscard]] wl_buffer* buffer() const override { return this->shmBuffer->buffer(); } + [[nodiscard]] QSize size() const override { return this->shmBuffer->size(); } + [[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override; + [[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override; + +private: + WlShmBuffer(QtWaylandClient::QWaylandShmBuffer* shmBuffer, uint32_t format) + : shmBuffer(shmBuffer) + , format(format) {} + + std::shared_ptr shmBuffer; + uint32_t format; + + friend class WlShmBufferQSGTexture; + friend class ShmbufManager; + friend QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer); +}; + +QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer); + +class WlShmBufferQSGTexture: public WlBufferQSGTexture { +public: + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture.get(); } + void sync(const WlBuffer* buffer, QQuickWindow* window) override; + +private: + WlShmBufferQSGTexture() = default; + + std::shared_ptr shmBuffer; + std::unique_ptr qsgTexture; + + friend class WlShmBuffer; +}; + +class ShmbufManager { +public: + [[nodiscard]] static WlBuffer* createShmbuf(const WlBufferRequest& request); +}; + +} // namespace qs::wayland::buffer::shm diff --git a/src/wayland/module.md b/src/wayland/module.md index d6376e3..db9bfb5 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -5,5 +5,6 @@ headers = [ "wlr_layershell.hpp", "session_lock.hpp", "toplevel_management/qml.hpp", + "screencopy/view.hpp", ] ----- diff --git a/src/wayland/screencopy/CMakeLists.txt b/src/wayland/screencopy/CMakeLists.txt new file mode 100644 index 0000000..97c4209 --- /dev/null +++ b/src/wayland/screencopy/CMakeLists.txt @@ -0,0 +1,42 @@ +qt_add_library(quickshell-wayland-screencopy STATIC + manager.cpp + view.cpp +) + +qt_add_qml_module(quickshell-wayland-screencopy + URI Quickshell.Wayland._Screencopy + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-screencopy) + +set(SCREENCOPY_MODULES) + +if (SCREENCOPY_ICC) + add_subdirectory(image_copy_capture) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-icc) +endif() + +if (SCREENCOPY_WLR) + add_subdirectory(wlr_screencopy) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-wlr) +endif() + +if (SCREENCOPY_HYPRLAND_TOPLEVEL) + add_subdirectory(hyprland_screencopy) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-hyprland) +endif() + +configure_file(build.hpp.in build.hpp @ONLY) +target_include_directories(quickshell-wayland-screencopy PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(quickshell-wayland-screencopy PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + quickshell-wayland-buffer + ${SCREENCOPY_MODULES} +) + +qs_module_pch(quickshell-wayland-screencopy SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-screencopyplugin) diff --git a/src/wayland/screencopy/build.hpp.in b/src/wayland/screencopy/build.hpp.in new file mode 100644 index 0000000..9276daa --- /dev/null +++ b/src/wayland/screencopy/build.hpp.in @@ -0,0 +1,6 @@ +#pragma once +// NOLINTBEGIN +#cmakedefine01 SCREENCOPY_ICC +#cmakedefine01 SCREENCOPY_WLR +#cmakedefine01 SCREENCOPY_HYPRLAND_TOPLEVEL +// NOLINTEND diff --git a/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt b/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt new file mode 100644 index 0000000..2600b73 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt @@ -0,0 +1,15 @@ +qt_add_library(quickshell-wayland-screencopy-hyprland STATIC + hyprland_screencopy.cpp +) + +wl_proto(wlp-hyprland-screencopy hyprland-toplevel-export-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(quickshell-wayland-screencopy-hyprland PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) + +target_link_libraries(quickshell-wayland-screencopy-hyprland PUBLIC + wlp-hyprland-screencopy wlp-foreign-toplevel +) + +qs_pch(quickshell-wayland-screencopy-hyprland SET large) diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml b/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml new file mode 100644 index 0000000..b1185aa --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml @@ -0,0 +1,228 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to ask for exporting another toplevel's + surface(s) to a buffer. + + Particularly useful for sharing a single window. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of a toplevel. (window) + + The captured frame will not contain any server-side decorations and will + ignore the compositor-set geometry, like e.g. rounded corners. + + It will contain all the subsurfaces and popups, however the latter will be clipped + to the geometry of the base surface. + + The handle parameter refers to the address of the window as seen in `hyprctl clients`. + For example, for d161e7b0 it would be 3512854448. + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. + + + + + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have the + correct size, see hyprland_toplevel_export_frame_v1.buffer and + hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" event is + sent. Otherwise, a "failed" event is sent. + + This event will wait for appropriate damage to be copied, unless the ignore_damage + arg is set to a non-zero value. + + + + + + + + This event is sent right before the ready event when ignore_damage was + not set. It may be generated multiple times for each copy + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp new file mode 100644 index 0000000..457f105 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -0,0 +1,122 @@ +#include "hyprland_screencopy.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" +#include "hyprland_screencopy_p.hpp" + +namespace qs::wayland::screencopy::hyprland { + +namespace { +Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.hyprland", QtWarningMsg); +} + +HyprlandScreencopyManager::HyprlandScreencopyManager(): QWaylandClientExtensionTemplate(2) { + this->initialize(); +} + +HyprlandScreencopyManager* HyprlandScreencopyManager::instance() { + static auto* instance = new HyprlandScreencopyManager(); + return instance; +} + +ScreencopyContext* HyprlandScreencopyManager::captureToplevel( + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors +) { + return new HyprlandScreencopyContext(this, handle, paintCursors); +} + +HyprlandScreencopyContext::HyprlandScreencopyContext( + HyprlandScreencopyManager* manager, + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors +) + : manager(manager) + , handle(handle) + , paintCursors(paintCursors) { + QObject::connect( + handle, + &QObject::destroyed, + this, + &HyprlandScreencopyContext::onToplevelDestroyed + ); +} + +HyprlandScreencopyContext::~HyprlandScreencopyContext() { + if (this->object()) this->destroy(); +} + +void HyprlandScreencopyContext::onToplevelDestroyed() { + qCWarning(logScreencopy) << "Toplevel destroyed while recording. Stopping" << this; + if (this->object()) this->destroy(); + emit this->stopped(); +} + +void HyprlandScreencopyContext::captureFrame() { + if (this->object()) return; + + this->init(this->manager->capture_toplevel_with_wlr_toplevel_handle( + this->paintCursors ? 1 : 0, + this->handle->object() + )); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer( + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t /*stride*/ +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.shm.formats.push(format); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_linux_dmabuf( + uint32_t format, + uint32_t width, + uint32_t height +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.dmabuf.formats.push(format); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_flags(uint32_t flags) { + if (flags & HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_Y_INVERT) { + this->mSwapchain.backbuffer()->transform = buffer::WlBufferTransform::Flipped180; + } +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer_done() { + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + this->copy(backbuffer->buffer(), this->copiedFirstFrame ? 0 : 1); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_ready( + uint32_t /*tvSecHi*/, + uint32_t /*tvSecLo*/, + uint32_t /*tvNsec*/ +) { + this->destroy(); + this->copiedFirstFrame = true; + this->mSwapchain.swapBuffers(); + emit this->frameCaptured(); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_failed() { + qCWarning(logScreencopy) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); +} + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp new file mode 100644 index 0000000..fbd08c5 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" + +namespace qs::wayland::screencopy::hyprland { + +class HyprlandScreencopyManager + : public QWaylandClientExtensionTemplate + , public QtWayland::hyprland_toplevel_export_manager_v1 { +public: + ScreencopyContext* + captureToplevel(toplevel_management::impl::ToplevelHandle* handle, bool paintCursors); + + static HyprlandScreencopyManager* instance(); + +private: + explicit HyprlandScreencopyManager(); + + friend class HyprlandScreencopyContext; +}; + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp new file mode 100644 index 0000000..199390e --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" + +namespace qs::wayland::screencopy::hyprland { + +class HyprlandScreencopyManager; + +class HyprlandScreencopyContext + : public ScreencopyContext + , public QtWayland::hyprland_toplevel_export_frame_v1 { +public: + explicit HyprlandScreencopyContext( + HyprlandScreencopyManager* manager, + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors + ); + + ~HyprlandScreencopyContext() override; + Q_DISABLE_COPY_MOVE(HyprlandScreencopyContext); + + void captureFrame() override; + +protected: + // clang-format off + void hyprland_toplevel_export_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) override; + void hyprland_toplevel_export_frame_v1_linux_dmabuf(uint32_t format, uint32_t width, uint32_t height) override; + void hyprland_toplevel_export_frame_v1_flags(uint32_t flags) override; + void hyprland_toplevel_export_frame_v1_buffer_done() override; + void hyprland_toplevel_export_frame_v1_ready(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvNsec) override; + void hyprland_toplevel_export_frame_v1_failed() override; + // clang-format on + +private slots: + void onToplevelDestroyed(); + +private: + HyprlandScreencopyManager* manager; + buffer::WlBufferRequest request; + bool copiedFirstFrame = false; + + toplevel_management::impl::ToplevelHandle* handle; + bool paintCursors; +}; + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/image_copy_capture/CMakeLists.txt b/src/wayland/screencopy/image_copy_capture/CMakeLists.txt new file mode 100644 index 0000000..25ed60d --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-wayland-screencopy-icc STATIC + image_copy_capture.cpp +) + +wl_proto(wlp-ext-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") +wl_proto(wlp-image-copy-capture ext-image-copy-capture-v1 "${WAYLAND_PROTOCOLS}/staging/ext-image-copy-capture") +wl_proto(wlp-image-capture-source ext-image-capture-source-v1 "${WAYLAND_PROTOCOLS}/staging/ext-image-capture-source") + +target_link_libraries(quickshell-wayland-screencopy-icc PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) + +target_link_libraries(quickshell-wayland-screencopy-icc PUBLIC + wlp-image-copy-capture wlp-image-capture-source + wlp-ext-foreign-toplevel # required for capture source to build +) + +qs_pch(quickshell-wayland-screencopy-icc SET large) diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp new file mode 100644 index 0000000..649b111 --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp @@ -0,0 +1,225 @@ +#include "image_copy_capture.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../manager.hpp" +#include "image_copy_capture_p.hpp" + +namespace qs::wayland::screencopy::icc { + +namespace { +Q_LOGGING_CATEGORY(logIcc, "quickshell.wayland.screencopy.icc", QtWarningMsg); +} + +using IccCaptureSession = QtWayland::ext_image_copy_capture_session_v1; +using IccCaptureFrame = QtWayland::ext_image_copy_capture_frame_v1; + +IccScreencopyContext::IccScreencopyContext(::ext_image_copy_capture_session_v1* session) + : IccCaptureSession(session) {} + +IccScreencopyContext::~IccScreencopyContext() { + if (this->IccCaptureSession::object()) { + this->IccCaptureSession::destroy(); + } + + if (this->IccCaptureFrame::object()) { + this->IccCaptureFrame::destroy(); + } +} + +void IccScreencopyContext::captureFrame() { + if (this->IccCaptureFrame::object() || this->capturePending) return; + + if (this->statePending) this->capturePending = true; + else this->doCapture(); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_buffer_size( + uint32_t width, + uint32_t height +) { + this->clearOldState(); + + this->request.width = width; + this->request.height = height; +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_shm_format(uint32_t format) { + this->clearOldState(); + + this->request.shm.formats.push(format); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_dmabuf_device(wl_array* device) { + this->clearOldState(); + + if (device->size != sizeof(dev_t)) { + qCFatal(logIcc) << "The size of dev_t used by the compositor and quickshell is mismatched. Try " + "recompiling both."; + } + + this->request.dmabuf.device = *reinterpret_cast(device->data); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_dmabuf_format( + uint32_t format, + wl_array* modifiers +) { + this->clearOldState(); + + auto* modifierArray = reinterpret_cast(modifiers->data); + auto modifierCount = modifiers->size / sizeof(uint64_t); + + auto reqFormat = buffer::WlBufferRequest::DmaFormat(format); + + for (uint16_t i = 0; i != modifierCount; i++) { + reqFormat.modifiers.push(modifierArray[i]); // NOLINT + } + + this->request.dmabuf.formats.push(reqFormat); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_done() { + this->statePending = false; + + if (this->capturePending) { + this->doCapture(); + } +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_stopped() { + qCInfo(logIcc) << "Ending recording due to screencopy stop for" << this; + emit this->stopped(); +} + +void IccScreencopyContext::clearOldState() { + if (!this->statePending) { + this->request = buffer::WlBufferRequest(); + this->statePending = true; + } +} + +void IccScreencopyContext::doCapture() { + this->capturePending = false; + + auto newBuffer = false; + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request, &newBuffer); + + this->IccCaptureFrame::init(this->IccCaptureSession::create_frame()); + this->IccCaptureFrame::attach_buffer(backbuffer->buffer()); + + if (newBuffer) { + // If the buffer was replaced, it will be blank and the compositor needs + // to repaint the whole thing. + this->IccCaptureFrame::damage_buffer( + 0, + 0, + static_cast(this->request.width), + static_cast(this->request.height) + ); + + // We don't care about partial damage if the whole buffer was replaced. + this->lastDamage = QRect(); + } else if (!this->lastDamage.isEmpty()) { + // If buffers were swapped between the last frame and the current one, request a repaint + // of the backbuffer in the same places that changes to the frontbuffer were recorded. + this->IccCaptureFrame::damage_buffer( + this->lastDamage.x(), + this->lastDamage.y(), + this->lastDamage.width(), + this->lastDamage.height() + ); + + // We don't need to do this more than once per buffer swap. + this->lastDamage = QRect(); + } + + this->IccCaptureFrame::capture(); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_transform(uint32_t transform) { + this->mSwapchain.backbuffer()->transform = transform; +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_damage( + int32_t x, + int32_t y, + int32_t width, + int32_t height +) { + this->damage = this->damage.united(QRect(x, y, width, height)); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_ready() { + this->IccCaptureFrame::destroy(); + + this->mSwapchain.swapBuffers(); + this->lastDamage = this->damage; + this->damage = QRect(); + + emit this->frameCaptured(); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_failed(uint32_t reason) { + switch (static_cast(reason)) { + case IccCaptureFrame::failure_reason_buffer_constraints: + qFatal(logIcc) << "Got a buffer_constraints failure, however the buffer matches the last sent " + "size. There is a bug in quickshell or your compositor."; + break; + case IccCaptureFrame::failure_reason_stopped: + // Handled in the ExtCaptureSession handler. + break; + case IccCaptureFrame::failure_reason_unknown: + qCWarning(logIcc) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); + break; + } +} + +IccManager::IccManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +IccManager* IccManager::instance() { + static auto* instance = new IccManager(); + return instance; +} + +ScreencopyContext* +IccManager::createSession(::ext_image_capture_source_v1* source, bool paintCursors) { + auto* session = this->create_session( + source, + paintCursors ? QtWayland::ext_image_copy_capture_manager_v1::options_paint_cursors : 0 + ); + return new IccScreencopyContext(session); +} + +IccOutputSourceManager::IccOutputSourceManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +IccOutputSourceManager* IccOutputSourceManager::instance() { + static auto* instance = new IccOutputSourceManager(); + return instance; +} + +ScreencopyContext* IccOutputSourceManager::captureOutput(QScreen* screen, bool paintCursors) { + auto* waylandScreen = dynamic_cast(screen->handle()); + if (!waylandScreen) return nullptr; + + return IccManager::instance()->createSession( + this->create_source(waylandScreen->output()), + paintCursors + ); +} + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp new file mode 100644 index 0000000..93ba36c --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::icc { + +class IccManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_image_copy_capture_manager_v1 { +public: + ScreencopyContext* createSession(::ext_image_capture_source_v1* source, bool paintCursors); + + static IccManager* instance(); + +private: + explicit IccManager(); +}; + +class IccOutputSourceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_output_image_capture_source_manager_v1 { +public: + ScreencopyContext* captureOutput(QScreen* screen, bool paintCursors); + + static IccOutputSourceManager* instance(); + +private: + explicit IccOutputSourceManager(); +}; + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp new file mode 100644 index 0000000..14f2067 --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::icc { + +class IccScreencopyContext + : public ScreencopyContext + , public QtWayland::ext_image_copy_capture_session_v1 + , public QtWayland::ext_image_copy_capture_frame_v1 { + +public: + IccScreencopyContext(::ext_image_copy_capture_session_v1* session); + ~IccScreencopyContext() override; + Q_DISABLE_COPY_MOVE(IccScreencopyContext); + + void captureFrame() override; + +protected: + // clang-formt off + void ext_image_copy_capture_session_v1_buffer_size(uint32_t width, uint32_t height) override; + void ext_image_copy_capture_session_v1_shm_format(uint32_t format) override; + void ext_image_copy_capture_session_v1_dmabuf_device(wl_array* device) override; + void + ext_image_copy_capture_session_v1_dmabuf_format(uint32_t format, wl_array* modifiers) override; + void ext_image_copy_capture_session_v1_done() override; + void ext_image_copy_capture_session_v1_stopped() override; + + void ext_image_copy_capture_frame_v1_transform(uint32_t transform) override; + void ext_image_copy_capture_frame_v1_damage(int32_t x, int32_t y, int32_t width, int32_t height) + override; + void ext_image_copy_capture_frame_v1_ready() override; + void ext_image_copy_capture_frame_v1_failed(uint32_t reason) override; + // clang-formt on + +private: + void clearOldState(); + void doCapture(); + + buffer::WlBufferRequest request; + bool statePending = true; + bool capturePending = false; + QRect damage; + QRect lastDamage; +}; + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/manager.cpp b/src/wayland/screencopy/manager.cpp new file mode 100644 index 0000000..8345e31 --- /dev/null +++ b/src/wayland/screencopy/manager.cpp @@ -0,0 +1,56 @@ +#include "manager.hpp" + +#include + +#include "build.hpp" + +#if SCREENCOPY_ICC || SCREENCOPY_WLR +#include "../../core/qmlscreen.hpp" +#endif + +#if SCREENCOPY_ICC +#include "image_copy_capture/image_copy_capture.hpp" +#endif + +#if SCREENCOPY_WLR +#include "wlr_screencopy/wlr_screencopy.hpp" +#endif + +#if SCREENCOPY_HYPRLAND_TOPLEVEL +#include "../toplevel_management/qml.hpp" +#include "hyprland_screencopy/hyprland_screencopy.hpp" +#endif + +namespace qs::wayland::screencopy { + +ScreencopyContext* ScreencopyManager::createContext(QObject* object, bool paintCursors) { + if (auto* screen = qobject_cast(object)) { +#if SCREENCOPY_ICC + { + auto* manager = icc::IccOutputSourceManager::instance(); + if (manager->isActive()) { + return manager->captureOutput(screen->screen, paintCursors); + } + } +#endif +#if SCREENCOPY_WLR + { + auto* manager = wlr::WlrScreencopyManager::instance(); + if (manager->isActive()) { + return manager->captureOutput(screen->screen, paintCursors); + } + } +#endif +#if SCREENCOPY_HYPRLAND_TOPLEVEL + } else if (auto* toplevel = qobject_cast(object)) { + auto* manager = hyprland::HyprlandScreencopyManager::instance(); + if (manager->isActive()) { + return manager->captureToplevel(toplevel->implHandle(), paintCursors); + } +#endif + } + + return nullptr; +} + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/manager.hpp b/src/wayland/screencopy/manager.hpp new file mode 100644 index 0000000..f58e005 --- /dev/null +++ b/src/wayland/screencopy/manager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "../buffer/manager.hpp" + +namespace qs::wayland::screencopy { + +class ScreencopyContext: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] buffer::WlBufferSwapchain& swapchain() { return this->mSwapchain; } + virtual void captureFrame() = 0; + +signals: + void frameCaptured(); + void stopped(); + +protected: + ScreencopyContext() = default; + + buffer::WlBufferSwapchain mSwapchain; +}; + +class ScreencopyManager { +public: + static ScreencopyContext* createContext(QObject* object, bool paintCursors); +}; + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/view.cpp b/src/wayland/screencopy/view.cpp new file mode 100644 index 0000000..fe51735 --- /dev/null +++ b/src/wayland/screencopy/view.cpp @@ -0,0 +1,149 @@ +#include "view.hpp" + +#include +#include +#include +#include +#include + +#include "../buffer/manager.hpp" +#include "../buffer/qsg.hpp" +#include "manager.hpp" + +namespace qs::wayland::screencopy { + +void ScreencopyView::setCaptureSource(QObject* captureSource) { + if (captureSource == this->mCaptureSource) return; + auto hadContext = this->context != nullptr; + this->destroyContext(false); + + this->mCaptureSource = captureSource; + + if (captureSource) { + QObject::connect( + captureSource, + &QObject::destroyed, + this, + &ScreencopyView::onCaptureSourceDestroyed + ); + + if (this->completed) this->createContext(); + } + + if (!this->context && hadContext) this->update(); + emit this->captureSourceChanged(); +} + +void ScreencopyView::onCaptureSourceDestroyed() { + this->mCaptureSource = nullptr; + this->destroyContext(); +} + +void ScreencopyView::setPaintCursors(bool paintCursors) { + if (paintCursors == this->mPaintCursors) return; + this->mPaintCursors = paintCursors; + if (this->completed && this->context) this->createContext(); + emit this->paintCursorsChanged(); +} + +void ScreencopyView::setLive(bool live) { + if (live == this->mLive) return; + + if (live && !this->mLive && this->context) { + this->context->captureFrame(); + } + + this->mLive = live; + emit this->liveChanged(); +} + +void ScreencopyView::createContext() { + this->destroyContext(false); + this->context = ScreencopyManager::createContext(this->mCaptureSource, this->mPaintCursors); + + if (!this->context) { + qmlWarning(this) << "Capture source set to non captureable object."; + return; + } + + QObject::connect( + this->context, + &ScreencopyContext::stopped, + this, + &ScreencopyView::destroyContextWithUpdate + ); + + QObject::connect( + this->context, + &ScreencopyContext::frameCaptured, + this, + &ScreencopyView::onFrameCaptured + ); + + this->context->captureFrame(); +} + +void ScreencopyView::destroyContext(bool update) { + auto hadContext = this->context != nullptr; + delete this->context; + this->context = nullptr; + this->bHasContent = false; + this->bSourceSize = QSize(); + if (hadContext && update) this->update(); +} + +void ScreencopyView::captureFrame() { + if (this->context) this->context->captureFrame(); + else qmlWarning(this) << "Cannot capture frame, as no recording context is ready."; +} + +void ScreencopyView::onFrameCaptured() { + this->setFlag(QQuickItem::ItemHasContents); + this->update(); + this->bHasContent = true; + this->bSourceSize = this->context->swapchain().frontbuffer()->size(); +} + +void ScreencopyView::componentComplete() { + this->QQuickItem::componentComplete(); + + auto* bufManager = buffer::WlBufferManager::instance(); + if (!bufManager->isReady()) { + QObject::connect( + bufManager, + &buffer::WlBufferManager::ready, + this, + &ScreencopyView::onBuffersReady + ); + } else { + this->onBuffersReady(); + } +} + +void ScreencopyView::onBuffersReady() { + this->completed = true; + if (this->mCaptureSource) this->createContext(); +} + +QSGNode* ScreencopyView::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* /*unused*/) { + if (!this->context || !this->bHasContent) { + delete oldNode; + this->setFlag(QQuickItem::ItemHasContents, false); + return nullptr; + } + + auto* node = static_cast(oldNode); // NOLINT + + if (!node) { + node = new buffer::WlBufferQSGDisplayNode(this->window()); + } + + auto& swapchain = this->context->swapchain(); + node->syncSwapchain(swapchain); + node->setRect(this->boundingRect()); + + if (this->mLive) this->context->captureFrame(); + return node; +} + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/view.hpp b/src/wayland/screencopy/view.hpp new file mode 100644 index 0000000..53f4239 --- /dev/null +++ b/src/wayland/screencopy/view.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::screencopy { + +///! Displays a video stream from other windows or a monitor. +/// ScreencopyView displays live video streams or single captured frames from valid +/// capture sources. See @@captureSource for details on which objects are accepted. +class ScreencopyView: public QQuickItem { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// The object to capture from. Accepts any of the following: + /// - `null` - Clears the displayed image. + /// - @@Quickshell.ShellScreen - A monitor. + /// Requires a compositor that supports `wlr-screencopy-unstable` + /// or both `ext-image-copy-capture-v1` and `ext-capture-source-v1`. + /// - @@Quickshell.Wayland.Toplevel - A toplevel window. + /// Requires a compositor that supports `hyprland-toplevel-export-v1`. + Q_PROPERTY(QObject* captureSource READ captureSource WRITE setCaptureSource NOTIFY captureSourceChanged); + /// If true, the system cursor will be painted on the image. Defaults to false. + Q_PROPERTY(bool paintCursor READ paintCursors WRITE setPaintCursors NOTIFY paintCursorsChanged); + /// If true, a live video feed from the capture source will be displayed instead of a still image. + /// Defaults to false. + Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged); + /// If true, the view has content ready to display. Content is not always immediately available, + /// and this property can be used to avoid displaying it until ready. + Q_PROPERTY(bool hasContent READ default NOTIFY hasContentChanged BINDABLE bindableHasContent); + /// The size of the source image. Valid when @@hasContent is true. + Q_PROPERTY(QSize sourceSize READ default NOTIFY sourceSizeChanged BINDABLE bindableSourceSize); + // clang-format on + +public: + explicit ScreencopyView(QQuickItem* parent = nullptr): QQuickItem(parent) {} + + void componentComplete() override; + + /// Capture a single frame. Has no effect if @@live is true. + Q_INVOKABLE void captureFrame(); + + [[nodiscard]] QObject* captureSource() const { return this->mCaptureSource; } + void setCaptureSource(QObject* captureSource); + + [[nodiscard]] bool paintCursors() const { return this->mPaintCursors; } + void setPaintCursors(bool paintCursors); + + [[nodiscard]] bool live() const { return this->mLive; } + void setLive(bool live); + + [[nodiscard]] QBindable bindableHasContent() { return &this->bHasContent; } + [[nodiscard]] QBindable bindableSourceSize() { return &this->bSourceSize; } + +signals: + /// The compositor has ended the video stream. Attempting to restart it may or may not work. + void stopped(); + + void captureSourceChanged(); + void paintCursorsChanged(); + void liveChanged(); + void hasContentChanged(); + void sourceSizeChanged(); + +protected: + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* data) override; + +private slots: + void onCaptureSourceDestroyed(); + void onFrameCaptured(); + void destroyContextWithUpdate() { this->destroyContext(); } + void onBuffersReady(); + +private: + void destroyContext(bool update = true); + void createContext(); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ScreencopyView, bool, bHasContent, &ScreencopyView::hasContentChanged); + Q_OBJECT_BINDABLE_PROPERTY(ScreencopyView, QSize, bSourceSize, &ScreencopyView::sourceSizeChanged); + // clang-format on + + QObject* mCaptureSource = nullptr; + bool mPaintCursors = false; + bool mLive = false; + ScreencopyContext* context = nullptr; + bool completed = false; +}; + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt b/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt new file mode 100644 index 0000000..17c13c2 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt @@ -0,0 +1,13 @@ +qt_add_library(quickshell-wayland-screencopy-wlr STATIC + wlr_screencopy.cpp +) + +wl_proto(wlp-wlr-screencopy wlr-screencopy-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(quickshell-wayland-screencopy-wlr PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) + +target_link_libraries(quickshell-wayland-screencopy-wlr PUBLIC wlp-wlr-screencopy) + +qs_pch(quickshell-wayland-screencopy-wlr SET large) diff --git a/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml b/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml new file mode 100644 index 0000000..50b1b7d --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml @@ -0,0 +1,232 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Andri Yngvason + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to ask the compositor to copy part of the + screen content to a client buffer. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of an entire output. + + + + + + + + + Capture the next frame of an output's region. + + The region is given in output logical coordinates, see + xdg_output.logical_size. The region will be clipped to the output's + extents. + + + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + For objects version 2 or lower, wl_shm buffers are always supported, ie. + the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have a the + correct size, see zwlr_screencopy_frame_v1.buffer and + zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" events are + sent. Otherwise, a "failed" event is sent. + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + + Same as copy, except it waits until there is damage to copy. + + + + + + + This event is sent right before the ready event when copy_with_damage is + requested. It may be generated multiple times for each copy_with_damage + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy_with_damage + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp new file mode 100644 index 0000000..8cc89bc --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -0,0 +1,133 @@ +#include "wlr_screencopy.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../buffer/manager.hpp" +#include "../manager.hpp" +#include "wlr_screencopy_p.hpp" + +namespace qs::wayland::screencopy::wlr { + +namespace { +Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.wlr", QtWarningMsg); +} + +WlrScreencopyManager::WlrScreencopyManager(): QWaylandClientExtensionTemplate(3) { + this->initialize(); +} + +WlrScreencopyManager* WlrScreencopyManager::instance() { + static auto* instance = new WlrScreencopyManager(); + return instance; +} + +ScreencopyContext* +WlrScreencopyManager::captureOutput(QScreen* screen, bool paintCursors, QRect region) { + if (!dynamic_cast(screen->handle())) return nullptr; + return new WlrScreencopyContext(this, screen, paintCursors, region); +} + +WlrScreencopyContext::WlrScreencopyContext( + WlrScreencopyManager* manager, + QScreen* screen, + bool paintCursors, + QRect region +) + : manager(manager) + , screen(dynamic_cast(screen->handle())) + , paintCursors(paintCursors) + , region(region) { + QObject::connect(screen, &QObject::destroyed, this, &WlrScreencopyContext::onScreenDestroyed); +} + +WlrScreencopyContext::~WlrScreencopyContext() { + if (this->object()) this->destroy(); +} + +void WlrScreencopyContext::onScreenDestroyed() { + qCWarning(logScreencopy) << "Screen destroyed while recording. Stopping" << this; + if (this->object()) this->destroy(); + emit this->stopped(); +} + +void WlrScreencopyContext::captureFrame() { + if (this->object()) return; + + if (this->region.isEmpty()) { + this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); + } else { + this->init(manager->capture_output_region( + this->paintCursors ? 1 : 0, + screen->output(), + this->region.x(), + this->region.y(), + this->region.width(), + this->region.height() + )); + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer( + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t /*stride*/ +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.shm.formats.push(format); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_linux_dmabuf( + uint32_t format, + uint32_t width, + uint32_t height +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.dmabuf.formats.push(format); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_flags(uint32_t flags) { + if (flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT) { + this->mSwapchain.backbuffer()->transform = buffer::WlBufferTransform::Flipped180; + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer_done() { + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + + if (this->copiedFirstFrame) { + this->copy_with_damage(backbuffer->buffer()); + } else { + this->copy(backbuffer->buffer()); + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_ready( + uint32_t /*tvSecHi*/, + uint32_t /*tvSecLo*/, + uint32_t /*tvNsec*/ +) { + this->destroy(); + this->copiedFirstFrame = true; + this->mSwapchain.swapBuffers(); + emit this->frameCaptured(); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_failed() { + qCWarning(logScreencopy) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); +} + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp new file mode 100644 index 0000000..bea1733 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::wlr { + +class WlrScreencopyManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_screencopy_manager_v1 { +public: + ScreencopyContext* captureOutput(QScreen* screen, bool paintCursors, QRect region = QRect()); + + static WlrScreencopyManager* instance(); + +private: + explicit WlrScreencopyManager(); + + friend class WlrScreencopyContext; +}; + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp new file mode 100644 index 0000000..7bdbafb --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::wlr { + +class WlrScreencopyManager; + +class WlrScreencopyContext + : public ScreencopyContext + , public QtWayland::zwlr_screencopy_frame_v1 { +public: + explicit WlrScreencopyContext( + WlrScreencopyManager* manager, + QScreen* screen, + bool paintCursors, + QRect region + ); + ~WlrScreencopyContext() override; + Q_DISABLE_COPY_MOVE(WlrScreencopyContext); + + void captureFrame() override; + +protected: + // clang-format off + void zwlr_screencopy_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) override; + void zwlr_screencopy_frame_v1_linux_dmabuf(uint32_t format, uint32_t width, uint32_t height) override; + void zwlr_screencopy_frame_v1_flags(uint32_t flags) override; + void zwlr_screencopy_frame_v1_buffer_done() override; + void zwlr_screencopy_frame_v1_ready(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvNsec) override; + void zwlr_screencopy_frame_v1_failed() override; + // clang-format on + +private slots: + void onScreenDestroyed(); + +private: + WlrScreencopyManager* manager; + buffer::WlBufferRequest request; + bool copiedFirstFrame = false; + + QtWaylandClient::QWaylandScreen* screen; + bool paintCursors; + QRect region; +}; + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 127b4c8..2034734 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -13,7 +13,7 @@ namespace qs::wayland::toplevel_management { namespace impl { -class ToplevelManager; +class ToplevelManager; // NOLINT class ToplevelHandle; } // namespace impl @@ -80,6 +80,8 @@ class Toplevel: public QObject { [[nodiscard]] bool fullscreen() const; void setFullscreen(bool fullscreen); + [[nodiscard]] impl::ToplevelHandle* implHandle() const { return this->handle; } + signals: void closed(); void appIdChanged();