Loading...
Searching...
No Matches
EglDmaBufExport.hpp
1#pragma once
2
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// EGL DMA-BUF exporter, sister to EglDmaBufImporter, for the PipeWire video
6// output path when the live QRhi backend is OpenGLES2: allocate a GBM buffer
7// object on /dev/dri/renderD128, export its DMA-BUF FD plus stride, offset and
8// modifier, hand the FD to PipeWire as a SPA_DATA_DmaBuf, and import the same
9// buffer back into the local EGL display as an EGLImage so the renderer can
10// target it through a persistent GL texture id and a QRhi createFrom wrapper.
11//
12// GBM rather than EGL_MESA_image_dma_buf_export because
13// SPA_FORMAT_VIDEO_modifier must be advertised before the first buffer is
14// allocated, and only GBM offers the modifier list up front. It is also a flat
15// C ABI, dlopen-friendly, and ships with every Mesa install.
16//
17// Buffers are created USE_RENDERING | USE_LINEAR: GPU-renderable on the
18// producer side, LINEAR-tiled so any consumer can sample them.
19//
20// Sync: glFinish() on the producer is sufficient before pw_stream_queue_buffer,
21// since a consumer importing the FD gets its own kernel-side barrier. Tighter
22// control would mean EGL_KHR_fence_sync -> EGL_ANDROID_native_fence_sync
23// stamped into explicit-sync metadata, the same upgrade path as the Vulkan
24// output.
25
26#if defined(__linux__)
27#include <Gfx/Graph/interop/EglDmaBufImport.hpp>
28
29#include <ossia/detail/dylib_loader.hpp>
30
31#include <QDebug>
32#include <QOpenGLContext>
33#include <QOpenGLExtraFunctions>
34
35#include <fcntl.h>
36#include <unistd.h>
37
38#include <cstdint>
39#include <cstring>
40#include <optional>
41
42namespace score::gfx
43{
44
47struct GbmDmaBufExport
48{
49 struct gbm_device;
50 struct gbm_bo;
51
52 // GBM_BO_USE_SCANOUT — required for a buffer the display engine reads
53 static constexpr uint32_t GBM_BO_USE_SCANOUT_v = 1u << 0;
54 // GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR — see /usr/include/gbm.h
55 static constexpr uint32_t GBM_BO_USE_RENDERING_v = (1u << 2);
56 static constexpr uint32_t GBM_BO_USE_LINEAR_v = (1u << 4);
57
58 // DRM fourccs from <drm_fourcc.h> — fourcc_code('A','R','2','4')
59 // expands to the 32-bit little-endian char concatenation.
60 static constexpr uint32_t DRM_FORMAT_ARGB8888_v = 0x34325241u; // "AR24"
61 static constexpr uint32_t DRM_FORMAT_ABGR8888_v = 0x34324241u; // "AB24"
62 static constexpr uint32_t DRM_FORMAT_XRGB8888_v = 0x34325258u; // "XR24"
63 static constexpr uint64_t DRM_FORMAT_MOD_LINEAR_v = 0ULL;
64
65 using FN_gbm_create_device = gbm_device* (*)(int fd);
66 using FN_gbm_device_destroy = void (*)(gbm_device* gbm);
67 using FN_gbm_bo_create
68 = gbm_bo* (*)(gbm_device*, uint32_t w, uint32_t h, uint32_t fmt,
69 uint32_t flags);
70 using FN_gbm_bo_create_with_modifiers2
71 = gbm_bo* (*)(gbm_device*, uint32_t w, uint32_t h, uint32_t fmt,
72 const uint64_t* mods, unsigned int count, uint32_t flags);
73 using FN_gbm_bo_get_fd = int (*)(gbm_bo*);
74 using FN_gbm_bo_get_stride = uint32_t (*)(gbm_bo*);
75 using FN_gbm_bo_get_offset = uint32_t (*)(gbm_bo*, int plane);
76 using FN_gbm_bo_get_modifier = uint64_t (*)(gbm_bo*);
77 using FN_gbm_bo_get_plane_count = int (*)(gbm_bo*);
78 using FN_gbm_bo_destroy = void (*)(gbm_bo*);
79
80 std::optional<ossia::dylib_loader> m_lib;
81 FN_gbm_create_device m_create_device{};
82 FN_gbm_device_destroy m_device_destroy{};
83 FN_gbm_bo_create m_bo_create{};
84 FN_gbm_bo_create_with_modifiers2 m_bo_create_with_modifiers2{};
85 FN_gbm_bo_get_fd m_bo_get_fd{};
86 FN_gbm_bo_get_stride m_bo_get_stride{};
87 FN_gbm_bo_get_offset m_bo_get_offset{};
88 FN_gbm_bo_get_modifier m_bo_get_modifier{};
89 FN_gbm_bo_get_plane_count m_bo_get_plane_count{};
90 FN_gbm_bo_destroy m_bo_destroy{};
91
92 int m_drmFd{-1};
93 gbm_device* m_device{nullptr};
94
98 static bool isAvailable() noexcept
99 {
100 try
101 {
102 ossia::dylib_loader probe(
103 std::vector<std::string_view>{"libgbm.so.1", "libgbm.so"});
104 if(!probe.symbol<FN_gbm_create_device>("gbm_create_device"))
105 return false;
106 }
107 catch(...)
108 {
109 return false;
110 }
111 // Render node must exist + be openable. ::access(F_OK) is the
112 // cheapest check — full open happens in init().
113 return ::access("/dev/dri/renderD128", R_OK | W_OK) == 0;
114 }
115
116 bool init() noexcept
117 {
118 try
119 {
120 m_lib.emplace(
121 std::vector<std::string_view>{"libgbm.so.1", "libgbm.so"});
122 }
123 catch(...)
124 {
125 return false;
126 }
127
128 m_create_device
129 = m_lib->symbol<FN_gbm_create_device>("gbm_create_device");
130 m_device_destroy
131 = m_lib->symbol<FN_gbm_device_destroy>("gbm_device_destroy");
132 m_bo_create = m_lib->symbol<FN_gbm_bo_create>("gbm_bo_create");
133 // _with_modifiers2 is newer (Mesa 22+); fall back to _bo_create.
134 m_bo_create_with_modifiers2
135 = m_lib->symbol<FN_gbm_bo_create_with_modifiers2>(
136 "gbm_bo_create_with_modifiers2");
137 m_bo_get_fd = m_lib->symbol<FN_gbm_bo_get_fd>("gbm_bo_get_fd");
138 m_bo_get_stride
139 = m_lib->symbol<FN_gbm_bo_get_stride>("gbm_bo_get_stride");
140 m_bo_get_offset
141 = m_lib->symbol<FN_gbm_bo_get_offset>("gbm_bo_get_offset");
142 m_bo_get_modifier
143 = m_lib->symbol<FN_gbm_bo_get_modifier>("gbm_bo_get_modifier");
144 m_bo_get_plane_count = m_lib->symbol<FN_gbm_bo_get_plane_count>(
145 "gbm_bo_get_plane_count");
146 m_bo_destroy = m_lib->symbol<FN_gbm_bo_destroy>("gbm_bo_destroy");
147
148 if(!m_create_device || !m_device_destroy || !m_bo_create
149 || !m_bo_get_fd || !m_bo_get_stride || !m_bo_destroy)
150 return false;
151
152 // Open the DRM render node: the privilege-free entry point, which lets
153 // unprivileged processes render without master access to the display.
154 // /dev/dri/renderD128 is the first card; multi-GPU selection would go
155 // through eglQueryDevicesEXT.
156 m_drmFd = ::open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
157 if(m_drmFd < 0)
158 {
159 qWarning() << "EglDmaBufExport: failed to open /dev/dri/renderD128";
160 return false;
161 }
162
163 m_device = m_create_device(m_drmFd);
164 if(!m_device)
165 {
166 qWarning() << "EglDmaBufExport: gbm_create_device failed";
167 ::close(m_drmFd);
168 m_drmFd = -1;
169 return false;
170 }
171 return true;
172 }
173
174 void shutdown() noexcept
175 {
176 if(m_device && m_device_destroy)
177 m_device_destroy(m_device);
178 m_device = nullptr;
179 if(m_drmFd >= 0)
180 ::close(m_drmFd);
181 m_drmFd = -1;
182 m_lib.reset();
183 }
184
188 struct Slot
189 {
190 gbm_bo* bo{nullptr};
191 int fd{-1};
192 uint32_t stride{};
193 uint32_t offset{};
194 uint64_t modifier{};
195 uint32_t width{};
196 uint32_t height{};
197 uint32_t drm_fourcc{};
198 EglDmaBufImporter::PlaneImport plane{}; // EGLImage
199 unsigned int glTexture{0};
200 std::size_t size{};
201 };
202
207 bool allocSlotGbmOnly(
208 Slot& out, uint32_t w, uint32_t h, uint32_t drm_fourcc,
209 uint32_t extraFlags = 0) noexcept
210 {
211 out = {};
212 out.width = w;
213 out.height = h;
214 out.drm_fourcc = drm_fourcc;
215
216 // Try the modifier-aware allocator first. Pass exactly one modifier
217 // (LINEAR) so GBM has no choice but to give us LINEAR — symmetric
218 // with what the SPA pod advertises in PipewireOutputDevice.cpp.
219 const uint64_t mods[] = {DRM_FORMAT_MOD_LINEAR_v};
220 const uint32_t flags
221 = GBM_BO_USE_RENDERING_v | GBM_BO_USE_LINEAR_v | extraFlags;
222 if(m_bo_create_with_modifiers2)
223 {
224 out.bo = m_bo_create_with_modifiers2(
225 m_device, w, h, drm_fourcc, mods,
226 sizeof(mods) / sizeof(mods[0]), flags);
227 }
228 if(!out.bo)
229 {
230 // Fallback: plain gbm_bo_create with USE_RENDERING | USE_LINEAR;
231 // the driver picks the modifier (will be LINEAR with USE_LINEAR).
232 out.bo = m_bo_create(m_device, w, h, drm_fourcc, flags);
233 }
234 if(!out.bo)
235 {
236 // NVIDIA's GBM backend (nvidia-drm) rejects USE_RENDERING and the
237 // with_modifiers2 entry point outright, but allocates fine with
238 // USE_LINEAR alone (verified: driver 595, all 8888 fourccs). The
239 // BO is only ever written through an EGLImage-bound GL texture
240 // copy, so the RENDERING usage bit is not load-bearing for us.
241 out.bo = m_bo_create(
242 m_device, w, h, drm_fourcc, GBM_BO_USE_LINEAR_v | extraFlags);
243 }
244 if(!out.bo)
245 {
246 qWarning() << "EglDmaBufExport: gbm_bo_create failed";
247 return false;
248 }
249
250 out.fd = m_bo_get_fd(out.bo);
251 out.stride = m_bo_get_stride(out.bo);
252 out.offset = m_bo_get_offset ? m_bo_get_offset(out.bo, 0) : 0;
253 out.modifier
254 = m_bo_get_modifier ? m_bo_get_modifier(out.bo) : 0ULL;
255 out.size = std::size_t(out.stride) * std::size_t(h);
256
257 if(out.fd < 0)
258 {
259 qWarning() << "EglDmaBufExport: gbm_bo_get_fd returned -1";
260 if(m_bo_destroy)
261 m_bo_destroy(out.bo);
262 out.bo = nullptr;
263 return false;
264 }
265 return true;
266 }
267
276 bool allocSlot(
277 Slot& out, uint32_t w, uint32_t h, uint32_t drm_fourcc,
278 EglDmaBufImporter& importer, uint32_t extraFlags = 0) noexcept
279 {
280 if(!allocSlotGbmOnly(out, w, h, drm_fourcc, extraFlags))
281 return false;
282
283 // Create a fresh GL texture to receive the import. Persistent: the
284 // OutputNode createFrom-wraps it once and the same texture id
285 // serves every frame this slot is the active pipewire buffer.
286 out.glTexture = createLinearClampGlTexture2D();
287 if(out.glTexture == 0)
288 {
289 qWarning() << "EglDmaBufExport: failed to create GL texture";
290 ::close(out.fd);
291 out.fd = -1;
292 if(m_bo_destroy)
293 m_bo_destroy(out.bo);
294 out.bo = nullptr;
295 return false;
296 }
297
298 // Import the same DMA-BUF back into our local EGL display and
299 // bind it to glTexture so the renderer sees an image-backed texture.
300 if(!importer.importPlane(
301 out.plane, out.glTexture, out.fd, out.modifier, out.offset,
302 out.stride, drm_fourcc, int(w), int(h)))
303 {
304 qWarning() << "EglDmaBufExport: importer.importPlane failed";
305 if(auto* ctx = QOpenGLContext::currentContext(); ctx)
306 ctx->extraFunctions()->glDeleteTextures(1, &out.glTexture);
307 out.glTexture = 0;
308 ::close(out.fd);
309 out.fd = -1;
310 if(m_bo_destroy)
311 m_bo_destroy(out.bo);
312 out.bo = nullptr;
313 return false;
314 }
315 return true;
316 }
317
321 void destroySlotGbmOnly(Slot& s) noexcept
322 {
323 if(s.fd >= 0)
324 {
325 ::close(s.fd);
326 s.fd = -1;
327 }
328 if(s.bo && m_bo_destroy)
329 m_bo_destroy(s.bo);
330 s.bo = nullptr;
331 }
332
333 void destroySlot(Slot& s, EglDmaBufImporter& importer) noexcept
334 {
335 if(s.plane.image)
336 importer.cleanupPlane(s.plane);
337 if(s.glTexture)
338 {
339 if(auto* ctx = QOpenGLContext::currentContext(); ctx)
340 ctx->extraFunctions()->glDeleteTextures(1, &s.glTexture);
341 s.glTexture = 0;
342 }
343 // Producer owns the FD (we got it via gbm_bo_get_fd, which dups);
344 // pipewire just borrows it for the buffer's lifetime. Mirrors the
345 // Vulkan output path's on_remove_buffer FD handling.
346 if(s.fd >= 0)
347 {
348 ::close(s.fd);
349 s.fd = -1;
350 }
351 if(s.bo && m_bo_destroy)
352 m_bo_destroy(s.bo);
353 s.bo = nullptr;
354 }
355};
356
357} // namespace score::gfx
358#endif // __linux__
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11