QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstGlVideoBuffer.cc
Go to the documentation of this file.
1#include "GstGlVideoBuffer.h"
2#include "GstHwVideoBuffer.h"
3
4#if defined(QGC_HAS_GST_GLMEMORY_GPU_PATH)
5
7
8#include <QtCore/QLoggingCategory>
9#include <QtCore/QSize>
10#include <private/qvideotexturehelper_p.h>
11#include <rhi/qrhi.h>
12
13#include <gst/gl/gl.h>
14#include <gst/gl/gstglbasememory.h>
15#include <gst/gl/gstglmemory.h>
16#include <gst/gl/gstglsyncmeta.h>
17#include <gst/video/video.h>
18
19// Qt's portable GL types (GLuint et al.) — works on Linux/macOS/Android without GLES2 SDK headers.
20#include <QtGui/qopengl.h>
21
22#include <array>
23#include <atomic>
24
25QGC_LOGGING_CATEGORY(GstGlBufLog, "Video.GStreamer.HwBuffers.GstGlBuf")
26
27namespace {
28
29constexpr int kMaxPlanes = 4;
30
31std::atomic<quint64> s_mapFailureCount{0};
32
33QVideoFrameTexturesUPtr fail()
34{
35 s_mapFailureCount.fetch_add(1, std::memory_order_relaxed);
36 return {};
37}
38
39class FrameTextures final : public QVideoFrameTextures
40{
41public:
42 FrameTextures(QRhi *rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat,
43 std::array<GLuint, kMaxPlanes> names, int count)
44 : _rhi(rhi), _size(size), _pixelFormat(pixelFormat), _names(names), _count(count)
45 {
46 const auto *desc = QVideoTextureHelper::textureDescription(pixelFormat);
47 if (!desc) {
48 return;
49 }
50 for (int i = 0; i < _count; ++i) {
51 // GL_NONE (0) would be silently accepted by createFrom and sample as black —
52 // gst-gl can hand us 0 if a plane wasn't uploaded yet.
53 if (names[i] == 0) {
54 qCWarning(GstGlBufLog) << "FrameTextures: GL texture id is 0 for plane" << i;
55 continue;
56 }
57 const QSize planeSize = desc->rhiPlaneSize(size, i, rhi);
58 _textures[i].reset(rhi->newTexture(desc->rhiTextureFormat(i, rhi), planeSize, 1, {}));
59 if (_textures[i] && !_textures[i]->createFrom({names[i], 0})) {
60 _textures[i].reset();
61 }
62 }
63 }
64
65 QRhiTexture *texture(uint plane) const override
66 {
67 return (int(plane) < _count) ? _textures[plane].get() : nullptr;
68 }
69
70 // Reuse-eligibility: same rhi+size+format and identical, non-zero per-plane GL texture ids.
71 // gst-gl rotates ids within a fixed pool; the QRhiTexture is just a thin view, so when the
72 // pool re-binds id N to new frame contents the wrapper transparently samples the new data.
73 bool matches(QRhi *rhi, QSize size, QVideoFrameFormat::PixelFormat pixelFormat,
74 const std::array<GLuint, kMaxPlanes> &names, int count) const
75 {
76 if (_rhi != rhi || _size != size || _pixelFormat != pixelFormat || _count != count) {
77 return false;
78 }
79 for (int i = 0; i < _count; ++i) {
80 if (_names[i] == 0 || _names[i] != names[i] || !_textures[i]) {
81 return false;
82 }
83 }
84 return true;
85 }
86
87private:
88 QRhi *_rhi = nullptr;
89 QSize _size;
90 QVideoFrameFormat::PixelFormat _pixelFormat = QVideoFrameFormat::Format_Invalid;
91 std::array<GLuint, kMaxPlanes> _names{};
92 int _count = 0;
93 std::unique_ptr<QRhiTexture> _textures[kMaxPlanes];
94};
95
96std::atomic<quint64> s_textureReuseHits{0};
97std::atomic<quint64> s_gpuWaitCount{0};
98std::atomic<quint64> s_cpuWaitCount{0};
99
100std::atomic<bool> s_loggedNullSample{false};
101std::atomic<bool> s_loggedBadBackend{false};
102
103#define GL_WARN_ONCE(flag, ...) \
104 do { if (!(flag).exchange(true, std::memory_order_relaxed)) qCWarning(GstGlBufLog) << __VA_ARGS__; } while (0)
105
106} // namespace
107
108GstGlVideoBuffer::GstGlVideoBuffer(GstSample *sample,
109 const GstVideoInfo &videoInfo,
110 const QVideoFrameFormat &format)
111 : GstHwVideoBuffer(QVideoFrame::RhiTextureHandle, sample, videoInfo, format)
112{
113}
114
115GstGlVideoBuffer::~GstGlVideoBuffer() = default;
116
117QAbstractVideoBuffer::MapData GstGlVideoBuffer::map(QVideoFrame::MapMode /*mode*/)
118{
119 return {};
120}
121
122bool GstGlVideoBuffer::validatePlaneHandles() const
123{
124 // Allocator-only — GLuint check needs GST_MAP_GL, too expensive on streaming thread.
125 if (!_sample) return false;
126 GstBuffer *buffer = gst_sample_get_buffer(_sample);
127 if (!buffer) return false;
128 const int memCount = qMin(int(gst_buffer_n_memory(buffer)), kMaxPlanes);
129 if (memCount <= 0) return false;
130 for (int i = 0; i < memCount; ++i) {
131 GstMemory *mem = gst_buffer_peek_memory(buffer, i);
132 if (!mem || !gst_is_gl_memory(mem)) return false;
133 }
134 return true;
135}
136
137QVideoFrameTexturesUPtr GstGlVideoBuffer::mapTextures(QRhi &rhi, QVideoFrameTexturesUPtr &old)
138{
139 Q_ASSERT(rhi.thread()->isCurrentThread()); // Qt's contract: mapTextures runs on the QRhi (render) thread.
140 if (!_sample) {
141 GL_WARN_ONCE(s_loggedNullSample, "mapTextures: GstSample is null");
142 return fail();
143 }
144 // QRhi::OpenGLES2 covers both desktop GL and GLES — Qt collapses both into
145 // the same backend enum value. There is no separate QRhi::OpenGL.
146 if (rhi.backend() != QRhi::OpenGLES2) {
147 GL_WARN_ONCE(s_loggedBadBackend, "QRhi backend is" << rhi.backendName()
148 << "(GL backend required); GLMemory path disabled");
149 return fail();
150 }
151
152 // QRhi::createFrom calls glBindTexture/glTexParameteri internally — defensive bracket against custom RHI integrations where QSGRenderThread doesn't have Qt's GL context bound on entry.
153 rhi.makeThreadLocalNativeContextCurrent();
154
155 GstBuffer *buffer = gst_sample_get_buffer(_sample);
156 if (!buffer) return fail();
157
158 GstMemory *mem0 = gst_buffer_peek_memory(buffer, 0);
159 if (!mem0 || !gst_is_gl_memory(mem0)) {
160 return fail();
161 }
162
163 GstVideoFrame frame{};
164 // GST_MAP_GL (a gst-gl extension flag) OR'd with GST_MAP_READ triggers GstGLMemory upload.
165 if (!gst_video_frame_map(&frame, &_videoInfo, buffer,
166 static_cast<GstMapFlags>(GST_MAP_READ | GST_MAP_GL))) {
167 qCWarning(GstGlBufLog) << "gst_video_frame_map(GST_MAP_READ | GST_MAP_GL) failed";
168 return fail();
169 }
170
171 // wait_cpu (CPU-blocking) is the only durably correct fence here. The cheaper GPU-side
172 // wait() — even when gst_gl_context_can_share(producer, primed) returns true — produces
173 // periodic torn / saturated-green NV12 frames on Linux Mesa: wait() only records ordering
174 // in the caller's GL command stream, and Mesa's per-thread queues don't always flush in
175 // time for QRhi to sample (UV plane reads partially-written memory). wait_cpu blocks
176 // until the producer's fence signals on the CPU side, which is what QRhi actually needs.
177 GstGLContext *glCtx = GST_GL_BASE_MEMORY_CAST(mem0)->context;
178 if (GstGLSyncMeta *syncMeta = gst_buffer_get_gl_sync_meta(buffer); syncMeta && glCtx) {
179 gst_gl_sync_meta_wait_cpu(syncMeta, glCtx);
180 s_cpuWaitCount.fetch_add(1, std::memory_order_relaxed);
181 }
182
183 const int planeCount = qBound(1, int(GST_VIDEO_FRAME_N_PLANES(&frame)), kMaxPlanes);
184 std::array<GLuint, kMaxPlanes> names{};
185 for (int i = 0; i < planeCount; ++i) {
186 // GST_MAP_GL maps return a *pointer to* the GLuint texture id, not the id itself.
187 const GLuint *texIdPtr = static_cast<const GLuint *>(GST_VIDEO_FRAME_PLANE_DATA(&frame, i));
188 names[i] = texIdPtr ? *texIdPtr : 0;
189 }
190
191 gst_video_frame_unmap(&frame);
192
193 // Reuse Qt's previous textures wrapper if the pool gave us identical GL ids on the same RHI.
194 // dynamic_cast (Qt builds with RTTI) safely no-ops when `old` came from a different video buffer
195 // type (its anon-namespace FrameTextures is a different translation-unit type).
196 if (old) {
197 if (auto *prev = dynamic_cast<FrameTextures *>(old.get());
198 prev && prev->matches(&rhi, _format.frameSize(), _format.pixelFormat(), names, planeCount)) {
199 s_textureReuseHits.fetch_add(1, std::memory_order_relaxed);
200 return std::move(old);
201 }
202 }
203
204 auto textures = std::make_unique<FrameTextures>(&rhi, _format.frameSize(),
205 _format.pixelFormat(), names, planeCount);
206 // FrameTextures null-resets any plane that QRhiTexture::createFrom rejects; for NV12/I420
207 // the chroma plane can fail while luma succeeds, producing a "successful" frame with
208 // missing chroma if we only check plane 0. Verify all planes (matches DMABuf path).
209 for (int i = 0; i < planeCount; ++i) {
210 if (!textures->texture(static_cast<uint>(i))) {
211 qCWarning(GstGlBufLog) << "createFrom failed for plane" << i
212 << "format=" << _format.pixelFormat();
213 return fail();
214 }
215 }
216 return textures;
217}
218
219quint64 GstGlVideoBuffer::peekTextureReuseHits()
220{
221 return s_textureReuseHits.load(std::memory_order_relaxed);
222}
223
224quint64 GstGlVideoBuffer::takeTextureReuseHits()
225{
226 return s_textureReuseHits.exchange(0, std::memory_order_relaxed);
227}
228
229quint64 GstGlVideoBuffer::takeSyncWaitCounts(quint64 &gpuWaits)
230{
231 gpuWaits = s_gpuWaitCount.exchange(0, std::memory_order_relaxed);
232 return s_cpuWaitCount.exchange(0, std::memory_order_relaxed);
233}
234
235quint64 GstGlVideoBuffer::takeMapFailureCount()
236{
237 return s_mapFailureCount.exchange(0, std::memory_order_relaxed);
238}
239
240quint64 GstGlVideoBuffer::peekMapFailureCount()
241{
242 return s_mapFailureCount.load(std::memory_order_relaxed);
243}
244
245#endif // QGC_HAS_GST_GLMEMORY_GPU_PATH
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QByteArray format(const QList< LogEntry > &entries, int fmt)