diff --git a/CMakeLists.txt b/CMakeLists.txt index a3a07da9..98537ea8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,9 @@ if(FEATURE_aurora_qpa) add_subdirectory(src/plugins/platforms/eglfs) # add_subdirectory(src/platformsupport/libinput) endif() +if(FEATURE_aurora_deviceintegration_drm) + add_subdirectory(src/plugins/deviceintegration/drm) +endif() if(FEATURE_aurora_deviceintegration_wayland) add_subdirectory(src/plugins/deviceintegration/wayland) endif() diff --git a/cmake/FindLibDisplayInfo.cmake b/cmake/FindLibDisplayInfo.cmake new file mode 100644 index 00000000..893db5f6 --- /dev/null +++ b/cmake/FindLibDisplayInfo.cmake @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT TARGET PkgConfig::LibDisplayInfo) + find_package(PkgConfig QUIET) + pkg_check_modules(LibDisplayInfo libdisplay-info IMPORTED_TARGET) +endif() diff --git a/cmake/FindLibxcvt.cmake b/cmake/FindLibxcvt.cmake new file mode 100644 index 00000000..993910e7 --- /dev/null +++ b/cmake/FindLibxcvt.cmake @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT TARGET PkgConfig::Libxcvt) + find_package(PkgConfig QUIET) + pkg_check_modules(Libxcvt libxcvt IMPORTED_TARGET) +endif() diff --git a/features.cmake b/features.cmake index 89d9ebac..4bef6ae3 100644 --- a/features.cmake +++ b/features.cmake @@ -331,6 +331,14 @@ if(FEATURE_aurora_qpa) message(WARNING "You need libdrm for Aurora::QPA") set(FEATURE_aurora_qpa OFF) endif() + if(NOT TARGET PkgConfig::Libxcvt) + message(WARNING "You need libxcvt for Aurora::QPA") + set(FEATURE_aurora_qpa OFF) + endif() + if(NOT TARGET PkgConfig::LibDisplayInfo) + message(WARNING "You need libdisplay-info for Aurora::QPA") + set(FEATURE_aurora_qpa OFF) + endif() if(NOT TARGET PkgConfig::Gbm) message(WARNING "You need gbm for Aurora::QPA") set(FEATURE_aurora_qpa OFF) @@ -398,6 +406,35 @@ endif() add_feature_info("Aurora::VulkanServerBuffer" FEATURE_aurora_vulkan_server_buffer "Build Wayland compositor with Vulkan-based server buffer integration") set(LIRI_FEATURE_aurora_vulkan_server_buffer "$") +# Device Integration: drm +option(FEATURE_aurora_deviceintegration_drm "Device Integration: drm" ON) +if(FEATURE_aurora_deviceintegration_drm) + find_package(EGL QUIET) + + if(NOT TARGET EGL::EGL) + message(WARNING "You need EGL for Aurora::DeviceIntegration::DRM") + set(FEATURE_aurora_deviceintegration_drm OFF) + endif() + if(NOT TARGET PkgConfig::Libudev) + message(WARNING "You need udev for Aurora::DeviceIntegration::DRM") + set(FEATURE_aurora_deviceintegration_drm OFF) + endif() + if(NOT TARGET PkgConfig::Libinput) + message(WARNING "You need libinput for Aurora::DeviceIntegration::DRM") + set(FEATURE_aurora_deviceintegration_drm OFF) + endif() + if(NOT TARGET PkgConfig::Libdrm) + message(WARNING "You need libdrm for Aurora::DeviceIntegration::DRM") + set(FEATURE_aurora_deviceintegration_drm OFF) + endif() + if(NOT TARGET PkgConfig::Gbm) + message(WARNING "You need gbm for Aurora::DeviceIntegration::DRM") + set(FEATURE_aurora_deviceintegration_drm OFF) + endif() +endif() +add_feature_info("Aurora::DeviceIntegration::DRM" FEATURE_aurora_deviceintegration_drm "Build DRM/KMS device integration") +set(LIRI_FEATURE_aurora_deviceintegration_drm "$") + # Device Integration: wayland option(FEATURE_aurora_deviceintegration_wayland "Device Integration: wayland" ON) if(FEATURE_aurora_deviceintegration_wayland) diff --git a/src/plugins/deviceintegration/drm/CMakeLists.txt b/src/plugins/deviceintegration/drm/CMakeLists.txt new file mode 100644 index 00000000..cb8e024d --- /dev/null +++ b/src/plugins/deviceintegration/drm/CMakeLists.txt @@ -0,0 +1,61 @@ +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + AuroraDeviceIntegrationDrm_SOURCES + HEADER "drmloggingcategories.h" + IDENTIFIER "Aurora::Platform::gLcDrm" + CATEGORY_NAME "aurora.platform.drm" + DEFAULT_SEVERITY "Info" + DESCRIPTION "Aurora device integration for DRM/KMS" +) + +qt6_add_plugin(AuroraDeviceIntegrationDrm + SHARED + CLASS_NAME DrmIntegrationPlugin + MANUAL_FINALIZATION + c_ptr.h + drmbackend.cpp drmbackend.h + drmblob.cpp drmblob.h + drmcrtc.cpp drmcrtc.h + drmconnector.cpp drmconnector.h + drmdevice.cpp drmdevice.h + drmeventreader.cpp drmeventreader.h + drmintegration.cpp drmintegration.h + drmintegrationplugin.cpp drmintegrationplugin.h + drmobject.cpp drmobject.h + drmoutput.cpp drmoutput.h + drmplane.cpp drmplane.h + drmpointer.h + drmproperty.cpp drmproperty.h + drmwindow.cpp drmwindow.h + edid.cpp edid.h + filedescriptor.cpp filedescriptor.h + udev.cpp udev.h + ${AuroraDeviceIntegrationDrm_SOURCES} +) + +set_target_properties(AuroraDeviceIntegrationDrm + PROPERTIES OUTPUT_NAME drm +) + +target_link_libraries(AuroraDeviceIntegrationDrm + PUBLIC + Qt6::Core + Qt6::Gui + Liri::AuroraCore + Liri::AuroraPlatform + EGL::EGL + PRIVATE + PkgConfig::Libdrm + PkgConfig::Gbm + PkgConfig::Libxcvt + PkgConfig::LibDisplayInfo + PkgConfig::Libudev + Liri::AuroraPlatformPrivate +) + +qt6_finalize_target(AuroraDeviceIntegrationDrm) + +install( + TARGETS AuroraDeviceIntegrationDrm + DESTINATION ${KDE_INSTALL_PLUGINDIR}/aurora/deviceintegration +) diff --git a/src/plugins/deviceintegration/drm/c_ptr.h b/src/plugins/deviceintegration/drm/c_ptr.h new file mode 100644 index 00000000..b056ae93 --- /dev/null +++ b/src/plugins/deviceintegration/drm/c_ptr.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace Aurora { + +namespace Platform { + +struct CDeleter +{ + template + void operator()(T *ptr) + { + if (ptr) { + free(ptr); + } + } +}; + +template +using UniqueCPtr = std::unique_ptr; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drm.json b/src/plugins/deviceintegration/drm/drm.json new file mode 100644 index 00000000..735ca9ae --- /dev/null +++ b/src/plugins/deviceintegration/drm/drm.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "drm" ] +} diff --git a/src/plugins/deviceintegration/drm/drmbackend.cpp b/src/plugins/deviceintegration/drm/drmbackend.cpp new file mode 100644 index 00000000..a0acbe98 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmbackend.cpp @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2015 Martin Gräßlin +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "drmbackend.h" +#include "drmdevice.h" +#include "drmloggingcategories.h" +#include "filedescriptor.h" +#include "udev.h" + +#include + +#include + +#ifndef EGL_EXT_platform_base +typedef EGLDisplay(EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC)(EGLenum platform, + void *native_display, + const EGLint *attrib_list); +#endif + +#ifndef EGL_PLATFORM_GBM_KHR +# define EGL_PLATFORM_GBM_KHR 0x31D7 +#endif + +using namespace Qt::StringLiterals; + +namespace Aurora { + +namespace Platform { + +Q_GLOBAL_STATIC(DrmBackend, gDrmBackend) + +static QStringList splitPathList(const QString &inputString, const QChar &delimiter) +{ + QStringList result; + QString segment; + + auto isDelimiter = [delimiter](QChar c) { return c == delimiter; }; + + for (QChar c : inputString) { + if (c == QLatin1Char('\\') && !segment.isEmpty()) { + segment.chop(1); // Remove the backslash and continue + } else if (std::ranges::any_of(segment, isDelimiter) && !segment.isEmpty()) { + result.push_back(std::move(segment)); + segment.clear(); + } else { + segment.push_back(c); + } + } + + if (!segment.isEmpty()) + result.push_back(std::move(segment)); + + return result; +} + +DrmBackend::DrmBackend(QObject *parent) + : QObject(parent) + , m_session(Session::create(this)) + , m_udev(new Udev(this)) + , m_udevMonitor(new UdevMonitor(m_udev, this)) + , m_explicitDevices(splitPathList(qEnvironmentVariable("AURORA_DRM_DEVICES"), QLatin1Char(':'))) +{ + if (m_session) + qCFatal(gLcDrm, "Session support not available, aborting..."); + else + qCInfo(gLcDrm) << "Session support:" << m_session->name(); + + connect(m_udevMonitor, &UdevMonitor::deviceAdded, this, &DrmBackend::handleDeviceAdded); + connect(m_udevMonitor, &UdevMonitor::deviceRemoved, this, &DrmBackend::handleDeviceRemoved); + connect(m_udevMonitor, &UdevMonitor::deviceChanged, this, &DrmBackend::handleDeviceChanged); +} + +DrmBackend::~DrmBackend() +{ + if (m_udevMonitor) { + m_udevMonitor->deleteLater(); + m_udevMonitor = nullptr; + } + + if (m_udev) { + m_udev->deleteLater(); + m_udev = nullptr; + } +} + +Session *DrmBackend::session() const +{ + return m_session; +} + +EGLDisplay DrmBackend::eglDisplay() const +{ + return m_eglDisplay; +} + +EGLNativeDisplayType DrmBackend::platformDisplay() const +{ + if (primaryDevice()) + return reinterpret_cast(primaryDevice()->gbmDevice()); + return EGL_CAST(EGLNativeDisplayType, 0); +} + +bool DrmBackend::isAtomicEnabled() const +{ + return m_enableAtomic; +} + +void DrmBackend::initialize() +{ + if (m_initialized) + return; + + m_initialized = true; + + // Session + connect(m_session, &Session::devicePaused, this, [this](dev_t deviceId) { + if (auto *device = findDevice(deviceId)) + device->setActive(false); + }); + connect(m_session, &Session::deviceResumed, this, [this](dev_t deviceId) { + if (auto *device = findDevice(deviceId)) + device->setActive(true); + }); + + // Find all GPUs + if (!m_explicitDevices.isEmpty()) { + for (const QString &fileName : m_explicitDevices) + addDevice(fileName); + } else { + UdevEnumerate enumerate(UdevDevice::PrimaryVideoDevice | UdevDevice::GenericVideoDevice, + m_udev); + const auto udevDevices = enumerate.scan(); + + if (!udevDevices.isEmpty()) { + qCDebug(gLcDrm, "Found the following video devices for the \"%s\" seat:", + qPrintable(m_session->seat())); + for (const auto &udevDevice : udevDevices) { + if (udevDevice->seat() == m_session->seat()) { + const auto path = udevDevice->deviceNode(); + qCDebug(gLcDrm, "\t%s", qPrintable(path)); + addDevice(path); + } + } + } + } + + if (Q_UNLIKELY(m_devices.empty())) + qCFatal(gLcDrm, "No suitable DRM device have been found"); + + // Create outputs + updateOutputs(); + + QCoreApplication::exit(1); + return; + + // Monitor devices + if (m_udevMonitor) + m_udevMonitor->filterSubSystemDevType("drm"_L1); + + // Create EGL display + createDisplay(); +} + +void DrmBackend::destroy() +{ +} + +DrmDevice *DrmBackend::primaryDevice() const +{ + return m_devices.empty() ? nullptr : m_devices.front().get(); +} + +Outputs DrmBackend::outputs() const +{ + return m_outputs; +} + +DrmBackend *DrmBackend::instance() +{ + return gDrmBackend(); +} + +DrmDevice *DrmBackend::addDevice(const QString &path) +{ + // Open the DRM device + int fd = m_session->openRestricted(path); + if (fd < 0) { + qCWarning(gLcDrm) << "Could not open DRM device" << path; + return nullptr; + } + + // Check whether DRM node supports KMS + if (!drmIsKMS(fd)) { + qCWarning(gLcDrm) << "Skipping KMS incapable DRM device" << path; + m_session->closeRestricted(fd); + return nullptr; + } + + // Get the device ID + struct stat sbuf; + if (fstat(fd, &sbuf) < 0) { + qCWarning(gLcDrm, "Failed to fstat \"%s\": %s", qPrintable(path), strerror(errno)); + m_session->closeRestricted(fd); + return nullptr; + } + + m_devices.push_back(std::make_unique(path, fd, sbuf.st_rdev)); + auto *device = m_devices.back().get(); + connect(device, &DrmDevice::outputAdded, this, &DrmBackend::addOutput); + connect(device, &DrmDevice::outputRemoved, this, &DrmBackend::removeOutput); + Q_EMIT deviceAdded(device); + return device; +} + +DrmDevice *DrmBackend::findDevice(dev_t deviceId) const +{ + auto it = std::find_if(m_devices.begin(), m_devices.end(), [deviceId](const auto &device) { + return device->deviceId() == deviceId; + }); + return it == m_devices.end() ? nullptr : it->get(); +} + +void DrmBackend::createDisplay() +{ + EGLNativeDisplayType nativeDisplay = platformDisplay(); + + PFNEGLGETPLATFORMDISPLAYEXTPROC getPlatformDisplay = nullptr; + const char *extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (extensions + && (strstr(extensions, "EGL_KHR_platform_gbm") + || strstr(extensions, "EGL_MESA_platform_gbm"))) { + getPlatformDisplay = reinterpret_cast( + eglGetProcAddress("eglGetPlatformDisplayEXT")); + } + + if (getPlatformDisplay) { + m_eglDisplay = getPlatformDisplay(EGL_PLATFORM_GBM_KHR, nativeDisplay, nullptr); + } else { + qCDebug(gLcDrm, "No eglGetPlatformDisplay for GBM, falling back to eglGetDisplay"); + m_eglDisplay = eglGetDisplay(nativeDisplay); + } +} + +void DrmBackend::updateOutputs() +{ + for (auto it = m_devices.begin(); it != m_devices.end(); ++it) { + if ((*it)->isRemoved()) + (*it)->removeOutputs(); + else + (*it)->updateOutputs(); + } + + Q_EMIT outputsQueried(); + + for (auto it = m_devices.begin(); it != m_devices.end();) { + auto *device = it->get(); + + if (device->isRemoved() || (device != primaryDevice() && device->drmOutputs().isEmpty())) { + qCDebug(gLcDrm, "Removing device \"%s\"", qPrintable(device->deviceNode())); + const std::unique_ptr keepAlive = std::move(*it); + it = m_devices.erase(it); + Q_EMIT deviceRemoved(keepAlive.get()); + } else { + it++; + } + } +} + +void DrmBackend::handleDeviceAdded(Aurora::Platform::UdevDevice *udevDevice) +{ + if (!isUdevDeviceValid(udevDevice)) + return; + + auto *device = findDevice(udevDevice->deviceId()); + if (device) { + qCWarning(gLcDrm) << "Received unexpected add udev event for:" << udevDevice->deviceNode(); + return; + } + + if (addDevice(udevDevice->deviceNode())) + updateOutputs(); +} + +void DrmBackend::handleDeviceRemoved(Aurora::Platform::UdevDevice *udevDevice) +{ + if (!isUdevDeviceValid(udevDevice)) + return; + + auto *device = findDevice(udevDevice->deviceId()); + if (device) { + if (primaryDevice() == device) { + qCCritical(gLcDrm, "Primary device has been removed, cannot continue!"); + QCoreApplication::exit(1); + return; + } else { + device->setRemoved(); + updateOutputs(); + } + } +} + +void DrmBackend::handleDeviceChanged(Aurora::Platform::UdevDevice *udevDevice) +{ + if (!isUdevDeviceValid(udevDevice)) + return; + + auto *device = findDevice(udevDevice->deviceId()); + if (!device) + device = addDevice(udevDevice->deviceNode()); + if (device && device->isActive()) { + qCDebug(gLcDrm) << "Received change event for monitored drm device" << device->deviceNode(); + updateOutputs(); + } +} + +void DrmBackend::addOutput(DrmOutput *output) +{ + m_outputs.append(output); + Q_EMIT outputAdded(output); +} + +void DrmBackend::removeOutput(DrmOutput *output) +{ + m_outputs.removeOne(output); + Q_EMIT outputRemoved(output); +} + +bool DrmBackend::isUdevDeviceValid(Aurora::Platform::UdevDevice *udevDevice) +{ + // Check the seat (but only if the device was not forced with AURORA_DRM_DEVICES) + if (Q_LIKELY(m_explicitDevices.isEmpty())) { + if (udevDevice->seat() == m_session->seat()) + return false; + } else { + const auto canonicalPath = QFileInfo(udevDevice->deviceNode()).canonicalPath(); + return std::any_of(m_explicitDevices.begin(), m_explicitDevices.end(), + [&canonicalPath](const QString &explicitPath) { + return QFileInfo(explicitPath).canonicalPath() == canonicalPath; + }); + } + + return true; +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmbackend.h b/src/plugins/deviceintegration/drm/drmbackend.h new file mode 100644 index 00000000..b95f876a --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmbackend.h @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2015 Martin Gräßlin +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +#include + +#include "drmoutput.h" + +class QSocketNotifier; + +namespace Aurora { + +namespace Platform { + +class DrmDevice; +class DrmOutput; +class Udev; +class UdevDevice; +class UdevMonitor; + +class DrmBackend : public QObject +{ + Q_OBJECT +public: + explicit DrmBackend(QObject *parent = nullptr); + ~DrmBackend(); + + Session *session() const; + + EGLDisplay eglDisplay() const; + EGLNativeDisplayType platformDisplay() const; + + bool isAtomicEnabled() const; + + void initialize(); + void destroy(); + + DrmDevice *primaryDevice() const; + + Outputs outputs() const; + + static DrmBackend *instance(); + +Q_SIGNALS: + void deviceAdded(DrmDevice *device); + void deviceRemoved(DrmDevice *device); + void outputAdded(DrmOutput *output); + void outputRemoved(DrmOutput *output); + void outputsQueried(); + +private: + bool m_initialized = false; + + Session *m_session = nullptr; + Udev *m_udev = nullptr; + UdevMonitor *m_udevMonitor = nullptr; + + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + bool m_enableAtomic = true; + + const QStringList m_explicitDevices; + std::vector> m_devices; + + Outputs m_outputs; + + DrmDevice *findDevice(dev_t deviceId) const; + DrmDevice *addDevice(const QString &path); + + void createDisplay(); + void updateOutputs(); + +private Q_SLOTS: + void handleDeviceAdded(Aurora::Platform::UdevDevice *udevDevice); + void handleDeviceRemoved(Aurora::Platform::UdevDevice *udevDevice); + void handleDeviceChanged(Aurora::Platform::UdevDevice *udevDevice); + void addOutput(DrmOutput *output); + void removeOutput(DrmOutput *output); + +private: + bool isUdevDeviceValid(Aurora::Platform::UdevDevice *udevDevice); +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmblob.cpp b/src/plugins/deviceintegration/drm/drmblob.cpp new file mode 100644 index 00000000..cb75d230 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmblob.cpp @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2023 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmblob.h" +#include "drmdevice.h" + +#include + +namespace Aurora { + +namespace Platform { + +DrmBlob::DrmBlob(DrmDevice *device, uint32_t blobId) + : m_device(device) + , m_blobId(blobId) +{ +} + +DrmBlob::~DrmBlob() +{ + if (m_blobId) + drmModeDestroyPropertyBlob(m_device->fd(), m_blobId); +} + +uint32_t DrmBlob::blobId() const +{ + return m_blobId; +} + +std::shared_ptr DrmBlob::create(DrmDevice *device, const void *data, uint32_t dataSize) +{ + uint32_t id = 0; + if (drmModeCreatePropertyBlob(device->fd(), data, dataSize, &id) == 0) + return std::make_shared(device, id); + else + return nullptr; +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmblob.h b/src/plugins/deviceintegration/drm/drmblob.h new file mode 100644 index 00000000..c1042ee6 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmblob.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2023 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +namespace Aurora { + +namespace Platform { + +class DrmDevice; + +class DrmBlob +{ +public: + explicit DrmBlob(DrmDevice *device, uint32_t blobId); + ~DrmBlob(); + + uint32_t blobId() const; + + static std::shared_ptr create(DrmDevice *device, const void *data, uint32_t dataSize); + +protected: + DrmDevice *const m_device; + const uint32_t m_blobId; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmconnector.cpp b/src/plugins/deviceintegration/drm/drmconnector.cpp new file mode 100644 index 00000000..ee57067f --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmconnector.cpp @@ -0,0 +1,553 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2021 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmconnector.h" +// #include "drmcommit.h" +#include "drmcrtc.h" +#include "drmdevice.h" +#include "drmloggingcategories.h" +#include "drmoutput.h" +// #include "drmpipeline.h" +#include "drmpointer.h" + +#include +#include +#include + +using namespace Qt::StringLiterals; + +namespace Aurora { + +namespace Platform { + +/* + * DrmConnectorMode + */ + +static QSize resolutionForMode(const drmModeModeInfo *info) +{ + return QSize(info->hdisplay, info->vdisplay); +} + +static quint64 refreshRateForMode(_drmModeModeInfo *m) +{ + // Calculate higher precision (mHz) refresh rate + // logic based on Weston, see compositor-drm.c + quint64 refreshRate = (m->clock * 1000000LL / m->htotal + m->vtotal / 2) / m->vtotal; + if (m->flags & DRM_MODE_FLAG_INTERLACE) + refreshRate *= 2; + if (m->flags & DRM_MODE_FLAG_DBLSCAN) + refreshRate /= 2; + if (m->vscan > 1) + refreshRate /= m->vscan; + return refreshRate; +} + +static DrmConnectorMode::Flags flagsForMode(const drmModeModeInfo *info, + DrmConnectorMode::Flags additionalFlags) +{ + DrmConnectorMode::Flags flags = additionalFlags; + if (info->type & DRM_MODE_TYPE_PREFERRED) + flags |= DrmConnectorMode::Flag::Preferred; + return flags; +} + +DrmConnectorMode::DrmConnectorMode(DrmConnector *connector, drmModeModeInfo nativeMode, + Flags additionalFlags) + : m_connector(connector) + , m_nativeMode(nativeMode) + , m_size(resolutionForMode(&nativeMode)) + , m_refreshRate(refreshRateForMode(&nativeMode)) + , m_flags(flagsForMode(&nativeMode, additionalFlags)) +{ +} + +std::shared_ptr DrmConnectorMode::blob() +{ + if (!m_blob) + m_blob = DrmBlob::create(m_connector->device(), &m_nativeMode, sizeof(m_nativeMode)); + return m_blob; +} + +std::chrono::nanoseconds DrmConnectorMode::vblankTime() const +{ + return std::chrono::nanoseconds(((m_nativeMode.vsync_end - m_nativeMode.vsync_start) + * m_nativeMode.htotal * 1'000'000ULL) + / m_nativeMode.clock); +} + +drmModeModeInfo *DrmConnectorMode::nativeMode() +{ + return &m_nativeMode; +} + +QSize DrmConnectorMode::size() const +{ + return m_size; +} + +quint32 DrmConnectorMode::refreshRate() const +{ + return m_refreshRate; +} + +DrmConnectorMode::Flags DrmConnectorMode::flags() const +{ + return m_flags; +} + +static inline bool checkIfEqual(const drmModeModeInfo *one, const drmModeModeInfo *two) +{ + return std::memcmp(one, two, sizeof(drmModeModeInfo)) == 0; +} + +bool DrmConnectorMode::operator==(const DrmConnectorMode &otherMode) +{ + return checkIfEqual(&m_nativeMode, &otherMode.m_nativeMode); +} + +bool DrmConnectorMode::operator==(const drmModeModeInfo &otherMode) +{ + return checkIfEqual(&m_nativeMode, &otherMode); +} + +/* + * DrmConnector + */ + +DrmConnector::DrmConnector(DrmDevice *device, uint32_t connectorId) + : DrmObject(device, connectorId, DRM_MODE_OBJECT_CONNECTOR) + , crtcId(this, QByteArrayLiteral("CRTC_ID")) + , nonDesktop(this, QByteArrayLiteral("non-desktop")) + , dpms(this, QByteArrayLiteral("DPMS")) + , edidProp(this, QByteArrayLiteral("EDID")) + , overscan(this, QByteArrayLiteral("overscan")) + , vrrCapable(this, QByteArrayLiteral("vrr_capable")) + , underscan(this, QByteArrayLiteral("underscan"), + { + QByteArrayLiteral("off"), + QByteArrayLiteral("on"), + QByteArrayLiteral("auto"), + }) + , underscanVBorder(this, QByteArrayLiteral("underscan vborder")) + , underscanHBorder(this, QByteArrayLiteral("underscan hborder")) + , broadcastRGB(this, QByteArrayLiteral("Broadcast RGB"), + { + QByteArrayLiteral("Automatic"), + QByteArrayLiteral("Full"), + QByteArrayLiteral("Limited 16:235"), + }) + , maxBpc(this, QByteArrayLiteral("max bpc")) + , linkStatus(this, QByteArrayLiteral("link-status"), + { + QByteArrayLiteral("Good"), + QByteArrayLiteral("Bad"), + }) + , contentType(this, QByteArrayLiteral("content type"), + { + QByteArrayLiteral("No Data"), + QByteArrayLiteral("Graphics"), + QByteArrayLiteral("Photo"), + QByteArrayLiteral("Cinema"), + QByteArrayLiteral("Game"), + }) + , panelOrientation(this, QByteArrayLiteral("panel orientation"), + { + QByteArrayLiteral("Normal"), + QByteArrayLiteral("Upside Down"), + QByteArrayLiteral("Left Side Up"), + QByteArrayLiteral("Right Side Up"), + }) + , hdrMetadata(this, QByteArrayLiteral("HDR_OUTPUT_METADATA")) + , scalingMode(this, QByteArrayLiteral("scaling mode"), + { + QByteArrayLiteral("None"), + QByteArrayLiteral("Full"), + QByteArrayLiteral("Center"), + QByteArrayLiteral("Full aspect"), + }) + , colorspace(this, QByteArrayLiteral("Colorspace"), + { + QByteArrayLiteral("Default"), + QByteArrayLiteral("BT709_YCC"), + QByteArrayLiteral("opRGB"), + QByteArrayLiteral("BT2020_RGB"), + QByteArrayLiteral("BT2020_YCC"), + }) + , path(this, QByteArrayLiteral("PATH")) + , m_conn(drmModeGetConnector(device->fd(), connectorId)) + #if 0 + , m_pipeline(m_conn ? std::make_unique(this) : nullptr) + #endif +{ + if (m_conn) { + for (int i = 0; i < m_conn->count_encoders; ++i) { + DrmUniquePtr enc(drmModeGetEncoder(device->fd(), m_conn->encoders[i])); + if (!enc) { + qCWarning(gLcDrm) << "failed to get encoder" << m_conn->encoders[i]; + continue; + } + m_possibleCrtcs |= enc->possible_crtcs; + } + } else { + qCWarning(gLcDrm) << "drmModeGetConnector failed!" << strerror(errno); + } +} + +bool DrmConnector::isConnected() const +{ + return !m_driverModes.empty() && m_conn && m_conn->connection == DRM_MODE_CONNECTED; +} + +QString DrmConnector::connectorName() const +{ + QString connectorName = + QString::fromLocal8Bit(drmModeGetConnectorTypeName(m_conn->connector_type)); + if (connectorName.isEmpty()) + connectorName = "Unknown"_L1; + return QStringLiteral("%1-%2").arg(connectorName).arg(m_conn->connector_type_id); +} + +QString DrmConnector::modelName() const +{ + if (m_edid.serialNumber().isEmpty()) + return connectorName() + QLatin1Char('-') + m_edid.nameString(); + else + return m_edid.nameString(); +} + +bool DrmConnector::isInternal() const +{ + return m_conn->connector_type == DRM_MODE_CONNECTOR_LVDS + || m_conn->connector_type == DRM_MODE_CONNECTOR_eDP + || m_conn->connector_type == DRM_MODE_CONNECTOR_DSI; +} + +QSize DrmConnector::physicalSize() const +{ + return m_physicalSize; +} + +QByteArray DrmConnector::mstPath() const +{ + return m_mstPath; +} + +QList> DrmConnector::modes() const +{ + return m_modes; +} + +std::shared_ptr DrmConnector::findMode(const drmModeModeInfo &modeInfo) const +{ + const auto it = + std::find_if(m_modes.constBegin(), m_modes.constEnd(), [&modeInfo](const auto &mode) { + return checkIfEqual(mode->nativeMode(), &modeInfo); + }); + return it == m_modes.constEnd() ? nullptr : *it; +} + +Output::Subpixel DrmConnector::subpixel() const +{ + switch (m_conn->subpixel) { + case DRM_MODE_SUBPIXEL_UNKNOWN: + return Output::Subpixel::Unknown; + case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: + return Output::Subpixel::HorizontalRGB; + case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: + return Output::Subpixel::HorizontalBGR; + case DRM_MODE_SUBPIXEL_VERTICAL_RGB: + return Output::Subpixel::VerticalRGB; + case DRM_MODE_SUBPIXEL_VERTICAL_BGR: + return Output::Subpixel::VerticalBGR; + case DRM_MODE_SUBPIXEL_NONE: + return Output::Subpixel::None; + default: + return Output::Subpixel::Unknown; + } +} + +bool DrmConnector::updateProperties() +{ + if (auto connector = drmModeGetConnector(device()->fd(), id())) + m_conn.reset(connector); + else if (!m_conn) + return false; + DrmPropertyList props = queryProperties(); + crtcId.update(props); + nonDesktop.update(props); + dpms.update(props); + edidProp.update(props); + overscan.update(props); + vrrCapable.update(props); + underscan.update(props); + underscanVBorder.update(props); + underscanHBorder.update(props); + broadcastRGB.update(props); + maxBpc.update(props); + linkStatus.update(props); + contentType.update(props); + panelOrientation.update(props); + hdrMetadata.update(props); + scalingMode.update(props); + colorspace.update(props); + path.update(props); + + if (device()->hasAtomicSupport() && !crtcId.isValid()) + return false; + + // parse edid + if (edidProp.immutableBlob()) { + m_edid = Edid(edidProp.immutableBlob()->data, edidProp.immutableBlob()->length); + if (!m_edid.isValid()) + qCWarning(gLcDrm) << "Couldn't parse EDID for connector" << this; + } else if (m_conn->connection == DRM_MODE_CONNECTED) { + qCDebug(gLcDrm) << "Could not find edid for connector" << this; + } + + // check the physical size + if (m_edid.physicalSize().isEmpty()) + m_physicalSize = QSize(m_conn->mmWidth, m_conn->mmHeight); + else + m_physicalSize = m_edid.physicalSize(); + + // update modes + bool equal = m_conn->count_modes == m_driverModes.count(); + for (int i = 0; equal && i < m_conn->count_modes; i++) + equal &= checkIfEqual(m_driverModes[i]->nativeMode(), &m_conn->modes[i]); + if (!equal && m_conn->count_modes > 0) { + // reload modes + m_driverModes.clear(); + for (int i = 0; i < m_conn->count_modes; i++) { + m_driverModes.append(std::make_shared(this, m_conn->modes[i], + DrmConnectorMode::Flags())); + } + m_modes.clear(); + m_modes.append(m_driverModes); + if (scalingMode.isValid() && scalingMode.hasEnum(ScalingMode::Full_Aspect)) + m_modes.append(generateCommonModes()); + #if 0 + if (m_pipeline->mode()) { + if (const auto mode = findMode(*m_pipeline->mode()->nativeMode())) + m_pipeline->setMode(mode); + else + m_pipeline->setMode(m_modes.constFirst()); + } else { + m_pipeline->setMode(m_modes.constFirst()); + } + m_pipeline->applyPendingChanges(); + if (m_pipeline->output()) + m_pipeline->output()->updateModes(); + #endif + } + + m_mstPath.clear(); + if (auto blob = path.immutableBlob()) { + QByteArray value = QByteArray(static_cast(blob->data), blob->length); + if (value.startsWith("mst:")) { + // for backwards compatibility reasons the string also contains the drm connector id + // remove that to get a more stable identifier + const ssize_t firstHyphen = value.indexOf('-'); + if (firstHyphen > 0) + m_mstPath = value.mid(firstHyphen); + else + qCWarning(gLcDrm) << "Unexpected format in path property:" << value; + } else { + qCWarning(gLcDrm) << "Unknown path type detected:" << value; + } + } + + return true; +} + +bool DrmConnector::isCrtcSupported(DrmCrtc *crtc) const +{ + return (m_possibleCrtcs & (1 << crtc->pipeIndex())); +} + +bool DrmConnector::isNonDesktop() const +{ + return nonDesktop.isValid() && nonDesktop.value() == 1; +} + +const Edid *DrmConnector::edid() const +{ + return &m_edid; +} + +#if 0 +DrmPipeline *DrmConnector::pipeline() const +{ + return m_pipeline.get(); +} + +void DrmConnector::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(crtcId, 0); +} +#endif + +static const QList s_commonModes = { + /* 4:3 (1.33) */ + QSize(1600, 1200), + QSize(1280, 1024), /* 5:4 (1.25) */ + QSize(1024, 768), + /* 16:10 (1.6) */ + QSize(2560, 1600), + QSize(1920, 1200), + QSize(1280, 800), + /* 16:9 (1.77) */ + QSize(5120, 2880), + QSize(3840, 2160), + QSize(3200, 1800), + QSize(2880, 1620), + QSize(2560, 1440), + QSize(1920, 1080), + QSize(1600, 900), + QSize(1368, 768), + QSize(1280, 720), +}; + +QList> DrmConnector::generateCommonModes() +{ + QList> ret; + QSize maxSize; + uint32_t maxSizeRefreshRate = 0; + for (const auto &mode : std::as_const(m_driverModes)) { + if (mode->size().width() >= maxSize.width() && mode->size().height() >= maxSize.height() + && mode->refreshRate() >= maxSizeRefreshRate) { + maxSize = mode->size(); + maxSizeRefreshRate = mode->refreshRate(); + } + } + const uint64_t maxBandwidthEstimation = + maxSize.width() * maxSize.height() * uint64_t(maxSizeRefreshRate); + for (const auto &size : s_commonModes) { + const uint64_t bandwidthEstimation = size.width() * size.height() * 60000ull; + if (size.width() > maxSize.width() || size.height() > maxSize.height() + || bandwidthEstimation > maxBandwidthEstimation) { + continue; + } + const auto generatedMode = generateMode(size, 60); + if (std::any_of(m_driverModes.cbegin(), m_driverModes.cend(), + [generatedMode](const auto &mode) { + return mode->size() == generatedMode->size() + && mode->refreshRate() == generatedMode->refreshRate(); + })) { + continue; + } + ret << generatedMode; + } + return ret; +} + +std::shared_ptr DrmConnector::generateMode(const QSize &size, float refreshRate) +{ + auto modeInfo = libxcvt_gen_mode_info(size.width(), size.height(), refreshRate, false, false); + + drmModeModeInfo mode{ + .clock = uint32_t(modeInfo->dot_clock), + .hdisplay = uint16_t(modeInfo->hdisplay), + .hsync_start = modeInfo->hsync_start, + .hsync_end = modeInfo->hsync_end, + .htotal = modeInfo->htotal, + .vdisplay = uint16_t(modeInfo->vdisplay), + .vsync_start = modeInfo->vsync_start, + .vsync_end = modeInfo->vsync_end, + .vtotal = modeInfo->vtotal, + .vscan = 1, + .vrefresh = uint32_t(modeInfo->vrefresh), + .flags = modeInfo->mode_flags, + .type = DRM_MODE_TYPE_USERDEF, + }; + + sprintf(mode.name, "%dx%d@%d", size.width(), size.height(), mode.vrefresh); + + free(modeInfo); + return std::make_shared(this, mode, DrmConnectorMode::Flag::Generated); +} + +QDebug &operator<<(QDebug &s, const Aurora::Platform::DrmConnector *obj) +{ + QDebugStateSaver saver(s); + if (obj) { + QString connState = QStringLiteral("Disconnected"); + if (!obj->m_conn || obj->m_conn->connection == DRM_MODE_UNKNOWNCONNECTION) + connState = QStringLiteral("Unknown Connection"); + else if (obj->m_conn->connection == DRM_MODE_CONNECTED) + connState = QStringLiteral("Connected"); + + s.nospace() << "DrmConnector(id=" << obj->id() << ", device=" << obj->device() + << ", name=" << obj->modelName() << ", connection=" << connState + << ", countMode=" << (obj->m_conn ? obj->m_conn->count_modes : 0) << ')'; + } else { + s << "DrmConnector(0x0)"; + } + return s; +} + +DrmConnector::DrmContentType DrmConnector::kwinToDrmContentType(ContentType type) +{ + switch (type) { + case ContentType::None: + return DrmContentType::Graphics; + case ContentType::Photo: + return DrmContentType::Photo; + case ContentType::Video: + return DrmContentType::Cinema; + case ContentType::Game: + return DrmContentType::Game; + default: + Q_UNREACHABLE(); + } +} + +OutputTransform DrmConnector::toKWinTransform(PanelOrientation orientation) +{ + switch (orientation) { + case PanelOrientation::Normal: + return KWin::OutputTransform::Normal; + case PanelOrientation::RightUp: + return KWin::OutputTransform::Rotate270; + case PanelOrientation::LeftUp: + return KWin::OutputTransform::Rotate90; + case PanelOrientation::UpsideDown: + return KWin::OutputTransform::Rotate180; + default: + Q_UNREACHABLE(); + } +} + +DrmConnector::BroadcastRgbOptions DrmConnector::rgbRangeToBroadcastRgb(Output::RgbRange rgbRange) +{ + switch (rgbRange) { + case Output::RgbRange::Automatic: + return BroadcastRgbOptions::Automatic; + case Output::RgbRange::Full: + return BroadcastRgbOptions::Full; + case Output::RgbRange::Limited: + return BroadcastRgbOptions::Limited; + default: + Q_UNREACHABLE(); + } +} + +Output::RgbRange DrmConnector::broadcastRgbToRgbRange(BroadcastRgbOptions rgbRange) +{ + switch (rgbRange) { + case BroadcastRgbOptions::Automatic: + return Output::RgbRange::Automatic; + case BroadcastRgbOptions::Full: + return Output::RgbRange::Full; + case BroadcastRgbOptions::Limited: + return Output::RgbRange::Limited; + default: + Q_UNREACHABLE(); + } +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmconnector.h b/src/plugins/deviceintegration/drm/drmconnector.h new file mode 100644 index 00000000..db4c13c7 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmconnector.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2021 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include + +#include + +#include "drmblob.h" +#include "drmobject.h" +#include "drmpointer.h" +#include "edid.h" + +namespace Aurora { + +namespace Platform { + +//class DrmPipeline; +class DrmConnector; +class DrmCrtc; + +/** + * The DrmConnectorMode class represents a native mode and the associated blob. + */ +class DrmConnectorMode +{ +public: + enum class Flag : uint { + Preferred = 1, + Generated + }; + Q_DECLARE_FLAGS(Flags, Flag) + + explicit DrmConnectorMode(DrmConnector *connector, drmModeModeInfo nativeMode, Flags additionalFlags); + + QSize size() const; + quint32 refreshRate() const; + Flags flags() const; + + drmModeModeInfo *nativeMode(); + std::shared_ptr blob(); + std::chrono::nanoseconds vblankTime() const; + + bool operator==(const DrmConnectorMode &otherMode); + bool operator==(const drmModeModeInfo &otherMode); + +private: + DrmConnector *m_connector; + drmModeModeInfo m_nativeMode; + QSize m_size; + quint32 m_refreshRate; + Flags m_flags; + std::shared_ptr m_blob; +}; + +class DrmConnector : public DrmObject +{ +public: + explicit DrmConnector(DrmDevice *device, uint32_t connectorId); + + bool updateProperties() override; +#if 0 + void disable(DrmAtomicCommit *commit) override; +#endif + + bool isCrtcSupported(DrmCrtc *crtc) const; + bool isConnected() const; + bool isNonDesktop() const; + bool isInternal() const; + #if 0 + DrmPipeline *pipeline() const; + #endif + + const Edid *edid() const; + QString connectorName() const; + QString modelName() const; + QSize physicalSize() const; + /** + * @returns the mst path of the connector. Is empty if invalid + */ + QByteArray mstPath() const; + + QList> modes() const; + std::shared_ptr findMode(const drmModeModeInfo &modeInfo) const; + + Output::Subpixel subpixel() const; + + enum class UnderscanOptions : uint64_t { + Off = 0, + On = 1, + Auto = 2, + }; + enum class BroadcastRgbOptions : uint64_t { + Automatic = 0, + Full = 1, + Limited = 2 + }; + enum class LinkStatus : uint64_t { + Good = 0, + Bad = 1 + }; + enum class DrmContentType : uint64_t { + None = 0, + Graphics = 1, + Photo = 2, + Cinema = 3, + Game = 4 + }; + enum class PanelOrientation : uint64_t { + Normal = 0, + UpsideDown = 1, + LeftUp = 2, + RightUp = 3 + }; + enum class ScalingMode : uint64_t { + None = 0, + Full = 1, + Center = 2, + Full_Aspect = 3 + }; + enum class Colorspace : uint64_t { + Default, + BT709_YCC, + opRGB, + BT2020_RGB, + BT2020_YCC, + }; + + DrmProperty crtcId; + DrmProperty nonDesktop; + DrmProperty dpms; + DrmProperty edidProp; + DrmProperty overscan; + DrmProperty vrrCapable; + DrmEnumProperty underscan; + DrmProperty underscanVBorder; + DrmProperty underscanHBorder; + DrmEnumProperty broadcastRGB; + DrmProperty maxBpc; + DrmEnumProperty linkStatus; + DrmEnumProperty contentType; + DrmEnumProperty panelOrientation; + DrmProperty hdrMetadata; + DrmEnumProperty scalingMode; + DrmEnumProperty colorspace; + DrmProperty path; + + static DrmContentType kwinToDrmContentType(ContentType type); + static OutputTransform toKWinTransform(PanelOrientation orientation); + static BroadcastRgbOptions rgbRangeToBroadcastRgb(Output::RgbRange rgbRange); + static Output::RgbRange broadcastRgbToRgbRange(BroadcastRgbOptions rgbRange); + +private: + QList> generateCommonModes(); + std::shared_ptr generateMode(const QSize &size, float refreshRate); + + DrmUniquePtr m_conn; + #if 0 + std::unique_ptr m_pipeline; + #endif + Edid m_edid; + QSize m_physicalSize = QSize(-1, -1); + QList> m_driverModes; + QList> m_modes; + uint32_t m_possibleCrtcs = 0; + QByteArray m_mstPath; + + friend QDebug &operator<<(QDebug &s, const Aurora::Platform::DrmConnector *obj); +}; + +QDebug &operator<<(QDebug &s, const Aurora::Platform::DrmConnector *obj); + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmcrtc.cpp b/src/plugins/deviceintegration/drm/drmcrtc.cpp new file mode 100644 index 00000000..ff4c5095 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmcrtc.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmcrtc.h" +#include "drmbackend.h" +#include "drmdevice.h" +#include "drmoutput.h" +#include "drmpointer.h" + +namespace Aurora { + +namespace Platform { + +DrmCrtc::DrmCrtc(DrmDevice *device, uint32_t crtcId, int pipeIndex, DrmPlane *primaryPlane, + DrmPlane *cursorPlane) + : DrmObject(device, crtcId, DRM_MODE_OBJECT_CRTC) + , modeId(this, QByteArrayLiteral("MODE_ID")) + , active(this, QByteArrayLiteral("ACTIVE")) + , vrrEnabled(this, QByteArrayLiteral("VRR_ENABLED")) + , gammaLut(this, QByteArrayLiteral("GAMMA_LUT")) + , gammaLutSize(this, QByteArrayLiteral("GAMMA_LUT_SIZE")) + , ctm(this, QByteArrayLiteral("CTM")) + , degammaLut(this, QByteArrayLiteral("DEGAMMA_LUT")) + , degammaLutSize(this, QByteArrayLiteral("DEGAMMA_LUT_SIZE")) + , m_crtc(drmModeGetCrtc(device->fd(), crtcId)) + , m_pipeIndex(pipeIndex) + , m_primaryPlane(primaryPlane) + , m_cursorPlane(cursorPlane) +{ +} + +bool DrmCrtc::updateProperties() +{ + if (!m_crtc) + return false; + + DrmPropertyList props = queryProperties(); + modeId.update(props); + active.update(props); + vrrEnabled.update(props); + gammaLut.update(props); + gammaLutSize.update(props); + ctm.update(props); + degammaLut.update(props); + degammaLutSize.update(props); + + return !device()->hasAtomicSupport() || (modeId.isValid() && active.isValid()); +} + +drmModeModeInfo DrmCrtc::queryCurrentMode() +{ + m_crtc.reset(drmModeGetCrtc(device()->fd(), id())); + return m_crtc->mode; +} + +int DrmCrtc::pipeIndex() const +{ + return m_pipeIndex; +} + +#if 0 +std::shared_ptr DrmCrtc::current() const +{ + return m_currentBuffer; +} + +void DrmCrtc::setCurrent(const std::shared_ptr &buffer) +{ + m_currentBuffer = buffer; +} +#endif + +int DrmCrtc::gammaRampSize() const +{ + if (device()->hasAtomicSupport()) { + // limit atomic gamma ramp to 4096 to work around + // https://gitlab.freedesktop.org/drm/intel/-/issues/3916 + if (gammaLutSize.isValid() && gammaLutSize.value() <= 4096) + return gammaLutSize.value(); + } + return m_crtc->gamma_size; +} + +DrmPlane *DrmCrtc::primaryPlane() const +{ + return m_primaryPlane; +} + +DrmPlane *DrmCrtc::cursorPlane() const +{ + return m_cursorPlane; +} + +#if 0 +void DrmCrtc::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(active, 0); + commit->addProperty(modeId, 0); +} + +void DrmCrtc::releaseCurrentBuffer() +{ + if (m_currentBuffer) + m_currentBuffer->releaseBuffer(); +} +#endif + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmcrtc.h b/src/plugins/deviceintegration/drm/drmcrtc.h new file mode 100644 index 00000000..f2c10fb7 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmcrtc.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "drmobject.h" + +#include + +namespace Aurora { + +namespace Platform { + +class DrmBackend; +class DrmDevice; +#if 0 +class DrmFramebuffer; +#endif +class GammaRamp; +class DrmPlane; + +class DrmCrtc : public DrmObject +{ +public: + DrmCrtc(DrmDevice *device, uint32_t crtcId, int pipeIndex, DrmPlane *primaryPlane, + DrmPlane *cursorPlane); + +#if 0 + void disable(DrmAtomicCommit *commit) override; +#endif + bool updateProperties() override; + + int pipeIndex() const; + int gammaRampSize() const; + DrmPlane *primaryPlane() const; + DrmPlane *cursorPlane() const; + drmModeModeInfo queryCurrentMode(); + +#if 0 + std::shared_ptr current() const; + void setCurrent(const std::shared_ptr &buffer); + void releaseCurrentBuffer(); +#endif + + DrmProperty modeId; + DrmProperty active; + DrmProperty vrrEnabled; + DrmProperty gammaLut; + DrmProperty gammaLutSize; + DrmProperty ctm; + DrmProperty degammaLut; + DrmProperty degammaLutSize; + +private: + DrmUniquePtr m_crtc; + #if 0 + std::shared_ptr m_currentBuffer; + #endif + int m_pipeIndex; + DrmPlane *m_primaryPlane; + DrmPlane *m_cursorPlane; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmdevice.cpp b/src/plugins/deviceintegration/drm/drmdevice.cpp new file mode 100644 index 00000000..29a9f92d --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmdevice.cpp @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: 2015-2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2020 Xaver Hugl +// SPDX-FileCopyrightText: 2016-2017 The Qt Company Ltd. +// SPDX-FileCopyrightText: 2016 Pelagicore AG +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "drmbackend.h" +#include "drmcrtc.h" +#include "drmconnector.h" +#include "drmdevice.h" +#include "drmloggingcategories.h" +#include "drmobject.h" +#include "drmpointer.h" +#include "drmplane.h" + +#include + +#include +#include + +#ifndef DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT +# define DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT 6 +#endif + +namespace Aurora { + +namespace Platform { + +DrmDevice::DrmDevice(const QString &path, int fd, dev_t deviceId, QObject *parent) + : QObject(parent) + , m_path(path) + , m_fd(fd) + , m_deviceId(deviceId) +{ + // Determine KMS driver for quirks + DrmUniquePtr version(drmGetVersion(m_fd)); + if (version) { + if (strstr(version->name, "i915")) + m_drmDriver = DrmDriver::I915; + else if (strstr(version->name, "nvidia-drm")) + m_drmDriver = DrmDriver::NVidia; + else if (strstr(version->name, "virtio") || strstr(version->name, "qxl") + || strstr(version->name, "vmwgfx") || strstr(version->name, "vboxvideo")) + m_drmDriver = DrmDriver::VirtualMachine; + qCDebug(gLcDrm) << "DRM Driver:" << version->name; + } else { + qCCritical(gLcDrm, "Failed to get DRM version for \"%s\"", qPrintable(path)); + QCoreApplication::exit(1); + return; + } + + // Capabilities + uint64_t capability = 0; + m_hasAddFB2ModifiersSupport = + drmGetCap(m_fd, DRM_CAP_ADDFB2_MODIFIERS, &capability) == 0 && capability == 1; + + // Reopen the drm node to create a new GEM handle namespace + m_gbmFd = FileDescriptor(open(qPrintable(path), O_RDWR | O_CLOEXEC)); + if (m_gbmFd.isValid()) { + drm_magic_t magic; + drmGetMagic(m_gbmFd.get(), &magic); + drmAuthMagic(m_fd, magic); + + // Create GBM device + m_gbmDevice = gbm_create_device(m_gbmFd.get()); + if (!m_gbmDevice) { + qCCritical(gLcDrm, "Could not create GBM device for \"%s\": %s", qPrintable(path), + strerror(errno)); + QCoreApplication::exit(1); + return; + } + } + + // Event reader + m_eventReader.create(this); + + // Enable universal planes + drmSetClientCap(m_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + + // Do we have atomic support? + m_hasAtomicSupport = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0; + if (m_hasAtomicSupport) { + qCInfo(gLcDrm, "Atomic reported as supported on \"%s\"", qPrintable(path)); + + const bool atomicDisabled = qEnvironmentVariableIsSet("AURORA_KMS_ATOMIC") + && !qEnvironmentVariableIntValue("AURORA_KMS_ATOMIC"); + if (atomicDisabled) { + m_hasAtomicSupport = false; + } else { + // Does it support cursor plane hotspot? + const bool supportsCursorHotspot = + drmSetClientCap(m_fd, DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT, 1) == 0; + if (m_drmDriver == DrmDriver::VirtualMachine && !supportsCursorHotspot) { + qCWarning(gLcDrm, + "Atomic disabled on \"%s\" due to missing cursor offset support in " + "virtual machines", + qPrintable(path)); + m_hasAtomicSupport = false; + } + } + } + + // Initialize DRM resources + initDrmResources(); +} + +DrmDevice::~DrmDevice() +{ + m_eventReader.destroy(); + + if (m_gbmDevice) + gbm_device_destroy(m_gbmDevice); + + m_gbmFd.reset(); + + if (m_fd != -1) + DrmBackend::instance()->session()->closeRestricted(m_fd); +} + +QString DrmDevice::deviceNode() const +{ + return m_path; +} + +int DrmDevice::fd() const +{ + return m_fd; +} + +dev_t DrmDevice::deviceId() const +{ + return m_deviceId; +} + +gbm_device *DrmDevice::gbmDevice() const +{ + return m_gbmDevice; +} + +bool DrmDevice::isActive() const +{ + return m_active; +} + +void DrmDevice::setActive(bool active) +{ + if (m_active != active) { + m_active = active; + Q_EMIT activeChanged(active); + } +} + +bool DrmDevice::isRemoved() const +{ + return m_removed; +} + +void DrmDevice::setRemoved() +{ + // Once the device is removed we no longer reuse it + m_removed = true; +} + +bool DrmDevice::hasAtomicSupport() const +{ + return m_hasAddFB2ModifiersSupport; +} + +bool DrmDevice::hasAddFB2ModifiersSupport() const +{ + return false; +} + +bool DrmDevice::updateOutputs() +{ + if (!isActive()) + return false; + + // waitIdle(); + + // Get resources + DrmUniquePtr resources(drmModeGetResources(m_fd)); + if (!resources) { + qCWarning(gLcDrm, "Failed to get DRM resources for \"%s\": %s", qPrintable(m_path), + strerror(errno)); + return false; + } + + if (resources->count_connectors == 0) { + qCWarning(gLcDrm, "No connectors found for \"%s\"", qPrintable(m_path)); + return false; + } + + // Check for added or removed connectors + QList existing; + QList addedOutputs; + for (int i = 0; i < resources->count_connectors; i++) { + const uint32_t currentConnector = resources->connectors[i]; + const auto it = std::find_if(m_connectors.begin(), m_connectors.end(), + [currentConnector](const auto &connector) { + return connector->id() == currentConnector; + }); + } + + // Create connectors + for (int i = 0; i < resources->count_connectors; i++) { } + + return false; +} + +void DrmDevice::removeOutputs() +{ +} + +QList DrmDevice::drmOutputs() const +{ + return m_drmOutputs; +} + +void DrmDevice::initDrmResources() +{ + // Find planes + DrmUniquePtr planeResources(drmModeGetPlaneResources(m_fd)); + if (planeResources) { + qCDebug(gLcDrm, "Number of planes on \"%s\": %d", qPrintable(m_path), + planeResources->count_planes); + for (unsigned int i = 0; i < planeResources->count_planes; ++i) { + DrmUniquePtr drmPlane(drmModeGetPlane(m_fd, planeResources->planes[i])); + auto plane = std::make_unique(this, drmPlane->plane_id); + if (plane->init()) { + m_allObjects.append(plane.get()); + m_planes.push_back(std::move(plane)); + } + } + } else { + qCWarning(gLcDrm, "Failed to get planes for \"%s\", disabling atomic", qPrintable(m_path)); + m_hasAtomicSupport = false; + } + + // Show wheather atmoc is supported (at this point support might be disabled + // during planes discovery) + if (m_hasAtomicSupport) + qCInfo(gLcDrm, "Atomic enabled on \"%s\"", qPrintable(m_path)); + else + qCInfo(gLcDrm, "Atomic disabled on \"%s\"", qPrintable(m_path)); + + // Get resources + DrmUniquePtr resources(drmModeGetResources(m_fd)); + if (!resources) { + qCCritical(gLcDrm, "Failed to get DRM resources for \"%s\": %s", qPrintable(m_path), + strerror(errno)); + QCoreApplication::exit(1); + return; + } + + if (resources->count_connectors == 0) { + qCCritical(gLcDrm, "No connectors found for \"%s\"", qPrintable(m_path)); + QCoreApplication::exit(1); + return; + } + + // Find best plane + QList assignedPlanes; + for (int i = 0; i < resources->count_crtcs; ++i) { + uint32_t crtcId = resources->crtcs[i]; + + QList primaryCandidates; + QList cursorCandidates; + + for (const auto &plane : m_planes) { + if (plane->isCrtcSupported(i) && !assignedPlanes.contains(plane.get())) { + if (plane->type.enumValue() == DrmPlane::TypeIndex::Primary) + primaryCandidates.push_back(plane.get()); + else if (plane->type.enumValue() == DrmPlane::TypeIndex::Cursor) + cursorCandidates.push_back(plane.get()); + } + } + + if (m_hasAtomicSupport && primaryCandidates.empty()) { + qCWarning(gLcDrm) << "Could not find a suitable primary plane for crtc" + << resources->crtcs[i]; + continue; + } + + const auto findBestPlane = [crtcId](const QList &list) { + // if the plane is already used with this crtc, prefer it + const auto connected = + std::find_if(list.begin(), list.end(), [crtcId](DrmPlane *plane) { + return plane->crtcId.value() == crtcId; + }); + if (connected != list.end()) { + return *connected; + } + // don't take away planes from other crtcs. The kernel currently rejects such commits + const auto notconnected = std::find_if(list.begin(), list.end(), [](DrmPlane *plane) { + return plane->crtcId.value() == 0; + }); + if (notconnected != list.end()) { + return *notconnected; + } + return list.empty() ? nullptr : list.front(); + }; + + DrmPlane *primary = findBestPlane(primaryCandidates); + DrmPlane *cursor = findBestPlane(cursorCandidates); + assignedPlanes.push_back(primary); + if (cursor) + assignedPlanes.push_back(cursor); + auto crtc = std::make_unique(this, crtcId, i, primary, cursor); + if (!crtc->init()) + continue; + m_allObjects.append(crtc.get()); + m_crtcs.push_back(std::move(crtc)); + } +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmdevice.h b/src/plugins/deviceintegration/drm/drmdevice.h new file mode 100644 index 00000000..7c2fef5a --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmdevice.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include + +#include "drmeventreader.h" +#include "filedescriptor.h" + +namespace Aurora { + +namespace Platform { + +class DrmConnector; +class DrmCrtc; +class DrmObject; +class DrmOutput; +class DrmPlane; + +class DrmDevice : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) +public: + enum class DrmDriver { + Unknown = 0, + I915, + NVidia, + VirtualMachine + }; + Q_ENUM(DrmDriver) + + explicit DrmDevice(const QString &path, int fd, dev_t deviceId, QObject *parent = nullptr); + ~DrmDevice(); + + QString deviceNode() const; + int fd() const; + dev_t deviceId() const; + + gbm_device *gbmDevice() const; + + bool isActive() const; + void setActive(bool active); + + bool isRemoved() const; + void setRemoved(); + + bool hasAtomicSupport() const; + bool hasAddFB2ModifiersSupport() const; + + bool updateOutputs(); + void removeOutputs(); + + QList drmOutputs() const; + +Q_SIGNALS: + void activeChanged(bool active); + void outputAdded(DrmOutput *output); + void outputRemoved(DrmOutput *output); + +private: + void initDrmResources(); + + const QString m_path; + const int m_fd = -1; + const dev_t m_deviceId = 0; + + FileDescriptor m_gbmFd; + gbm_device *m_gbmDevice = nullptr; + + DrmDriver m_drmDriver = DrmDriver::Unknown; + + DrmEventReader m_eventReader; + + bool m_active = true; + bool m_removed = false; + bool m_hasAtomicSupport = false; + bool m_hasAddFB2ModifiersSupport = false; + + QList m_allObjects; + std::vector> m_planes; + std::vector> m_crtcs; + std::vector> m_connectors; + + QList m_drmOutputs; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmeventreader.cpp b/src/plugins/deviceintegration/drm/drmeventreader.cpp new file mode 100644 index 00000000..934ff11b --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmeventreader.cpp @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2019 The Qt Company Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "drmdevice.h" +#include "drmeventreader.h" +#include "drmloggingcategories.h" + +#include + +namespace Aurora { + +namespace Platform { + +static void pageFlipHandler(int fd, unsigned int sequence, unsigned int tv_sec, + unsigned int tv_usec, void *user_data) +{ + Q_UNUSED(fd); + Q_UNUSED(sequence); + Q_UNUSED(tv_sec); + Q_UNUSED(tv_usec); + + DrmEventReaderThread *t = static_cast(QThread::currentThread()); + t->eventHost()->handlePageFlipCompleted(user_data); +} + +class RegisterWaitFlipEvent : public QEvent +{ +public: + static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 1); + + RegisterWaitFlipEvent(void *key, QMutex *mutex, QWaitCondition *cond) + : QEvent(TYPE) + , key(key) + , mutex(mutex) + , cond(cond) + { + } + + void *key; + QMutex *mutex; + QWaitCondition *cond; +}; + +/* + * DrmEventHost + */ + +bool DrmEventHost::event(QEvent *event) +{ + if (event->type() == RegisterWaitFlipEvent::TYPE) { + RegisterWaitFlipEvent *e = static_cast(event); + PendingFlipWait *p = &pendingFlipWaits[0]; + PendingFlipWait *end = p + MAX_FLIPS; + while (p < end) { + if (!p->key) { + p->key = e->key; + p->mutex = e->mutex; + p->cond = e->cond; + updateStatus(); + return true; + } + ++p; + } + qCWarning(gLcDrm, "Cannot queue page flip wait (more than %d screens?)", MAX_FLIPS); + e->mutex->lock(); + e->cond->wakeOne(); + e->mutex->unlock(); + return true; + } + return QObject::event(event); +} + +void DrmEventHost::updateStatus() +{ + void **begin = &completedFlips[0]; + void **end = begin + MAX_FLIPS; + + for (int i = 0; i < MAX_FLIPS; ++i) { + PendingFlipWait *w = pendingFlipWaits + i; + if (!w->key) + continue; + + void **p = begin; + while (p < end) { + if (*p == w->key) { + *p = nullptr; + w->key = nullptr; + w->mutex->lock(); + w->cond->wakeOne(); + w->mutex->unlock(); + return; + } + ++p; + } + } +} + +void DrmEventHost::handlePageFlipCompleted(void *key) +{ + void **begin = &completedFlips[0]; + void **end = begin + MAX_FLIPS; + void **p = begin; + while (p < end) { + if (*p == key) { + updateStatus(); + return; + } + ++p; + } + p = begin; + while (p < end) { + if (!*p) { + *p = key; + updateStatus(); + return; + } + ++p; + } + qCWarning(gLcDrm, "Cannot store page flip status (more than %d screens?)", MAX_FLIPS); +} + +/* + * DrmEventReaderThread + */ + +DrmEventReaderThread::DrmEventReaderThread(int fd, QObject *parent) + : QThread(parent) + , m_fd(fd) + , m_eventHost(new DrmEventHost()) +{ +} + +DrmEventReaderThread::~DrmEventReaderThread() +{ +} + +DrmEventHost *DrmEventReaderThread::eventHost() const +{ + return m_eventHost.data(); +} + +void DrmEventReaderThread::run() +{ + qCDebug(gLcDrm, "Event reader thread: entering event loop"); + + QSocketNotifier notifier(m_fd, QSocketNotifier::Read); + QObject::connect(¬ifier, &QSocketNotifier::activated, ¬ifier, [this] { + drmEventContext drmEvent; + memset(&drmEvent, 0, sizeof(drmEvent)); + drmEvent.version = 2; + drmEvent.vblank_handler = nullptr; + drmEvent.page_flip_handler = pageFlipHandler; + drmHandleEvent(m_fd, &drmEvent); + }); + + exec(); + + // Move back to the thread where m_eventHost was created + m_eventHost->moveToThread(thread()); + + qCDebug(gLcDrm, "Event reader thread: event loop stopped"); +} + +/* + * DrmEventReader + */ + +DrmEventReader::~DrmEventReader() +{ + destroy(); +} + +void DrmEventReader::create(DrmDevice *device) +{ + destroy(); + + if (!device) + return; + + m_device = device; + + qCDebug(gLcDrm, "Initalizing event reader for device \"%s\" fd %d", + qPrintable(m_device->deviceNode()), m_device->fd()); + + m_thread.reset(new DrmEventReaderThread(m_device->fd())); + m_thread->start(); + + // Change thread affinity for the event host, so that postEvent() + // goes through the event reader thread's event loop for that object. + m_thread->eventHost()->moveToThread(m_thread.get()); +} + +void DrmEventReader::destroy() +{ + if (!m_device) + return; + + qCDebug(gLcDrm, "Stopping event reader for device \"%s\"", qPrintable(m_device->deviceNode())); + + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + m_thread.reset(); + } + + m_device.clear(); +} + +void DrmEventReader::startWaitFlip(void *key, QMutex *mutex, QWaitCondition *cond) +{ + if (m_thread) { + QCoreApplication::postEvent(m_thread->eventHost(), + new RegisterWaitFlipEvent(key, mutex, cond)); + } +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmeventreader.h b/src/plugins/deviceintegration/drm/drmeventreader.h new file mode 100644 index 00000000..4271a20a --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmeventreader.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2019 The Qt Company Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace Aurora { + +namespace Platform { + +class DrmDevice; + +struct DrmEventHost : public QObject +{ + struct PendingFlipWait + { + void *key; + QMutex *mutex; + QWaitCondition *cond; + }; + + static const int MAX_FLIPS = 32; + void *completedFlips[MAX_FLIPS] = {}; + DrmEventHost::PendingFlipWait pendingFlipWaits[MAX_FLIPS] = {}; + + bool event(QEvent *event) override; + void updateStatus(); + void handlePageFlipCompleted(void *key); +}; + +class DrmEventReaderThread : public QThread +{ + Q_OBJECT +public: + explicit DrmEventReaderThread(int fd, QObject *parent = nullptr); + ~DrmEventReaderThread(); + + DrmEventHost *eventHost() const; + +protected: + void run() override; + +private: + int m_fd = -1; + QScopedPointer m_eventHost; +}; + +class DrmEventReader +{ +public: + ~DrmEventReader(); + + void create(DrmDevice *device); + void destroy(); + + void startWaitFlip(void *key, QMutex *mutex, QWaitCondition *cond); + +private: + QPointer m_device; + QScopedPointer m_thread; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmintegration.cpp b/src/plugins/deviceintegration/drm/drmintegration.cpp new file mode 100644 index 00000000..7382a6b1 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmintegration.cpp @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "drmbackend.h" +#include "drmdevice.h" +#include "drmintegration.h" +#include "drmloggingcategories.h" +#include "drmoutput.h" +#include "drmwindow.h" + +#include + +namespace Aurora { + +namespace Platform { + +DrmIntegration::DrmIntegration(QObject *parent) + : DeviceIntegration(parent) +{ +} + +DrmIntegration::~DrmIntegration() +{ +} + +void DrmIntegration::initialize() +{ + DrmBackend::instance()->initialize(); +} + +void DrmIntegration::destroy() +{ + qCDebug(gLcDrm, "DRM device integration is about to be destroyed..."); + + DrmBackend::instance()->destroy(); + + qCInfo(gLcDrm, "DRM device integration destroyed successfully"); +} + +EGLNativeDisplayType DrmIntegration::platformDisplay() const +{ + return DrmBackend::instance()->platformDisplay(); +} + +EGLDisplay DrmIntegration::eglDisplay() const +{ + return DrmBackend::instance()->eglDisplay(); +} + +EGLNativeWindowType DrmIntegration::createNativeWindow(Window *window, const QSize &size, + const QSurfaceFormat &format) +{ + Q_UNUSED(format) + + auto *drmOutput = static_cast(window->output()); + auto *gbmDevice = DrmBackend::instance()->primaryDevice()->gbmDevice(); + auto *gbmSurface = + gbm_surface_create(gbmDevice, size.width(), size.height(), drmOutput->format(), + GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING); + + return reinterpret_cast(gbmSurface); +} + +void DrmIntegration::destroyNativeWindow(EGLNativeWindowType nativeWindow) +{ + auto *surface = reinterpret_cast(nativeWindow); + gbm_surface_destroy(surface); +} + +QSurfaceFormat DrmIntegration::surfaceFormatFor(const QSurfaceFormat &inputFormat) const +{ + QSurfaceFormat format(inputFormat); + format.setRenderableType(QSurfaceFormat::OpenGLES); + format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + format.setRedBufferSize(8); + format.setGreenBufferSize(8); + format.setBlueBufferSize(8); + return format; +} + +void DrmIntegration::waitForVSync(Window *window) const +{ + Q_UNUSED(window) +} + +void DrmIntegration::presentBuffer(Window *window) +{ + Q_UNUSED(window) +} + +Window *DrmIntegration::createWindow(Output *output, QWindow *qtWindow) +{ + // return new DrmWindow(output, qtWindow, this); + return nullptr; +} + +Window *DrmIntegration::getWindow(QWindow *qtWindow) const +{ + return nullptr; +} + +InputManager *DrmIntegration::createInputManager(QObject *parent) +{ + Q_UNUSED(parent) + return nullptr; +} + +Outputs DrmIntegration::outputs() const +{ + return Outputs(); +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmintegration.h b/src/plugins/deviceintegration/drm/drmintegration.h new file mode 100644 index 00000000..6005b256 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmintegration.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace Aurora { + +namespace Platform { + +class DrmWindow; + +class DrmIntegration : public DeviceIntegration +{ + Q_OBJECT +public: + explicit DrmIntegration(QObject *parent = nullptr); + ~DrmIntegration(); + + void initialize() override; + void destroy() override; + + EGLNativeDisplayType platformDisplay() const override; + EGLDisplay eglDisplay() const override; + + EGLNativeWindowType createNativeWindow(Window *window, const QSize &size, + const QSurfaceFormat &format) override; + void destroyNativeWindow(EGLNativeWindowType nativeWindow) override; + + QSurfaceFormat surfaceFormatFor(const QSurfaceFormat &inputFormat) const override; + + void waitForVSync(Window *window) const override; + void presentBuffer(Window *window) override; + + Window *createWindow(Output *output, QWindow *qtWindow) override; + Window *getWindow(QWindow *qtWindow) const override; + + InputManager *createInputManager(QObject *parent = nullptr) override; + + Outputs outputs() const override; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmintegrationplugin.cpp b/src/plugins/deviceintegration/drm/drmintegrationplugin.cpp new file mode 100644 index 00000000..10e3a9b6 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmintegrationplugin.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "drmintegration.h" +#include "drmintegrationplugin.h" + +namespace Aurora { + +namespace Platform { + +DrmIntegrationPlugin::DrmIntegrationPlugin(QObject *parent) + : DeviceIntegrationPlugin(parent) +{ +} + +DeviceIntegration *DrmIntegrationPlugin::create() +{ + return new DrmIntegration(this); +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmintegrationplugin.h b/src/plugins/deviceintegration/drm/drmintegrationplugin.h new file mode 100644 index 00000000..235dadf1 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmintegrationplugin.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace Aurora { + +namespace Platform { + +class DrmIntegrationPlugin : public DeviceIntegrationPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.liri.Aurora.DeviceIntegrationPlugin/1" FILE "drm.json") +public: + explicit DrmIntegrationPlugin(QObject *parent = nullptr); + + DeviceIntegration *create() override; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmobject.cpp b/src/plugins/deviceintegration/drm/drmobject.cpp new file mode 100644 index 00000000..b98cf68e --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmobject.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmobject.h" +#include "drmdevice.h" +#include "drmloggingcategories.h" +#include "drmpointer.h" + +#include + +namespace Aurora { + +namespace Platform { + +DrmObject::DrmObject(DrmDevice *device, uint32_t objectId, uint32_t objectType) + : m_device(device) + , m_id(objectId) + , m_objectType(objectType) +{ +} + +bool DrmObject::init() +{ + return updateProperties(); +} + +DrmPropertyList DrmObject::queryProperties() const +{ + DrmUniquePtr properties( + drmModeObjectGetProperties(m_device->fd(), m_id, m_objectType)); + if (!properties) { + qCWarning(gLcDrm) << "Failed to get properties for object" << m_id; + return {}; + } + DrmPropertyList ret; + for (uint32_t i = 0; i < properties->count_props; i++) { + DrmUniquePtr prop( + drmModeGetProperty(m_device->fd(), properties->props[i])); + if (!prop) { + qCWarning(gLcDrm, "Getting property %d of object %d failed!", properties->props[i], + m_id); + continue; + } + ret.addProperty(std::move(prop), properties->prop_values[i]); + } + return ret; +} + +uint32_t DrmObject::id() const +{ + return m_id; +} + +DrmDevice *DrmObject::device() const +{ + return m_device; +} + +uint32_t DrmObject::type() const +{ + return m_objectType; +} + +QString DrmObject::typeName() const +{ + switch (m_objectType) { + case DRM_MODE_OBJECT_CONNECTOR: + return QStringLiteral("connector"); + case DRM_MODE_OBJECT_CRTC: + return QStringLiteral("crtc"); + case DRM_MODE_OBJECT_PLANE: + return QStringLiteral("plane"); + default: + return QStringLiteral("unknown?"); + } +} + +void DrmPropertyList::addProperty(DrmUniquePtr &&prop, uint64_t value) +{ + m_properties.push_back(std::make_pair(std::move(prop), value)); +} + +std::optional, uint64_t>> +DrmPropertyList::takeProperty(const QByteArray &name) +{ + const auto it = std::find_if(m_properties.begin(), m_properties.end(), + [&name](const auto &pair) { return pair.first->name == name; }); + if (it != m_properties.end()) { + auto ret = std::move(*it); + m_properties.erase(it); + return ret; + } else { + return std::nullopt; + } +} + +} // namespace Platform + +} // namespace Aurora + +QDebug operator<<(QDebug s, const Aurora::Platform::DrmObject *obj) +{ + QDebugStateSaver saver(s); + if (obj) + s.nospace() << "DrmObject(id=" << obj->id() << ", device=" << obj->device() << ')'; + else + s << "DrmObject(0x0)"; + return s; +} diff --git a/src/plugins/deviceintegration/drm/drmobject.h b/src/plugins/deviceintegration/drm/drmobject.h new file mode 100644 index 00000000..b5a03c84 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmobject.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#include + +#include "drmpointer.h" +#include "drmproperty.h" + +namespace Aurora { + +namespace Platform { + +class DrmBackend; +class DrmDevice; +class DrmOutput; +class DrmAtomicCommit; + +class DrmPropertyList +{ +public: + void addProperty(DrmUniquePtr &&prop, uint64_t value); + std::optional, uint64_t>> + takeProperty(const QByteArray &name); + +private: + std::vector, uint64_t>> m_properties; +}; + +class DrmObject +{ +public: + virtual ~DrmObject() = default; + DrmObject(const DrmObject &) = delete; + + /** + * Must be called to query necessary data directly after creation. + * @return true when initializing was successful + */ + bool init(); + +#if 0 + /** + * Set the properties in such a way that this resource won't be used anymore + */ + virtual void disable(DrmAtomicCommit *commit) = 0; +#endif + + virtual bool updateProperties() = 0; + + uint32_t id() const; + DrmDevice *device() const; + uint32_t type() const; + QString typeName() const; + +protected: + DrmObject(DrmDevice *device, uint32_t objectId, uint32_t objectType); + + DrmPropertyList queryProperties() const; + +private: + DrmDevice *m_device; + const uint32_t m_id; + const uint32_t m_objectType; +}; + +} // namespace Platform + +} // namespace Aurora + +QDebug operator<<(QDebug stream, const Aurora::Platform::DrmObject *); diff --git a/src/plugins/deviceintegration/drm/drmoutput.cpp b/src/plugins/deviceintegration/drm/drmoutput.cpp new file mode 100644 index 00000000..4fda5bcc --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmoutput.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "drmoutput.h" + +namespace Aurora { + +namespace Platform { + +DrmOutput::DrmOutput(QObject *parent) + : Output(parent) +{ +} + +DrmOutput::~DrmOutput() +{ + destroy(); +} + +QString DrmOutput::name() const +{ + return QString(); +} + +QString DrmOutput::description() const +{ + return QString(); +} + +void DrmOutput::destroy() +{ +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmoutput.h b/src/plugins/deviceintegration/drm/drmoutput.h new file mode 100644 index 00000000..a31f1701 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmoutput.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace Aurora { + +namespace Platform { + +class DrmConnector; + +class DrmOutput : public Output +{ + Q_OBJECT +public: + explicit DrmOutput(QObject *parent = nullptr); + ~DrmOutput() override; + + QString name() const override; + QString description() const override; + + void destroy(); +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmplane.cpp b/src/plugins/deviceintegration/drm/drmplane.cpp new file mode 100644 index 00000000..b38b482d --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmplane.cpp @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmdevice.h" +#include "drmplane.h" +#include "drmloggingcategories.h" +#include "drmpointer.h" + +#include + +namespace Aurora { + +namespace Platform { + +DrmPlane::DrmPlane(DrmDevice *device, uint32_t planeId) + : DrmObject(device, planeId, DRM_MODE_OBJECT_PLANE) + , type(this, QByteArrayLiteral("type"), + { + QByteArrayLiteral("Overlay"), + QByteArrayLiteral("Primary"), + QByteArrayLiteral("Cursor"), + }) + , srcX(this, QByteArrayLiteral("SRC_X")) + , srcY(this, QByteArrayLiteral("SRC_Y")) + , srcW(this, QByteArrayLiteral("SRC_W")) + , srcH(this, QByteArrayLiteral("SRC_H")) + , crtcX(this, QByteArrayLiteral("CRTC_X")) + , crtcY(this, QByteArrayLiteral("CRTC_Y")) + , crtcW(this, QByteArrayLiteral("CRTC_W")) + , crtcH(this, QByteArrayLiteral("CRTC_H")) + , fbId(this, QByteArrayLiteral("FB_ID")) + , crtcId(this, QByteArrayLiteral("CRTC_ID")) + , rotation(this, QByteArrayLiteral("rotation"), + { + QByteArrayLiteral("rotate-0"), + QByteArrayLiteral("rotate-90"), + QByteArrayLiteral("rotate-180"), + QByteArrayLiteral("rotate-270"), + QByteArrayLiteral("reflect-x"), + QByteArrayLiteral("reflect-y"), + }) + , inFormats(this, QByteArrayLiteral("IN_FORMATS")) + , alpha(this, QByteArrayLiteral("alpha")) + , pixelBlendMode(this, QByteArrayLiteral("pixel blend mode"), + { + QByteArrayLiteral("None"), + QByteArrayLiteral("Pre-multiplied"), + QByteArrayLiteral("Coverage"), + }) + , colorEncoding(this, QByteArrayLiteral("COLOR_ENCODING"), + { + QByteArrayLiteral("ITU-R BT.601 YCbCr"), + QByteArrayLiteral("ITU-R BT.709 YCbCr"), + QByteArrayLiteral("ITU-R BT.2020 YCbCr"), + }) + , colorRange(this, QByteArrayLiteral("COLOR_RANGE"), + { + QByteArrayLiteral("YCbCr limited range"), + QByteArrayLiteral("YCbCr full range"), + }) + , vmHotspotX(this, QByteArrayLiteral("HOTSPOT_X")) + , vmHotspotY(this, QByteArrayLiteral("HOTSPOT_Y")) + , inFenceFd(this, QByteArrayLiteral("IN_FENCE_FD")) +{ +} + +bool DrmPlane::updateProperties() +{ + DrmUniquePtr p(drmModeGetPlane(device()->fd(), id())); + if (!p) { + qCWarning(gLcDrm) << "Failed to get kernel plane" << id(); + return false; + } + DrmPropertyList props = queryProperties(); + type.update(props); + srcX.update(props); + srcY.update(props); + srcW.update(props); + srcH.update(props); + crtcX.update(props); + crtcY.update(props); + crtcW.update(props); + crtcH.update(props); + fbId.update(props); + crtcId.update(props); + rotation.update(props); + inFormats.update(props); + alpha.update(props); + pixelBlendMode.update(props); + colorEncoding.update(props); + colorRange.update(props); + vmHotspotX.update(props); + vmHotspotY.update(props); + inFenceFd.update(props); + + if (!type.isValid() || !srcX.isValid() || !srcY.isValid() || !srcW.isValid() || !srcH.isValid() + || !crtcX.isValid() || !crtcY.isValid() || !crtcW.isValid() || !crtcH.isValid() + || !fbId.isValid()) { + return false; + } + + m_possibleCrtcs = p->possible_crtcs; + + // read formats from blob if available and if modifiers are supported, and from the plane object + // if not + m_supportedFormats.clear(); + if (inFormats.isValid() && inFormats.immutableBlob() && device()->hasAddFB2ModifiersSupport()) { + drmModeFormatModifierIterator iterator{}; + while (drmModeFormatModifierBlobIterNext(inFormats.immutableBlob(), &iterator)) { + m_supportedFormats[iterator.fmt].push_back(iterator.mod); + } + } else { + // if we don't have modifier support, assume the cursor needs a linear buffer + const QList modifiers = { type.enumValue() == TypeIndex::Cursor + ? DRM_FORMAT_MOD_LINEAR + : DRM_FORMAT_MOD_INVALID }; + for (uint32_t i = 0; i < p->count_formats; i++) { + m_supportedFormats.insert(p->formats[i], modifiers); + } + if (m_supportedFormats.isEmpty()) { + qCWarning(gLcDrm) << "Driver doesn't advertise any formats for this plane. Falling " + "back to XRGB8888 without explicit modifiers"; + m_supportedFormats.insert(DRM_FORMAT_XRGB8888, modifiers); + } + } + return true; +} + +#if 0 +void DrmPlane::set(DrmAtomicCommit *commit, const QPoint &srcPos, const QSize &srcSize, + const QRect &dst) +{ + // Src* are in 16.16 fixed point format + commit->addProperty(srcX, srcPos.x() << 16); + commit->addProperty(srcX, srcPos.x() << 16); + commit->addProperty(srcY, srcPos.y() << 16); + commit->addProperty(srcW, srcSize.width() << 16); + commit->addProperty(srcH, srcSize.height() << 16); + commit->addProperty(crtcX, dst.x()); + commit->addProperty(crtcY, dst.y()); + commit->addProperty(crtcW, dst.width()); + commit->addProperty(crtcH, dst.height()); +} +#endif + +bool DrmPlane::isCrtcSupported(int pipeIndex) const +{ + return (m_possibleCrtcs & (1 << pipeIndex)); +} + +QMap> DrmPlane::formats() const +{ + return m_supportedFormats; +} + +#if 0 +std::shared_ptr DrmPlane::currentBuffer() const +{ + return m_current; +} + +void DrmPlane::setCurrentBuffer(const std::shared_ptr &b) +{ + m_current = b; +} + +void DrmPlane::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(crtcId, 0); + commit->addBuffer(this, nullptr); +} + +void DrmPlane::releaseCurrentBuffer() +{ + if (m_current) + m_current->releaseBuffer(); +} +#endif + +int32_t DrmPlane::transformationToDegrees(DrmPlane::Transformations transformation) +{ + if (transformation & DrmPlane::Transformation::Rotate0) { + return 0; + } else if (transformation & DrmPlane::Transformation::Rotate90) { + return 90; + } else if (transformation & DrmPlane::Transformation::Rotate180) { + return 180; + } else { + return 270; + } +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmplane.h b/src/plugins/deviceintegration/drm/drmplane.h new file mode 100644 index 00000000..8e9e22cf --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmplane.h @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "drmobject.h" + +#include +#include +#include +#include +#include + +namespace Aurora { + +namespace Platform { + +#if 0 +class DrmFramebuffer; +#endif +class DrmCrtc; + +class DrmPlane : public DrmObject +{ + Q_GADGET +public: + DrmPlane(DrmDevice *device, uint32_t planeId); + + bool updateProperties() override; +#if 0 + void disable(DrmAtomicCommit *commit) override; +#endif + + bool isCrtcSupported(int pipeIndex) const; + QMap> formats() const; + +#if 0 + std::shared_ptr currentBuffer() const; + void setCurrentBuffer(const std::shared_ptr &b); + void releaseCurrentBuffer(); + + void set(DrmAtomicCommit *commit, const QPoint &srcPos, const QSize &srcSize, const QRect &dst); +#endif + + enum class TypeIndex : uint64_t { + Overlay = 0, + Primary = 1, + Cursor = 2 + }; + enum class Transformation : uint32_t { + Rotate0 = 1 << 0, + Rotate90 = 1 << 1, + Rotate180 = 1 << 2, + Rotate270 = 1 << 3, + ReflectX = 1 << 4, + ReflectY = 1 << 5 + }; + Q_ENUM(Transformation) + Q_DECLARE_FLAGS(Transformations, Transformation) + enum class PixelBlendMode : uint64_t { + None, + PreMultiplied, + Coverage + }; + enum class ColorEncoding : uint64_t { + BT601_YCbCr, + BT709_YCbCr, + BT2020_YCbCr + }; + enum class ColorRange : uint64_t { + Limited_YCbCr, + Full_YCbCr + }; + + DrmEnumProperty type; + DrmProperty srcX; + DrmProperty srcY; + DrmProperty srcW; + DrmProperty srcH; + DrmProperty crtcX; + DrmProperty crtcY; + DrmProperty crtcW; + DrmProperty crtcH; + DrmProperty fbId; + DrmProperty crtcId; + DrmEnumProperty rotation; + DrmProperty inFormats; + DrmProperty alpha; + DrmEnumProperty pixelBlendMode; + DrmEnumProperty colorEncoding; + DrmEnumProperty colorRange; + DrmProperty vmHotspotX; + DrmProperty vmHotspotY; + DrmProperty inFenceFd; + + static int32_t transformationToDegrees(Transformations transformation); + +private: +#if 0 + std::shared_ptr m_current; +#endif + + QMap> m_supportedFormats; + uint32_t m_possibleCrtcs; +}; + +} // namespace Platform + +} // namespace Aurora + +Q_DECLARE_OPERATORS_FOR_FLAGS(Aurora::Platform::DrmPlane::Transformations) diff --git a/src/plugins/deviceintegration/drm/drmpointer.h b/src/plugins/deviceintegration/drm/drmpointer.h new file mode 100644 index 00000000..bb645e04 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmpointer.h @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2015 Martin Gräßlin +// SPDX-FileCopyrightText: 2019 Vlad Zahorodnii +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace Aurora { + +namespace Platform { + +template +struct DrmDeleter; + +template <> +struct DrmDeleter +{ + void operator()(drmVersion *version) + { + drmFreeVersion(version); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeAtomicReq *req) + { + drmModeAtomicFree(req); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeConnector *connector) + { + drmModeFreeConnector(connector); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeCrtc *crtc) + { + drmModeFreeCrtc(crtc); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeFB *fb) + { + drmModeFreeFB(fb); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeEncoder *encoder) + { + drmModeFreeEncoder(encoder); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeModeInfo *info) + { + drmModeFreeModeInfo(info); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeObjectProperties *properties) + { + drmModeFreeObjectProperties(properties); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModePlane *plane) + { + drmModeFreePlane(plane); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModePlaneRes *resources) + { + drmModeFreePlaneResources(resources); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModePropertyRes *property) + { + drmModeFreeProperty(property); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModePropertyBlobRes *blob) + { + drmModeFreePropertyBlob(blob); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeRes *resources) + { + drmModeFreeResources(resources); + } +}; + +template <> +struct DrmDeleter +{ + void operator()(drmModeLesseeListRes *ptr) + { + drmFree(ptr); + } +}; + +template +using DrmUniquePtr = std::unique_ptr>; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmproperty.cpp b/src/plugins/deviceintegration/drm/drmproperty.cpp new file mode 100644 index 00000000..197e9f83 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmproperty.cpp @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2021-2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "drmproperty.h" +#include "drmdevice.h" +#include "drmloggingcategories.h" +#include "drmobject.h" + +#include + +namespace Aurora { + +namespace Platform { + +DrmProperty::DrmProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames) + : m_obj(obj) + , m_propName(name) + , m_enumNames(enumNames) +{ +} + +bool DrmProperty::setPropertyLegacy(uint64_t value) +{ + if (m_current == value) { + return true; + } else if (drmModeObjectSetProperty(m_obj->device()->fd(), m_obj->id(), m_obj->type(), m_propId, + value) + == 0) { + m_current = value; + return true; + } else { + return false; + } +} + +void DrmProperty::update(DrmPropertyList &propertyList) +{ + if (const auto opt = propertyList.takeProperty(m_propName)) { + const auto &[prop, value] = *opt; + m_propId = prop->prop_id; + m_current = value; + m_immutable = prop->flags & DRM_MODE_PROP_IMMUTABLE; + m_isBlob = prop->flags & DRM_MODE_PROP_BLOB; + m_isBitmask = prop->flags & DRM_MODE_PROP_BITMASK; + if (prop->flags & DRM_MODE_PROP_RANGE) { + Q_ASSERT(prop->count_values > 1); + m_minValue = prop->values[0]; + m_maxValue = prop->values[1]; + } + m_enumToPropertyMap.clear(); + m_propertyToEnumMap.clear(); + // bitmasks need translation too, not just enums + if (prop->flags & (DRM_MODE_PROP_ENUM | DRM_MODE_PROP_BITMASK)) { + for (int i = 0; i < prop->count_enums; i++) { + struct drm_mode_property_enum *en = &prop->enums[i]; + int j = m_enumNames.indexOf(QByteArray(en->name)); + if (j >= 0) { + if (m_isBitmask) { + m_enumToPropertyMap[1 << j] = 1 << en->value; + m_propertyToEnumMap[1 << en->value] = 1 << j; + } else { + m_enumToPropertyMap[j] = en->value; + m_propertyToEnumMap[en->value] = j; + } + } + } + } + if (m_immutable && m_isBlob) { + if (m_current != 0) { + m_immutableBlob.reset(drmModeGetPropertyBlob(m_obj->device()->fd(), m_current)); + if (m_immutableBlob && (!m_immutableBlob->data || !m_immutableBlob->length)) + m_immutableBlob.reset(); + } else { + m_immutableBlob.reset(); + } + } + } else { + m_propId = 0; + m_immutableBlob.reset(); + m_enumToPropertyMap.clear(); + m_propertyToEnumMap.clear(); + } +} + +uint64_t DrmProperty::value() const +{ + return m_current; +} + +bool DrmProperty::hasAllEnums() const +{ + return m_enumToPropertyMap.count() == m_enumNames.count(); +} + +uint32_t DrmProperty::propId() const +{ + return m_propId; +} + +const QByteArray &DrmProperty::name() const +{ + return m_propName; +} + +bool DrmProperty::isImmutable() const +{ + return m_immutable; +} + +bool DrmProperty::isBitmask() const +{ + return m_isBitmask; +} + +uint64_t DrmProperty::minValue() const +{ + return m_minValue; +} + +uint64_t DrmProperty::maxValue() const +{ + return m_maxValue; +} + +drmModePropertyBlobRes *DrmProperty::immutableBlob() const +{ + return m_immutableBlob.get(); +} + +DrmObject *DrmProperty::drmObject() const +{ + return m_obj; +} + +bool DrmProperty::isValid() const +{ + return m_propId != 0; +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmproperty.h b/src/plugins/deviceintegration/drm/drmproperty.h new file mode 100644 index 00000000..5c5f747b --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmproperty.h @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2016 Roman Gilg +// SPDX-FileCopyrightText: 2021-2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "drmpointer.h" +#include "drmloggingcategories.h" + +#include +#include +#include + +#include + +namespace Aurora { + +namespace Platform { + +class DrmObject; +class DrmPropertyList; + +class DrmProperty +{ +public: + DrmProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames = {}); + + const QByteArray &name() const; + DrmObject *drmObject() const; + + uint32_t propId() const; + bool isImmutable() const; + bool isBitmask() const; + bool hasAllEnums() const; + uint64_t value() const; + drmModePropertyBlobRes *immutableBlob() const; + uint64_t minValue() const; + uint64_t maxValue() const; + bool isValid() const; + + void update(DrmPropertyList &propertyList); + bool setPropertyLegacy(uint64_t value); + +protected: + DrmObject *const m_obj; + const QByteArray m_propName; + const QList m_enumNames; + + uint32_t m_propId = 0; + // the last known value from the kernel + uint64_t m_current = 0; + DrmUniquePtr m_immutableBlob; + + uint64_t m_minValue = -1; + uint64_t m_maxValue = -1; + + QMap m_enumToPropertyMap; + QMap m_propertyToEnumMap; + bool m_immutable = false; + bool m_isBlob = false; + bool m_isBitmask = false; +}; + +template +class DrmEnumProperty : public DrmProperty +{ +public: + DrmEnumProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames) + : DrmProperty(obj, name, enumNames) + { + } + + Enum enumValue() const + { + return enumForValue(value()); + } + + bool hasEnum(Enum value) const + { + const uint64_t integerValue = static_cast(value); + if (m_isBitmask) { + for (uint64_t mask = 1; integerValue >= mask && mask != 0; mask <<= 1) { + if ((integerValue & mask) && !m_enumToPropertyMap.contains(mask)) { + return false; + } + } + return true; + } else { + return m_enumToPropertyMap.contains(integerValue); + } + } + + Enum enumForValue(uint64_t value) const + { + if (m_isBitmask) { + uint64_t ret = 0; + for (uint64_t mask = 1; value >= mask && mask != 0; mask <<= 1) { + if (value & mask) + ret |= m_propertyToEnumMap[mask]; + } + return static_cast(ret); + } else { + return static_cast(m_propertyToEnumMap[value]); + } + } + + uint64_t valueForEnum(Enum enumValue) const + { + const uint64_t integer = static_cast(enumValue); + if (m_isBitmask) { + uint64_t set = 0; + for (uint64_t mask = 1; integer >= mask && mask != 0; mask <<= 1) { + if (integer & mask) + set |= m_enumToPropertyMap[mask]; + } + return set; + } else { + return m_enumToPropertyMap[integer]; + } + } + + bool setEnumLegacy(Enum value) + { + if (hasEnum(value)) + return setPropertyLegacy(valueForEnum(value)); + else + return false; + } +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmwindow.cpp b/src/plugins/deviceintegration/drm/drmwindow.cpp new file mode 100644 index 00000000..9852df51 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmwindow.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "drmoutput.h" +#include "drmwindow.h" + +namespace Aurora { + +namespace Platform { + +DrmWindow::DrmWindow(DrmOutput *output, QWindow *qtWindow, QObject *parent) + : Window(output, qtWindow, parent) +{ +} + +bool DrmWindow::create() +{ + return false; +} + +void DrmWindow::destroy() +{ +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/drmwindow.h b/src/plugins/deviceintegration/drm/drmwindow.h new file mode 100644 index 00000000..bc9e7566 --- /dev/null +++ b/src/plugins/deviceintegration/drm/drmwindow.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +namespace Aurora { + +namespace Platform { + +class DrmOutput; + +class DrmWindow : public Window +{ + Q_OBJECT +public: + explicit DrmWindow(DrmOutput *output, QWindow *qtWindow, QObject *parent = nullptr); + + bool create() override; + void destroy() override; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/edid.cpp b/src/plugins/deviceintegration/drm/edid.cpp new file mode 100644 index 00000000..588fb39b --- /dev/null +++ b/src/plugins/deviceintegration/drm/edid.cpp @@ -0,0 +1,285 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2015 Martin Flöser +// SPDX-FileCopyrightText: 2019 Vlad Zahorodnii +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include + +#include "c_ptr.h" +#include "drmloggingcategories.h" +#include "edid.h" + +extern "C" { +#include +#include +#include +} + +namespace Aurora { + +namespace Platform { + +static QByteArray parsePnpId(const uint8_t *data) +{ + // Decode PNP ID from three 5 bit words packed into 2 bytes: + // + // | Byte | Bit | + // | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | + // ---------------------------------------- + // | 1 | 0)| (4| 3 | 2 | 1 | 0)| (4| 3 | + // | | * | Character 1 | Char 2| + // ---------------------------------------- + // | 2 | 2 | 1 | 0)| (4| 3 | 2 | 1 | 0)| + // | | Character2| Character 3 | + // ---------------------------------------- + const uint offset = 0x8; + + char pnpId[4]; + pnpId[0] = 'A' + ((data[offset + 0] >> 2) & 0x1f) - 1; + pnpId[1] = 'A' + (((data[offset + 0] & 0x3) << 3) | ((data[offset + 1] >> 5) & 0x7)) - 1; + pnpId[2] = 'A' + (data[offset + 1] & 0x1f) - 1; + pnpId[3] = '\0'; + + return QByteArray(pnpId); +} + +static QByteArray parseEisaId(const uint8_t *data) +{ + for (int i = 72; i <= 108; i += 18) { + // Skip the block if it isn't used as monitor descriptor. + if (data[i]) { + continue; + } + if (data[i + 1]) { + continue; + } + + // We have found the EISA ID, it's stored as ASCII. + if (data[i + 3] == 0xfe) { + return QByteArray(reinterpret_cast(&data[i + 5]), 13).trimmed(); + } + } + + // If there isn't an ASCII EISA ID descriptor, try to decode PNP ID + return parsePnpId(data); +} + +static QByteArray parseVendor(const uint8_t *data) +{ + const auto pnpId = parsePnpId(data); + + // Map to vendor name + QFile pnpFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("hwdata/pnp.ids"))); + if (pnpFile.exists() && pnpFile.open(QIODevice::ReadOnly)) { + while (!pnpFile.atEnd()) { + const auto line = pnpFile.readLine(); + if (line.startsWith(pnpId)) { + return line.mid(4).trimmed(); + } + } + } + + return {}; +} + +Edid::Edid() +{ +} + +Edid::Edid(const void *data, uint32_t size) +{ + m_raw.resize(size); + memcpy(m_raw.data(), data, size); + + const uint8_t *bytes = static_cast(data); + + auto info = di_info_parse_edid(data, size); + if (!info) { + qCWarning(gLcDrm, "parsing edid failed"); + return; + } + const di_edid *edid = di_info_get_edid(info); + const di_edid_vendor_product *productInfo = di_edid_get_vendor_product(edid); + const di_edid_screen_size *screenSize = di_edid_get_screen_size(edid); + + // basic output information + m_physicalSize = QSize(screenSize->width_cm, screenSize->height_cm) * 10; + m_eisaId = parseEisaId(bytes); + UniqueCPtr monitorName{ di_info_get_model(info) }; + m_monitorName = QByteArray(monitorName.get()); + m_serialNumber = QByteArray::number(productInfo->serial); + m_vendor = parseVendor(bytes); + QCryptographicHash hash(QCryptographicHash::Md5); + hash.addData(m_raw); + m_hash = QString::fromLatin1(hash.result().toHex()); + + m_identifier = QByteArray(productInfo->manufacturer, 3) + " " + + QByteArray::number(productInfo->product) + " " + + QByteArray::number(productInfo->serial) + " " + + QByteArray::number(productInfo->manufacture_week) + " " + + QByteArray::number(productInfo->manufacture_year) + " " + + QByteArray::number(productInfo->model_year); + + // colorimetry and HDR metadata + const auto chromaticity = di_edid_get_chromaticity_coords(edid); + if (chromaticity) { + m_colorimetry = Core::Colorimetry{ + QVector2D{ chromaticity->red_x, chromaticity->red_y }, + QVector2D{ chromaticity->green_x, chromaticity->green_y }, + QVector2D{ chromaticity->blue_x, chromaticity->blue_y }, + QVector2D{ chromaticity->white_x, chromaticity->white_y }, + }; + } else { + m_colorimetry.reset(); + } + + const di_edid_cta *cta = nullptr; + const di_edid_ext *const *exts = di_edid_get_extensions(edid); + const di_cta_hdr_static_metadata_block *hdr_static_metadata = nullptr; + const di_cta_colorimetry_block *colorimetry = nullptr; + for (; *exts != nullptr; exts++) { + if ((cta = di_edid_ext_get_cta(*exts))) { + break; + } + } + if (cta) { + const di_cta_data_block *const *blocks = di_edid_cta_get_data_blocks(cta); + for (; *blocks != nullptr; blocks++) { + if (!hdr_static_metadata + && (hdr_static_metadata = di_cta_data_block_get_hdr_static_metadata(*blocks))) { + continue; + } + if (!colorimetry && (colorimetry = di_cta_data_block_get_colorimetry(*blocks))) { + continue; + } + } + if (hdr_static_metadata) { + m_hdrMetadata = HDRMetadata{ + .desiredContentMinLuminance = hdr_static_metadata->desired_content_min_luminance, + .desiredContentMaxLuminance = hdr_static_metadata->desired_content_max_luminance > 0 + ? std::make_optional(hdr_static_metadata->desired_content_max_luminance) + : std::nullopt, + .desiredMaxFrameAverageLuminance = + hdr_static_metadata->desired_content_max_frame_avg_luminance > 0 + ? std::make_optional( + hdr_static_metadata->desired_content_max_frame_avg_luminance) + : std::nullopt, + .supportsPQ = hdr_static_metadata->eotfs->pq, + .supportsBT2020 = colorimetry && colorimetry->bt2020_rgb, + }; + } + } + + m_isValid = true; + di_info_destroy(info); +} + +bool Edid::isValid() const +{ + return m_isValid; +} + +QSize Edid::physicalSize() const +{ + return m_physicalSize; +} + +QByteArray Edid::eisaId() const +{ + return m_eisaId; +} + +QByteArray Edid::monitorName() const +{ + return m_monitorName; +} + +QByteArray Edid::serialNumber() const +{ + return m_serialNumber; +} + +QByteArray Edid::vendor() const +{ + return m_vendor; +} + +QByteArray Edid::raw() const +{ + return m_raw; +} + +QString Edid::manufacturerString() const +{ + QString manufacturer; + if (!m_vendor.isEmpty()) { + manufacturer = QString::fromLatin1(m_vendor); + } else if (!m_eisaId.isEmpty()) { + manufacturer = QString::fromLatin1(m_eisaId); + } + return manufacturer; +} + +QString Edid::nameString() const +{ + if (!m_monitorName.isEmpty()) { + QString m = QString::fromLatin1(m_monitorName); + if (!m_serialNumber.isEmpty()) { + m.append(QLatin1Char('/')); + m.append(QString::fromLatin1(m_serialNumber)); + } + return m; + } else if (!m_serialNumber.isEmpty()) { + return QString::fromLatin1(m_serialNumber); + } else { + return QObject::tr("unknown"); + } +} + +QString Edid::hash() const +{ + return m_hash; +} + +std::optional Edid::colorimetry() const +{ + return m_colorimetry; +} + +double Edid::desiredMinLuminance() const +{ + return m_hdrMetadata ? m_hdrMetadata->desiredContentMinLuminance : 0; +} + +std::optional Edid::desiredMaxFrameAverageLuminance() const +{ + return m_hdrMetadata ? m_hdrMetadata->desiredMaxFrameAverageLuminance : std::nullopt; +} + +std::optional Edid::desiredMaxLuminance() const +{ + return m_hdrMetadata ? m_hdrMetadata->desiredContentMaxLuminance : std::nullopt; +} + +bool Edid::supportsPQ() const +{ + return m_hdrMetadata && m_hdrMetadata->supportsPQ; +} + +bool Edid::supportsBT2020() const +{ + return m_hdrMetadata && m_hdrMetadata->supportsBT2020; +} + +QByteArray Edid::identifier() const +{ + return m_identifier; +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/edid.h b/src/plugins/deviceintegration/drm/edid.h new file mode 100644 index 00000000..ed843581 --- /dev/null +++ b/src/plugins/deviceintegration/drm/edid.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2019 Vlad Zahorodnii +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +namespace Aurora { + +namespace Platform { + +/** + * Helper class that can be used for parsing EDID blobs. + * + * http://read.pudn.com/downloads110/ebook/456020/E-EDID%20Standard.pdf + */ +class Edid +{ +public: + Edid(); + Edid(const void *data, uint32_t size); + + /** + * Whether this instance of EDID is valid. + */ + bool isValid() const; + + /** + * Returns physical dimensions of the monitor, in millimeters. + */ + QSize physicalSize() const; + + /** + * Returns EISA ID of the manufacturer of the monitor. + */ + QByteArray eisaId() const; + + /** + * Returns the product name of the monitor. + */ + QByteArray monitorName() const; + + /** + * Returns the serial number of the monitor. + */ + QByteArray serialNumber() const; + + /** + * Returns the name of the vendor. + */ + QByteArray vendor() const; + + /** + * Returns the raw edid + */ + QByteArray raw() const; + + /** + * returns the vendor if included, the EISA ID if not + */ + QString manufacturerString() const; + + /** + * returns a string representing the monitor name + * Can be a serial number or "unknown" if the name is empty + */ + QString nameString() const; + + QString hash() const; + + std::optional colorimetry() const; + + double desiredMinLuminance() const; + std::optional desiredMaxFrameAverageLuminance() const; + std::optional desiredMaxLuminance() const; + bool supportsPQ() const; + bool supportsBT2020() const; + + /** + * @returns a string that is intended to identify the monitor uniquely. + * Note that multiple monitors can have the same EDID, so this is not always actually unique + */ + QByteArray identifier() const; + +private: + QSize m_physicalSize; + QByteArray m_vendor; + QByteArray m_eisaId; + QByteArray m_monitorName; + QByteArray m_serialNumber; + QString m_hash; + std::optional m_colorimetry; + struct HDRMetadata + { + double desiredContentMinLuminance; + std::optional desiredContentMaxLuminance; + std::optional desiredMaxFrameAverageLuminance; + bool supportsPQ; + bool supportsBT2020; + }; + std::optional m_hdrMetadata; + + QByteArray m_identifier; + + QByteArray m_raw; + bool m_isValid = false; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/filedescriptor.cpp b/src/plugins/deviceintegration/drm/filedescriptor.cpp new file mode 100644 index 00000000..702f96b0 --- /dev/null +++ b/src/plugins/deviceintegration/drm/filedescriptor.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "filedescriptor.h" + +#include +#include +#include +#include + +namespace Aurora { + +namespace Platform { + +FileDescriptor::FileDescriptor(int fd) + : m_fd(fd) +{ +} + +FileDescriptor::FileDescriptor(FileDescriptor &&other) + : m_fd(std::exchange(other.m_fd, -1)) +{ +} + +FileDescriptor &FileDescriptor::operator=(FileDescriptor &&other) +{ + if (m_fd != -1) { + ::close(m_fd); + } + m_fd = std::exchange(other.m_fd, -1); + return *this; +} + +FileDescriptor::~FileDescriptor() +{ + if (m_fd != -1) { + ::close(m_fd); + } +} + +bool FileDescriptor::isValid() const +{ + return m_fd != -1; +} + +int FileDescriptor::get() const +{ + return m_fd; +} + +int FileDescriptor::take() +{ + return std::exchange(m_fd, -1); +} + +void FileDescriptor::reset() +{ + if (m_fd != -1) { + ::close(m_fd); + m_fd = -1; + } +} + +FileDescriptor FileDescriptor::duplicate() const +{ + if (m_fd != -1) + return FileDescriptor{ fcntl(m_fd, F_DUPFD_CLOEXEC, 0) }; + else + return {}; +} + +bool FileDescriptor::isClosed() const +{ + return isClosed(m_fd); +} + +bool FileDescriptor::isReadable() const +{ + return isReadable(m_fd); +} + +bool FileDescriptor::isClosed(int fd) +{ + pollfd pfd = { + .fd = fd, + .events = POLLIN, + .revents = 0, + }; + if (poll(&pfd, 1, 0) < 0) { + return true; + } + return pfd.revents & (POLLHUP | POLLERR); +} + +bool FileDescriptor::isReadable(int fd) +{ + pollfd pfd = { + .fd = fd, + .events = POLLIN, + .revents = 0, + }; + return poll(&pfd, 1, 0) && (pfd.revents & (POLLIN | POLLNVAL)) != 0; +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/filedescriptor.h b/src/plugins/deviceintegration/drm/filedescriptor.h new file mode 100644 index 00000000..13a010e0 --- /dev/null +++ b/src/plugins/deviceintegration/drm/filedescriptor.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Pier Luigi Fiorini +// SPDX-FileCopyrightText: 2022 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace Aurora { + +namespace Platform { + +class FileDescriptor +{ +public: + FileDescriptor() = default; + explicit FileDescriptor(int fd); + FileDescriptor(FileDescriptor &&); + FileDescriptor &operator=(FileDescriptor &&); + ~FileDescriptor(); + + bool isValid() const; + int get() const; + int take(); + void reset(); + FileDescriptor duplicate() const; + + bool isReadable() const; + bool isClosed() const; + + static bool isReadable(int fd); + static bool isClosed(int fd); + +private: + int m_fd = -1; +}; + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/overview.md b/src/plugins/deviceintegration/drm/overview.md new file mode 100644 index 00000000..a7295952 --- /dev/null +++ b/src/plugins/deviceintegration/drm/overview.md @@ -0,0 +1,51 @@ + +Documentation on the drm/kms API is sparse and split up in a few places and files, mostly in the Linux kernel code. +This file is meant to provide an overview for drm/kms and gbm to provide a starting point for understanding how the APIs and the drm backend work. + +# drm/kms + +In the drm API there is a few objects that we care about: + +- `connector`s. A drm connector represents either an actual physical connector out of your graphics card or, in the case of DisplayPort MultiStream, a virtual one +- `crtc`s. A drm crtc represents hardware that can drive a connector uniquely - be it physical or virtual. Crtcs can also drive multiple connectors by cloning, aka showing the same stuff on all of them. The number of crtcs on your GPU or display device is a hard limit of how many display you can drive with it +- `plane`s. A drm plane represents scanout hardware that can be used for hardware composition, so cropping, scaling and rotation. There is multiple types of planes: + + * `primary`: it can be asssumed that without the primary plane the output won't work. The kernel will always expose one per crtc + * `cursor`: cursor planes are what they sound like, they allow to use special hardware built for cursors. They have special restrictions like size (DRM_CAP_CURSOR_WIDTH, DRM_CAP_CURSOR_HEIGHT), scaling (on AMD gpus its scaling must be the same as with the primary scale) and even position on some very funky hardware (Apple stuff). Cursor planes are always optional + * `overlay`: overlay planes are what they sound like as well, they allow to use special hardware built for overlays (or underlays). The restrictions on this are of arbitrary complexity, you can never just assume they work. Overlay planes are also always optional +- `framebuffer`s, fb for short. These represent some sort of buffer that we'd like to show up on the screen and have to be created and destroyed by us +- `encoder`s. Can effectively be ignored, they were exposed in the API more or less by mistake and are just there for backwards compatibility + +All drm objects have properties with a name and a value. Depending on the type this value can have a different meaning; some are a bitfield, some are just integer values and some are for arbitrary data blobs. Properties can be read-only (immutable) and are only informational, some are needed for functionality. + +There's two APIs for drm, legacy and atomic modesetting (AMS). + +Legacy only exposes connectors and crtcs, and only some of their properties. You first enable a connector and set a mode with `drmModeSetCrtc` and then push new frames with `drmModePageFlip`. `drmModePageFlip` has two flags we care about: + +- `DRM_MODE_PAGE_FLIP_EVENT` tells the kernel to generate a page flip event for the crtc, which tells us when the new framebuffer has actually been set / when the old one is not needed anymore +- `DRM_MODE_PAGE_FLIP_ASYNC` tells the kernel that it should immediately apply the new framebuffer without waiting. This may cause tearing + +For dynamic power management (dpms) you set the dpms property with `drmModeObjectSetProperty` and the kernel will handle the rest behind the scenes, or fail the request. Same story with `VRR_ENABLED`, `overscan` and similar. + +With atomic modesetting all objects and properties are exposed. AMS works very differently from legacy: it has one generic function `drmModeAtomicCommit` that is used for pretty much everything. How this function works is that you first fill a `drmModeAtomicReq` with the properties you want to set, then you call `drmModeAtomicCommit` with some combination of flags. These flags decide on what the function actually does: + +- `DRM_MODE_ATOMIC_TEST_ONLY` only tests whether or not the configuration would work but is guaranteed to not change anything +- `DRM_MODE_PAGE_FLIP_EVENT` tells the kernel that it should generate a page flip event for all crtcs that we change in the commit +- `DRM_MODE_ATOMIC_NONBLOCK` tells the kernel to make this function not blocking; it should not wait until things are actually applied before returning +- `DRM_MODE_ATOMIC_ALLOW_MODESET` tells the kernel that it is allowed to make our changes happen with a modeset, that is an event that can cause the display(s) to flicker or black out for a moment +- `DRM_MODE_PAGE_FLIP_ASYNC` is currently *not* supported. All requests with this flag set fail + +Some upstream documentation can be found at https://www.kernel.org/doc/html/latest/gpu/drm-kms.html, https://01.org/linuxgraphics/gfx-docs/drm/drm-kms-properties.html and in the files at https://github.com/torvalds/linux/tree/master/drivers/gpu/drm. + +For a lot of documentation on properties and capabilities of devices there's also https://drmdb.emersion.fr/ + +# gbm + +The generic buffer manager API allows us to allocate buffers in graphics memory with a few properties. It's a relatively straight forward API: + +- `gbm_bo` is a gbm buffer. It can be manually created and destroyed +- `gbm_surface` is a gbm surface, which allows us to create an egl surface that's using gbm buffers. With it we can render in egl and then create framebuffers from the things rendered in egl and present them on the display +- the `GBM_FORMAT_*` defines are just copies of the `DRM_FORMAT_*` defines in drm_fourcc.h and describe a buffer format. For example `DRM_FORMAT_XRGB8888` describes a buffer with 8 bits of red, 8 bits of green, 8 bits of blue and 8 bits of unused alpha (that's what the `X` stands for). Do not use the `GBM_BO_FORMAT_*` enum, it can cause problems! In general, ignore the buffer formats from the gbm header and instead use what drm_fourcc.h provides +- modifiers describe the actual memory layout that needs to be assumed for accessing the buffer. Older drivers like `radeon` don't support modifiers at all, on the other end of the spectrum the NVidia driver requires them. When we don't use functions that have us explicitly provide modifiers that's called an "implicit modifier" - that means the driver automatically picks a modifier for the use case. With implicit modifiers we have no guarantees about multi-gpu compatibility by default, instead the `GBM_BO_USE_LINEAR` usage flag has to be set when creating the buffer to enforce a linear format that all drivers can access without messing up the image + +For gbm most of the upstream documentation is contained in https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gbm/main/gbm.c diff --git a/src/plugins/deviceintegration/drm/udev.cpp b/src/plugins/deviceintegration/drm/udev.cpp new file mode 100644 index 00000000..66354e0d --- /dev/null +++ b/src/plugins/deviceintegration/drm/udev.cpp @@ -0,0 +1,478 @@ +// SPDX-FileCopyrightText: 2018-2024 Pier Luigi Fiorini +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include + +#include "drmloggingcategories.h" +#include "udev.h" + +extern "C" { +#include +} + +namespace Aurora { + +namespace Platform { + +static inline QStringList listFromEntries(udev_list_entry *l) +{ + QStringList list; + struct udev_list_entry *entry; + + udev_list_entry_foreach(entry, l) + { + list.append(QString::fromUtf8(udev_list_entry_get_name(entry))); + } + + return list; +} + +/* + * Udev + */ + +Udev::Udev(QObject *parent) + : QObject(parent) + , m_udev(udev_new()) +{ + if (!m_udev) + qCWarning(gLcDrm, "Unable to get udev library context: no devices can be detected"); +} + +Udev::~Udev() +{ + if (m_udev) + udev_unref(m_udev); +} + +bool Udev::isValid() const +{ + return m_udev != nullptr; +} + +UdevDevice *Udev::deviceFromFileName(const QString &fileName) +{ + if (!isValid()) + return nullptr; + + QT_STATBUF sb; + + if (QT_STAT(qPrintable(fileName), &sb) != 0) + return nullptr; + + struct udev_device *dev = nullptr; + + if (S_ISBLK(sb.st_mode)) + dev = udev_device_new_from_devnum(m_udev, 'b', sb.st_rdev); + else if (S_ISCHR(sb.st_mode)) + dev = udev_device_new_from_devnum(m_udev, 'c', sb.st_rdev); + + if (!dev) + return nullptr; + + return new UdevDevice(dev, this); +} + +UdevDevice *Udev::deviceFromSubSystemAndName(const QString &subSystem, const QString &name) +{ + if (!isValid()) + return nullptr; + + auto *dev = + udev_device_new_from_subsystem_sysname(m_udev, qPrintable(subSystem), qPrintable(name)); + return new UdevDevice(dev, this); +} + +UdevDevice *Udev::deviceFromSysfsPath(const QString &sysfsPath) +{ + if (!isValid()) + return nullptr; + + auto *dev = udev_device_new_from_syspath(m_udev, qPrintable(sysfsPath)); + return new UdevDevice(dev, this); +} + +/* + * UdevDevice + */ + +UdevDevice::UdevDevice(struct udev_device *device, QObject *parent) + : QObject(parent) + , m_device(device) +{ +} + +UdevDevice::~UdevDevice() +{ + if (m_device) + udev_device_unref(m_device); +} + +bool UdevDevice::isValid() const +{ + return m_device != nullptr; +} + +UdevDevice::DeviceTypes UdevDevice::type() const +{ + DeviceTypes result; + + if (!m_device) + return result; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_KEYBOARD"), "1") == 0) + result |= KeyboardDevice; + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_KEY"), "1") == 0) + result |= KeyboardDevice; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_MOUSE"), "1") == 0) + result |= MouseDevice; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_TOUCHPAD"), "1") == 0) + result |= TouchpadDevice; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_TOUCHSCREEN"), "1") == 0) + result |= TouchscreenDevice; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_TABLET"), "1") == 0) + result |= TabletDevice; + + if (qstrcmp(udev_device_get_property_value(m_device, "ID_INPUT_JOYSTICK"), "1") == 0) + result |= JoystickDevice; + + if (qstrcmp(udev_device_get_subsystem(m_device), "drm") == 0) { + bool isSet = false; + + auto *pci = udev_device_get_parent_with_subsystem_devtype(m_device, "pci", nullptr); + if (pci) { + if (qstrcmp(udev_device_get_sysattr_value(pci, "boot_vga"), "1") == 0) { + result |= PrimaryVideoDevice; + isSet = true; + } + } + + if (!isSet) + result |= GenericVideoDevice; + } + + return result; +} + +QString UdevDevice::subsystem() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_subsystem(m_device)); +} + +QString UdevDevice::devType() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_devtype(m_device)); +} + +QString UdevDevice::name() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_sysname(m_device)); +} + +QString UdevDevice::driver() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_driver(m_device)); +} + +QString UdevDevice::deviceNode() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_devnode(m_device)); +} + +dev_t UdevDevice::deviceId() const +{ + return udev_device_get_devnum(m_device); +} + +QStringList UdevDevice::alternateDeviceSymlinks() const +{ + if (!m_device) + return QStringList(); + return listFromEntries(udev_device_get_devlinks_list_entry(m_device)); +} + +QString UdevDevice::sysfsPath() const +{ + if (!m_device) + return QString(); + return QString::fromUtf8(udev_device_get_syspath(m_device)); +} + +int UdevDevice::sysfsNumber() const +{ + if (!m_device) + return -1; + return QByteArray(udev_device_get_sysnum(m_device)).toInt(); +} + +QString UdevDevice::seat() const +{ + QString seat = QString::fromUtf8(udev_device_get_property_value(m_device, "ID_SEAT")); + if (seat.isEmpty()) + seat = QStringLiteral("seat0"); + return seat; +} + +QString UdevDevice::property(const QString &name) const +{ + if (!m_device) + return QString(); + return QString::fromLatin1( + udev_device_get_property_value(m_device, name.toLatin1().constData())); +} + +bool UdevDevice::hasProperty(const QString &name) const +{ + if (!m_device) + return false; + return udev_device_get_property_value(m_device, name.toLatin1().constData()) != nullptr; +} + +QStringList UdevDevice::deviceProperties() const +{ + if (!m_device) + return QStringList(); + return listFromEntries(udev_device_get_properties_list_entry(m_device)); +} + +QStringList UdevDevice::sysfsProperties() const +{ + if (!m_device) + return QStringList(); + return listFromEntries(udev_device_get_sysattr_list_entry(m_device)); +} + +UdevDevice *UdevDevice::parent() const +{ + if (!m_device) + return nullptr; + + auto *p = udev_device_get_parent(m_device); + if (p) + return new UdevDevice(p, parent()); + return nullptr; +} + +QDebug operator<<(QDebug dbg, const UdevDevice &device) +{ + QDebugStateSaver saver(dbg); + if (device.isValid()) + dbg.nospace() << "UdevDevice(" << device.deviceNode() << ')'; + else + dbg.nospace() << "Invalid UdevDevice)"; + return dbg; +} + +/* + * UdevEnumerate + */ + +UdevEnumerate::UdevEnumerate(UdevDevice::DeviceTypes types, Udev *udev, QObject *parent) + : QObject(parent) + , m_types(types) + , m_udev(udev) +{ + qRegisterMetaType(); + + m_enumerate = udev_enumerate_new(*udev); + if (!m_enumerate) { + qCWarning(gLcDrm, "Unable to enumerate connected devices"); + return; + } + + if (types.testFlag(UdevDevice::InputDevice_Mask)) + udev_enumerate_add_match_subsystem(m_enumerate, "input"); + + if (types.testFlag(UdevDevice::VideoDevice_Mask)) { + udev_enumerate_add_match_subsystem(m_enumerate, "drm"); + udev_enumerate_add_match_sysname(m_enumerate, "card[0-9]*"); + } + + if (types.testFlag(UdevDevice::KeyboardDevice)) { + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_KEYBOARD", "1"); + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_KEY", "1"); + } + + if (types.testFlag(UdevDevice::MouseDevice)) + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_MOUSE", "1"); + + if (types.testFlag(UdevDevice::TouchpadDevice)) + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_TOUCHPAD", "1"); + + if (types.testFlag(UdevDevice::TouchscreenDevice)) + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_TOUCHSCREEN", "1"); + + if (types.testFlag(UdevDevice::TabletDevice)) + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_TABLET", "1"); + + if (types.testFlag(UdevDevice::JoystickDevice)) + udev_enumerate_add_match_property(m_enumerate, "ID_INPUT_JOYSTICK", "1"); +} + +UdevEnumerate::~UdevEnumerate() +{ + if (m_enumerate) + udev_enumerate_unref(m_enumerate); +} + +QList UdevEnumerate::scan() const +{ + QList list; + + if (!m_enumerate) + return list; + + if (udev_enumerate_scan_devices(m_enumerate) != 0) { + qCWarning(gLcDrm, "Unable to enumerate connected devices"); + return list; + } + + udev_device *drmDevice = nullptr; + udev_device *drmPrimaryDevice = nullptr; + + udev_list_entry *entry; + udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(m_enumerate)) + { + const char *syspath = udev_list_entry_get_name(entry); + udev_device *dev = udev_device_new_from_syspath(*m_udev, syspath); + if (!dev) + continue; + + QString node = QString::fromUtf8(udev_device_get_devnode(dev)); + + if (m_types.testFlag(UdevDevice::InputDevice_Mask) + && node.startsWith(QLatin1String("/dev/input/event"))) + list.append(new UdevDevice(dev, m_udev)); + + if (m_types.testFlag(UdevDevice::VideoDevice_Mask) + && node.startsWith(QLatin1String("/dev/dri/card"))) { + // We can have more than one DRM device on our seat, so the filter + // might want us to pick up only the primary video device + // In any case we'll be adding just one DRM device to the list + if (m_types.testFlag(UdevDevice::PrimaryVideoDevice)) { + auto *pci = udev_device_get_parent_with_subsystem_devtype(dev, "pci", nullptr); + if (pci) { + if (qstrcmp(udev_device_get_sysattr_value(pci, "boot_vga"), "1") == 0) + drmPrimaryDevice = dev; + } + } + if (!drmPrimaryDevice) { + if (drmDevice) + udev_device_unref(drmDevice); + drmDevice = dev; + } + } + } + + // Add any DRM device previously enumerated + if (drmPrimaryDevice) + list.append(new UdevDevice(drmPrimaryDevice, m_udev)); + else if (drmDevice) + list.append(new UdevDevice(drmDevice, m_udev)); + + return list; +} + +/* + * UdevMonitor + */ + +UdevMonitor::UdevMonitor(Udev *udev, QObject *parent) + : QObject(parent) + , m_udev(udev) +{ + qRegisterMetaType(); + + m_monitor = udev_monitor_new_from_netlink(*m_udev, "udev"); + if (!m_monitor) { + qCWarning(gLcDrm, "Unable to create an udev monitor: no devices can be detected"); + return; + } + + udev_monitor_enable_receiving(m_monitor); + + int fd = udev_monitor_get_fd(m_monitor); + auto *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(notifier, SIGNAL(activated(int)), this, SLOT(handleUdevEvent())); +} + +UdevMonitor::~UdevMonitor() +{ + if (m_monitor) + udev_monitor_unref(m_monitor); +} + +bool UdevMonitor::isValid() const +{ + return m_monitor != nullptr; +} + +int UdevMonitor::fd() const +{ + if (m_monitor) + return udev_monitor_get_fd(m_monitor); + return -1; +} + +void UdevMonitor::filterSubSystemDevType(const QString &subSystem, const QString &devType) +{ + if (!isValid()) + return; + + udev_monitor_filter_add_match_subsystem_devtype(m_monitor, qPrintable(subSystem), + qPrintable(devType)); +} + +void UdevMonitor::filterTag(const QString &tag) +{ + if (!isValid()) + return; + + udev_monitor_filter_add_match_tag(m_monitor, qPrintable(tag)); +} + +void UdevMonitor::handleUdevEvent() +{ + auto *dev = udev_monitor_receive_device(m_monitor); + if (!dev) + return; + + const char *action = udev_device_get_action(dev); + if (!action) { + udev_device_unref(dev); + return; + } + + auto *device = new UdevDevice(dev, m_udev); + + if (qstrcmp(action, "add") == 0) + Q_EMIT deviceAdded(device); + else if (qstrcmp(action, "remove") == 0) + Q_EMIT deviceRemoved(device); + else if (qstrcmp(action, "change") == 0) + Q_EMIT deviceChanged(device); + else if (qstrcmp(action, "online") == 0) + Q_EMIT deviceOnlined(device); + else if (qstrcmp(action, "offline") == 0) + Q_EMIT deviceOfflined(device); +} + +} // namespace Platform + +} // namespace Aurora diff --git a/src/plugins/deviceintegration/drm/udev.h b/src/plugins/deviceintegration/drm/udev.h new file mode 100644 index 00000000..be337e33 --- /dev/null +++ b/src/plugins/deviceintegration/drm/udev.h @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2018-2024 Pier Luigi Fiorini +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +struct udev; +struct udev_device; +struct udev_monitor; +struct udev_enumerate; + +namespace Aurora { + +namespace Platform { + +class UdevDevice : public QObject +{ + Q_OBJECT +public: + enum DeviceType { + UnknownDevice = 0x00, + KeyboardDevice = 0x01, + MouseDevice = 0x02, + TouchpadDevice = 0x04, + TouchscreenDevice = 0x08, + TabletDevice = 0x10, + JoystickDevice = 0x20, + GenericVideoDevice = 0x40, + PrimaryVideoDevice = 0x80, + InputDevice_Mask = KeyboardDevice | MouseDevice | TouchpadDevice | TouchscreenDevice + | TabletDevice | JoystickDevice, + VideoDevice_Mask = GenericVideoDevice + }; + Q_DECLARE_FLAGS(DeviceTypes, DeviceType) + + ~UdevDevice(); + + bool isValid() const; + + DeviceTypes type() const; + + QString subsystem() const; + QString devType() const; + QString name() const; + QString driver() const; + + QString deviceNode() const; + dev_t deviceId() const; + QStringList alternateDeviceSymlinks() const; + + QString sysfsPath() const; + int sysfsNumber() const; + + QString seat() const; + + QString property(const QString &name) const; + bool hasProperty(const QString &name) const; + + QStringList deviceProperties() const; + QStringList sysfsProperties() const; + + UdevDevice *parent() const; + + operator struct udev_device *() const + { + return m_device; + } + operator struct udev_device *() + { + return m_device; + } + +private: + explicit UdevDevice(struct udev_device *device, QObject *parent = nullptr); + + struct udev_device *m_device = nullptr; + + friend class Udev; + friend class UdevEnumerate; + friend class UdevMonitor; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(UdevDevice::DeviceTypes) + +QDebug operator<<(QDebug, const UdevDevice &); + +// + +class Udev : public QObject +{ + Q_OBJECT +public: + explicit Udev(QObject *parent = nullptr); + ~Udev(); + + bool isValid() const; + + UdevDevice *deviceFromFileName(const QString &fileName); + UdevDevice *deviceFromSubSystemAndName(const QString &subSystem, const QString &name); + UdevDevice *deviceFromSysfsPath(const QString &sysfsPath); + + operator struct udev *() const + { + return m_udev; + } + operator struct udev *() + { + return m_udev; + } + +private: + struct udev *m_udev = nullptr; +}; + +// + +class UdevEnumerate : public QObject +{ + Q_OBJECT +public: + explicit UdevEnumerate(UdevDevice::DeviceTypes types, Udev *udev, QObject *parent = nullptr); + ~UdevEnumerate(); + + QList scan() const; + + operator struct udev_enumerate *() const + { + return m_enumerate; + } + operator struct udev_enumerate *() + { + return m_enumerate; + } + +private: + UdevDevice::DeviceTypes m_types = UdevDevice::UnknownDevice; + Udev *m_udev = nullptr; + struct udev_enumerate *m_enumerate = nullptr; +}; + +// + +class UdevMonitor : public QObject +{ + Q_OBJECT +public: + explicit UdevMonitor(Udev *udev, QObject *parent = nullptr); + ~UdevMonitor(); + + bool isValid() const; + + int fd() const; + + void filterSubSystemDevType(const QString &subSystem, const QString &devType = QString()); + void filterTag(const QString &tag); + + operator struct udev_monitor *() const + { + return m_monitor; + } + operator struct udev_monitor *() + { + return m_monitor; + } + +Q_SIGNALS: + void deviceAdded(Aurora::Platform::UdevDevice *device); + void deviceRemoved(Aurora::Platform::UdevDevice *device); + void deviceChanged(Aurora::Platform::UdevDevice *device); + void deviceOnlined(Aurora::Platform::UdevDevice *device); + void deviceOfflined(Aurora::Platform::UdevDevice *device); + +private: + Udev *m_udev = nullptr; + struct udev_monitor *m_monitor = nullptr; + +private Q_SLOTS: + void handleUdevEvent(); +}; + +} // namespace Platform + +} // namespace Aurora + +Q_DECLARE_METATYPE(Aurora::Platform::UdevDevice)