QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstD3D11VideoBuffer.cc
Go to the documentation of this file.
2
3#if defined(Q_OS_WIN) && defined(QGC_HAS_GST_D3D11_GPU_PATH)
4
5#include <QtCore/QMutex>
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <d3d11.h>
10#include <gst/d3d11/gstd3d11.h>
11
16#include "GstHwImportCache.h"
17#include "GstHwPathTelemetry.h"
18#include "QGCLoggingCategory.h"
19
20QGC_LOGGING_CATEGORY(GstD3D11Log, "Video.GStreamer.HwBuffers.GstD3D11Buf")
21
22namespace {
23
24using GstD3DVideoBufferCommon::kMaxPlanes;
25using GstD3DVideoBufferCommon::MapDiagnostics;
26using D3D11FrameTextures = GstD3DVideoBufferCommon::FrameTextures<ID3D11Texture2D>;
27using GstD3DVideoBufferCommon::StagingKey;
28using GstD3DVideoBufferCommon::StagingKeyHash;
29
30MapDiagnostics s_diag;
31
36class StagingTexturePool
37{
38public:
39 static StagingTexturePool& instance()
40 {
41 static StagingTexturePool pool;
42 return pool;
43 }
44
45 ~StagingTexturePool() { clear(); }
46
49 ID3D11Texture2D* acquire(ID3D11Device* dev, const StagingKey& key, const D3D11_TEXTURE2D_DESC& dstDesc,
50 int planeIdx, guint subIdx)
51 {
52 QMutexLocker lock(&_mutex);
53 StagingRing* ring = _entries.find(key);
54 if (!ring) {
55 _entries.insert(key, StagingRing{});
56 ring = _entries.find(key);
57 }
58 const std::size_t slot = ring->next;
59 ring->next = (ring->next + 1) % kRingSize;
60 if (ring->textures[slot]) {
62 ring->textures[slot]->AddRef();
63 return ring->textures[slot];
64 }
65 ID3D11Texture2D* tex = nullptr;
66 if (FAILED(dev->CreateTexture2D(&dstDesc, nullptr, &tex))) {
67 QGC_HW_WARN_ONCE(GstD3D11Log, s_diag.loggedTextureCreateFail,
68 "mapTextures: CreateTexture2D for slice copy failed (plane=" << planeIdx << "subresource="
69 << subIdx << ")");
70 return nullptr;
71 }
72 tex->AddRef();
73 ring->textures[slot] = tex; // pool keeps the create ref
75 return tex;
76 }
77
78 void clear()
79 {
80 QMutexLocker lock(&_mutex);
81 _entries.clear();
82 }
83
84private:
85 // Ring depth 3: the streaming thread can copy a new frame while QRhi still samples the previous one.
86 static constexpr std::size_t kRingSize = 3;
87
88 struct StagingRing
89 {
90 std::array<ID3D11Texture2D*, kRingSize> textures{};
91 std::size_t next = 0;
92 };
93
94 static constexpr std::size_t kMaxEntries = 8;
95 QMutex _mutex;
97 kMaxEntries,
98 [](const StagingKey&, StagingRing& ring) {
99 for (ID3D11Texture2D*& tex : ring.textures) {
100 if (tex) {
101 tex->Release();
102 tex = nullptr;
103 }
104 }
105 }};
106};
107
110ID3D11Texture2D* copySliceToStaging(ID3D11Texture2D* tex, guint subIdx, int planeIdx,
111 const D3D11_TEXTURE2D_DESC& srcDesc, GstD3D11Memory* d3dmem)
112{
113 ID3D11Device* d3dDev = gst_d3d11_device_get_device_handle(d3dmem->device);
114 ID3D11DeviceContext* d3dCtx = gst_d3d11_device_get_device_context_handle(d3dmem->device);
115 D3D11_TEXTURE2D_DESC dstDesc = srcDesc;
116 dstDesc.ArraySize = 1;
117 dstDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
118 dstDesc.MiscFlags = 0;
119 dstDesc.MipLevels = 1;
120 const StagingKey key{srcDesc.Width, srcDesc.Height, UINT(srcDesc.Format), planeIdx};
121 ID3D11Texture2D* stagingTex = StagingTexturePool::instance().acquire(d3dDev, key, dstDesc, planeIdx, subIdx);
122 if (!stagingTex) {
123 return nullptr;
124 }
125 // The immediate ID3D11DeviceContext is not free-threaded; gst-d3d11 device-lock contract requires this guard.
126 gst_d3d11_device_lock(d3dmem->device);
127 d3dCtx->CopySubresourceRegion(stagingTex, 0, 0, 0, 0, tex, subIdx, nullptr);
128 gst_d3d11_device_unlock(d3dmem->device);
129 return stagingTex;
130}
131
132} // namespace
133
134void GstD3D11VideoBuffer::resetCachedState() noexcept
135{
136 StagingTexturePool::instance().clear();
137 s_diag.reset();
138}
139
140namespace {
141struct D3D11CacheResetRegistrar
142{
143 D3D11CacheResetRegistrar() { GstContextBridgeRegistry::registerCacheReset(&GstD3D11VideoBuffer::resetCachedState); }
144};
145
146const D3D11CacheResetRegistrar s_d3d11CacheResetRegistrar;
147} // namespace
148
149GstD3D11VideoBuffer::GstD3D11VideoBuffer(GstSample* sample, const GstVideoInfo& videoInfo,
150 const QVideoFrameFormat& format)
151 : GstHwVideoBuffer(QVideoFrame::RhiTextureHandle, sample, videoInfo, format)
152{
153 // Device guard + slice copy + flush on the streaming thread; mapTextures (render thread) only imports resolved
154 // textures.
155 resolvePlaneResources();
156}
157
158GstD3D11VideoBuffer::~GstD3D11VideoBuffer()
159{
160 for (int i = 0; i < _resolvedCount; ++i) {
161 if (_textures[i])
162 _textures[i]->Release();
163 }
164}
165
166bool GstD3D11VideoBuffer::validatePlaneHandles() const
167{
168 // Constructor-time resolve failed (device mismatch, null resource, staging copy) — let the factory demote to CPU.
169 if (!_resolved) {
170 return false;
171 }
172 return validatePlanes([](GstMemory* mem) {
173 if (!mem || !gst_is_d3d11_memory(mem))
174 return false;
175 // Cheap field read; confirms the wrapper actually backs an ID3D11Texture2D.
176 return gst_d3d11_memory_get_resource_handle(GST_D3D11_MEMORY_CAST(mem)) != nullptr;
177 });
178}
179
180void GstD3D11VideoBuffer::resolvePlaneResources()
181{
182 GstBuffer* buffer = _sample ? gst_sample_get_buffer(_sample) : nullptr;
183 if (!buffer)
184 return;
185
186 const int memCount = (std::min)(int(gst_buffer_n_memory(buffer)), kMaxPlanes);
187 GstD3DVideoBufferCommon::PlaneResourceGuard<ID3D11Texture2D> guard;
188 GstD3D11Device* copyDevice = nullptr;
189
190 for (int i = 0; i < memCount; ++i) {
191 GstMemory* mem = gst_buffer_peek_memory(buffer, i);
192 if (!mem || !gst_is_d3d11_memory(mem)) {
193 QGC_HW_WARN_ONCE(GstD3D11Log, s_diag.loggedNonD3DMemory,
194 "resolve: plane" << i << "memory is not GstD3D11Memory (allocator="
195 << (mem && mem->allocator ? mem->allocator->mem_type : "null") << ")");
196 return;
197 }
198 // Device guard on plane 0: gst-d3d11 may land on an isolated device when NEED_CONTEXT was preempted; sampling a
199 // foreign-device texture from QRhi corrupts silently.
200 if (i == 0) {
201 // currentDevice() is transfer-full — unref both branches to avoid UAF after reset().
202 GstD3D11Device* bridgeDev = GstD3D11ContextBridge::currentDevice();
203 GstD3D11Device* bufDev = GST_D3D11_MEMORY_CAST(mem)->device;
204 if (bridgeDev && bufDev != bridgeDev) {
205 const gint64 bridgeLuid = GstD3DContextBridgeCommon::readAdapterLuid(bridgeDev);
206 const gint64 bufLuid = GstD3DContextBridgeCommon::readAdapterLuid(bufDev);
207 gst_object_unref(bridgeDev);
208 QGC_HW_WARN_ONCE(GstD3D11Log, s_diag.loggedDeviceMismatch,
209 "resolve: GstD3D11Memory on foreign device (bridge LUID="
210 << bridgeLuid << "buffer LUID=" << bufLuid
211 << "); bridge missed NEED_CONTEXT race — rejecting frame");
212 return;
213 }
214 if (bridgeDev)
215 gst_object_unref(bridgeDev);
216 }
217 ID3D11Texture2D* tex =
218 reinterpret_cast<ID3D11Texture2D*>(gst_d3d11_memory_get_resource_handle(GST_D3D11_MEMORY_CAST(mem)));
219 if (!tex) {
220 QGC_HW_WARN_ONCE(GstD3D11Log, s_diag.loggedNullResource,
221 "resolve: gst_d3d11_memory_get_resource_handle returned null for plane" << i);
222 return;
223 }
224 // QRhi::createFrom has no subresource selector — copy array slices here on the streaming thread.
225 const guint subIdx = gst_d3d11_memory_get_subresource_index(GST_D3D11_MEMORY_CAST(mem));
226 D3D11_TEXTURE2D_DESC srcDesc = {};
227 tex->GetDesc(&srcDesc);
228 if (subIdx > 0 || srcDesc.ArraySize > 1) {
229 ID3D11Texture2D* stagingTex = copySliceToStaging(tex, subIdx, i, srcDesc, GST_D3D11_MEMORY_CAST(mem));
230 if (!stagingTex) {
231 return;
232 }
233 copyDevice = GST_D3D11_MEMORY_CAST(mem)->device;
234 guard.handles[i] = stagingTex;
235 } else {
236 tex->AddRef();
237 guard.handles[i] = tex;
238 }
239 guard.owned = i + 1;
240 }
241
242 // Single flush for every staged slice instead of one per plane: QRhi must see the copies in the immediate queue.
243 if (copyDevice) {
244 ID3D11DeviceContext* d3dCtx = gst_d3d11_device_get_device_context_handle(copyDevice);
245 gst_d3d11_device_lock(copyDevice);
246 d3dCtx->Flush();
247 gst_d3d11_device_unlock(copyDevice);
248 }
249
250 _textures = guard.handles;
251 _resolvedCount = memCount;
252 _resolved = true;
253 guard.commit();
254}
255
256QVideoFrameTexturesUPtr GstD3D11VideoBuffer::mapTextures(QRhi& rhi, QVideoFrameTexturesUPtr& old)
257{
258 // GstD3D11ContextBridge must be primed; without a shared device createFrom() silently renders garbage.
259 GstBuffer* buffer = nullptr;
260 if (!checkMapPreconditions(rhi, static_cast<int>(QRhi::D3D11), GstD3D11Log(), s_diag, buffer)) {
261 return GstHwPathTelemetry::fail(HwVideoBufferPath::D3D11);
262 }
263 if (!_resolved) {
264 return GstHwPathTelemetry::fail(HwVideoBufferPath::D3D11);
265 }
266
267 return GstD3DVideoBufferCommon::mapResolvedTextures(
268 *this, rhi, old, HwVideoBufferPath::D3D11, _textures, _resolvedCount, _format.frameSize(),
269 _format.pixelFormat(), s_diag.loggedFirstSuccess, s_diag.loggedTextureCreateFail, GstD3D11Log, "D3D11");
270}
271
272#endif // Q_OS_WIN && QGC_HAS_GST_D3D11_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)
Common base for GStreamer-backed QHwVideoBuffer subclasses.
void recordImageCacheHit(HwVideoBufferPath path) noexcept
Native image/texture cache hit/miss accounting.
void recordImageCacheMiss(HwVideoBufferPath path) noexcept
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)