QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstDmaBufVideoBuffer.cc
Go to the documentation of this file.
2
5#include "GstDmaFourcc.h"
6#include "GstEglHelpers.h"
8#include "GstHwImportCache.h"
10#include "GstHwPathTelemetry.h"
11#include "GstHwVideoBuffer.h"
13#include "HwBuffers.h"
14
15#if defined(QGC_HAS_GST_DMABUF_GPU_PATH)
16
17#include <QtCore/QByteArray>
18#include <QtCore/QLoggingCategory>
19#include <QtCore/QMutex>
20#include <QtCore/QScopeGuard>
21#include <QtCore/QSize>
22#include <QtGui/QGuiApplication>
23#include <QtGui/QOpenGLContext>
24#include <QtGui/QOpenGLFunctions>
25#include <QtGui/qopenglcontext_platform.h>
26#include <gst/allocators/gstdmabuf.h>
27#include <gst/video/video.h>
28#include <private/qvideotexturehelper_p.h>
29#include <rhi/qrhi.h>
30
31#include "QGCLoggingCategory.h"
32#if GST_CHECK_VERSION(1, 24, 0)
33#include <gst/video/video-info-dma.h>
34#endif
35
36#include <GLES2/gl2.h>
37#include <GLES2/gl2ext.h>
38#include <algorithm>
39#include <array>
40#include <atomic>
41#include <drm_fourcc.h>
42#include <limits>
43#include <unistd.h>
44#include <utility>
45#include <vector>
46
47// Producer dma-buf fence export: optional. The header exists on Ubuntu 22.04 but predates the
48// EXPORT_SYNC_FILE uAPI (kernel ~6.0), so gate on the ioctl token, not just header presence.
49#if __has_include(<linux/dma-buf.h>)
50#include <linux/dma-buf.h>
51#include <sys/ioctl.h>
52#ifdef DMA_BUF_IOCTL_EXPORT_SYNC_FILE
53#define QGC_HAS_DMABUF_EXPORT_SYNC 1
54#endif
55#endif
56
57#ifndef EGL_ANDROID_native_fence_sync
58#define EGL_SYNC_NATIVE_FENCE_ANDROID 0x3144
59#define EGL_SYNC_NATIVE_FENCE_FD_ANDROID 0x3145
60#define EGL_NO_NATIVE_FENCE_FD_ANDROID -1
61#endif
62
63QGC_LOGGING_CATEGORY(GstDmaBufLog, "Video.GStreamer.HwBuffers.GstDmaBuf")
64
65namespace {
66
68
69std::atomic<bool> s_loggedBadBackend{false};
70
71// EGL_BAD_MATCH from eglCreateImage = broken EGL/VA-API combo (QTBUG-112312); short-circuits mapTextures on first hit.
72std::atomic<bool> s_permanentlyDisabled{false};
73
74void maybePermanentlyDisable(EGLDisplay eglDpy, int plane, const char* pathTag)
75{
76 if (!s_permanentlyDisabled.exchange(true, std::memory_order_relaxed)) {
79 qCWarning(GstDmaBufLog) << "eglCreateImage returned EGL_BAD_MATCH on" << pathTag << "plane" << plane
80 << "— disabling DMABuf zero-copy for this process"
81 << "(EGL_VENDOR=" << eglQueryString(eglDpy, EGL_VENDOR)
82 << "EGL_VERSION=" << eglQueryString(eglDpy, EGL_VERSION) << ")";
83 }
84}
85
86// EGLDisplay is process-stable; cache the value (not a context pointer) to avoid dangling-key hazard.
87std::atomic<EGLDisplay> s_cachedDisplay{EGL_NO_DISPLAY};
88std::atomic<bool> s_loggedSingleFdDisabled{false};
89std::atomic<bool> s_loggedFenceTimeout{false};
90
91// First-import-per-epoch bind validation: glGetError flushes the pipeline, so steady-state frames skip it.
92std::atomic<bool> s_bindValidated{false};
93std::atomic<bool> s_loggedBindFailed{false};
94
95constexpr const char* kModifiersExt = "EGL_EXT_image_dma_buf_import_modifiers";
96
97constexpr const char* kFenceSyncExt = "EGL_KHR_fence_sync";
98
99constexpr const char* kNativeFenceExt = "EGL_ANDROID_native_fence_sync";
100
101// EGLImage cache (DEFAULT OFF, QGC_GST_DMABUF_CACHE=1): fd/GstMemory ABA means a recycled pool buffer can serve a
102// stale image, so we key on fd plus the full EGL layout and weak-ref the GstMemory to evict before the fd is reused.
103bool imageCacheEnabled() noexcept
104{
106}
107
108bool texStorageImportAllowed(bool contextIsOpenGles, bool hasTexStorageExt, guint64 drmModifier) noexcept
109{
110 return drmModifier == 0 && !contextIsOpenGles && hasTexStorageExt;
111}
112
113bool directGlImportAllowed(bool hasModifiersExt, guint64 drmModifier) noexcept
114{
115 Q_UNUSED(hasModifiersExt);
116 return drmModifier == 0;
117}
118
119struct DmaImageKey
120{
121 int fd = -1;
122 guint64 modifier = 0;
123 int width = 0;
124 int height = 0;
125 int fourcc = 0;
126 int planeCount = 0;
127 std::array<gsize, kMaxPlanes> offsets{};
128 std::array<int, kMaxPlanes> strides{};
129
130 bool operator==(const DmaImageKey& o) const noexcept
131 {
132 return fd == o.fd && modifier == o.modifier && width == o.width && height == o.height && fourcc == o.fourcc &&
133 planeCount == o.planeCount && offsets == o.offsets && strides == o.strides;
134 }
135};
136
137template <typename T>
138void hashCombine(std::size_t& h, const T& value) noexcept
139{
140 h ^= std::hash<T>{}(value) + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
141}
142
143struct DmaImageKeyHash
144{
145 std::size_t operator()(const DmaImageKey& k) const noexcept
146 {
147 std::size_t h = std::hash<int>{}(k.fd);
148 hashCombine(h, k.modifier);
149 hashCombine(h, k.width);
150 hashCombine(h, k.height);
151 hashCombine(h, k.fourcc);
152 hashCombine(h, k.planeCount);
153 for (int p = 0; p < k.planeCount; ++p) {
154 hashCombine(h, k.offsets[p]);
155 hashCombine(h, k.strides[p]);
156 }
157 return h;
158 }
159};
160
161struct DmaCachedImage
162{
163 EGLDisplay display = EGL_NO_DISPLAY;
164 EGLImage image = EGL_NO_IMAGE_KHR;
165 GstMemory* mem = nullptr; // weak-ref source; never dereferenced after notify
166};
167
168constexpr int kImageCacheCapacity = 8;
169
170QMutex s_dmaCacheMutex;
171// EGLImages orphaned by an off-thread weak-notify; destroyed on the render thread (drainOrphanedImagesLocked).
172std::vector<std::pair<EGLDisplay, EGLImage>> s_orphanedImages;
173
174void onSourceMemoryFinalized(gpointer userData, GstMiniObject* finalizedMem);
175
176// Render-thread; caller holds s_dmaCacheMutex with Qt's GL context current.
177void drainOrphanedImagesLocked()
178{
179 for (const auto& [display, image] : s_orphanedImages) {
180 if (image != EGL_NO_IMAGE_KHR && display != EGL_NO_DISPLAY) {
181 eglDestroyImage(display, image);
182 }
183 }
184 s_orphanedImages.clear();
185}
186
187// Eviction/erase/clear hook. Render thread (GL context current): destroy the EGLImage and drop the GstMemory weak-ref.
188// Off-thread weak-notify (no GL context): defer the image to s_orphanedImages and leave the weak-ref alone — the notify
189// itself consumes it.
190void destroyCachedDmaImage(const DmaImageKey&, DmaCachedImage& entry)
191{
192 const bool onRenderThread = QOpenGLContext::currentContext() != nullptr;
193 if (entry.image != EGL_NO_IMAGE_KHR && entry.display != EGL_NO_DISPLAY) {
194 if (onRenderThread) {
195 eglDestroyImage(entry.display, entry.image);
196 } else {
197 s_orphanedImages.emplace_back(entry.display, entry.image);
198 }
199 }
200 if (entry.mem && onRenderThread) {
201 gst_mini_object_weak_unref(GST_MINI_OBJECT(entry.mem), onSourceMemoryFinalized, nullptr);
202 }
203}
204
206 destroyCachedDmaImage};
207
208// Weak-notify on the pool/streaming thread: no GL context, so the stale EGLImage is queued for render-thread
209// destruction (drainOrphanedImagesLocked) rather than destroyed here.
210void onSourceMemoryFinalized(gpointer userData, GstMiniObject* finalizedMem)
211{
212 const QMutexLocker lock(&s_dmaCacheMutex);
213 s_dmaImageCache.eraseIf([&](const DmaImageKey&, const DmaCachedImage& e) {
214 return e.mem == reinterpret_cast<GstMemory*>(finalizedMem);
215 });
216 Q_UNUSED(userData);
217}
218
219// Render-thread; caller holds s_dmaCacheMutex with Qt's GL context current.
220void clearDmaImageCacheLocked(EGLDisplay eglDpy)
221{
222 s_dmaImageCache.clear();
223 drainOrphanedImagesLocked();
224 Q_UNUSED(eglDpy);
225}
226
227EGLImage cachedDmaImageLocked(const DmaImageKey& key, EGLDisplay eglDpy)
228{
229 if (DmaCachedImage* entry = s_dmaImageCache.find(key);
230 entry && entry->display == eglDpy && entry->image != EGL_NO_IMAGE_KHR) {
231 return entry->image;
232 }
233 return EGL_NO_IMAGE_KHR;
234}
235
236void insertDmaImageLocked(const DmaImageKey& key, EGLDisplay eglDpy, EGLImage image, GstMemory* mem)
237{
238 if (mem) {
239 gst_mini_object_weak_ref(GST_MINI_OBJECT(mem), onSourceMemoryFinalized, nullptr);
240 }
241 s_dmaImageCache.insert(key, DmaCachedImage{eglDpy, image, mem});
242}
243
244std::atomic<bool> s_dmaCacheResetPending{false};
245
246bool singleFdImportEnabled() noexcept
247{
249}
250
251// Per-plane EGL attrib keys — spec defines distinct enums per plane index.
252static constexpr EGLint kPlFd[4] = {
253 EGL_DMA_BUF_PLANE0_FD_EXT,
254 EGL_DMA_BUF_PLANE1_FD_EXT,
255 EGL_DMA_BUF_PLANE2_FD_EXT,
256 EGL_DMA_BUF_PLANE3_FD_EXT,
257};
258static constexpr EGLint kPlOffset[4] = {
259 EGL_DMA_BUF_PLANE0_OFFSET_EXT,
260 EGL_DMA_BUF_PLANE1_OFFSET_EXT,
261 EGL_DMA_BUF_PLANE2_OFFSET_EXT,
262 EGL_DMA_BUF_PLANE3_OFFSET_EXT,
263};
264static constexpr EGLint kPlPitch[4] = {
265 EGL_DMA_BUF_PLANE0_PITCH_EXT,
266 EGL_DMA_BUF_PLANE1_PITCH_EXT,
267 EGL_DMA_BUF_PLANE2_PITCH_EXT,
268 EGL_DMA_BUF_PLANE3_PITCH_EXT,
269};
270static constexpr EGLint kPlModLo[4] = {
271 EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
272 EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
273 EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,
274 EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT,
275};
276static constexpr EGLint kPlModHi[4] = {
277 EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
278 EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
279 EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,
280 EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT,
281};
282
283class FrameTextures final : public GstGlFrameTextures
284{
285public:
286 FrameTextures(QRhi* rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat,
287 std::array<GLuint, kMaxPlanes> names, int count)
288 : GstGlFrameTextures(rhi, size, pixelFormat, names, count, FallbackPolicy::Disable)
289 {}
290
291 ~FrameTextures() override
292 {
293 releaseGLTextures(); // safety net if onFrameEndInvoked() was never called
294 }
295
296 void onFrameEndInvoked() override
297 {
298 releaseGLTextures();
300 }
301
302private:
303 void releaseGLTextures()
304 {
305 if (_released || !_rhi || _count == 0)
306 return;
307 // Bind the QRhi GL context so currentContext() returns it (mirrors ~QGstQVideoFrameTextures), avoiding the
308 // backend-conditional NativeHandles cast.
309 _rhi->makeThreadLocalNativeContextCurrent();
310 if (QOpenGLContext* ctx = QOpenGLContext::currentContext()) {
311 ctx->functions()->glDeleteTextures(int(_count), _names.data());
312 _released = true; // only after deletion, so the dtor can retry
313 }
314 }
315
316 bool _released = false;
317};
318
319// RPi eglfs quirk: V3D EGL implicitly converts YUYV/UYVY DMABuf to RGBA, so declare RGBA8888 to pick Qt's RGB shader
320// and avoid a double YUV->RGB (mirrors Qt's eglfs branch).
321QVideoFrameFormat applyEglfsFormatQuirk(const QVideoFrameFormat& format)
322{
323 static const bool isEglfsQPA = QGuiApplication::platformName() == QLatin1String("eglfs");
324 if (!isEglfsQPA)
325 return format;
326 const auto fmt = format.pixelFormat();
327 if (fmt != QVideoFrameFormat::Format_UYVY && fmt != QVideoFrameFormat::Format_YUYV) {
328 return format;
329 }
330 QVideoFrameFormat spoofed(format.frameSize(), QVideoFrameFormat::Format_RGBA8888);
331 spoofed.setStreamFrameRate(format.streamFrameRate());
332 spoofed.setColorRange(format.colorRange());
333 spoofed.setColorSpace(format.colorSpace());
334 spoofed.setColorTransfer(format.colorTransfer());
335 spoofed.setViewport(format.viewport());
336 return spoofed;
337}
338
339std::atomic<bool> s_loggedExplicitFence{false};
340
341// Best-effort GPU-side wait on the producer's fence: export a sync_file fd from the dma-buf, import it as an
342// EGL_SYNC_NATIVE_FENCE_ANDROID sync and eglWaitSyncKHR (server wait). Returns true only on a confirmed GPU-side wait
343// that makes the mmap CPU barrier redundant; any failure leaves the caller's existing barrier path untouched.
344bool tryExplicitFenceWait(EGLDisplay eglDpy, int dmaFd)
345{
346#if defined(QGC_HAS_DMABUF_EXPORT_SYNC)
347 if (eglDpy == EGL_NO_DISPLAY || dmaFd < 0) {
348 return false;
349 }
350 if (!GstEglHelpers::displaySupportsExtension(eglDpy, kNativeFenceExt)) {
351 return false;
352 }
353 static const auto createSyncKHR = reinterpret_cast<PFNEGLCREATESYNCKHRPROC>(eglGetProcAddress("eglCreateSyncKHR"));
354 static const auto waitSyncKHR = reinterpret_cast<PFNEGLWAITSYNCKHRPROC>(eglGetProcAddress("eglWaitSyncKHR"));
355 static const auto destroySyncKHR =
356 reinterpret_cast<PFNEGLDESTROYSYNCKHRPROC>(eglGetProcAddress("eglDestroySyncKHR"));
357 if (!createSyncKHR || !waitSyncKHR || !destroySyncKHR) {
358 return false;
359 }
360
361 dma_buf_export_sync_file req = {};
362 req.flags = DMA_BUF_SYNC_READ; // wait only on writers (the decoder), not on prior readers
363 req.fd = -1;
364 if (ioctl(dmaFd, DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &req) != 0 || req.fd < 0) {
365 return false;
366 }
367
368 // EGL takes ownership of req.fd on a successful create (closes it via eglDestroySync); close it ourselves only if
369 // create fails.
370 const EGLint attribs[] = {EGL_SYNC_NATIVE_FENCE_FD_ANDROID, req.fd, EGL_NONE};
371 EGLSyncKHR sync = createSyncKHR(eglDpy, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
372 if (sync == EGL_NO_SYNC_KHR) {
373 ::close(req.fd);
374 return false;
375 }
376 const EGLint waited = waitSyncKHR(eglDpy, sync, 0);
377 destroySyncKHR(eglDpy, sync);
378 if (waited != EGL_TRUE) {
379 return false;
380 }
381 QGC_HW_WARN_ONCE(GstDmaBufLog, s_loggedExplicitFence,
382 "DMABuf: using producer dma-buf fence for GPU-side sync (mmap barrier skipped)");
385 return true;
386#else
387 Q_UNUSED(eglDpy);
388 Q_UNUSED(dmaFd);
389 return false;
390#endif
391}
392
393} // namespace
394
395GstDmaBufVideoBuffer::GstDmaBufVideoBuffer(GstSample* sample, const GstVideoInfo& videoInfo,
396 const QVideoFrameFormat& format, EGLDisplay eglDisplay)
397 : GstHwVideoBuffer(QVideoFrame::RhiTextureHandle, sample, videoInfo, applyEglfsFormatQuirk(format)),
398 _eglDisplay(eglDisplay)
399{
400#if GST_CHECK_VERSION(1, 24, 0)
401 if (GstCaps* caps = gst_sample_get_caps(sample); caps && gst_video_is_dma_drm_caps(caps)) {
402 GstVideoInfoDmaDrm drmInfo = {};
403 gst_video_info_dma_drm_init(&drmInfo);
404 // DRM_FORMAT_MOD_INVALID would yield EGL_BAD_PARAMETER on strict drivers (AMD RADV); leave _drmModifier at 0
405 // (LINEAR).
406 if (gst_video_info_dma_drm_from_caps(&drmInfo, caps) && drmInfo.drm_modifier != DRM_FORMAT_MOD_INVALID) {
407 _drmModifier = drmInfo.drm_modifier;
408 }
409 }
410#endif
411}
412
413void GstDmaBufVideoBuffer::resetCachedState() noexcept
414{
415 s_cachedDisplay.store(EGL_NO_DISPLAY, std::memory_order_release);
416 s_permanentlyDisabled.store(false, std::memory_order_release);
417 s_loggedSingleFdDisabled.store(false, std::memory_order_release);
418#if defined(QGC_HAS_GST_VULKAN_GPU_PATH)
419 GstDmaBufVulkan::resetLoggedState();
420#endif
421 s_loggedBadBackend.store(false, std::memory_order_release);
422 s_loggedFenceTimeout.store(false, std::memory_order_release);
423 s_bindValidated.store(false, std::memory_order_release);
424 s_loggedBindFailed.store(false, std::memory_order_release);
425 if (QOpenGLContext::currentContext()) {
426 // GL context current (render-thread reset, e.g. sceneGraphInvalidated): drain now so cached/orphaned EGLImages
427 // can't leak when no DMABuf frame is ever mapped again.
428 const QMutexLocker lock(&s_dmaCacheMutex);
429 clearDmaImageCacheLocked(EGL_NO_DISPLAY);
430 s_dmaCacheResetPending.store(false, std::memory_order_release);
431 } else {
432 // Defer EGLImage teardown to the next render-thread mapTextures(); eglDestroyImage off the GL thread is
433 // unsafe.
434 s_dmaCacheResetPending.store(true, std::memory_order_release);
435 }
436}
437
438namespace {
439struct DmaBufCacheResetRegistrar
440{
441 DmaBufCacheResetRegistrar()
442 {
443 GstContextBridgeRegistry::registerCacheReset(&GstDmaBufVideoBuffer::resetCachedState);
444 }
445};
446
447const DmaBufCacheResetRegistrar s_dmaBufCacheResetRegistrar;
448} // namespace
449
450#ifdef QGC_GST_BUILD_TESTING
451bool GstDmaBufVideoBuffer::singleFdImportEnabledForTest() noexcept
452{
453 // Read live: production singleFdImportEnabled() reads the parse-once env cache, which can't observe
454 // per-case env mutations under test. Mirrors HwBuffers truthy() with default=on.
455 const QByteArray v = qgetenv("QGC_GST_DMABUF_SINGLE_EGLIMAGE").trimmed().toLower();
456 if (v.isEmpty()) {
457 return true;
458 }
459 return v != "0" && v != "false" && v != "off" && v != "no";
460}
461
462bool GstDmaBufVideoBuffer::texStorageImportAllowedForTest(bool contextIsOpenGles, bool hasTexStorageExt,
463 guint64 drmModifier) noexcept
464{
465 return texStorageImportAllowed(contextIsOpenGles, hasTexStorageExt, drmModifier);
466}
467
468bool GstDmaBufVideoBuffer::directGlImportAllowedForTest(bool hasModifiersExt, guint64 drmModifier) noexcept
469{
470 return directGlImportAllowed(hasModifiersExt, drmModifier);
471}
472#endif
473
474bool GstDmaBufVideoBuffer::validatePlaneHandles() const
475{
476 // EGL_BAD_MATCH latch: fail validation so the factory demotes to CPU instead of building doomed DmaBuf frames.
477 if (s_permanentlyDisabled.load(std::memory_order_relaxed)) {
478 return false;
479 }
480 const bool planesOk = validatePlanes([](GstMemory* mem) {
481 if (!mem || !gst_is_dmabuf_memory(mem))
482 return false;
483 return gst_dmabuf_memory_get_fd(mem) >= 0;
484 });
485 if (!planesOk)
486 return false;
487 // Reject tiled (non-LINEAR) DMABuf without EGL_EXT_image_dma_buf_import_modifiers — mapTextures() needs the
488 // modifier attribs to import tiled layouts.
489#if GST_CHECK_VERSION(1, 24, 0)
490 // Without the modifiers ext, mmap of tiled strides produces garbage and faults libgstvideo.
491 if (_drmModifier != 0 &&
492 (_eglDisplay == EGL_NO_DISPLAY || !GstEglHelpers::displaySupportsExtension(_eglDisplay, kModifiersExt))) {
493 return false;
494 }
495#endif
496 return true;
497}
498
499QVideoFrameTexturesUPtr GstDmaBufVideoBuffer::mapTextures(QRhi& rhi, QVideoFrameTexturesUPtr& /*old*/)
500{
502 // Qt's contract: mapTextures runs on the QRhi (render) thread. Bail rather than crash if ever called off-thread.
503 if (!rhi.thread()->isCurrentThread()) {
504 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
505 }
506
507 if (!_sample) {
508 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
509 }
510
511 // Bail once a prior frame hit EGL_BAD_MATCH (broken EGL/VA-API, QTBUG-112312); retrying stalls the pipeline.
512 if (s_permanentlyDisabled.load(std::memory_order_relaxed)) {
513 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
514 }
515
516 // Modifier cached from caps in the ctor (0 = LINEAR / non-DRM / normalized INVALID).
517 const guint64 drmModifier = _drmModifier;
518
519 // Sync barrier for Intel iHD legacy LINEAR (no implicit fence). Prefer an EGL fence over a full-frame mmap;
520 // the fence path needs the GL context current, so it runs below. QGC_GST_DMABUF_NO_MMAP_FENCE=1 skips both where
521 // PRIME 2 fence FDs make the barrier redundant.
522 const bool skipLinearFence = HwBuffers::hwBufferEnvConfig().dmaBufNoMmapFence;
523 const bool needLinearFence = (drmModifier == 0 /* DRM_FORMAT_MOD_LINEAR / unknown */ && !skipLinearFence);
524
525#if defined(QGC_HAS_GST_VULKAN_GPU_PATH)
526 if (rhi.backend() == QRhi::Vulkan) {
527 return importVulkan(rhi);
528 }
529#endif
530 if (rhi.backend() != QRhi::OpenGLES2) {
531 QGC_HW_WARN_ONCE(GstDmaBufLog, s_loggedBadBackend,
532 "QRhi backend is not OpenGLES2/Vulkan; zero-copy DMABuf path unsupported");
533 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
534 }
535
536 // Bind Qt's GL context before any EGL/GL call, else the calls no-op into a foreign/null context (import succeeds
537 // but samples green).
538 rhi.makeThreadLocalNativeContextCurrent();
539
540 // Mirror Qt 6.10 qgstvideobuffer.cpp:349 — surface latent GL/EGL state from prior frames.
541 // Qt 6.12 moved its DMABuf gate from eglfs-only to qGstEglCanMapDmaBuf() and added modifier checks; keep this
542 // richer QGC path in sync when raising the Qt-private-source baseline.
543 if (Q_UNLIKELY(GstDmaBufLog().isDebugEnabled())) {
544 const GLenum glErr = glGetError();
545 const EGLint eglErr = eglGetError();
546 if (glErr != GL_NO_ERROR || eglErr != EGL_SUCCESS) {
547 qCDebug(GstDmaBufLog) << "mapTextures entry, latent glError=" << Qt::hex << glErr << "eglError=" << eglErr;
548 }
549 }
550
551 const auto* nativeHandles = static_cast<const QRhiGles2NativeHandles*>(rhi.nativeHandles());
552 if (!nativeHandles || !nativeHandles->context) {
553 qCWarning(GstDmaBufLog) << "QRhi exposes no GL context";
554 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
555 }
556
557 // Prefer QEGLContext::display(): EGLImages from one display can't be sampled by a context on another, and
558 // eglGetCurrentDisplay/eglGetDisplay disagree under xcb_egl.
559 EGLDisplay eglDpy = s_cachedDisplay.load(std::memory_order_acquire);
560 if (eglDpy == EGL_NO_DISPLAY) {
561 eglDpy = GstEglHelpers::resolveEglDisplay(nativeHandles->context);
562 if (eglDpy == EGL_NO_DISPLAY)
563 eglDpy = _eglDisplay;
564 if (eglDpy == EGL_NO_DISPLAY) {
565 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
566 }
567 // First writer wins; racing renders that resolve the same display make this idempotent.
568 EGLDisplay expected = EGL_NO_DISPLAY;
569 s_cachedDisplay.compare_exchange_strong(expected, eglDpy, std::memory_order_release);
570 }
571
572 // Render thread, GL context current: reap EGLImages orphaned by off-thread weak-notifies, then any pending reset.
573 if (imageCacheEnabled()) {
574 const bool resetPending = s_dmaCacheResetPending.exchange(false, std::memory_order_acq_rel);
575 const QMutexLocker lock(&s_dmaCacheMutex);
576 if (!s_orphanedImages.empty()) {
577 drainOrphanedImagesLocked();
578 }
579 if (resetPending) {
580 clearDmaImageCacheLocked(eglDpy);
581 }
582 }
583
584 // LINEAR sync barrier: prefer EGL_KHR_fence_sync (insert+client-wait, a GPU-completion wait) over mmapping the
585 // whole frame. mmap remains the fallback when the fence ext is absent (a CPU-side read barrier).
586 if (needLinearFence) {
587 // Prefer importing the producer's dma-buf fence for a pure GPU-side wait; only if that fails do we fall
588 // back to the EGL-fence-then-mmap barrier below (never removed — just skipped on a confirmed GPU wait).
589 int producerFd = -1;
590 if (GstBuffer* fenceBuf = gst_sample_get_buffer(_sample)) {
591 if (GstMemory* m0 = gst_buffer_peek_memory(fenceBuf, 0); m0 && gst_is_dmabuf_memory(m0)) {
592 producerFd = gst_dmabuf_memory_get_fd(m0);
593 }
594 }
595 const bool producerFenced = tryExplicitFenceWait(eglDpy, producerFd);
596 if (!producerFenced) {
597 static std::pair<EGLDisplay, bool> fenceExtProbe{EGL_NO_DISPLAY, false};
598 if (fenceExtProbe.first != eglDpy) {
599 fenceExtProbe = {eglDpy, GstEglHelpers::displaySupportsExtension(eglDpy, kFenceSyncExt)};
600 }
601 const bool hasFenceExt = fenceExtProbe.second;
602 static const auto createSyncKHR =
603 reinterpret_cast<PFNEGLCREATESYNCKHRPROC>(eglGetProcAddress("eglCreateSyncKHR"));
604 static const auto clientWaitSyncKHR =
605 reinterpret_cast<PFNEGLCLIENTWAITSYNCKHRPROC>(eglGetProcAddress("eglClientWaitSyncKHR"));
606 static const auto destroySyncKHR =
607 reinterpret_cast<PFNEGLDESTROYSYNCKHRPROC>(eglGetProcAddress("eglDestroySyncKHR"));
608 bool fenced = false;
609 if (hasFenceExt && createSyncKHR && clientWaitSyncKHR && destroySyncKHR) {
610 if (EGLSyncKHR sync = createSyncKHR(eglDpy, EGL_SYNC_FENCE_KHR, nullptr); sync != EGL_NO_SYNC_KHR) {
611 // Bounded wait: this runs on Qt's render thread, so a GPU hang must not block forever — time out
612 // and fall through to the mmap barrier instead.
613 const EGLint waitResult = clientWaitSyncKHR(eglDpy, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
614 static_cast<EGLTimeKHR>(100000000));
615 destroySyncKHR(eglDpy, sync);
617 if (waitResult == EGL_CONDITION_SATISFIED_KHR) {
618 fenced = true;
619 } else {
624 GstDmaBufLog, s_loggedFenceTimeout,
625 "EGL fence wait did not satisfy within 100 ms (GPU stall?) — using mmap barrier");
626 }
627 }
628 }
629 if (!fenced) {
630 // Fallback: mmap the frame purely as a CPU-side completion barrier (defeats zero-copy for one map
631 // cycle).
632 if (GstBuffer* syncBuf = gst_sample_get_buffer(_sample)) {
633 GstVideoFrame syncFrame;
634 if (gst_video_frame_map(&syncFrame, &_videoInfo, syncBuf, GST_MAP_READ)) {
635 gst_video_frame_unmap(&syncFrame);
638 }
639 }
640 }
641 }
642 }
643
644 GstBuffer* buffer = gst_sample_get_buffer(_sample);
645 if (!buffer || !gst_is_dmabuf_memory(gst_buffer_peek_memory(buffer, 0))) {
646 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
647 }
648 const bool hasModifiersExt = GstEglHelpers::displaySupportsExtension(eglDpy, kModifiersExt);
649 if (!directGlImportAllowed(hasModifiersExt, drmModifier)) {
650 // Reaching here means caps/filtering changed between the validate and render threads or an upstream element
651 // pushed a modifier layout we do not advertise. Fall back before either GL import path can bind the EGLImage.
652 static std::atomic<bool> warnedModifierRejected{false};
653 if (!warnedModifierRejected.exchange(true, std::memory_order_relaxed)) {
654 qCWarning(GstDmaBufLog)
655 << "DMABuf modifier" << Qt::hex << drmModifier
656 << "rejected for direct GL import"
657 << (hasModifiersExt ? "(EGL modifier import present but disabled for safety)"
658 : "(EGL_EXT_image_dma_buf_import_modifiers absent)")
659 << "— falling back to CPU";
660 }
663 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
664 }
665
666 static const auto eglImageTargetTexture2D =
667 reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
668 if (!eglImageTargetTexture2D) {
669 qCWarning(GstDmaBufLog) << "glEGLImageTargetTexture2DOES unavailable";
670 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
671 }
672
673 // Desktop GL with GL_EXT_EGL_image_storage: bind LINEAR DMABuf via immutable storage to skip the per-frame
674 // texture-storage realloc the mutable OES path forces (OBS measured ~100x on discrete Intel). Tiled DMABuf stays
675 // on glEGLImageTargetTexture2DOES; Mesa/Gallium can segfault when tiled VA images take the tex-storage path.
676 // GLES also keeps the OES path: the storage ext isn't reliably exposed and the OES bind is the contract there.
677 PFNGLEGLIMAGETARGETTEXSTORAGEEXTPROC eglImageTargetTexStorage = nullptr;
678 if (const QOpenGLContext* ctx = QOpenGLContext::currentContext()) {
679 const bool useTexStorage =
680 texStorageImportAllowed(ctx->isOpenGLES(),
681 ctx->hasExtension(QByteArrayLiteral("GL_EXT_EGL_image_storage")), drmModifier);
682 eglImageTargetTexStorage = useTexStorage
683 ? reinterpret_cast<PFNGLEGLIMAGETARGETTEXSTORAGEEXTPROC>(
684 eglGetProcAddress("glEGLImageTargetTexStorageEXT"))
685 : nullptr;
686 }
687 const auto bindEglImage = [&](GLenum target, EGLImage img) {
688 if (eglImageTargetTexStorage)
689 eglImageTargetTexStorage(target, img, nullptr);
690 else
691 eglImageTargetTexture2D(target, img);
692 };
693 // Validate binds with glGetError only on the first import per epoch (reset via resetCachedState); steady-state
694 // frames skip the pipeline flush.
695 const bool validateBind = !s_bindValidated.load(std::memory_order_relaxed);
696
697 // gst_video_frame_map(GST_MAP_READ) on DMABuf mmaps and defeats zero-copy; read offsets from GstVideoMeta.
698 // Qt 6.12's upstream GStreamer path also prefers GstVideoMeta/video-info offsets; preserve this no-mmap behavior when
699 // reconciling against newer Qt sources.
700 GstVideoMeta* vmeta = gst_buffer_get_video_meta(buffer);
701 const int planeCount = std::clamp(int(GST_VIDEO_INFO_N_PLANES(&_videoInfo)), 1, kMaxPlanes);
702 const int memCount = int(gst_buffer_n_memory(buffer));
703 // memCount==1: either multi-plane shared fd (NV12) or packed single-plane (YUY2/UYVY/…).
704 const int cachedSingleFourcc = (memCount == 1) ? GstHw::drmFourccForSingleFd(_videoInfo) : -1;
705 bool singleFdPacking = (cachedSingleFourcc != -1);
706 if (singleFdPacking && !singleFdImportEnabled()) {
707 if (!s_loggedSingleFdDisabled.exchange(true, std::memory_order_relaxed)) {
708 qCInfo(GstDmaBufLog) << "Single-EGLImage DMABuf import disabled by"
709 << "QGC_GST_DMABUF_SINGLE_EGLIMAGE; using per-plane import"
710 << "when the buffer layout permits, otherwise CPU fallback";
711 }
712 singleFdPacking = false;
713 }
714 if (memCount != planeCount && !singleFdPacking) {
715 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
716 }
717
718 // Pre-flight RHI format/size support before any EGLImage import / createFrom() so an unsupported frame demotes to
719 // CPU on a query instead of after the GL/EGL work.
720 if (!GstHwImportPreflight::preflightOrRecord(&rhi, HwVideoBufferPath::DmaBuf, _format.pixelFormat(),
721 _format.frameSize())) {
722 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
723 }
724
725 // No texture reuse: caching QRhiTextures across frames caused periodic green frames on real RTSP H.264, and the
726 // ~us/frame win isn't worth it.
727 std::array<GLuint, kMaxPlanes> names{};
728 QOpenGLFunctions functions(nativeHandles->context);
729 functions.glGenTextures(planeCount, names.data());
730
731 // Modifier attribs only with the ext present; otherwise impl assumes LINEAR (matches our rejection above).
732 const EGLAttrib modLo = static_cast<EGLAttrib>(drmModifier & 0xFFFFFFFFu);
733 const EGLAttrib modHi = static_cast<EGLAttrib>((drmModifier >> 32) & 0xFFFFFFFFu);
734
735 bool ok = true;
736
737 // Single-fd fast path: one EGLImage for all planes (Mesa VA-API NV12 ships memCount==1; iHD rejects split-plane
738 // R8/GR88 imports).
739 if (singleFdPacking) {
740 if (planeCount > kMaxPlanes) {
741 qCWarning(GstDmaBufLog) << "single-fd format has" << planeCount << "planes (>" << kMaxPlanes
742 << "); falling back to per-plane";
743 goto per_plane_path;
744 }
745 const int singleFourcc = cachedSingleFourcc;
746 if (singleFourcc == -1) {
747 goto per_plane_path;
748 }
749 const int fd0 = gst_dmabuf_memory_get_fd(gst_buffer_peek_memory(buffer, 0));
750 // attribPush writes 2 EGLAttribs/call: 3 header keys + up to 5/plane*kMaxPlanes, *2, +1 EGL_NONE terminator.
751 EGLAttrib attribs[2 * (3 + kMaxPlanes * 5) + 1];
752 int n = 0;
753 bool attribOverflow = false;
754 auto attribPush = [&](EGLAttrib k, EGLAttrib v) {
755 if (n >= int(std::size(attribs)) - 1) {
756 attribOverflow = true;
757 return;
758 }
759 attribs[n++] = k;
760 attribs[n++] = v;
761 };
762 attribPush(EGL_WIDTH, GST_VIDEO_INFO_WIDTH(&_videoInfo));
763 attribPush(EGL_HEIGHT, GST_VIDEO_INFO_HEIGHT(&_videoInfo));
764 attribPush(EGL_LINUX_DRM_FOURCC_EXT, singleFourcc);
765 std::array<gsize, kMaxPlanes> planeOffsets{};
766 std::array<int, kMaxPlanes> planeStrides{};
767 for (int p = 0; p < planeCount; ++p) {
768 const auto off = vmeta ? vmeta->offset[p] : GST_VIDEO_INFO_PLANE_OFFSET(&_videoInfo, p);
769 const auto pitch = vmeta ? vmeta->stride[p] : GST_VIDEO_INFO_PLANE_STRIDE(&_videoInfo, p);
770 if (pitch <= 0 ||
771 static_cast<quint64>(off) > static_cast<quint64>((std::numeric_limits<EGLAttrib>::max)())) {
772 qCWarning(GstDmaBufLog) << "implausible plane stride/offset (stride=" << pitch << "offset=" << off
773 << "); CPU fallback";
774 ok = false;
775 goto textures_built;
776 }
777 planeOffsets[p] = off;
778 planeStrides[p] = pitch;
779 attribPush(kPlFd[p], fd0);
780 attribPush(kPlOffset[p], static_cast<EGLAttrib>(off));
781 attribPush(kPlPitch[p], static_cast<EGLAttrib>(pitch));
782 if (hasModifiersExt) { // interleaved per spec: modifier attribs follow FD/OFFSET/PITCH for same plane
783 attribPush(kPlModLo[p], modLo);
784 attribPush(kPlModHi[p], modHi);
785 }
786 }
787 if (attribOverflow) {
788 qCWarning(GstDmaBufLog) << "single-fd EGL attrib list overflow; falling back to per-plane";
789 goto per_plane_path;
790 }
791 attribs[n++] = EGL_NONE;
792 const bool cacheOn = imageCacheEnabled();
793 const DmaImageKey key{fd0,
794 drmModifier,
795 GST_VIDEO_INFO_WIDTH(&_videoInfo),
796 GST_VIDEO_INFO_HEIGHT(&_videoInfo),
797 singleFourcc,
798 planeCount,
799 planeOffsets,
800 planeStrides};
801 EGLImage singleImage = EGL_NO_IMAGE_KHR;
802 bool singleImageCached = false;
803 if (cacheOn) {
804 const QMutexLocker lock(&s_dmaCacheMutex);
805 singleImage = cachedDmaImageLocked(key, eglDpy);
806 singleImageCached = (singleImage != EGL_NO_IMAGE_KHR);
807 }
808 if (singleImage == EGL_NO_IMAGE_KHR) {
809 singleImage = eglCreateImage(eglDpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs);
810 if (cacheOn) {
812 }
813 } else if (cacheOn) {
815 }
816 if (singleImage != EGL_NO_IMAGE_KHR) {
817 bool bindFailed = false;
818 for (int p = 0; p < planeCount; ++p) {
819 functions.glBindTexture(GL_TEXTURE_2D, names[p]);
820 bindEglImage(GL_TEXTURE_2D, singleImage);
821 // glGetError forces a pipeline flush — only first import per epoch or when category debug is on.
822 if (Q_UNLIKELY(validateBind || GstDmaBufLog().isDebugEnabled())) {
823 if (const GLenum glErr = functions.glGetError(); glErr != GL_NO_ERROR) {
824 if (validateBind) {
825 QGC_HW_WARN_ONCE(GstDmaBufLog, s_loggedBindFailed,
826 "single-fd EGLImage bind failed plane"
827 << p << "glError=" << Qt::hex << glErr << "eglError=" << eglGetError()
828 << "— CPU fallback");
829 bindFailed = true;
830 break;
831 }
832 qCDebug(GstDmaBufLog) << "single-fd eglImageTargetTexture2D plane" << p << "glError=" << Qt::hex
833 << glErr << "eglError=" << eglGetError();
834 }
835 }
836 }
837 if (bindFailed) {
838 // Evict/destroy the suspect image so it can't serve later frames.
839 if (cacheOn && singleImageCached) {
840 const QMutexLocker lock(&s_dmaCacheMutex);
841 s_dmaImageCache.eraseIf([&](const DmaImageKey& k, const DmaCachedImage&) { return k == key; });
842 } else {
843 eglDestroyImage(eglDpy, singleImage);
844 }
845 ok = false;
846 goto textures_built;
847 }
848 if (cacheOn && !singleImageCached) {
849 const QMutexLocker lock(&s_dmaCacheMutex);
850 insertDmaImageLocked(key, eglDpy, singleImage, gst_buffer_peek_memory(buffer, 0));
851 } else if (!cacheOn) {
852 eglDestroyImage(eglDpy, singleImage);
853 }
854 goto textures_built;
855 }
856#if defined(DRM_FORMAT_Y210) || defined(DRM_FORMAT_Y410)
857 // Y210/Y410 have no per-plane fallback; warn once that frames will be black.
858 {
859 static std::atomic<bool> s_warnedY210{false};
860 static std::atomic<bool> s_warnedY410{false};
861#if defined(DRM_FORMAT_Y210)
862 if (singleFourcc == DRM_FORMAT_Y210 && !s_warnedY210.exchange(true, std::memory_order_relaxed))
863 qCWarning(GstDmaBufLog) << "EGL DMABuf import of Y210 unsupported on this driver; frames will be black";
864#endif
865#if defined(DRM_FORMAT_Y410)
866 if (singleFourcc == DRM_FORMAT_Y410 && !s_warnedY410.exchange(true, std::memory_order_relaxed))
867 qCWarning(GstDmaBufLog) << "EGL DMABuf import of Y410 unsupported on this driver; frames will be black";
868#endif
869 }
870#endif
871 const EGLint singleFdErr = eglGetError();
872 if (drmModifier != 0) {
873 // No per-plane fallback for tiled: GL_TEXTURE_2D import on tiled memory is unsafe (driver
874 // crash/corruption).
875 qCWarning(GstDmaBufLog) << "single-fd tiled eglCreateImage failed (" << Qt::hex << singleFdErr
876 << ") — no per-plane fallback for tiled";
877 if (singleFdErr == EGL_BAD_MATCH) {
878 maybePermanentlyDisable(eglDpy, 0, "single-fd-tiled");
879 }
880 ok = false;
881 goto textures_built;
882 }
883 qCWarning(GstDmaBufLog) << "single-fd eglCreateImage failed (" << Qt::hex << singleFdErr
884 << "); falling back to per-plane";
885 if (singleFdErr == EGL_BAD_MATCH) {
886 // BAD_MATCH on single-fd is permanent (per-plane hits the same issue); mark disabled but still try
887 // per-plane to surface the second log.
888 maybePermanentlyDisable(eglDpy, 0, "single-fd");
889 }
890 }
891
892per_plane_path:
893 for (int i = 0; i < planeCount && ok; ++i) {
894 const int memIdx = singleFdPacking ? 0 : i;
895 const int fd = gst_dmabuf_memory_get_fd(gst_buffer_peek_memory(buffer, memIdx));
896 // Dedicated-fd planes already start at the plane data; the offset only applies to single-fd packing.
897 const auto offset =
898 singleFdPacking ? (vmeta ? vmeta->offset[i] : GST_VIDEO_INFO_PLANE_OFFSET(&_videoInfo, i)) : 0;
899 const auto stride = vmeta ? vmeta->stride[i] : GST_VIDEO_INFO_PLANE_STRIDE(&_videoInfo, i);
900 if (stride <= 0 ||
901 static_cast<quint64>(offset) > static_cast<quint64>((std::numeric_limits<EGLAttrib>::max)())) {
902 qCWarning(GstDmaBufLog) << "implausible plane stride/offset (stride=" << stride << "offset=" << offset
903 << "); CPU fallback";
904 ok = false;
905 break;
906 }
907 const int planeWidth = GST_VIDEO_INFO_COMP_WIDTH(&_videoInfo, i);
908 const int planeHeight = GST_VIDEO_INFO_COMP_HEIGHT(&_videoInfo, i);
909 const int fourcc = GstHw::drmFourccForPlane(_videoInfo, i);
910 if (fourcc == -1) {
911 qCWarning(GstDmaBufLog) << "no DRM fourcc for format"
912 << gst_video_format_to_string(GST_VIDEO_INFO_FORMAT(&_videoInfo));
913 ok = false;
914 break;
915 }
916
917 // EGL attribute list — sized for the maximum (with modifier ext) and zero-terminated.
918 EGLAttrib attribs[18];
919 int n = 0;
920 bool attribOverflow = false;
921 auto attribPush = [&](EGLAttrib k, EGLAttrib v) {
922 if (n >= int(std::size(attribs)) - 1) {
923 attribOverflow = true;
924 return;
925 }
926 attribs[n++] = k;
927 attribs[n++] = v;
928 };
929 attribPush(EGL_WIDTH, planeWidth);
930 attribPush(EGL_HEIGHT, planeHeight);
931 attribPush(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
932 attribPush(kPlFd[0], fd);
933 attribPush(kPlOffset[0], static_cast<EGLAttrib>(offset));
934 attribPush(kPlPitch[0], static_cast<EGLAttrib>(stride));
935 if (hasModifiersExt) {
936 // Modifier keys index EGLImage plane slots, not source planes; data sits in slot 0 so the modifier goes to
937 // PLANE0 ([i] would push PLANE1+ attribs that AMD RADV rejects).
938 attribPush(kPlModLo[0], modLo);
939 attribPush(kPlModHi[0], modHi);
940 }
941 if (attribOverflow) {
942 qCWarning(GstDmaBufLog) << "per-plane EGL attrib list overflow; CPU fallback";
943 ok = false;
944 break;
945 }
946 attribs[n++] = EGL_NONE;
947 const bool cacheOn = imageCacheEnabled();
948 // Per-plane key: same fd may back several planes (NV12 single-fd), so include geometry and layout.
949 std::array<gsize, kMaxPlanes> planeOffsets{};
950 std::array<int, kMaxPlanes> planeStrides{};
951 planeOffsets[0] = offset;
952 planeStrides[0] = stride;
953 const DmaImageKey key{fd, drmModifier, planeWidth, planeHeight, fourcc, 1, planeOffsets, planeStrides};
954 EGLImage image = EGL_NO_IMAGE_KHR;
955 bool imageCached = false;
956 if (cacheOn) {
957 const QMutexLocker lock(&s_dmaCacheMutex);
958 image = cachedDmaImageLocked(key, eglDpy);
959 imageCached = (image != EGL_NO_IMAGE_KHR);
960 }
961 if (image == EGL_NO_IMAGE_KHR) {
962 image = eglCreateImage(eglDpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs);
963 if (cacheOn) {
965 }
966 } else if (cacheOn) {
968 }
969 if (image == EGL_NO_IMAGE_KHR) {
970 const EGLint eglErr = eglGetError();
971 qCWarning(GstDmaBufLog) << "eglCreateImage failed plane" << i << "err" << Qt::hex << eglErr;
972 if (eglErr == EGL_BAD_MATCH) {
973 maybePermanentlyDisable(eglDpy, i, "per-plane");
974 }
975 ok = false;
976 break;
977 }
978 functions.glBindTexture(GL_TEXTURE_2D, names[i]);
979 bindEglImage(GL_TEXTURE_2D, image);
980 if (Q_UNLIKELY(validateBind || GstDmaBufLog().isDebugEnabled())) {
981 if (const GLenum glErr = functions.glGetError(); glErr != GL_NO_ERROR) {
982 if (validateBind) {
983 QGC_HW_WARN_ONCE(GstDmaBufLog, s_loggedBindFailed,
984 "per-plane EGLImage bind failed plane"
985 << i << "glError=" << Qt::hex << glErr << "eglError=" << eglGetError()
986 << "— CPU fallback");
987 // Evict/destroy the suspect image so it can't serve later frames.
988 if (cacheOn && imageCached) {
989 const QMutexLocker lock(&s_dmaCacheMutex);
990 s_dmaImageCache.eraseIf([&](const DmaImageKey& k, const DmaCachedImage&) { return k == key; });
991 } else {
992 eglDestroyImage(eglDpy, image);
993 }
994 ok = false;
995 break;
996 }
997 qCDebug(GstDmaBufLog) << "per-plane eglImageTargetTexture2D plane" << i << "glError=" << Qt::hex
998 << glErr << "eglError=" << eglGetError();
999 }
1000 }
1001 if (cacheOn && !imageCached) {
1002 const QMutexLocker lock(&s_dmaCacheMutex);
1003 insertDmaImageLocked(key, eglDpy, image, gst_buffer_peek_memory(buffer, memIdx));
1004 } else if (!cacheOn) {
1005 eglDestroyImage(eglDpy, image);
1006 }
1007 }
1008
1009textures_built:
1010
1011 if (ok && validateBind) {
1012 s_bindValidated.store(true, std::memory_order_relaxed);
1013 }
1014 if (!ok) {
1015 functions.glDeleteTextures(planeCount, names.data());
1016 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
1017 }
1018
1019 auto frameTextures =
1020 std::make_unique<FrameTextures>(&rhi, _format.frameSize(), _format.pixelFormat(), names, planeCount);
1021 // FrameTextures null-resets planes createFrom() rejects; a partial bundle renders black without bumping the failure
1022 // counter, so bail loudly instead.
1023 for (int i = 0; i < planeCount; ++i) {
1024 if (!frameTextures->texture(i)) {
1025 qCWarning(GstDmaBufLog) << "FrameTextures plane" << i << "createFrom failed — dropping frame";
1026 // ~FrameTextures glDeleteTextures via releaseGLTextures() — names are owned now.
1027 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
1028 }
1029 }
1030 frameTextures->setSourceSample(takeSample());
1031 return frameTextures;
1032}
1033
1034#endif // QGC_HAS_GST_DMABUF_GPU_PATH
#define QGC_HW_WARN_ONCE(LOGCAT, FLAG,...)
Logs once via qCWarning(LOGCAT) the first time FLAG flips true; subsequent trips are silent.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Shared base for GL-texture-backed QVideoFrameTextures wrappers (GLMemory and DMABuf-via-EGLImage).
RAII timer: records mapTextures() wall time into the path's EWMA on scope exit.
Common base for GStreamer-backed QHwVideoBuffer subclasses.
void recordSyncWait(HwVideoBufferPath path, bool gpuSide) noexcept
GL fence sync wait; split CPU-blocking vs GPU-side.
void recordFallbackReason(HwVideoBufferPath attemptedPath, HwFallbackReason reason) noexcept
Per-(path,reason) fallback accounting; lets a bug report show why a path demoted to CPU.
void recordImageCacheHit(HwVideoBufferPath path) noexcept
Native image/texture cache hit/miss accounting.
void recordImageCacheMiss(HwVideoBufferPath path) noexcept
void recordExplicitFenceWait(HwVideoBufferPath path) noexcept
DMABuf imported the producer's dma-buf/native fence and did a GPU-side wait (skipped the mmap barrier...
void recordFenceTimeout(HwVideoBufferPath path) noexcept
DMABuf EGL fence wait timed out (GPU stall) and fell through to the mmap barrier.
void recordMmapBarrierHit(HwVideoBufferPath path) noexcept
DMABuf mmap CPU-side completion barrier taken (no usable fence ext).
constexpr int kMaxPlanes
Matches GST_VIDEO_MAX_PLANES (gst-video pins it at 4); single source of truth for every per-platform ...
const HwBufferEnvConfig & hwBufferEnvConfig() noexcept
Lazily parses + logs the toggle config on first call; thread-safe via static-init guarantees.
Definition HwBuffers.cc:144
QByteArray format(const QList< LogEntry > &entries, int fmt)