3#if defined(QGC_HAS_GST_AHARDWAREBUFFER_GPU_PATH)
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>
19#include <gst/android/gstandroid.h>
20#include <gst/gl/gstglsyncmeta.h>
21#include <gst/video/video.h>
23#include <private/qvideotexturehelper_p.h>
25#include <rhi/qrhi_platform.h>
40constexpr int kImageCacheCapacity = 4;
42std::atomic<bool> s_loggedBadBackend{
false};
44constexpr const char* kNativeBufferExt =
"EGL_ANDROID_image_native_buffer";
46std::atomic<bool> s_loggedNoFenceSync{
false};
54 EGLDisplay display = EGL_NO_DISPLAY;
55 EGLImageKHR image = EGL_NO_IMAGE_KHR;
56 GLuint textureName = 0;
57 quint64 lastUsedTick = 0;
62QMutex s_imageCacheMutex;
63std::array<AhbCacheEntry, kImageCacheCapacity> s_imageCache{};
64std::vector<AhbCacheEntry> s_deferredImageCache;
65quint64 s_imageCacheTick = 0;
69std::atomic<bool> s_imageCacheResetPending{
false};
72void destroyCacheEntryLocked(AhbCacheEntry& e, PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
74 if (e.image != EGL_NO_IMAGE_KHR && e.display != EGL_NO_DISPLAY && destroyImage) {
75 destroyImage(e.display, e.image);
77 if (e.textureName != 0 && gl) {
78 gl->glDeleteTextures(1, &e.textureName);
81 AHardwareBuffer_release(e.ahb);
86void drainDeferredImageCacheLocked(PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl, QRhi* rhi =
nullptr)
88 if (!destroyImage || !gl)
91 for (
auto it = s_deferredImageCache.begin(); it != s_deferredImageCache.end();) {
92 if (it->inUse > 0 || (rhi && it->rhi != rhi)) {
97 destroyCacheEntryLocked(*it, destroyImage, gl);
98 it = s_deferredImageCache.erase(it);
102GLuint cachedTextureNameLocked(QRhi* rhi,
AHardwareBuffer* ahb, EGLDisplay eglDpy)
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;
116bool insertCacheEntryLocked(QRhi* rhi,
AHardwareBuffer* ahb, EGLDisplay eglDpy, EGLImageKHR image, GLuint textureName,
117 PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
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) {
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))
135 qCWarning(GstAHWBufLog) <<
"AHB texture cache full and all entries in use; skipping cache insert";
138 destroyCacheEntryLocked(*target, destroyImage, gl);
140 AHardwareBuffer_acquire(ahb);
143 target->display = eglDpy;
144 target->image = image;
145 target->textureName = textureName;
146 target->lastUsedTick = ++s_imageCacheTick;
150void clearImageCacheLocked(PFNEGLDESTROYIMAGEKHRPROC destroyImage, QOpenGLFunctions* gl)
152 drainDeferredImageCacheLocked(destroyImage, gl);
154 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
155 auto& entry = s_imageCache[i];
156 if (entry.inUse > 0) {
158 qCDebug(GstAHWBufLog) <<
"AHB texture cache reset deferred for in-use texture" << entry.textureName;
159 s_deferredImageCache.push_back(entry);
160 entry = AhbCacheEntry{};
163 destroyCacheEntryLocked(entry, destroyImage, gl);
165 drainDeferredImageCacheLocked(destroyImage, gl);
166 s_imageCacheTick = 0;
171void acquireCacheEntryLocked(GLuint name)
175 for (std::size_t i = 0; i < s_imageCache.size(); ++i) {
176 auto& e = s_imageCache[i];
177 if (e.textureName == name) {
182 for (
auto& e : s_deferredImageCache) {
183 if (e.textureName == name) {
190void releaseCacheEntryLocked(GLuint name)
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) {
201 for (
auto& e : s_deferredImageCache) {
202 if (e.textureName == name && e.inUse > 0) {
214 FrameTextures(QRhi* rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat, GLuint name)
215 : _rhi(rhi), _size(size), _pixelFormat(pixelFormat), _name(name)
218 const auto* desc = QVideoTextureHelper::textureDescription(pixelFormat);
220 qCWarning(GstAHWBufLog) <<
"no QVideoTextureHelper description for format" << pixelFormat;
223 const QSize planeSize = desc->rhiPlaneSize(size, 0, rhi);
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();
233 QMutexLocker lock(&s_imageCacheMutex);
234 acquireCacheEntryLocked(_name);
237 ~FrameTextures()
override
239 QOpenGLFunctions* gl =
nullptr;
241 _rhi->makeThreadLocalNativeContextCurrent();
242 if (QOpenGLContext* ctx = QOpenGLContext::currentContext()) {
243 gl = ctx->functions();
246 static const auto eglDestroyImageKHR_ =
247 reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC
>(eglGetProcAddress(
"eglDestroyImageKHR"));
249 QMutexLocker lock(&s_imageCacheMutex);
250 releaseCacheEntryLocked(_name);
251 drainDeferredImageCacheLocked(eglDestroyImageKHR_, gl, _rhi);
258 bool matches(QRhi* rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat, GLuint name)
const noexcept
260 return _rhi == rhi && _size == size && _pixelFormat == pixelFormat && _name == name && _name != 0 &&
265 QRhi* _rhi =
nullptr;
267 QVideoFrameFormat::PixelFormat _pixelFormat = QVideoFrameFormat::Format_Invalid;
276void waitProducerComplete(GstBuffer* buffer)
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);
286 "AHardwareBuffer: no GstGLSyncMeta producer fence; frames may tear on recycle (no consumer-side "
287 "barrier can substitute)");
292GstAHardwareBufferVideoBuffer::GstAHardwareBufferVideoBuffer(GstSample* sample,
const GstVideoInfo& videoInfo,
293 const QVideoFrameFormat& format, EGLDisplay eglDisplay)
300QMatrix4x4 GstAHardwareBufferVideoBuffer::externalTextureMatrix()
const
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);
311bool GstAHardwareBufferVideoBuffer::validatePlaneHandles()
const
315 return validatePlanes([](GstMemory* mem) {
316 if (!mem || !gst_is_ahardware_buffer_memory(mem))
318 return gst_ahardware_buffer_memory_get_buffer(GST_AHARDWARE_BUFFER_MEMORY_CAST(mem)) !=
nullptr;
323void GstAHardwareBufferVideoBuffer::resetImageCache() noexcept
325 s_imageCacheResetPending.store(
true, std::memory_order_release);
329struct AhbCacheResetRegistrar
331 AhbCacheResetRegistrar() { GstContextBridgeRegistry::registerCacheReset(&GstAHardwareBufferVideoBuffer::resetImageCache); }
333const AhbCacheResetRegistrar s_ahbCacheResetRegistrar;
336QVideoFrameTexturesUPtr GstAHardwareBufferVideoBuffer::mapTextures(QRhi& rhi, QVideoFrameTexturesUPtr& old)
339 if (!rhi.thread()->isCurrentThread()) {
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";
354 rhi.makeThreadLocalNativeContextCurrent();
356 const auto* nativeHandles =
static_cast<const QRhiGles2NativeHandles*
>(rhi.nativeHandles());
357 if (!nativeHandles || !nativeHandles->context) {
358 qCWarning(GstAHWBufLog) <<
"QRhi exposes no GL context";
363 EGLDisplay eglDpy = GstEglHelpers::resolveEglDisplay(nativeHandles->context);
364 if (eglDpy == EGL_NO_DISPLAY)
365 eglDpy = _eglDisplay;
366 if (eglDpy == EGL_NO_DISPLAY) {
370 GstBuffer* buffer = gst_sample_get_buffer(_sample);
374 GstMemory* mem0 = gst_buffer_peek_memory(buffer, 0);
375 if (!mem0 || !gst_is_ahardware_buffer_memory(mem0)) {
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";
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"));
396 if (!eglGetNativeClientBufferANDROID_ || !eglCreateImageKHR_ || !eglDestroyImageKHR_ ||
397 !glEGLImageTargetTexture2DOES_) {
398 qCWarning(GstAHWBufLog) <<
"Required EGL/GL proc addresses unavailable";
402 AHardwareBuffer* ahwb = gst_ahardware_buffer_memory_get_buffer(GST_AHARDWARE_BUFFER_MEMORY_CAST(mem0));
404 qCWarning(GstAHWBufLog) <<
"gst_ahardware_buffer_memory_get_buffer returned null";
409 waitProducerComplete(buffer);
411 QOpenGLFunctions functions(nativeHandles->context);
415 QMutexLocker lock(&s_imageCacheMutex);
416 if (s_imageCacheResetPending.exchange(
false, std::memory_order_acq_rel)) {
417 clearImageCacheLocked(eglDestroyImageKHR_, &functions);
423 QMutexLocker lock(&s_imageCacheMutex);
424 name = cachedTextureNameLocked(&rhi, ahwb, eglDpy);
426 acquireCacheEntryLocked(name);
434 EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID_(ahwb);
436 qCWarning(GstAHWBufLog) <<
"eglGetNativeClientBufferANDROID returned null";
440 const EGLint attribs[] = {EGL_NONE};
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();
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();
458 QMutexLocker lock(&s_imageCacheMutex);
461 if (GLuint existing = cachedTextureNameLocked(&rhi, ahwb, eglDpy); existing != 0) {
462 eglDestroyImageKHR_(eglDpy, image);
463 functions.glDeleteTextures(1, &name);
465 }
else if (!insertCacheEntryLocked(&rhi, ahwb, eglDpy, image, name, eglDestroyImageKHR_, &functions)) {
467 eglDestroyImageKHR_(eglDpy, image);
468 functions.glDeleteTextures(1, &name);
471 acquireCacheEntryLocked(name);
479 const auto pinGuard = qScopeGuard([&] {
480 const QMutexLocker lock(&s_imageCacheMutex);
481 releaseCacheEntryLocked(name);
482 drainDeferredImageCacheLocked(eglDestroyImageKHR_, &functions, &rhi);
486 if (prev->matches(&rhi, _format.frameSize(), QVideoFrameFormat::Format_SamplerExternalOES, name)) {
488 prev->setSourceSample(takeSample());
489 QVideoFrameTexturesUPtr reused = std::move(old);
496 QVideoFrameFormat::Format_SamplerExternalOES, _format.frameSize(),
497 QRhiTexture::ExternalOES)) {
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)";
507 textures->setSourceSample(takeSample());
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 ...