1
0
Fork 0

wayland/screencopy: add screencopy

This commit is contained in:
outfoxxed 2025-01-14 04:43:05 -08:00
parent 918dd2392d
commit cd429142a4
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
37 changed files with 3149 additions and 3 deletions

View file

@ -43,6 +43,7 @@ jobs:
qt6-shadertools \
wayland-protocols \
wayland \
libdrm \
libxcb \
libpipewire \
cli11 \

View file

@ -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.

View file

@ -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)

View file

@ -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)

159
src/core/stacklist.hpp Normal file
View file

@ -0,0 +1,159 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <vector>
#include <qtypes.h>
template <class T, size_t N>
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<StackList<T, N>*>(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<T, N>& 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 <typename Self, typename ListPtr, typename IT>
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<Self*>(this);
}
Self& operator--() {
--this->i;
return *static_cast<Self*>(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<int64_t>(this->i) - static_cast<int64_t>(other.i);
}
Self& operator+(difference_type offset) {
return Self(this->list, static_cast<int64_t>(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<Iterator, StackList<T, N>*, T> {
Iterator() = default;
Iterator(StackList<T, N>* list, size_t i)
: BaseIterator<Iterator, StackList<T, N>*, T>(list, i) {}
};
struct ConstIterator: public BaseIterator<ConstIterator, const StackList<T, N>*, const T> {
ConstIterator() = default;
ConstIterator(const StackList<T, N>* list, size_t i)
: BaseIterator<ConstIterator, const StackList<T, N>*, 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<char*>(alloc) + arraylen, // NOLINT
this->vec.data(),
this->vec.size() * sizeof(T)
);
}
return static_cast<T*>(alloc);
}
private:
std::array<T, N> array {};
std::vector<T> 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

View file

@ -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()

View file

@ -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)

View file

@ -0,0 +1,659 @@
#include "dmabuf.hpp"
#include <algorithm>
#include <array>
#include <csignal>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GLES3/gl32.h>
#include <fcntl.h>
#include <gbm.h>
#include <libdrm/drm_fourcc.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qopenglcontext.h>
#include <qopenglcontext_platform.h>
#include <qpair.h>
#include <qquickwindow.h>
#include <qscopedpointer.h>
#include <qsgtexture_platform.h>
#include <qtclasshelpermacros.h>
#include <qwayland-linux-dmabuf-v1.h>
#include <qwaylandclientextension.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <wayland-client-protocol.h>
#include <wayland-linux-dmabuf-v1-client-protocol.h>
#include <wayland-util.h>
#include <xf86drm.h>
#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<char>(code >> 0 & 0xff),
static_cast<char>(code >> 8 & 0xff),
static_cast<char>(code >> 16 & 0xff),
static_cast<char>(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<char, 5> 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<const void*>(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<dev_t*>(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<dev_t*>(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<FormatTableEntry*>(this->formatTable);
auto* indexTable = reinterpret_cast<uint16_t*>(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<drmDevice, DrmFree>(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<WlDmaBuffer>(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<int32_t>(width), static_cast<int32_t>(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<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(
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<QNativeInterface::QEGLContext>();
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<EGLAttrib, 6 * 2 + 1> {
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<int>(this->width), static_cast<int>(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<QNativeInterface::QEGLContext>()->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

View file

@ -0,0 +1,195 @@
#pragma once
#include <cstdint>
#include <EGL/egl.h>
#include <gbm.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlist.h>
#include <qquickwindow.h>
#include <qsgtexture.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <qtypes.h>
#include <qwayland-linux-dmabuf-v1.h>
#include <qwaylandclientextension.h>
#include <sys/types.h>
#include <wayland-linux-dmabuf-v1-client-protocol.h>
#include <wayland-util.h>
#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<int>(this->width), static_cast<int>(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<uint64_t, 10> modifiers;
bool implicit = false;
};
struct LinuxDmabufFormatSelection {
bool sorted = false;
StackList<QPair<uint32_t, LinuxDmabufModifiers>, 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<LinuxDmabufTranche> tranches;
void* formatTable = nullptr;
uint32_t formatTableSize = 0;
};
class LinuxDmabufManager
: public QWaylandClientExtensionTemplate<LinuxDmabufManager>
, 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<LinuxDmabufTranche> tranches;
QList<SharedGbmDevice> gbmDevices;
WlBufferManagerPrivate* manager;
friend class LinuxDmabufFeedback;
friend class GbmDeviceHandle;
};
} // namespace qs::wayland::buffer::dmabuf

View file

@ -0,0 +1,114 @@
#include "manager.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qmatrix4x4.h>
#include <qnamespace.h>
#include <qquickwindow.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qvectornd.h>
#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<float>(center.x());
auto centerY = static_cast<float>(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

View file

@ -0,0 +1,134 @@
#pragma once
#include <cstdint>
#include <memory>
#include <qhash.h>
#include <qlist.h>
#include <qmatrix4x4.h>
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qvariant.h>
#include <wayland-client-protocol.h>
#include <wayland-client.h>
#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>(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<float>(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<uint64_t, 10> modifiers;
};
struct {
StackList<uint32_t, 1> formats;
} shm;
struct {
dev_t device = 0;
StackList<DmaFormat, 1> 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<WlBuffer> buffer1;
std::unique_ptr<WlBuffer> 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

View file

@ -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

View file

@ -0,0 +1,45 @@
#pragma once
#include <memory>
#include <qcontainerfwd.h>
#include <qquickwindow.h>
#include <qsgimagenode.h>
#include <qsgnode.h>
#include <qsgtexture.h>
#include <qvectornd.h>
#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<WlBuffer*, std::unique_ptr<WlBufferQSGTexture>> buffer1;
QPair<WlBuffer*, std::unique_ptr<WlBufferQSGTexture>> buffer2;
bool presentSecondBuffer = false;
};
} // namespace qs::wayland::buffer

View file

@ -0,0 +1,91 @@
#include "shm.hpp"
#include <algorithm>
#include <memory>
#include <private/qwaylanddisplay_p.h>
#include <private/qwaylandintegration_p.h>
#include <private/qwaylandshm_p.h>
#include <private/qwaylandshmbackingstore_p.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qquickwindow.h>
#include <qsize.h>
#include <wayland-client-protocol.h>
#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<int>(request.width), static_cast<int>(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<const void*>(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<int>(request.width), static_cast<int>(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

View file

@ -0,0 +1,63 @@
#pragma once
#include <memory>
#include <private/qwaylandshmbackingstore_p.h>
#include <qquickwindow.h>
#include <qsgtexture.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <wayland-client-protocol.h>
#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<QtWaylandClient::QWaylandShmBuffer> 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<QtWaylandClient::QWaylandShmBuffer> shmBuffer;
std::unique_ptr<QSGTexture> qsgTexture;
friend class WlShmBuffer;
};
class ShmbufManager {
public:
[[nodiscard]] static WlBuffer* createShmbuf(const WlBufferRequest& request);
};
} // namespace qs::wayland::buffer::shm

View file

@ -5,5 +5,6 @@ headers = [
"wlr_layershell.hpp",
"session_lock.hpp",
"toplevel_management/qml.hpp",
"screencopy/view.hpp",
]
-----

View file

@ -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)

View file

@ -0,0 +1,6 @@
#pragma once
// NOLINTBEGIN
#cmakedefine01 SCREENCOPY_ICC
#cmakedefine01 SCREENCOPY_WLR
#cmakedefine01 SCREENCOPY_HYPRLAND_TOPLEVEL
// NOLINTEND

View file

@ -0,0 +1,16 @@
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
Qt::Quick # for pch
)
target_link_libraries(quickshell-wayland-screencopy-hyprland PUBLIC
wlp-hyprland-screencopy wlp-foreign-toplevel
)
qs_pch(quickshell-wayland-screencopy-hyprland SET large)

View file

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprland_toplevel_export_v1">
<copyright>
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.
</copyright>
<description summary="capturing the contents of toplevel windows">
This protocol allows clients to ask for exporting another toplevel's
surface(s) to a buffer.
Particularly useful for sharing a single window.
</description>
<interface name="hyprland_toplevel_export_manager_v1" version="2">
<description summary="manager to inform clients and begin capturing">
This object is a manager which offers requests to start capturing from a
source.
</description>
<request name="capture_toplevel">
<description summary="capture a toplevel">
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.
</description>
<arg name="frame" type="new_id" interface="hyprland_toplevel_export_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="handle" type="uint" summary="the handle of the toplevel (window) to be captured"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
<!-- Version 2 -->
<request name="capture_toplevel_with_wlr_toplevel_handle" since="2">
<description summary="capture a toplevel">
Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle.
</description>
<arg name="frame" type="new_id" interface="hyprland_toplevel_export_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="handle" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="false" summary="the zwlr_foreign_toplevel_handle_v1 handle of the toplevel to be captured"/>
</request>
<!-- End Version 2 -->
</interface>
<interface name="hyprland_toplevel_export_frame_v1" version="2">
<description summary="a frame ready for copy">
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.
</description>
<event name="buffer">
<description summary="wl_shm buffer information">
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.
</description>
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
<arg name="stride" type="uint" summary="buffer stride"/>
</event>
<request name="copy">
<description summary="copy the frame">
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.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
<arg name="ignore_damage" type="int"/>
</request>
<event name="damage">
<description summary="carries the coordinates of the damaged region">
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.
</description>
<arg name="x" type="uint" summary="damaged x coordinates"/>
<arg name="y" type="uint" summary="damaged y coordinates"/>
<arg name="width" type="uint" summary="current width"/>
<arg name="height" type="uint" summary="current height"/>
</event>
<enum name="error">
<entry name="already_used" value="0"
summary="the object has already been used to copy a wl_buffer"/>
<entry name="invalid_buffer" value="1"
summary="buffer attributes are invalid"/>
</enum>
<enum name="flags" bitfield="true">
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
</enum>
<event name="flags">
<description summary="frame flags">
Provides flags about the frame. This event is sent once before the
"ready" event.
</description>
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
</event>
<event name="ready">
<description summary="indicates frame is available for reading">
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.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<event name="failed">
<description summary="frame copy failed">
This event indicates that the attempted frame copy has failed.
After receiving this event, the client should destroy the object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="delete this object, used or not">
Destroys the frame. This request can be sent at any time by the client.
</description>
</request>
<event name="linux_dmabuf">
<description summary="linux-dmabuf buffer information">
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.
</description>
<arg name="format" type="uint" summary="fourcc pixel format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
</event>
<event name="buffer_done">
<description summary="all buffer types reported">
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.
</description>
</event>
</interface>
</protocol>

View file

@ -0,0 +1,122 @@
#include "hyprland_screencopy.hpp"
#include <cstdint>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qwaylandclientextension.h>
#include <wayland-hyprland-toplevel-export-v1-client-protocol.h>
#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

View file

@ -0,0 +1,26 @@
#pragma once
#include <qwayland-hyprland-toplevel-export-v1.h>
#include <qwaylandclientextension.h>
#include "../../toplevel_management/handle.hpp"
#include "../manager.hpp"
namespace qs::wayland::screencopy::hyprland {
class HyprlandScreencopyManager
: public QWaylandClientExtensionTemplate<HyprlandScreencopyManager>
, 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

View file

@ -0,0 +1,50 @@
#pragma once
#include <qtclasshelpermacros.h>
#include <qwayland-hyprland-toplevel-export-v1.h>
#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

View file

@ -0,0 +1,19 @@
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
Qt::Quick # for pch
)
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)

View file

@ -0,0 +1,225 @@
#include "image_copy_capture.hpp"
#include <cstdint>
#include <private/qwaylandscreen_p.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qrect.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qwayland-ext-image-copy-capture-v1.h>
#include <qwaylandclientextension.h>
#include <sys/types.h>
#include <wayland-ext-image-copy-capture-v1-client-protocol.h>
#include <wayland-util.h>
#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<dev_t*>(device->data);
}
void IccScreencopyContext::ext_image_copy_capture_session_v1_dmabuf_format(
uint32_t format,
wl_array* modifiers
) {
this->clearOldState();
auto* modifierArray = reinterpret_cast<uint64_t*>(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<int>(this->request.width),
static_cast<int>(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<IccCaptureFrame::failure_reason>(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<QtWaylandClient::QWaylandScreen*>(screen->handle());
if (!waylandScreen) return nullptr;
return IccManager::instance()->createSession(
this->create_source(waylandScreen->output()),
paintCursors
);
}
} // namespace qs::wayland::screencopy::icc

View file

@ -0,0 +1,36 @@
#pragma once
#include <qscreen.h>
#include <qwayland-ext-image-capture-source-v1.h>
#include <qwayland-ext-image-copy-capture-v1.h>
#include <qwaylandclientextension.h>
#include "../manager.hpp"
namespace qs::wayland::screencopy::icc {
class IccManager
: public QWaylandClientExtensionTemplate<IccManager>
, 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<IccOutputSourceManager>
, 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

View file

@ -0,0 +1,53 @@
#pragma once
#include <cstdint>
#include <qrect.h>
#include <qtclasshelpermacros.h>
#include <qwayland-ext-image-copy-capture-v1.h>
#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

View file

@ -0,0 +1,56 @@
#include "manager.hpp"
#include <qobject.h>
#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<QuickshellScreenInfo*>(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<toplevel_management::Toplevel*>(object)) {
auto* manager = hyprland::HyprlandScreencopyManager::instance();
if (manager->isActive()) {
return manager->captureToplevel(toplevel->implHandle(), paintCursors);
}
#endif
}
return nullptr;
}
} // namespace qs::wayland::screencopy

View file

@ -0,0 +1,33 @@
#pragma once
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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

View file

@ -0,0 +1,149 @@
#include "view.hpp"
#include <qobject.h>
#include <qqmlinfo.h>
#include <qquickitem.h>
#include <qsize.h>
#include <qtmetamacros.h>
#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<buffer::WlBufferQSGDisplayNode*>(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

View file

@ -0,0 +1,96 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qsgnode.h>
#include <qtmetamacros.h>
#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<bool> bindableHasContent() { return &this->bHasContent; }
[[nodiscard]] QBindable<QSize> 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

View file

@ -0,0 +1,14 @@
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
Qt::Quick # for pch
)
target_link_libraries(quickshell-wayland-screencopy-wlr PUBLIC wlp-wlr-screencopy)
qs_pch(quickshell-wayland-screencopy-wlr SET large)

View file

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_screencopy_unstable_v1">
<copyright>
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.
</copyright>
<description summary="screen content capturing on client buffers">
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.
</description>
<interface name="zwlr_screencopy_manager_v1" version="3">
<description summary="manager to inform clients and begin capturing">
This object is a manager which offers requests to start capturing from a
source.
</description>
<request name="capture_output">
<description summary="capture an output">
Capture the next frame of an entire output.
</description>
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="output" type="object" interface="wl_output"/>
</request>
<request name="capture_output_region">
<description summary="capture an output's region">
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.
</description>
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="output" type="object" interface="wl_output"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
</interface>
<interface name="zwlr_screencopy_frame_v1" version="3">
<description summary="a frame ready for copy">
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.
</description>
<event name="buffer">
<description summary="wl_shm buffer information">
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.
</description>
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
<arg name="stride" type="uint" summary="buffer stride"/>
</event>
<request name="copy">
<description summary="copy the frame">
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.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
</request>
<enum name="error">
<entry name="already_used" value="0"
summary="the object has already been used to copy a wl_buffer"/>
<entry name="invalid_buffer" value="1"
summary="buffer attributes are invalid"/>
</enum>
<enum name="flags" bitfield="true">
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
</enum>
<event name="flags">
<description summary="frame flags">
Provides flags about the frame. This event is sent once before the
"ready" event.
</description>
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
</event>
<event name="ready">
<description summary="indicates frame is available for reading">
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.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<event name="failed">
<description summary="frame copy failed">
This event indicates that the attempted frame copy has failed.
After receiving this event, the client should destroy the object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="delete this object, used or not">
Destroys the frame. This request can be sent at any time by the client.
</description>
</request>
<!-- Version 2 additions -->
<request name="copy_with_damage" since="2">
<description summary="copy the frame when it's damaged">
Same as copy, except it waits until there is damage to copy.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
</request>
<event name="damage" since="2">
<description summary="carries the coordinates of the damaged region">
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.
</description>
<arg name="x" type="uint" summary="damaged x coordinates"/>
<arg name="y" type="uint" summary="damaged y coordinates"/>
<arg name="width" type="uint" summary="current width"/>
<arg name="height" type="uint" summary="current height"/>
</event>
<!-- Version 3 additions -->
<event name="linux_dmabuf" since="3">
<description summary="linux-dmabuf buffer information">
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.
</description>
<arg name="format" type="uint" summary="fourcc pixel format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
</event>
<event name="buffer_done" since="3">
<description summary="all buffer types reported">
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.
</description>
</event>
</interface>
</protocol>

View file

@ -0,0 +1,133 @@
#include "wlr_screencopy.hpp"
#include <cstdint>
#include <private/qwaylandscreen_p.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qwaylandclientextension.h>
#include <wayland-wlr-screencopy-unstable-v1-client-protocol.h>
#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<QtWaylandClient::QWaylandScreen*>(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<QtWaylandClient::QWaylandScreen*>(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

View file

@ -0,0 +1,25 @@
#pragma once
#include <qscreen.h>
#include <qwayland-wlr-screencopy-unstable-v1.h>
#include <qwaylandclientextension.h>
#include "../manager.hpp"
namespace qs::wayland::screencopy::wlr {
class WlrScreencopyManager
: public QWaylandClientExtensionTemplate<WlrScreencopyManager>
, 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

View file

@ -0,0 +1,51 @@
#pragma once
#include <private/qwaylandscreen_p.h>
#include <qtclasshelpermacros.h>
#include <qwayland-wlr-screencopy-unstable-v1.h>
#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

View file

@ -13,7 +13,7 @@
namespace qs::wayland::toplevel_management {
namespace impl {
class ToplevelManager;
class ToplevelManager; // NOLINT
class ToplevelHandle;
} // namespace impl
@ -80,6 +80,8 @@ public:
[[nodiscard]] bool fullscreen() const;
void setFullscreen(bool fullscreen);
[[nodiscard]] impl::ToplevelHandle* implHandle() const { return this->handle; }
signals:
void closed();
void appIdChanged();