diff options
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | include/drm++/drm.hpp | 195 | ||||
| -rw-r--r-- | include/drm++/gem.hpp | 114 | ||||
| -rw-r--r-- | include/drm++/ioctl.hpp | 11 | ||||
| -rw-r--r-- | include/drm++/syncobject.hpp | 51 | ||||
| -rw-r--r-- | src/drm.cpp | 48 | ||||
| -rw-r--r-- | src/gem.cpp | 2 |
7 files changed, 371 insertions, 51 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a23eddd..a37e540 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ project(drm++ include(cmake/Diagnostics.cmake) add_library(drm++ SHARED + "src/drm.cpp" "src/gem.cpp" "src/ioctl.cpp" "src/syncobject.cpp") diff --git a/include/drm++/drm.hpp b/include/drm++/drm.hpp new file mode 100644 index 0000000..f128930 --- /dev/null +++ b/include/drm++/drm.hpp @@ -0,0 +1,195 @@ +/* SPDX-License-Identifier: MIT */ + +#pragma once + +/// +/// * Direct Rendering Manager +/// +/// The Direct Rendering Manager (DRM) is a Linux subsystem that provides an interface +/// for GPU drivers. The interface is split into common DRM core and driver-specific components. +/// +/// This library provides a C++ wrapper around the core DRM interface, not including +/// any driver-specific ioctls. This library also focuses on modern DRM and intentionally +/// does not provide wrappers for legacy features (which are a no-op on most modern +/// GPU drivers anyways). +/// +/// The kernel exposes a DRM device as a device file, usually located at `/dev/dri/cardX`, and all +/// ioctls are performed on a file descriptor to this device. +/// +/// * Authentication +/// +/// Access to `/dev/dri/cardX` is restricted to the DRM master, or any client that has been +/// granted access. While not all ioctls are restricted, client should use `/dev/dri/renderDXXX` +/// for purely unauthenticated access (such as simple buffer management, compute, etc). +/// +/// Acquiring DRM master happens automatically when the first process opens `/dev/dri/cardX`, but +/// can be explicitly requested as well. DRM master is exclusive, and only one process can be +/// DRM master at a time. +/// +/// If another client wishes to use restricted ioctls, it must send its magic token to +/// the DRM master, which can authenticate the file descriptor. +/// +/// * Graphics Execution Manager +/// +/// The Graphics Execution Manager (GEM) provides an API for memory management in video memory, +/// however in core DRM its functionality is limited to a minimal set of features. +/// +/// A memory object living in video memory is called a GEM object and is identified by a handle. +/// In modern (core) DRM, GEM objects are usually only imported from DMA-BUFs allocated +/// through other APIs (such as GBM, Vulkan, etc.). This is known as "PRIME". +/// +/// However, most drivers will also support "dumb buffers", which are simple linear buffers held +/// in either system memory or video memory, but always accessible through the CPU. +/// +/// * Kernel Mode Setting +/// +/// (TODO) +/// +/// * Sync Objects +/// +/// Sync objects are a relatively recent addition to DRM and are primarily used by the +/// Vulkan WSI and Wayland for explicit synchronization. +/// +/// A sync object can hold one or more DRM fences, which are a kernel object that can be signaled +/// by the GPU when a certain operation has been completed. Fences inside sync objects can be +/// exported to file descriptors called "sync files", which can be used by other APIs (such as +/// Vulkan) to synchronize with the GPU. +/// +/// They can also be used purely CPU-side for synchronization between processes. +/// +/// * Legacy features +/// +/// As said previously, this library intentionally does not provide wrappers for legacy features, +/// however it is a good idea to be aware of their existence. This list is ordered from most +/// relevant to least relevant. +/// +/// 1. The single most relevant feature is legacy modesetting. This was the original way to set +/// display modes, manage CRTCs, etc. and has been superseded all the way back in 2015 by the +/// far superior atomic modesetting, which addresses several severe flaws in the original design. +/// However only recently (around ~2023) has atomic modesetting been stable enough in +/// drivers (specifically NVIDIA) to be used everywhere. Nowdays it is required for explicit +/// synchronization and therefore this library chooses to only support atomic modesetting. +/// +/// 2. Prior to PRIME, sharing GEM objects across processes was done via "flink" and "open". Each +/// GEM object could be given a unique name which other processes could then open. Due to the +/// security implications of this, it should never be used unless legacy API require it. +/// +/// 3. Next to GEM there is also the Translation Table Maps (TTM) memory manager. The user-facing +/// API was deemed way too complex, as it tried to unify various memory constellations into a +/// single interface. While some drivers still use TTM internally, GEM acts as a common interface +/// and should be used instead (see https://docs.kernel.org/gpu/drm-mm.html). +/// +/// 4. In the distant past, GPU context management, draw areas, AGP maintenance and GPU locking +/// were all handled by userspace (also known as userspace mode settings / UMS). In v6.8-rc1 +/// these were finally retired from the kernel codebase entirely, although they haven't had +/// an implementation in decades anyways (see 184dcdc). +/// +/// * Further reading +/// +/// Unfortunately documentation for DRM is kind of all over the place. Crucial information is +/// often hidden in individual commit messages and requires reading kernel code to understand. +/// +/// A good starting point is the wikipedia page: +/// https://en.wikipedia.org/wiki/Direct_Rendering_Manager +/// +/// For more in-depth information, the kernel documentation is a good resource: +/// https://www.kernel.org/doc/html/latest/gpu/index.html +/// + +/// +/// * API Usage +/// +/// Using the C++ API is fairly straightforward. +/// +/// Components of the DRM API are split into their own header files, each in their own namespace. +/// +/// Each header will have a large comment block at the top explaining the concept of the +/// component and giving essential information about its usage, such that individual methods +/// aren't cluttered with documentation. +/// +/// Most methods utilize ioctls and will throw drm::ioctl::Exception on failure. This is always +/// documented in the class comment block and applies for all methods unless otherwise specified +/// or annotated with `noexcept`. +/// +/// Should a DRM capability be required for a certain feature, this will also be documented in the +/// class comment block, unless only a single method requires it, in which case it will be +/// documented in the method accordingly. +/// +/// Most classes cannot be copied, however there are exceptions (such as SyncObject) in which a +/// copy constructor/operator is provided. Please note that these likely utilize ioctls and +/// may throw an exception on failure. +/// +/// Occasionally strictly non-virtual inheritance is used to minimize code duplication and +/// macros and type definitions are used to shorten getters and integer types. +/// +/// * Requirements +/// +/// The majority of the DRM API requires a Linux kernel version of at least (TODO), and individual +/// components requiring an even newer version are documented accordingly (FIXME). +/// +/// This library requires a C++ compiler capable of C++20 or newer. +/// + +#include "drm++/helper.hpp" + +namespace drm { + +/// +/// DRM device node (e.g. `/dev/dri/cardX`). +/// +/// @throws drm::ioctl::Exception on failure +/// +class Device { +public: + /// Wrap around an existing file descriptor, taking ownership and closing it on destruction. + /// @throws std::invalid_argument if fd is invalid + Device(int fd); + + /// Attempt to acquire DRM master on the device. This will fail if another process is + /// already DRM master. + void acquireMaster() const; + + /// Release DRM master on the device. + void releaseMaster() const; + + /// Authenticate a file descriptor via magic token, allowing for access to + /// restricted ioctls (such as mode setting). + void authenticate(u32 magic) const; + + // Private access + GETTER(fd) + GETTER(magic) + + // Move constructor/operator + Device(Device&& other) noexcept : m_fd(other.m_fd), m_magic(other.m_magic) { + other.m_fd = -1; // invalidate other + } + + Device& operator=(Device&& other) noexcept { + if (this != &other) { + this->destruct(); + this->m_fd = other.m_fd; + this->m_magic = other.m_magic; + other.m_fd = -1; // invalidate other + } + + return *this; + } + + // Copy constructor/operator + Device(const Device& other) = delete; + Device& operator=(const Device& other) = delete; + + // Destructor + ~Device() noexcept { + this->destruct(); + } + +private: + int m_fd; // indicates device validity (>= 0) + u32 m_magic; + + void destruct() noexcept; +}; + +} diff --git a/include/drm++/gem.hpp b/include/drm++/gem.hpp index 1dfe1ba..4a02e55 100644 --- a/include/drm++/gem.hpp +++ b/include/drm++/gem.hpp @@ -2,19 +2,82 @@ #pragma once +/// +/// * GEM Objects +/// +/// A GEM object is a reference-counted kernel object wrapping a memory allocation of any kind, +/// identified by one or more handles. +/// +/// In core DRM there are 2 ways to create a GEM object: +/// - Import a DMA-BUF via PRIME +/// - Create a linear dumb buffer +/// +/// Upon importing a DMA-BUF, a new handle to the underlying GEM object is created and the +/// reference count is increased. The file descriptor itself also holds a reference and must +/// therefore be closed. +/// +/// When creating a dumb buffer, there are no guarantees about the underlying memory. It may be +/// in system memory or GPU memory and there are no guarantees about the performance of read/write +/// operations. A dumb buffer can however always be mapped into CPU-accessible memory. +/// +/// * Dumb Buffers +/// +/// A dumb buffer is a primitive DRM-native driver independent GEM object, wrapping around a linear +/// memory allocation. +/// +/// The only creation parameters are width, height and bpp (bits per pixel / color mode). The +/// bpp parameter also specifies the DRM formats this buffer can be used with. The table below +/// can serve as a reference, however there are no guarantees that a format is compatible. +/// +/// Most drivers will support DRM_FORMAT_XRGB8888 with 32 bits per pixel. +/// +/// +-----+------------------------+------------------------+ +/// | BPP | Framebuffer format | Compatible formats | +/// +=====+========================+========================+ +/// | 32 | * DRM_FORMAT_XRGB8888 | * DRM_FORMAT_BGRX8888 | +/// | | | * DRM_FORMAT_RGBX8888 | +/// | | | * DRM_FORMAT_XBGR8888 | +/// +-----+------------------------+------------------------+ +/// | 24 | * DRM_FORMAT_RGB888 | * DRM_FORMAT_BGR888 | +/// +-----+------------------------+------------------------+ +/// | 16 | * DRM_FORMAT_RGB565 | * DRM_FORMAT_BGR565 | +/// +-----+------------------------+------------------------+ +/// | 15 | * DRM_FORMAT_XRGB1555 | * DRM_FORMAT_BGRX1555 | +/// | | | * DRM_FORMAT_RGBX1555 | +/// | | | * DRM_FORMAT_XBGR1555 | +/// ------+------------------------+------------------------+ +/// | 8 | * DRM_FORMAT_C8 | * DRM_FORMAT_D8 | +/// | | | * DRM_FORMAT_R8 | +/// +-----+------------------------+------------------------+ +/// | 4 | * DRM_FORMAT_C4 | * DRM_FORMAT_D4 | +/// | | | * DRM_FORMAT_R4 | +/// +-----+------------------------+------------------------+ +/// | 2 | * DRM_FORMAT_C2 | * DRM_FORMAT_D2 | +/// | | | * DRM_FORMAT_R2 | +/// +-----+------------------------+------------------------+ +/// | 1 | * DRM_FORMAT_C1 | * DRM_FORMAT_D1 | +/// | | | * DRM_FORMAT_R1 | +/// +-----+------------------------+------------------------+ +/// +/// This table is not exhaustive, but it is not recommended to use any other values for bpp. +/// + #include "drm++/helper.hpp" namespace drm::gem { -/// A GEM object wrapping memory. +/// +/// GEM object wrapping a memory allocation +/// /// @throws drm::ioctl::Exception on failure +/// class Object { friend class DumbBuffer; public: /// Import a DMA-BUF file descriptor into a GEM object. /// Requires DRM_PRIME_CAP_IMPORT. /// - /// An import can fail for various driver-specific reasons. + /// An import can fail for various driver-specific reasons, especially for foreign DMA-BUFs. /// /// @param close Close fd after import (regardless of success) Object(int fd, int dmabuf_fd, bool close = true); @@ -36,11 +99,6 @@ public: [[nodiscard]] int exportFd(ExportFlags flags = ExportFlags::None) const; /// Change the handle of the GEM object. - /// - /// SAFETY: This is a dangerous operation, as the previous handle will be invalidated. There is - /// no guarantee that importing a DMA-BUF file descriptor will return a new handle, so other - /// instances of this class on the same file descriptor may become invalid. Use with caution. - /// /// @param handle An unused GEM handle to change into. void changeHandle(u32 handle); @@ -77,51 +135,19 @@ private: int m_fd; // indicates object validity (>= 0) u32 m_handle; + Object(int fd) : m_fd(fd), m_handle(0) {} void destruct() noexcept; }; -/// A dumb buffer GEM object. +/// +/// Dumb buffer wrapping a linear memory allocation. /// Requires DRM_CAP_DUMB_BUFFER. +/// /// @throws drm::ioctl::Exception on failure +/// class DumbBuffer : public Object { public: /// Create a new dumb buffer. - /// - /// The bits per pixel (bpp) also indicates the format and compatible formats - /// with similar pixel layouts. All buffers are strictly linear. - /// - /// +-----+------------------------+------------------------+ - /// | BPP | Framebuffer format | Compatible formats | - /// +=====+========================+========================+ - /// | 32 | * DRM_FORMAT_XRGB8888 | * DRM_FORMAT_BGRX8888 | - /// | | | * DRM_FORMAT_RGBX8888 | - /// | | | * DRM_FORMAT_XBGR8888 | - /// +-----+------------------------+------------------------+ - /// | 24 | * DRM_FORMAT_RGB888 | * DRM_FORMAT_BGR888 | - /// +-----+------------------------+------------------------+ - /// | 16 | * DRM_FORMAT_RGB565 | * DRM_FORMAT_BGR565 | - /// +-----+------------------------+------------------------+ - /// | 15 | * DRM_FORMAT_XRGB1555 | * DRM_FORMAT_BGRX1555 | - /// | | | * DRM_FORMAT_RGBX1555 | - /// | | | * DRM_FORMAT_XBGR1555 | - /// ------+------------------------+------------------------+ - /// | 8 | * DRM_FORMAT_C8 | * DRM_FORMAT_D8 | - /// | | | * DRM_FORMAT_R8 | - /// +-----+------------------------+------------------------+ - /// | 4 | * DRM_FORMAT_C4 | * DRM_FORMAT_D4 | - /// | | | * DRM_FORMAT_R4 | - /// +-----+------------------------+------------------------+ - /// | 2 | * DRM_FORMAT_C2 | * DRM_FORMAT_D2 | - /// | | | * DRM_FORMAT_R2 | - /// +-----+------------------------+------------------------+ - /// | 1 | * DRM_FORMAT_C1 | * DRM_FORMAT_D1 | - /// | | | * DRM_FORMAT_R1 | - /// +-----+------------------------+------------------------+ - /// - /// Table is not exhaustive, however other bpp values should only be used for legacy purposes. - /// - /// Support for all bits per pixel is optional and successful creation of a dumb buffer does - /// not guarantee that all related formats are compatible. DumbBuffer(int fd, u32 width, u32 height, u32 bpp); /// Map the dumb buffer into userspace memory. May be called multiple times, diff --git a/include/drm++/ioctl.hpp b/include/drm++/ioctl.hpp index 1a68dae..29b08ae 100644 --- a/include/drm++/ioctl.hpp +++ b/include/drm++/ioctl.hpp @@ -46,8 +46,15 @@ private: /// Perform an ioctl() call. /// @throws drm::ioctl::Exception on failure +inline void perform(int fd, unsigned long op) { + if (::ioctl(fd, op, nullptr) < 0) + throw Exception(fd, op); +} + +/// Perform an ioctl() call. +/// @throws drm::ioctl::Exception on failure template<typename T> -T perform(int fd, unsigned long op) { +inline T perform(int fd, unsigned long op) { T data{}; if (::ioctl(fd, op, &data) < 0) @@ -59,7 +66,7 @@ T perform(int fd, unsigned long op) { /// Perform an ioctl() call. /// @throws drm::ioctl::Exception on failure template<typename T> -void perform(int fd, unsigned long op, T& data) { +inline void perform(int fd, unsigned long op, T& data) { if (::ioctl(fd, op, &data) < 0) throw Exception(fd, op); } diff --git a/include/drm++/syncobject.hpp b/include/drm++/syncobject.hpp index f530c49..9ecbf17 100644 --- a/include/drm++/syncobject.hpp +++ b/include/drm++/syncobject.hpp @@ -2,6 +2,39 @@ #pragma once +/// +/// * Sync Object +/// +/// A sync object is a reference-counted kernel object which can hold a DRM fence. A fence can +/// either be signaled or unsignaled, but a sync object can also hold no fence at all. +/// +/// Fences are typically imported from a sync file, which has been exported from an API such as +/// Vulkan, which will signal the fence when a certain operation has been completed. +/// +/// In core DRM, it is possible to emplace a trivially signaled fence into a sync object, or +/// remove ("reset") a fence from a sync object. It is also possible to copy a fence from another +/// sync object. +/// +/// Fences can be imported and exported from a sync object via sync files, but it is also possible +/// to export a reference to the sync object itself, which can be imported by another process. +/// +/// Finally, sync objects can be waited on, performing a CPU-side wait. It is also possible +/// to merely wait for a sync object to be emplaced with a fence, without waiting for it to be +/// signaled. During a wait, it is possible to set a deadline hint on the fence, described by +/// the kernel as "to provide the fence signaler with an appropriate sense of urgency". +/// +/// * Timeline Sync Object +/// +/// A timeline sync object is a sync object which can hold multiple fences, each identified by a +/// 64-bit unsigned integer "point". The point should be monotonically increasing, as this +/// is what other APIs are designed with (e.g. Vulkan). +/// +/// While the kernel does not differentiate between a (binary) sync object and a timeline +/// sync object, there exists a clear distinction and mixing ioctls can lead to undefined +/// behavior. This library protects against this by providing separate classes for each type +/// of sync object. +/// + #include "drm++/helper.hpp" #include <optional> @@ -12,7 +45,12 @@ namespace drm::syncobj { class SyncObject; class TimelineSyncObject; -/// Common base class for SyncObject and TimelineSyncObject. +/// +/// Common base class for sync objects. +/// Requires DRM_CAP_SYNCOBJ. +/// +/// @throws drm::ioctl::Exception on failure +/// class SyncObjectBase { friend class SyncObject; friend class TimelineSyncObject; @@ -79,9 +117,12 @@ enum class WaitMode : u8 { WaitAvailable //!< Wait for empty sync objects to be filled, but do not wait for signaling }; -/// A (binary) synchronization object is a reference-counted container which can hold a DRM fence. +/// +/// A (binary) synchronization object. /// Requires DRM_CAP_SYNCOBJ. +/// /// @throws drm::ioctl::Exception on failure +/// class SyncObject : public SyncObjectBase { public: using SyncObjectBase::SyncObjectBase; @@ -119,10 +160,12 @@ public: void registerEventFd(int eventfd_fd, bool waitAvailable = false) const; }; -/// A timeline synchronization object can hold multiple DRM fences, identified via a -/// monotonically increasing 64-bit unsigned integer "point". +/// +/// A timeline synchronization object. /// Requires DRM_CAP_SYNCOBJ_TIMELINE. +/// /// @throws drm::ioctl::Exception on failure +/// class TimelineSyncObject : public SyncObjectBase { public: using SyncObjectBase::SyncObjectBase; diff --git a/src/drm.cpp b/src/drm.cpp new file mode 100644 index 0000000..6bb0a3a --- /dev/null +++ b/src/drm.cpp @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: MIT */ + +#include "drm++/drm.hpp" +#include "drm++/helper.hpp" +#include "drm++/ioctl.hpp" + +#include <stdexcept> + +#include <drm.h> +#include <unistd.h> + +using namespace drm; + +Device::Device(int fd) : m_fd(fd) { + if (this->m_fd < 0) { + throw std::invalid_argument("invalid file descriptor"); + } + + drm_auth args{ + + }; + ioctl::perform(this->m_fd, DRM_IOCTL_GET_MAGIC, args); + this->m_magic = args.magic; +} + +void Device::acquireMaster() const { + ioctl::perform(this->m_fd, DRM_IOCTL_SET_MASTER); +} + +void Device::releaseMaster() const { + ioctl::perform(this->m_fd, DRM_IOCTL_DROP_MASTER); +} + +void Device::authenticate(u32 magic) const { + drm_auth args{ + .magic = magic + }; + ioctl::perform(this->m_fd, DRM_IOCTL_AUTH_MAGIC, args); +} + +void Device::destruct() noexcept { + if (this->m_fd < 0) { + return; + } + + ::close(this->m_fd); + this->m_fd = -1; +} diff --git a/src/gem.cpp b/src/gem.cpp index 4b81703..0572c3b 100644 --- a/src/gem.cpp +++ b/src/gem.cpp @@ -78,7 +78,7 @@ void Object::destruct() noexcept { /* Dumb buffer class */ DumbBuffer::DumbBuffer(int fd, u32 width, u32 height, u32 bpp) - : Object(fd, -1), m_width(width), m_height(height), m_bpp(bpp) { + : Object(fd), m_width(width), m_height(height), m_bpp(bpp) { drm_mode_create_dumb args{ .height = height, .width = width, |
