QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstAHardwareBufferVideoBuffer.cc
Go to the documentation of this file.
2
3#if defined(QGC_HAS_GST_AHARDWAREBUFFER_GPU_PATH)
4
5#include <EGL/eglext.h>
6#include <GLES2/gl2.h>
7#include <GLES2/gl2ext.h>
8#include <QtCore/QLoggingCategory>
9#include <QtCore/QMutex>
10#include <QtCore/QMutexLocker>
11#include <QtCore/QScopeGuard>
12#include <QtCore/QSize>
13#include <QtGui/QOpenGLContext>
14#include <QtGui/QOpenGLFunctions>
15#include <android/hardware_buffer.h>
16#include <array>
17#include <atomic>
18#include <vector>
19#include <gst/android/gstandroid.h>
20#include <gst/gl/gstglsyncmeta.h>
21#include <gst/video/video.h>
22#include <limits>
23#include <private/qvideotexturehelper_p.h>
24#include <rhi/qrhi.h>
25#include <rhi/qrhi_platform.h>
26
28#include "GstEglHelpers.h"
31#include "GstHwPathTelemetry.h"
32#include "GstHwVideoBuffer.h"
33#include "QGCLoggingCategory.h"
34
35QGC_LOGGING_CATEGORY(GstAHWBufLog, "Video.GStreamer.HwBuffers.GstAHWBuf")
36
37namespace {
38
40constexpr int kImageCacheCapacity = 4;
41
42std::atomic<bool> s_loggedBadBackend{false};
43
44constexpr const char* kNativeBufferExt = "EGL_ANDROID_image_native_buffer";
45
46std::atomic<bool> s_loggedNoFenceSync{false};
47
48// LRU cache (AHardwareBuffer*, EGLDisplay) -> (EGLImage, GL texture): MediaCodec recycles the same AHB so the import
49// stays valid, saving ~50-200 us/frame on Mali/Adreno.
50struct AhbCacheEntry
51{
52 QRhi* rhi = nullptr;
53 AHardwareBuffer* ahb = nullptr;
54 EGLDisplay display = EGL_NO_DISPLAY;
55 EGLImageKHR image = EGL_NO_IMAGE_KHR;
56 GLuint textureName = 0;
57 quint64 lastUsedTick = 0;
58 int inUse = 0; // live FrameTextures wrappers referencing this name; never evict while > 0
59};
60
61// Guards s_imageCache, s_deferredImageCache, and s_imageCacheTick.
62QMutex s_imageCacheMutex;
63std::array<AhbCacheEntry, kImageCacheCapacity> s_imageCache{};
64std::vector<AhbCacheEntry> s_deferredImageCache;
65quint64 s_imageCacheTick = 0;
66
67// Set by resetImageCache() from any thread; teardown deferred to the next mapTextures() since glDeleteTextures
68// off-thread leaks and deleting a live-wrapped name is a UAF.
69std::atomic<bool> s_imageCacheResetPending{false};
70
71// Render-thread-only; callers must hold s_imageCacheMutex AND have Qt's GL context current.
72void destroyCacheEntryLocked(AhbCacheEntry& e, PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
73{
74 if (e.image != EGL_NO_IMAGE_KHR && e.display != EGL_NO_DISPLAY && destroyImage) {
75 destroyImage(e.display, e.image);
76 }
77 if (e.textureName != 0 && gl) {
78 gl->glDeleteTextures(1, &e.textureName);
79 }
80 if (e.ahb) {
81 AHardwareBuffer_release(e.ahb);
82 }
83 e = AhbCacheEntry{};
84}
85
86void drainDeferredImageCacheLocked(PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl, QRhi* rhi = nullptr)
87{
88 if (!destroyImage || !gl)
89 return;
90
91 for (auto it = s_deferredImageCache.begin(); it != s_deferredImageCache.end();) {
92 if (it->inUse > 0 || (rhi && it->rhi != rhi)) {
93 ++it;
94 continue;
95 }
96
97 destroyCacheEntryLocked(*it, destroyImage, gl);
98 it = s_deferredImageCache.erase(it);
99 }
100}
101
102GLuint cachedTextureNameLocked(QRhi* rhi, AHardwareBuffer* ahb, EGLDisplay eglDpy)
103{
104 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
105 auto& e = s_imageCache[i];
106 if (e.rhi == rhi && e.ahb == ahb && e.display == eglDpy && e.textureName != 0) {
107 e.lastUsedTick = ++s_imageCacheTick;
108 return e.textureName;
109 }
110 }
111 return 0;
112}
113
114// Insert into cache (evicts LRU when full), taking ownership of image/textureName; caller holds s_imageCacheMutex with
115// GL current. False (no ownership taken) when every entry is pinned in-use.
116bool insertCacheEntryLocked(QRhi* rhi, AHardwareBuffer* ahb, EGLDisplay eglDpy, EGLImageKHR image, GLuint textureName,
117 PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
118{
119 AhbCacheEntry* target = nullptr;
120 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
121 auto& e = s_imageCache[i];
122 if (e.textureName == 0) {
123 target = &e;
124 break;
125 }
126 }
127 if (!target) {
128 // Evict the LRU entry that no live frame still references; in-use names would UAF on render.
129 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
130 auto& e = s_imageCache[i];
131 if (e.inUse == 0 && (!target || e.lastUsedTick < target->lastUsedTick))
132 target = &e;
133 }
134 if (!target) {
135 qCWarning(GstAHWBufLog) << "AHB texture cache full and all entries in use; skipping cache insert";
136 return false;
137 }
138 destroyCacheEntryLocked(*target, destroyImage, gl);
139 }
140 AHardwareBuffer_acquire(ahb);
141 target->rhi = rhi;
142 target->ahb = ahb;
143 target->display = eglDpy;
144 target->image = image;
145 target->textureName = textureName;
146 target->lastUsedTick = ++s_imageCacheTick;
147 return true;
148}
149
150void clearImageCacheLocked(PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
151{
152 drainDeferredImageCacheLocked(destroyImage, gl);
153
154 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
155 auto& entry = s_imageCache[i];
156 if (entry.inUse > 0) {
157 // Live FrameTextures still holds this name; destroy when its pin is released.
158 qCDebug(GstAHWBufLog) << "AHB texture cache reset deferred for in-use texture" << entry.textureName;
159 s_deferredImageCache.push_back(entry);
160 entry = AhbCacheEntry{};
161 continue;
162 }
163 destroyCacheEntryLocked(entry, destroyImage, gl);
164 }
165 drainDeferredImageCacheLocked(destroyImage, gl);
166 s_imageCacheTick = 0;
167}
168
169// Pin a cache entry by texture name so LRU eviction/reset can't free a name a live frame holds; caller holds
170// s_imageCacheMutex. Unmatched names are no-ops.
171void acquireCacheEntryLocked(GLuint name)
172{
173 if (name == 0)
174 return;
175 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
176 auto& e = s_imageCache[i];
177 if (e.textureName == name) {
178 ++e.inUse;
179 return;
180 }
181 }
182 for (auto& e : s_deferredImageCache) {
183 if (e.textureName == name) {
184 ++e.inUse;
185 return;
186 }
187 }
188}
189
190void releaseCacheEntryLocked(GLuint name)
191{
192 if (name == 0)
193 return;
194 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
195 auto& e = s_imageCache[i];
196 if (e.textureName == name && e.inUse > 0) {
197 --e.inUse;
198 return;
199 }
200 }
201 for (auto& e : s_deferredImageCache) {
202 if (e.textureName == name && e.inUse > 0) {
203 --e.inUse;
204 return;
205 }
206 }
207}
208
209class FrameTextures final : public GstHwFrameTexturesBase
210{
211public:
212 // Always single-plane external-OES; the GL texture name is owned by the process-wide AhbCache, not this object
213 // (shared across frames on AHB recycle).
214 FrameTextures(QRhi* rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat, GLuint name)
215 : _rhi(rhi), _size(size), _pixelFormat(pixelFormat), _name(name)
216 {
217 _count = 1;
218 const auto* desc = QVideoTextureHelper::textureDescription(pixelFormat);
219 if (!desc) {
220 qCWarning(GstAHWBufLog) << "no QVideoTextureHelper description for format" << pixelFormat;
221 return;
222 }
223 const QSize planeSize = desc->rhiPlaneSize(size, 0, rhi);
224 // ExternalOES: bind GL_TEXTURE_EXTERNAL_OES + emit SamplerExternalOES, else OES_EGL_image_external samples
225 // black on GLES2.
226 _textures[0].reset(rhi->newTexture(
227 desc->rhiTextureFormat(0, rhi, QVideoTextureHelper::TextureDescription::FallbackPolicy::Disable), planeSize,
228 1, QRhiTexture::ExternalOES));
229 if (_textures[0] && !_textures[0]->createFrom({name, 0})) {
230 qCWarning(GstAHWBufLog) << "QRhiTexture::createFrom failed for AHardwareBuffer plane 0";
231 _textures[0].reset();
232 }
233 QMutexLocker lock(&s_imageCacheMutex);
234 acquireCacheEntryLocked(_name);
235 }
236
237 ~FrameTextures() override
238 {
239 QOpenGLFunctions* gl = nullptr;
240 if (_rhi) {
241 _rhi->makeThreadLocalNativeContextCurrent();
242 if (QOpenGLContext* ctx = QOpenGLContext::currentContext()) {
243 gl = ctx->functions();
244 }
245 }
246 static const auto eglDestroyImageKHR_ =
247 reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
248
249 QMutexLocker lock(&s_imageCacheMutex);
250 releaseCacheEntryLocked(_name);
251 drainDeferredImageCacheLocked(eglDestroyImageKHR_, gl, _rhi);
252 }
253
255
256 // Reuse eligibility: same rhi+size+format and identical GL texture name (AhbCache returns the same name on AHB
257 // recycle).
258 bool matches(QRhi* rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat, GLuint name) const noexcept
259 {
260 return _rhi == rhi && _size == size && _pixelFormat == pixelFormat && _name == name && _name != 0 &&
261 _textures[0];
262 }
263
264private:
265 QRhi* _rhi = nullptr;
266 QSize _size;
267 QVideoFrameFormat::PixelFormat _pixelFormat = QVideoFrameFormat::Format_Invalid;
268 GLuint _name = 0;
269};
270
271// MediaCodec writes the AHB asynchronously; without a producer-side wait the EGLImage import can sample a half-written,
272// recycled buffer (tearing). Only the buffer's GstGLSyncMeta is a real producer fence — it carries the decoder's
273// GPU-completion point. A consumer-side EGL fence (glFlush + eglCreateSync in the GL context) cannot observe
274// MediaCodec's async write, so it would synchronise nothing while masking the hazard; we deliberately do not use one.
275// When no sync meta is present, warn once and proceed (no bogus barrier).
276void waitProducerComplete(GstBuffer* buffer)
277{
278 if (GstGLSyncMeta* syncMeta = buffer ? gst_buffer_get_gl_sync_meta(buffer) : nullptr) {
279 if (GstGLContext* glObj = syncMeta->context) {
280 gst_gl_sync_meta_wait(syncMeta, glObj);
281 return;
282 }
283 }
284
285 QGC_HW_WARN_ONCE(GstAHWBufLog, s_loggedNoFenceSync,
286 "AHardwareBuffer: no GstGLSyncMeta producer fence; frames may tear on recycle (no consumer-side "
287 "barrier can substitute)");
288}
289
290} // namespace
291
292GstAHardwareBufferVideoBuffer::GstAHardwareBufferVideoBuffer(GstSample* sample, const GstVideoInfo& videoInfo,
293 const QVideoFrameFormat& format, EGLDisplay eglDisplay)
294 : GstHwVideoBuffer(QVideoFrame::RhiTextureHandle, sample, videoInfo, format), _eglDisplay(eglDisplay)
295{}
296
297// The AHB is imported directly as an EGLImage (no SurfaceTexture::updateTexImage()), so no per-buffer
298// getTransformMatrix() exists here; external-OES sampling of a directly-imported buffer is top-left origin while GL
299// texcoords are bottom-left, so apply the static Y-flip Qt's SurfaceTexture path also applies (flipV, row-major).
300QMatrix4x4 GstAHardwareBufferVideoBuffer::externalTextureMatrix() const
301{
302 // GStreamer exposes no per-buffer decoder transform at this layer, so we apply the static external-OES Y-flip
303 // (top-left vs GL bottom-left origin). If a device ever needs crop-inset/rotation, source it from GstVideoCropMeta.
304 static const QMatrix4x4 flipV(1.0f, 0.0f, 0.0f, 0.0f,
305 0.0f, -1.0f, 0.0f, 1.0f,
306 0.0f, 0.0f, 1.0f, 0.0f,
307 0.0f, 0.0f, 0.0f, 1.0f);
308 return flipV;
309}
310
311bool GstAHardwareBufferVideoBuffer::validatePlaneHandles() const
312{
313 // Qt 6.10-6.12 has the right ExternalOES rendering path, but upstream GStreamer still does not expose this
314 // AHardwareBuffer memory API. If a future GStreamer/Qt release grows an official API, replace these vendor hooks.
315 return validatePlanes([](GstMemory* mem) {
316 if (!mem || !gst_is_ahardware_buffer_memory(mem))
317 return false;
318 return gst_ahardware_buffer_memory_get_buffer(GST_AHARDWARE_BUFFER_MEMORY_CAST(mem)) != nullptr;
319 });
320}
321
322// Teardown deferred to the render thread at a frame boundary — glDeleteTextures off-thread is unsafe.
323void GstAHardwareBufferVideoBuffer::resetImageCache() noexcept
324{
325 s_imageCacheResetPending.store(true, std::memory_order_release);
326}
327
328namespace {
329struct AhbCacheResetRegistrar
330{
331 AhbCacheResetRegistrar() { GstContextBridgeRegistry::registerCacheReset(&GstAHardwareBufferVideoBuffer::resetImageCache); }
332};
333const AhbCacheResetRegistrar s_ahbCacheResetRegistrar;
334} // namespace
335
336QVideoFrameTexturesUPtr GstAHardwareBufferVideoBuffer::mapTextures(QRhi& rhi, QVideoFrameTexturesUPtr& old)
337{
338 // Qt's contract: mapTextures runs on the QRhi (render) thread. Bail rather than crash if ever called off-thread.
339 if (!rhi.thread()->isCurrentThread()) {
340 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
341 }
342
343 if (!_sample) {
344 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
345 }
346 if (rhi.backend() != QRhi::OpenGLES2) {
347 if (!s_loggedBadBackend.exchange(true, std::memory_order_relaxed)) {
348 qCWarning(GstAHWBufLog) << "QRhi backend is not OpenGLES2; AHardwareBuffer path unsupported";
349 }
350 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
351 }
352
353 // Bind Qt's GL context before any EGL/GL call, else the calls silently no-op into a foreign/null context.
354 rhi.makeThreadLocalNativeContextCurrent();
355
356 const auto* nativeHandles = static_cast<const QRhiGles2NativeHandles*>(rhi.nativeHandles());
357 if (!nativeHandles || !nativeHandles->context) {
358 qCWarning(GstAHWBufLog) << "QRhi exposes no GL context";
359 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
360 }
361
362 // Prefer QEGLContext::display() — sampling on a foreign display silently returns black.
363 EGLDisplay eglDpy = GstEglHelpers::resolveEglDisplay(nativeHandles->context);
364 if (eglDpy == EGL_NO_DISPLAY)
365 eglDpy = _eglDisplay;
366 if (eglDpy == EGL_NO_DISPLAY) {
367 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
368 }
369
370 GstBuffer* buffer = gst_sample_get_buffer(_sample);
371 if (!buffer)
372 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
373
374 GstMemory* mem0 = gst_buffer_peek_memory(buffer, 0);
375 if (!mem0 || !gst_is_ahardware_buffer_memory(mem0)) {
376 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
377 }
378
379 if (!GstEglHelpers::displaySupportsExtension(eglDpy, kNativeBufferExt)) {
380 static std::atomic<bool> s_warnedNativeBufferExt{false};
381 if (!s_warnedNativeBufferExt.exchange(true, std::memory_order_relaxed)) {
382 qCWarning(GstAHWBufLog) << "EGL_ANDROID_image_native_buffer unavailable; AHardwareBuffer path disabled";
383 }
384 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
385 }
386
387 static const auto eglGetNativeClientBufferANDROID_ =
388 reinterpret_cast<PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC>(eglGetProcAddress("eglGetNativeClientBufferANDROID"));
389 static const auto eglCreateImageKHR_ =
390 reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
391 static const auto eglDestroyImageKHR_ =
392 reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
393 static const auto glEGLImageTargetTexture2DOES_ =
394 reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
395
396 if (!eglGetNativeClientBufferANDROID_ || !eglCreateImageKHR_ || !eglDestroyImageKHR_ ||
397 !glEGLImageTargetTexture2DOES_) {
398 qCWarning(GstAHWBufLog) << "Required EGL/GL proc addresses unavailable";
399 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
400 }
401
402 AHardwareBuffer* ahwb = gst_ahardware_buffer_memory_get_buffer(GST_AHARDWARE_BUFFER_MEMORY_CAST(mem0));
403 if (!ahwb) {
404 qCWarning(GstAHWBufLog) << "gst_ahardware_buffer_memory_get_buffer returned null";
405 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
406 }
407
408 // Block until the producer's GPU write completes before importing/sampling the recycled AHB.
409 waitProducerComplete(buffer);
410
411 QOpenGLFunctions functions(nativeHandles->context);
412
413 // Clearing forces a lookup miss so the prior `old` textures aren't reused over freed GL names.
414 {
415 QMutexLocker lock(&s_imageCacheMutex);
416 if (s_imageCacheResetPending.exchange(false, std::memory_order_acq_rel)) {
417 clearImageCacheLocked(eglDestroyImageKHR_, &functions);
418 }
419 }
420
421 GLuint name = 0;
422 {
423 QMutexLocker lock(&s_imageCacheMutex);
424 name = cachedTextureNameLocked(&rhi, ahwb, eglDpy);
425 if (name != 0) {
426 acquireCacheEntryLocked(name); // provisional pin: hold the entry across the unlocked gap below
428 }
429 }
430
431 if (name == 0) {
433
434 EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID_(ahwb);
435 if (!clientBuffer) {
436 qCWarning(GstAHWBufLog) << "eglGetNativeClientBufferANDROID returned null";
437 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
438 }
439
440 const EGLint attribs[] = {EGL_NONE};
441 EGLImageKHR image =
442 eglCreateImageKHR_(eglDpy, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attribs);
443 if (image == EGL_NO_IMAGE_KHR) {
444 qCWarning(GstAHWBufLog) << "eglCreateImageKHR failed, err=" << Qt::hex << eglGetError();
445 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
446 }
447
448 functions.glGenTextures(1, &name);
449 functions.glBindTexture(GL_TEXTURE_EXTERNAL_OES, name);
450 glEGLImageTargetTexture2DOES_(GL_TEXTURE_EXTERNAL_OES, image);
451 if (Q_UNLIKELY(GstAHWBufLog().isDebugEnabled())) {
452 if (const GLenum glErr = functions.glGetError(); glErr != GL_NO_ERROR) {
453 qCDebug(GstAHWBufLog) << "glEGLImageTargetTexture2DOES glError=" << Qt::hex << glErr
454 << "eglError=" << eglGetError();
455 }
456 }
457
458 QMutexLocker lock(&s_imageCacheMutex);
459 // Real race: s_imageCache is process-wide and each window's render thread can import the same recycled ahb, so
460 // re-check and reuse theirs if present.
461 if (GLuint existing = cachedTextureNameLocked(&rhi, ahwb, eglDpy); existing != 0) {
462 eglDestroyImageKHR_(eglDpy, image);
463 functions.glDeleteTextures(1, &name);
464 name = existing;
465 } else if (!insertCacheEntryLocked(&rhi, ahwb, eglDpy, image, name, eglDestroyImageKHR_, &functions)) {
466 // Cache full and every entry pinned: insert took no ownership, so free what we made here.
467 eglDestroyImageKHR_(eglDpy, image);
468 functions.glDeleteTextures(1, &name);
469 name = 0;
470 }
471 acquireCacheEntryLocked(name); // provisional pin (no-op if name == 0)
472 }
473
474 if (name == 0) {
475 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
476 }
477
478 // Provisional pin keeps LRU eviction from freeing the name before FrameTextures takes its own pin.
479 const auto pinGuard = qScopeGuard([&] {
480 const QMutexLocker lock(&s_imageCacheMutex);
481 releaseCacheEntryLocked(name);
482 drainDeferredImageCacheLocked(eglDestroyImageKHR_, &functions, &rhi);
483 });
484
485 if (auto* prev = GstHwFrameTexturesBase::reusableBundle<FrameTextures>(old, HwVideoBufferPath::AHardwareBuffer)) {
486 if (prev->matches(&rhi, _format.frameSize(), QVideoFrameFormat::Format_SamplerExternalOES, name)) {
488 prev->setSourceSample(takeSample());
489 QVideoFrameTexturesUPtr reused = std::move(old);
490 return reused;
491 }
492 }
493
494 // Pre-flight the external-OES RHI format/size before createFrom() so an unsupported import demotes to CPU on a query.
495 if (!GstHwImportPreflight::preflightOrRecord(&rhi, HwVideoBufferPath::AHardwareBuffer,
496 QVideoFrameFormat::Format_SamplerExternalOES, _format.frameSize(),
497 QRhiTexture::ExternalOES)) {
498 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
499 }
500
501 auto textures =
502 std::make_unique<FrameTextures>(&rhi, _format.frameSize(), QVideoFrameFormat::Format_SamplerExternalOES, name);
503 if (!textures->texture(0)) {
504 qCWarning(GstAHWBufLog) << "createFrom failed for plane 0 (SamplerExternalOES)";
505 return GstHwPathTelemetry::fail(HwVideoBufferPath::AHardwareBuffer);
506 }
507 textures->setSourceSample(takeSample());
508 return textures;
509}
510
511#endif // QGC_HAS_GST_AHARDWAREBUFFER_GPU_PATH
HwVideoBufferPath
Identifies which GPU path was chosen; used by the adapter to increment the right counter.
#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)
Common base for per-platform FrameTextures : QVideoFrameTextures from *VideoBuffer::mapTextures().
virtual HwVideoBufferPath sourcePath() const
GPU path that produced this bundle; used after a type-safe downcast to decide path-local reuse.
Common base for GStreamer-backed QHwVideoBuffer subclasses.
void recordImageCacheHit(HwVideoBufferPath path) noexcept
Native image/texture cache hit/miss accounting.
void recordImageCacheMiss(HwVideoBufferPath path) noexcept
void recordTextureReuse(HwVideoBufferPath path) noexcept
Prior frame's QRhiTexture wrappers reused (decoder pool returned same native handle).
constexpr int kMaxPlanes
Matches GST_VIDEO_MAX_PLANES (gst-video pins it at 4); single source of truth for every per-platform ...
QByteArray format(const QList< LogEntry > &entries, int fmt)