#include "dmabuf.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../core/stacklist.hpp" #include "manager.hpp" #include "manager_p.hpp" namespace qs::wayland::buffer::dmabuf { namespace { Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); LinuxDmabufManager* MANAGER = nullptr; // NOLINT class FourCCStr { public: explicit FourCCStr(uint32_t code) : chars( {static_cast(code >> 0 & 0xff), static_cast(code >> 8 & 0xff), static_cast(code >> 16 & 0xff), static_cast(code >> 24 & 0xff), '\0'} ) { for (auto i = 3; i != 0; i--) { if (chars[i] == ' ') chars[i] = '\0'; else break; } } [[nodiscard]] const char* cStr() const { return this->chars.data(); } private: std::array chars {}; }; class FourCCModStr { public: explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {} ~FourCCModStr() { if (this->drmStr) drmFree(this->drmStr); } Q_DISABLE_COPY_MOVE(FourCCModStr); [[nodiscard]] const char* cStr() const { return this->drmStr; } private: char* drmStr; }; QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { debug << fourcc.cStr(); return debug; } QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc) { debug << fourcc.cStr(); return debug; } } // namespace QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { auto saver = QDebugStateSaver(debug); debug.nospace(); if (buffer) { debug << "WlDmaBuffer(" << static_cast(buffer) << ", size=" << buffer->width << 'x' << buffer->height << ", format=" << FourCCStr(buffer->format) << ", modifier=`" << FourCCModStr(buffer->modifier) << "`)"; } else { debug << "WlDmaBuffer(0x0)"; } return debug; } GbmDeviceHandle::~GbmDeviceHandle() { if (device) { MANAGER->unrefGbmDevice(this->device); } } // This will definitely backfire later void LinuxDmabufFormatSelection::ensureSorted() { if (this->sorted) return; auto beginIter = this->formats.begin(); auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { return format.first == DRM_FORMAT_XRGB8888; }); if (xrgbIter != this->formats.end()) { std::swap(*beginIter, *xrgbIter); ++beginIter; } auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { return format.first == DRM_FORMAT_ARGB8888; }); if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); this->sorted = true; } LinuxDmabufFeedback::LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback) : zwp_linux_dmabuf_feedback_v1(feedback) {} LinuxDmabufFeedback::~LinuxDmabufFeedback() { this->destroy(); } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) { this->formatTableSize = size; this->formatTable = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); if (this->formatTable == MAP_FAILED) { this->formatTable = nullptr; qCFatal(logDmabuf) << "Failed to mmap format table."; } qCDebug(logDmabuf) << "Got format table"; } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) { if (device->size != sizeof(dev_t)) { qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " "Try recompiling both."; } this->mainDevice = *reinterpret_cast(device->data); qCDebug(logDmabuf) << "Got main device id" << this->mainDevice; } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) { if (device->size != sizeof(dev_t)) { qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " "Try recompiling both."; } auto& tranche = this->tranches.emplaceBack(); tranche.device = *reinterpret_cast(device->data); qCDebug(logDmabuf) << "Got target device id" << this->mainDevice; } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) { this->tranches.back().flags = flags; qCDebug(logDmabuf) << "Got target device flags" << flags; } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) { struct FormatTableEntry { uint32_t format; uint32_t padding; uint64_t modifier; }; static_assert(sizeof(FormatTableEntry) == 16, "Format table entry was not packed to 16 bytes."); if (this->formatTable == nullptr) { qCFatal(logDmabuf) << "Received tranche formats before format table."; } auto& tranche = this->tranches.back(); auto* table = reinterpret_cast(this->formatTable); auto* indexTable = reinterpret_cast(indices->data); auto indexTableLength = indices->size / sizeof(uint16_t); uint32_t lastFormat = 0; LinuxDmabufModifiers* lastModifiers = nullptr; LinuxDmabufModifiers* modifiers = nullptr; for (uint16_t ti = 0; ti != indexTableLength; ++ti) { auto i = indexTable[ti]; // NOLINT const auto& entry = table[i]; // NOLINT // Compositors usually send a single format's modifiers as a block. if (!modifiers || entry.format != lastFormat) { // We can often share modifier lists between formats if (lastModifiers && modifiers->modifiers == lastModifiers->modifiers) { // avoids storing a second list modifiers->modifiers = lastModifiers->modifiers; } lastFormat = entry.format; lastModifiers = modifiers; auto modifiersIter = std::ranges::find_if(tranche.formats.formats, [&](const auto& pair) { return pair.first == entry.format; }); if (modifiersIter == tranche.formats.formats.end()) { tranche.formats.formats.push(qMakePair(entry.format, LinuxDmabufModifiers())); modifiers = &(--tranche.formats.formats.end())->second; } else { modifiers = &modifiersIter->second; } } if (entry.modifier == DRM_FORMAT_MOD_INVALID) { modifiers->implicit = true; } else { modifiers->modifiers.push(entry.modifier); } } if (lastModifiers && modifiers && modifiers->modifiers == lastModifiers->modifiers) { modifiers->modifiers = lastModifiers->modifiers; } } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_done() { qCDebug(logDmabuf) << "Got tranche end."; } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_done() { qCDebug(logDmabuf) << "Got feedback done."; if (this->formatTable) { munmap(this->formatTable, this->formatTableSize); this->formatTable = nullptr; } if (logDmabuf().isDebugEnabled()) { qCDebug(logDmabuf) << "Dmabuf tranches:"; for (auto& tranche: this->tranches) { qCDebug(logDmabuf) << " Tranche on device" << tranche.device; // will be sorted on first use otherwise tranche.formats.ensureSorted(); for (auto& [format, modifiers]: tranche.formats.formats) { qCDebug(logDmabuf) << " Format" << FourCCStr(format); if (modifiers.implicit) { qCDebug(logDmabuf) << " Implicit Modifier"; } for (const auto& modifier: modifiers.modifiers) { qCDebug(logDmabuf) << " Explicit Modifier" << FourCCModStr(modifier); } } } } // Copy tranches to the manager. If the compositor ever updates // our tranches, we'll start from a clean slate. MANAGER->tranches = this->tranches; this->tranches.clear(); MANAGER->feedbackDone(); } LinuxDmabufManager::LinuxDmabufManager(WlBufferManagerPrivate* manager) : QWaylandClientExtensionTemplate(5) , manager(manager) { MANAGER = this; this->initialize(); if (this->isActive()) { qCDebug(logDmabuf) << "Requesting default dmabuf feedback..."; new LinuxDmabufFeedback(this->get_default_feedback()); } } void LinuxDmabufManager::feedbackDone() { this->manager->dmabufReady(); } GbmDeviceHandle LinuxDmabufManager::getGbmDevice(dev_t handle) { struct DrmFree { static void cleanup(drmDevice* d) { drmFreeDevice(&d); } }; std::string renderNodeStorage; std::string* renderNode = nullptr; auto sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { return d.handle == handle; }); if (sharedDevice != this->gbmDevices.end()) { renderNode = &sharedDevice->renderNode; } else { drmDevice* drmDevPtr = nullptr; if (auto error = drmGetDeviceFromDevId(handle, 0, &drmDevPtr); error != 0) { qCWarning(logDmabuf) << "Failed to get drm device information from handle:" << qt_error_string(error); return nullptr; } auto drmDev = QScopedPointer(drmDevPtr); if (!(drmDev->available_nodes & (1 << DRM_NODE_RENDER))) { qCDebug(logDmabuf) << "Cannot create GBM device: DRM device does not have render node."; return nullptr; } renderNodeStorage = drmDev->nodes[DRM_NODE_RENDER]; // NOLINT renderNode = &renderNodeStorage; sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { return d.renderNode == renderNodeStorage; }); } if (sharedDevice != this->gbmDevices.end()) { qCDebug(logDmabuf) << "Used existing GBM device on render node" << *renderNode; ++sharedDevice->refcount; return sharedDevice->device; } else { auto fd = open(renderNode->c_str(), O_RDWR | O_CLOEXEC); if (fd < 0) { qCDebug(logDmabuf) << "Could not open render node" << *renderNode << ":" << qt_error_string(fd); return nullptr; } auto* device = gbm_create_device(fd); if (!device) { qCDebug(logDmabuf) << "Failed to create GBM device from render node" << *renderNode; close(fd); return nullptr; } qCDebug(logDmabuf) << "Created GBM device on render node" << *renderNode; this->gbmDevices.push_back({ .handle = handle, .renderNode = std::move(renderNodeStorage), .device = device, .refcount = 1, }); return device; } } void LinuxDmabufManager::unrefGbmDevice(gbm_device* device) { auto iter = std::ranges::find_if(this->gbmDevices, [device](const SharedGbmDevice& d) { return d.device == device; }); if (iter == this->gbmDevices.end()) return; qCDebug(logDmabuf) << "Lost reference to GBM device" << device; if (--iter->refcount == 0) { auto fd = gbm_device_get_fd(iter->device); gbm_device_destroy(iter->device); close(fd); this->gbmDevices.erase(iter); qCDebug(logDmabuf) << "Destroyed GBM device" << device; } } GbmDeviceHandle LinuxDmabufManager::dupHandle(const GbmDeviceHandle& handle) { if (!handle) return GbmDeviceHandle(); auto iter = std::ranges::find_if(this->gbmDevices, [&handle](const SharedGbmDevice& d) { return d.device == *handle; }); if (iter == this->gbmDevices.end()) return GbmDeviceHandle(); qCDebug(logDmabuf) << "Duplicated GBM device handle" << *handle; ++iter->refcount; return GbmDeviceHandle(*handle); } WlBuffer* LinuxDmabufManager::createDmabuf(const WlBufferRequest& request) { for (auto& tranche: this->tranches) { if (request.dmabuf.device != 0 && tranche.device != request.dmabuf.device) { continue; } LinuxDmabufFormatSelection formats; for (const auto& format: request.dmabuf.formats) { if (!format.modifiers.isEmpty()) { formats.formats.push( qMakePair(format.format, LinuxDmabufModifiers {.modifiers = format.modifiers}) ); } else { for (const auto& trancheFormat: tranche.formats.formats) { if (trancheFormat.first == format.format) { formats.formats.push(trancheFormat); } } } } if (formats.formats.isEmpty()) continue; formats.ensureSorted(); auto gbmDevice = this->getGbmDevice(tranche.device); if (!gbmDevice) { qCWarning(logDmabuf) << "Hit unusable tranche device while trying to create dmabuf."; continue; } for (const auto& [format, modifiers]: formats.formats) { if (auto* buf = this->createDmabuf(gbmDevice, format, modifiers, request.width, request.height)) { return buf; } } } qCWarning(logDmabuf) << "Unable to create dmabuf for request: No matching formats."; return nullptr; } WlBuffer* LinuxDmabufManager::createDmabuf( GbmDeviceHandle& device, uint32_t format, const LinuxDmabufModifiers& modifiers, uint32_t width, uint32_t height ) { auto buffer = std::unique_ptr(new WlDmaBuffer()); auto& bo = buffer->bo; const uint32_t flags = GBM_BO_USE_RENDERING; if (modifiers.modifiers.isEmpty()) { if (!modifiers.implicit) { qCritical(logDmabuf ) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers."; return nullptr; } qCDebug(logDmabuf) << "Creating gbm_bo without modifiers..."; bo = gbm_bo_create(*device, width, height, format, flags); } else { qCDebug(logDmabuf) << "Creating gbm_bo with modifiers..."; STACKLIST_VLA_VIEW(uint64_t, modifiers.modifiers, modifiersData); bo = gbm_bo_create_with_modifiers2( *device, width, height, format, modifiersData, modifiers.modifiers.length(), flags ); } if (!bo) { qCritical(logDmabuf) << "Failed to create gbm_bo."; return nullptr; } buffer->planeCount = gbm_bo_get_plane_count(bo); buffer->planes = new WlDmaBuffer::Plane[buffer->planeCount](); buffer->modifier = gbm_bo_get_modifier(bo); auto params = QtWayland::zwp_linux_buffer_params_v1(this->create_params()); for (auto i = 0; i < buffer->planeCount; ++i) { auto& plane = buffer->planes[i]; // NOLINT plane.fd = gbm_bo_get_fd_for_plane(bo, i); if (plane.fd < 0) { qCritical(logDmabuf) << "Failed to get gbm_bo fd for plane" << i << qt_error_string(plane.fd); params.destroy(); gbm_bo_destroy(bo); return nullptr; } plane.stride = gbm_bo_get_stride_for_plane(bo, i); plane.offset = gbm_bo_get_offset(bo, i); params.add( plane.fd, i, plane.offset, plane.stride, buffer->modifier >> 32, buffer->modifier & 0xffffffff ); } buffer->mBuffer = params.create_immed(static_cast(width), static_cast(height), format, 0); params.destroy(); buffer->device = this->dupHandle(device); buffer->width = width; buffer->height = height; buffer->format = format; qCDebug(logDmabuf) << "Created dmabuf" << buffer.get(); return buffer.release(); } WlDmaBuffer::WlDmaBuffer(WlDmaBuffer&& other) noexcept : device(std::move(other.device)) , bo(other.bo) , mBuffer(other.mBuffer) , planes(other.planes) { other.mBuffer = nullptr; other.bo = nullptr; other.planeCount = 0; } WlDmaBuffer& WlDmaBuffer::operator=(WlDmaBuffer&& other) noexcept { this->~WlDmaBuffer(); new (this) WlDmaBuffer(std::move(other)); return *this; } WlDmaBuffer::~WlDmaBuffer() { if (this->mBuffer) { wl_buffer_destroy(this->mBuffer); } if (this->bo) { gbm_bo_destroy(this->bo); qCDebug(logDmabuf) << "Destroyed" << this << "freeing bo" << this->bo; } for (auto i = 0; i < this->planeCount; ++i) { const auto& plane = this->planes[i]; // NOLINT if (plane.fd) close(plane.fd); } delete[] this->planes; } bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { if (request.width != this->width || request.height != this->height) return false; auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { return format.format == this->format && (format.modifiers.isEmpty() || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); }); return matchingFormat != request.dmabuf.formats.end(); } WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { static auto* glEGLImageTargetTexture2DOES = []() { auto* fn = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES") ); if (!fn) { qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: " "glEGLImageTargetTexture2DOES is missing."; } return fn; }(); auto* context = QOpenGLContext::currentContext(); if (!context) { qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No GL context."; } auto* qEglContext = context->nativeInterface(); if (!qEglContext) { qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No EGL context."; } auto* display = qEglContext->display(); // clang-format off auto attribs = std::array { EGL_WIDTH, this->width, EGL_HEIGHT, this->height, EGL_LINUX_DRM_FOURCC_EXT, this->format, EGL_DMA_BUF_PLANE0_FD_EXT, this->planes[0].fd, // NOLINT EGL_DMA_BUF_PLANE0_OFFSET_EXT, this->planes[0].offset, // NOLINT EGL_DMA_BUF_PLANE0_PITCH_EXT, this->planes[0].stride, // NOLINT EGL_NONE }; // clang-format on auto* eglImage = eglCreateImage(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()); if (eglImage == EGL_NO_IMAGE) { qFatal() << "failed to make egl image" << eglGetError(); return nullptr; } window->beginExternalCommands(); GLuint glTexture = 0; glGenTextures(1, &glTexture); glBindTexture(GL_TEXTURE_2D, glTexture); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage); glBindTexture(GL_TEXTURE_2D, 0); window->endExternalCommands(); auto* qsgTexture = QNativeInterface::QSGOpenGLTexture::fromNative( glTexture, window, QSize(static_cast(this->width), static_cast(this->height)) ); auto* tex = new WlDmaBufferQSGTexture(eglImage, glTexture, qsgTexture); qCDebug(logDmabuf) << "Created WlDmaBufferQSGTexture" << tex << "from" << this; return tex; } WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { auto* context = QOpenGLContext::currentContext(); auto* display = context->nativeInterface()->display(); if (this->glTexture) glDeleteTextures(1, &this->glTexture); if (this->eglImage) eglDestroyImage(display, this->eglImage); delete this->qsgTexture; qCDebug(logDmabuf) << "WlDmaBufferQSGTexture" << this << "destroyed."; } } // namespace qs::wayland::buffer::dmabuf