QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
CpuVideoFramePool.cc
Go to the documentation of this file.
1#include "CpuVideoFramePool.h"
2
3#include <QtCore/QLoggingCategory>
4#include <QtMultimedia/QAbstractVideoBuffer>
5#include <algorithm>
6#include <atomic>
7#include <cstring>
8#include <gst/video/video.h>
9#include <memory>
10#include <utility>
11
12#include "QGCLoggingCategory.h"
13#if defined(QGC_HAS_ANY_GPU_PATH)
14#include "QGCRhiCapture.h"
15#endif
16
17QGC_LOGGING_CATEGORY(CpuVideoFramePoolLog, "Video.GStreamer.HwBuffers.CpuPool")
18
19namespace {
20
21class PooledCpuVideoBuffer final : public QAbstractVideoBuffer
22{
23public:
24 PooledCpuVideoBuffer(QVideoFrameFormat format, CpuVideoFramePool::PlaneLayout layout, QByteArray backing)
25 : _format(std::move(format)), _layout(layout), _backing(std::move(backing))
26 {}
27
28 ~PooledCpuVideoBuffer() override
29 {
30 if (!_backing.isEmpty()) {
31 CpuVideoFramePool::instance().releaseBacking(_format.frameSize(), _format.pixelFormat(),
32 std::move(_backing));
33 }
34 }
35
36 MapData map(QVideoFrame::MapMode mode) override
37 {
38 if (mode == QVideoFrame::NotMapped) {
39 return {};
40 }
41 MapData data;
42 data.planeCount = _layout.planeCount;
43 uchar* base = reinterpret_cast<uchar*>(_backing.data());
44 for (int i = 0; i < _layout.planeCount; ++i) {
45 data.data[i] = base + _layout.planeOffset[i];
46 data.bytesPerLine[i] = _layout.bytesPerLine[i];
47 data.dataSize[i] = static_cast<qsizetype>(_layout.bytesPerLine[i]) * _layout.height[i];
48 }
49 return data;
50 }
51
52 QVideoFrameFormat format() const override { return _format; }
53
54private:
55 QVideoFrameFormat _format;
57 QByteArray _backing;
58};
59
60// Refs the source GstBuffer and maps its system memory through to Qt with no copy. The decoder buffer stays pinned for
61// the frame's lifetime — the same tradeoff the GPU paths already make by anchoring the sample.
62class MapThroughGstVideoBuffer final : public QAbstractVideoBuffer
63{
64public:
65 // Outstanding zero-copy buffers each pin a decoder buffer; cap how many can be in flight before
66 // wrapZeroCopy falls back to a copy so a compositor stall can't drain the decoder's HW pool. The cap is
67 // derived from the RHI's frames-in-flight.
68 static int maxLive() noexcept
69 {
70 static const int s_max = []() -> int {
71 constexpr int kFloor = 6;
72#if defined(QGC_HAS_ANY_GPU_PATH)
73 // Snapshot populated render-side; never dereference QRhi from this (streaming) thread.
74 int fif = QGCRhiCapture::deviceSnapshot().framesInFlight.load(std::memory_order_acquire);
75 if (fif <= 0) {
76 fif = 2; // QRhi's typical default before the snapshot exists
77 }
78 return std::clamp(fif * 2, kFloor, 16);
79#else
80 return kFloor;
81#endif
82 }();
83 return s_max;
84 }
85 static std::atomic<int>& liveCount()
86 {
87 static std::atomic<int> s_live{0};
88 return s_live;
89 }
90
91 MapThroughGstVideoBuffer(GstBuffer* buffer, const GstVideoInfo& info, QVideoFrameFormat format)
92 : _buffer(gst_buffer_ref(buffer)), _info(info), _format(std::move(format))
93 {
94 liveCount().fetch_add(1, std::memory_order_relaxed);
95 }
96
97 ~MapThroughGstVideoBuffer() override
98 {
99 if (_mapped) {
100 gst_video_frame_unmap(&_frame);
101 }
102 gst_buffer_unref(_buffer);
103 liveCount().fetch_sub(1, std::memory_order_relaxed);
104 }
105
106 MapData map(QVideoFrame::MapMode mode) override
107 {
108 if (mode == QVideoFrame::NotMapped) {
109 return {};
110 }
111 if (mode != QVideoFrame::ReadOnly) {
112 return {};
113 }
114 if (!_mapped) {
115 if (!gst_video_frame_map(&_frame, &_info, _buffer, GST_MAP_READ)) {
116 return {};
117 }
118 _mapped = true;
119 }
120 MapData data;
121 data.planeCount = GST_VIDEO_FRAME_N_PLANES(&_frame);
122 for (int i = 0; i < data.planeCount; ++i) {
123 data.data[i] = static_cast<uchar*>(GST_VIDEO_FRAME_PLANE_DATA(&_frame, i));
124 data.bytesPerLine[i] = GST_VIDEO_FRAME_PLANE_STRIDE(&_frame, i);
125 data.dataSize[i] = static_cast<qsizetype>(GST_VIDEO_FRAME_PLANE_STRIDE(&_frame, i)) *
126 GST_VIDEO_FRAME_COMP_HEIGHT(&_frame, i);
127 }
128 return data;
129 }
130
131 void unmap() override
132 {
133 if (_mapped) {
134 gst_video_frame_unmap(&_frame);
135 _mapped = false;
136 }
137 }
138
139 QVideoFrameFormat format() const override { return _format; }
140
141private:
142 GstBuffer* _buffer;
143 GstVideoInfo _info;
144 QVideoFrameFormat _format;
145 GstVideoFrame _frame = {};
146 bool _mapped = false;
147};
148
149} // namespace
150
152{
153 static CpuVideoFramePool s_pool;
154 return s_pool;
155}
156
158{
159 PlaneLayout L = {};
160 const int n = GST_VIDEO_INFO_N_PLANES(&info);
161 if (n <= 0 || n > 4) {
162 return L;
163 }
164 L.planeCount = n;
165 for (int i = 0; i < n; ++i) {
166 L.bytesPerLine[i] = GST_VIDEO_INFO_PLANE_STRIDE(&info, i);
167 L.planeOffset[i] = static_cast<qsizetype>(GST_VIDEO_INFO_PLANE_OFFSET(&info, i));
168 L.height[i] = static_cast<int>(GST_VIDEO_INFO_COMP_HEIGHT(&info, i));
169 }
170 L.byteSize = static_cast<qsizetype>(GST_VIDEO_INFO_SIZE(&info));
171 return L;
172}
173
174QByteArray CpuVideoFramePool::acquireBacking(const PlaneLayout& layout, QSize size,
175 QVideoFrameFormat::PixelFormat format)
176{
177 QMutexLocker lock(&_mutex);
178 for (std::size_t i = 0; i < _slots.size(); ++i) {
179 auto& slot = _slots[i];
180 if (slot.size == size && slot.format == format) {
181 // (size,format) implies a fixed byteSize; discard any stale mis-sized backing and keep scanning rather
182 // than stranding it, which would force a permanent miss loop.
183 while (slot.availableCount > 0) {
184 QByteArray b = std::move(slot.available[--slot.availableCount]);
185 if (b.size() == layout.byteSize) {
186 _hits.fetch_add(1, std::memory_order_relaxed);
187 return b;
188 }
189 }
190 break;
191 }
192 }
193 _misses.fetch_add(1, std::memory_order_relaxed);
194 // Qt::Uninitialized skips the value-init memset; caller overwrites every plane.
195 return QByteArray(layout.byteSize, Qt::Uninitialized);
196}
197
198void CpuVideoFramePool::releaseBacking(QSize size, QVideoFrameFormat::PixelFormat format, QByteArray&& backing)
199{
200 QMutexLocker lock(&_mutex);
201 for (std::size_t i = 0; i < _slots.size(); ++i) {
202 auto& slot = _slots[i];
203 if (slot.size == size && slot.format == format) {
204 if (slot.availableCount < kMaxPerSlot) {
205 slot.available[slot.availableCount++] = std::move(backing);
206 }
207 return;
208 }
209 }
210 // No slot for this (size,format): claim an empty slot or round-robin evict to stay bounded.
211 Slot* target = nullptr;
212 for (std::size_t i = 0; i < _slots.size(); ++i) {
213 auto& slot = _slots[i];
214 if (slot.format == QVideoFrameFormat::Format_Invalid) {
215 target = &slot;
216 break;
217 }
218 }
219 if (!target) {
220 target = &_slots[_evictCursor];
221 _evictCursor = (_evictCursor + 1) % kMaxSlots;
222 *target = Slot{};
223 }
224 target->size = size;
225 target->format = format;
226 target->available[0] = std::move(backing);
227 target->availableCount = 1;
228}
229
230QVideoFrame CpuVideoFramePool::acquireFrame(const QVideoFrameFormat& format, const GstVideoInfo& info)
231{
232 const PlaneLayout layout = computeLayout(info);
233 if (layout.planeCount == 0 || layout.byteSize <= 0) {
234 return {};
235 }
236 QByteArray backing = acquireBacking(layout, format.frameSize(), format.pixelFormat());
237 auto buffer = std::make_unique<PooledCpuVideoBuffer>(format, layout, std::move(backing));
238 return QVideoFrame(std::move(buffer));
239}
240
241std::unique_ptr<QAbstractVideoBuffer> CpuVideoFramePool::wrapZeroCopy(GstBuffer* buffer, const GstVideoInfo& info,
242 const QVideoFrameFormat& format)
243{
244 if (!buffer || !format.isValid()) {
245 return nullptr;
246 }
247 if (MapThroughGstVideoBuffer::liveCount().load(std::memory_order_relaxed) >= MapThroughGstVideoBuffer::maxLive()) {
248 return nullptr;
249 }
250 auto buf = std::make_unique<MapThroughGstVideoBuffer>(buffer, info, format);
251 // Probe mappability: system memory always maps, GPU memory that slipped past the factory does not (caller copies).
252 // Keep the mapping rather than unmapping — the sink's subsequent map() reuses it instead of re-running the map.
253 const auto probe = buf->map(QVideoFrame::ReadOnly);
254 if (probe.planeCount <= 0) {
255 return nullptr;
256 }
257 return buf;
258}
259
260QVideoFrame CpuVideoFramePool::copyFromBuffer(GstBuffer* buffer, const GstVideoInfo& videoInfo,
261 const QVideoFrameFormat& format)
262{
263 if (!buffer || !format.isValid()) {
264 return {};
265 }
266
267 GstVideoFrame gstFrame;
268 if (!gst_video_frame_map(&gstFrame, &videoInfo, buffer, GST_MAP_READ)) {
269 static std::atomic<quint64> s_failCount{0};
270 const quint64 count = s_failCount.fetch_add(1, std::memory_order_relaxed) + 1;
271 if ((count & 0x3F) == 1) {
272 qCWarning(CpuVideoFramePoolLog) << "copyFromBuffer: gst_video_frame_map failed (count=" << count << ")";
273 }
274 return {};
275 }
276
277 QVideoFrame videoFrame = instance().acquireFrame(format, videoInfo);
278 if (!videoFrame.isValid() || !videoFrame.map(QVideoFrame::WriteOnly)) {
279 gst_video_frame_unmap(&gstFrame);
280 return {};
281 }
282
283 const int srcPlanes = GST_VIDEO_INFO_N_PLANES(&videoInfo);
284 if (srcPlanes != videoFrame.planeCount()) {
285 static std::atomic<bool> s_warnedPlaneMismatch{false};
286 if (!s_warnedPlaneMismatch.exchange(true, std::memory_order_relaxed)) {
287 qCWarning(CpuVideoFramePoolLog) << "copyFromBuffer: plane-count mismatch src" << srcPlanes << "dst"
288 << videoFrame.planeCount() << "— dropping frame";
289 }
290 videoFrame.unmap();
291 gst_video_frame_unmap(&gstFrame);
292 return {};
293 }
294
295 for (int p = 0; p < srcPlanes; ++p) {
296 const int dstStride = videoFrame.bytesPerLine(p);
297 const int srcStride = GST_VIDEO_FRAME_PLANE_STRIDE(&gstFrame, p);
298 const int planeHeight = GST_VIDEO_FRAME_COMP_HEIGHT(&gstFrame, p);
299 const int activeRowBytes =
300 GST_VIDEO_FRAME_COMP_WIDTH(&gstFrame, p) * GST_VIDEO_FRAME_COMP_PSTRIDE(&gstFrame, p);
301 // Reject before copy: a dst stride too narrow for a source row would tear the plane.
302 if (activeRowBytes > dstStride) {
303 static std::atomic<bool> s_warnedStrideOverflow{false};
304 if (!s_warnedStrideOverflow.exchange(true, std::memory_order_relaxed)) {
305 qCWarning(CpuVideoFramePoolLog) << "copyFromBuffer: plane" << p << "activeRowBytes" << activeRowBytes
306 << "> dstStride" << dstStride << "— dropping frame";
307 }
308 videoFrame.unmap();
309 gst_video_frame_unmap(&gstFrame);
310 return {};
311 }
312 const uchar* src = static_cast<const uchar*>(GST_VIDEO_FRAME_PLANE_DATA(&gstFrame, p));
313 uchar* dst = videoFrame.bits(p);
314 if (!dst) {
315 continue;
316 }
317 if (srcStride == dstStride && activeRowBytes == srcStride) {
318 std::memcpy(dst, src, static_cast<size_t>(planeHeight) * srcStride);
319 } else {
320 for (int y = 0; y < planeHeight; ++y) {
321 std::memcpy(dst + y * dstStride, src + y * srcStride, static_cast<size_t>(activeRowBytes));
322 }
323 }
324 }
325
326 videoFrame.unmap();
327 gst_video_frame_unmap(&gstFrame);
328 return videoFrame;
329}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Recycles CPU-backed QVideoFrame storage keyed by (size, pixelFormat) to avoid per-frame malloc churn.
static CpuVideoFramePool & instance()
static QVideoFrame copyFromBuffer(GstBuffer *buffer, const GstVideoInfo &videoInfo, const QVideoFrameFormat &format)
Copy buffer's planes into a pool-allocated QVideoFrame; invalid on stride overflow or unsupported for...
void releaseBacking(QSize size, QVideoFrameFormat::PixelFormat format, QByteArray &&backing)
Return a backing array to the pool; called by PooledCpuVideoBuffer's destructor only.
static PlaneLayout computeLayout(const GstVideoInfo &info)
Destination plane layout mirroring info (strides/offsets from the decoder). planeCount==0 means unsup...
QVideoFrame acquireFrame(const QVideoFrameFormat &format, const GstVideoInfo &info)
Returns a pool-backed frame sized to info, or freshly allocates on a pool miss; invalid if unsupporte...
static std::unique_ptr< QAbstractVideoBuffer > wrapZeroCopy(GstBuffer *buffer, const GstVideoInfo &info, const QVideoFrameFormat &format)
QByteArray format(const QList< LogEntry > &entries, int fmt)
DeviceSnapshot & deviceSnapshot() noexcept
Returns the global snapshot. Atomic fields make individual reads thread-safe.
std::atomic< int > framesInFlight
QRhi::FramesInFlight resource limit (0 = unset)