QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstD3D12VideoBuffer.cc
Go to the documentation of this file.
2
3#if defined(Q_OS_WIN) && defined(QGC_HAS_GST_D3D12_GPU_PATH)
4
5#include <QtCore/QMutex>
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <d3d12.h>
10#include <gst/d3d12/gstd3d12.h>
11#include <utility>
12
17#include "GstHwImportCache.h"
18#include "GstHwPathTelemetry.h"
19#include "QGCLoggingCategory.h"
20
21QGC_LOGGING_CATEGORY(GstD3D12Log, "Video.GStreamer.HwBuffers.GstD3D12Buf")
22
23namespace {
24
25using GstD3DVideoBufferCommon::kMaxPlanes;
26using GstD3DVideoBufferCommon::MapDiagnostics;
27using D3D12FrameTextures = GstD3DVideoBufferCommon::FrameTextures<ID3D12Resource>;
28using GstD3DVideoBufferCommon::StagingKey;
29using GstD3DVideoBufferCommon::StagingKeyHash;
30
31MapDiagnostics s_diag;
32
36struct StagingEntry
37{
38 ID3D12Resource* resource = nullptr;
39 ID3D12CommandAllocator* allocator = nullptr;
40 ID3D12GraphicsCommandList* list = nullptr;
41};
42
43void releaseEntryResources(const StagingEntry& e)
44{
45 if (e.list)
46 e.list->Release();
47 if (e.allocator)
48 e.allocator->Release();
49 if (e.resource)
50 e.resource->Release();
51}
52
53class StagingEntryRef
54{
55public:
56 StagingEntryRef() = default;
57
58 explicit StagingEntryRef(const StagingEntry& e) : _entry(e) {}
59
60 ~StagingEntryRef() { releaseEntryResources(_entry); }
61
62 StagingEntryRef(const StagingEntryRef&) = delete;
63 StagingEntryRef& operator=(const StagingEntryRef&) = delete;
64
65 StagingEntryRef(StagingEntryRef&& other) noexcept : _entry(other._entry) { other._entry = {}; }
66
67 StagingEntryRef& operator=(StagingEntryRef&& other) noexcept
68 {
69 if (this != &other) {
70 releaseEntryResources(_entry);
71 _entry = other._entry;
72 other._entry = {};
73 }
74 return *this;
75 }
76
77 bool valid() const noexcept { return _entry.resource && _entry.allocator && _entry.list; }
78
79 ID3D12Resource* resource() const noexcept { return _entry.resource; }
80
81 ID3D12CommandAllocator* allocator() const noexcept { return _entry.allocator; }
82
83 ID3D12GraphicsCommandList* list() const noexcept { return _entry.list; }
84
85 ID3D12Resource* takeResource() noexcept
86 {
87 ID3D12Resource* resource = _entry.resource;
88 _entry.resource = nullptr;
89 return resource;
90 }
91
92private:
93 StagingEntry _entry;
94};
95
96StagingEntryRef addRefEntry(const StagingEntry& e)
97{
98 if (e.resource)
99 e.resource->AddRef();
100 if (e.allocator)
101 e.allocator->AddRef();
102 if (e.list)
103 e.list->AddRef();
104 return StagingEntryRef(e);
105}
106
109class StagingResourcePool
110{
111public:
112 static StagingResourcePool& instance()
113 {
114 static StagingResourcePool pool;
115 return pool;
116 }
117
118 ~StagingResourcePool() { clear(); }
119
122 StagingEntryRef acquire(ID3D12Device* dev, D3D12_COMMAND_LIST_TYPE queueType, const StagingKey& key,
123 const D3D12_RESOURCE_DESC& dstDesc, const D3D12_HEAP_PROPERTIES& heapProps, int planeIdx,
124 guint subIdx)
125 {
126 QMutexLocker lock(&_mutex);
127 StagingEntry* cachedEntry = _entries.find(key);
128 if (cachedEntry && cachedEntry->resource && cachedEntry->allocator && cachedEntry->list) {
130 return addRefEntry(*cachedEntry);
131 }
132
133 StagingEntry e;
134 if (FAILED(dev->CreateCommittedResource(&heapProps, D3D12_HEAP_FLAG_NONE, &dstDesc, D3D12_RESOURCE_STATE_COMMON,
135 nullptr, IID_PPV_ARGS(&e.resource)))) {
136 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedTextureCreateFail,
137 "mapTextures: CreateCommittedResource for slice copy failed (plane="
138 << planeIdx << " subresource=" << subIdx << ")");
139 return {};
140 }
141 if (FAILED(dev->CreateCommandAllocator(queueType, IID_PPV_ARGS(&e.allocator))) ||
142 FAILED(dev->CreateCommandList(0, queueType, e.allocator, nullptr, IID_PPV_ARGS(&e.list)))) {
143 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedTextureCreateFail,
144 "mapTextures: command allocator/list create failed (plane=" << planeIdx << ")");
145 if (e.list)
146 e.list->Release();
147 if (e.allocator)
148 e.allocator->Release();
149 e.resource->Release();
150 return {};
151 }
152 // Freshly created lists are open; close so the per-frame Reset() path is uniform.
153 e.list->Close();
154
155 StagingEntryRef ref = addRefEntry(e);
156 _entries.insert(key, e);
158 return ref;
159 }
160
161 void clear()
162 {
163 QMutexLocker lock(&_mutex);
164 _entries.clear();
165 }
166
167private:
168 static constexpr std::size_t kMaxEntries = 8;
169 QMutex _mutex;
171 kMaxEntries,
172 [](const StagingKey&, StagingEntry& e) {
173 releaseEntryResources(e);
174 e = {};
175 }};
176};
177
181ID3D12Resource* copySliceToStaging(ID3D12Resource* resource, guint subIdx, int planeIdx,
182 const D3D12_RESOURCE_DESC& srcDesc, ID3D12Device* d3dDev, GstD3D12CmdQueue* cmdQueue,
183 D3D12_COMMAND_LIST_TYPE queueType, guint64& maxFenceValue,
184 StagingEntryRef& retainedEntry)
185{
186 D3D12_RESOURCE_DESC dstDesc = srcDesc;
187 dstDesc.DepthOrArraySize = 1;
188 dstDesc.MipLevels = 1;
189
190 D3D12_HEAP_PROPERTIES heapProps = {};
191 heapProps.Type = D3D12_HEAP_TYPE_DEFAULT;
192
193 const StagingKey key{srcDesc.Width, srcDesc.Height, UINT(srcDesc.Format), planeIdx};
194 StagingEntryRef entry =
195 StagingResourcePool::instance().acquire(d3dDev, queueType, key, dstDesc, heapProps, planeIdx, subIdx);
196 if (!entry.valid()) {
197 return nullptr;
198 }
199
200 if (FAILED(entry.allocator()->Reset()) || FAILED(entry.list()->Reset(entry.allocator(), nullptr))) {
201 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedTextureCreateFail,
202 "mapTextures: command allocator/list reset failed (plane=" << planeIdx << ")");
203 return nullptr;
204 }
205 ID3D12GraphicsCommandList* cmdList = entry.list();
206
207 D3D12_TEXTURE_COPY_LOCATION srcLoc = {};
208 srcLoc.pResource = resource;
209 srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
210 srcLoc.SubresourceIndex = subIdx;
211
212 D3D12_TEXTURE_COPY_LOCATION dstLoc = {};
213 dstLoc.pResource = entry.resource();
214 dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
215 dstLoc.SubresourceIndex = 0;
216
217 // Decoder output is parked in COMMON by gst_d3d12_memory_sync; gst-d3d12 exposes no per-resource state getter, so
218 // COMMON is the only valid before-state.
219 D3D12_RESOURCE_BARRIER toCopySrc = {};
220 toCopySrc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
221 toCopySrc.Transition.pResource = resource;
222 toCopySrc.Transition.Subresource = subIdx;
223 toCopySrc.Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON;
224 toCopySrc.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
225 cmdList->ResourceBarrier(1, &toCopySrc);
226
227 cmdList->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, nullptr);
228
229 D3D12_RESOURCE_BARRIER toCommon = toCopySrc;
230 toCommon.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE;
231 toCommon.Transition.StateAfter = D3D12_RESOURCE_STATE_COMMON;
232 cmdList->ResourceBarrier(1, &toCommon);
233
234 cmdList->Close();
235
236 ID3D12CommandList* lists[] = {cmdList};
237 guint64 fenceValue = 0;
238 // Wrapper execute takes gst-d3d12's queue lock, serializing the copy against decoder-thread submits.
239 if (FAILED(gst_d3d12_cmd_queue_execute_command_lists(cmdQueue, 1, lists, &fenceValue))) {
240 // The caller skips the fence wait on failure, so the pooled allocator's GPU work may still be in flight; drop
241 // the pool so the next frame rebuilds clean command objects instead of resetting a busy allocator.
242 StagingResourcePool::instance().clear();
243 return nullptr;
244 }
245 if (fenceValue > maxFenceValue) {
246 maxFenceValue = fenceValue;
247 }
248 ID3D12Resource* stagingResource = entry.takeResource();
249 retainedEntry = std::move(entry);
250 return stagingResource;
251}
252
253} // namespace
254
255void GstD3D12VideoBuffer::resetCachedState() noexcept
256{
257 StagingResourcePool::instance().clear();
258 s_diag.reset();
259}
260
261namespace {
262struct D3D12CacheResetRegistrar
263{
264 D3D12CacheResetRegistrar() { GstContextBridgeRegistry::registerCacheReset(&GstD3D12VideoBuffer::resetCachedState); }
265};
266
267const D3D12CacheResetRegistrar s_d3d12CacheResetRegistrar;
268} // namespace
269
270GstD3D12VideoBuffer::GstD3D12VideoBuffer(GstSample* sample, const GstVideoInfo& videoInfo,
271 const QVideoFrameFormat& format)
272 : GstHwVideoBuffer(QVideoFrame::RhiTextureHandle, sample, videoInfo, format)
273{
274 // Resolve on the streaming thread, never in mapTextures (render thread); blocking here is harmless since the
275 // decoder is async.
276 resolvePlaneResources();
277}
278
279GstD3D12VideoBuffer::~GstD3D12VideoBuffer()
280{
281 for (int i = 0; i < _resolvedCount; ++i) {
282 if (_resources[i])
283 _resources[i]->Release();
284 }
285}
286
287bool GstD3D12VideoBuffer::validatePlaneHandles() const
288{
289 // Constructor-time resolve failed (device mismatch, sync/copy failure) — let the factory demote to CPU.
290 if (!_resolved) {
291 return false;
292 }
293 return validatePlanes([](GstMemory* mem) {
294 if (!mem || !gst_is_d3d12_memory(mem))
295 return false;
296 return gst_d3d12_memory_get_resource_handle(GST_D3D12_MEMORY_CAST(mem)) != nullptr;
297 });
298}
299
300void GstD3D12VideoBuffer::resolvePlaneResources()
301{
302 GstBuffer* buffer = _sample ? gst_sample_get_buffer(_sample) : nullptr;
303 if (!buffer)
304 return;
305
306 const int memCount = (std::min)(int(gst_buffer_n_memory(buffer)), kMaxPlanes);
307 GstD3DVideoBufferCommon::PlaneResourceGuard<ID3D12Resource> guard;
308 std::array<StagingEntryRef, kMaxPlanes> stagingLeases;
309
310 // Shared copy state: the queue is resolved lazily on the first staged plane (constant per device) and all planes
311 // submit to it, then a single fence wait covers every submit. The guard unrefs the held queue ref on any early
312 // return path.
313 struct CmdQueueRef
314 {
315 GstD3D12CmdQueue* q = nullptr;
316
317 ~CmdQueueRef()
318 {
319 if (q)
320 gst_object_unref(q);
321 }
322 } queueRef;
323
324 D3D12_COMMAND_LIST_TYPE queueType = D3D12_COMMAND_LIST_TYPE_COPY;
325 guint64 maxFenceValue = 0;
326 bool anyCopy = false;
327 struct SubmittedCopyWait
328 {
329 GstD3D12CmdQueue*& queue;
330 guint64& maxFenceValue;
331 bool& anyCopy;
332 bool armed = true;
333
334 ~SubmittedCopyWait()
335 {
336 if (armed)
337 wait();
338 }
339
340 bool waitAndDisarm()
341 {
342 armed = false;
343 return wait();
344 }
345
346 bool wait()
347 {
348 if (!anyCopy || !queue) {
349 return true;
350 }
352 if (FAILED(gst_d3d12_cmd_queue_fence_wait(queue, maxFenceValue))) {
353 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedTextureCreateFail,
354 "resolve: gst_d3d12_cmd_queue_fence_wait failed");
355 return false;
356 }
357 return true;
358 }
359 } submittedCopyWait{queueRef.q, maxFenceValue, anyCopy};
360
361 for (int i = 0; i < memCount; ++i) {
362 GstMemory* mem = gst_buffer_peek_memory(buffer, i);
363 if (!mem || !gst_is_d3d12_memory(mem)) {
364 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedNonD3DMemory,
365 "resolve: plane" << i << "memory is not GstD3D12Memory (allocator="
366 << (mem && mem->allocator ? mem->allocator->mem_type : "null") << ")");
367 return;
368 }
369 GstD3D12Memory* d3dmem = GST_D3D12_MEMORY_CAST(mem);
370 // Device guard (plane 0): gst-d3d12 may land on an isolated device when NEED_CONTEXT was preempted, and
371 // importing a foreign-device resource into QRhi corrupts.
372 if (i == 0) {
373 // currentDevice() is transfer-full — unref both branches to avoid UAF after reset().
374 GstD3D12Device* bridgeDev = GstD3D12ContextBridge::currentDevice();
375 if (bridgeDev && d3dmem->device != bridgeDev) {
376 const gint64 bridgeLuid = GstD3DContextBridgeCommon::readAdapterLuid(bridgeDev);
377 const gint64 bufLuid = GstD3DContextBridgeCommon::readAdapterLuid(d3dmem->device);
378 gst_object_unref(bridgeDev);
379 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedDeviceMismatch,
380 "resolve: GstD3D12Memory on foreign device (bridge LUID="
381 << bridgeLuid << "buffer LUID=" << bufLuid
382 << "); bridge missed NEED_CONTEXT race — rejecting frame");
383 return;
384 }
385 if (bridgeDev)
386 gst_object_unref(bridgeDev);
387 }
388 // Block on the decoder's fence before reading, else QRhi samples mid-write while the decoder is still writing
389 // this resource.
390 if (!gst_d3d12_memory_sync(d3dmem)) {
391 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedNullResource,
392 "resolve: gst_d3d12_memory_sync failed for plane" << i);
393 return;
394 }
395 ID3D12Resource* resource = gst_d3d12_memory_get_resource_handle(d3dmem);
396 if (!resource) {
397 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedNullResource,
398 "resolve: gst_d3d12_memory_get_resource_handle returned null for plane" << i);
399 return;
400 }
401 // QRhi::createFrom has no subresource parameter — copy the slice (with its blocking fence wait) here, not in
402 // mapTextures.
403 guint subIdx = 0;
404 gst_d3d12_memory_get_subresource_index(d3dmem, guint(i), &subIdx);
405 D3D12_RESOURCE_DESC srcDesc = resource->GetDesc();
406 if (subIdx > 0 || srcDesc.DepthOrArraySize > 1) {
407 ID3D12Device* d3dDev = gst_d3d12_device_get_device_handle(d3dmem->device);
408 if (!queueRef.q) {
409 queueType = D3D12_COMMAND_LIST_TYPE_COPY;
410 GstD3D12CmdQueue* q = gst_d3d12_device_get_cmd_queue(d3dmem->device, queueType);
411 if (!q) {
412 queueType = D3D12_COMMAND_LIST_TYPE_DIRECT;
413 q = gst_d3d12_device_get_cmd_queue(d3dmem->device, queueType);
414 }
415 if (q) {
416 // Hold a ref for the whole loop so it serializes against gst-d3d12's queue lock like the
417 // decoder-thread submits do.
418 gst_object_ref(q);
419 queueRef.q = q;
420 }
421 }
422 if (!d3dDev || !queueRef.q) {
423 QGC_HW_WARN_ONCE(GstD3D12Log, s_diag.loggedTextureCreateFail,
424 "mapTextures: could not obtain D3D12 device/queue for slice copy (plane=" << i << ")");
425 return;
426 }
427 ID3D12Resource* stagingResource = copySliceToStaging(resource, subIdx, i, srcDesc, d3dDev, queueRef.q,
428 queueType, maxFenceValue, stagingLeases[i]);
429 if (!stagingResource) {
430 return;
431 }
432 anyCopy = true;
433 guard.handles[i] = stagingResource;
434 } else {
435 resource->AddRef();
436 guard.handles[i] = resource;
437 }
438 guard.owned = i + 1;
439 }
440
441 // One fence wait on the highest value covers every plane (same queue, in order) and guarantees the pooled
442 // allocators are idle before the next frame resets them.
443 if (!submittedCopyWait.waitAndDisarm()) {
444 return;
445 }
446
447 _resources = guard.handles;
448 _resolvedCount = memCount;
449 _resolved = true;
450 guard.commit();
451}
452
453QVideoFrameTexturesUPtr GstD3D12VideoBuffer::mapTextures(QRhi& rhi, QVideoFrameTexturesUPtr& old)
454{
455 // GstD3D12ContextBridge wires a shared adapter; without it createFrom() succeeds but resources sit on an isolated
456 // device and rendering corrupts.
457 GstBuffer* buffer = nullptr;
458 if (!checkMapPreconditions(rhi, static_cast<int>(QRhi::D3D12), GstD3D12Log(), s_diag, buffer)) {
459 return GstHwPathTelemetry::fail(HwVideoBufferPath::D3D12);
460 }
461 if (!_resolved) {
462 // Failure cause was warned in resolvePlaneResources() on the streaming thread.
463 return GstHwPathTelemetry::fail(HwVideoBufferPath::D3D12);
464 }
465
466 return GstD3DVideoBufferCommon::mapResolvedTextures(
467 *this, rhi, old, HwVideoBufferPath::D3D12, _resources, _resolvedCount, _format.frameSize(),
468 _format.pixelFormat(), s_diag.loggedFirstSuccess, s_diag.loggedTextureCreateFail, GstD3D12Log, "D3D12");
469}
470
471#endif // Q_OS_WIN && QGC_HAS_GST_D3D12_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 recordSyncWait(HwVideoBufferPath path, bool gpuSide) noexcept
GL fence sync wait; split CPU-blocking vs GPU-side.
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)