QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstDmaBufVulkanImport.cc
Go to the documentation of this file.
2
3#if defined(QGC_HAS_GST_DMABUF_GPU_PATH) && defined(QGC_HAS_GST_VULKAN_GPU_PATH)
4
5#include <QtCore/QLoggingCategory>
6#include <QtCore/QScopeGuard>
7#include <QtGui/QVulkanInstance>
8#include <algorithm>
9#include <atomic>
10#include <drm_fourcc.h>
11#include <fcntl.h>
12#include <gst/allocators/gstdmabuf.h>
13#include <gst/video/video.h>
14#include <limits>
15#include <mutex>
16#include <rhi/qrhi.h>
17#include <rhi/qrhi_platform.h>
18#include <unistd.h>
19#include <vulkan/vulkan.h>
20
22#include "GstDmaFourcc.h"
24#include "GstHwPathTelemetry.h"
25#include "GstHwVideoBuffer.h"
27#include "QGCLoggingCategory.h"
28
29QGC_LOGGING_CATEGORY(GstDmaBufVulkanLog, "Video.GStreamer.HwBuffers.GstDmaBufVulkan")
30
31namespace {
32
33std::atomic<bool> s_loggedVulkanUnimpl{false};
34
35// DRM fourcc -> single-plane VkFormat for formats importable as one VkImage. Returns VK_FORMAT_UNDEFINED for layouts
36// that need a multi-disjoint-plane import (handled by the CPU fallback).
37VkFormat vkFormatForDrmFourcc(int fourcc)
38{
39 switch (fourcc) {
40 case DRM_FORMAT_ABGR8888:
41 case DRM_FORMAT_XBGR8888:
42 return VK_FORMAT_R8G8B8A8_UNORM;
43 case DRM_FORMAT_ARGB8888:
44 case DRM_FORMAT_XRGB8888:
45 return VK_FORMAT_B8G8R8A8_UNORM;
46 case DRM_FORMAT_NV12:
47 return VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
48 case DRM_FORMAT_P010:
49 return VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16;
50 case DRM_FORMAT_YUV420:
51 return VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM;
52 default:
53 return VK_FORMAT_UNDEFINED;
54 }
55}
56
57// Process-cached Vulkan dispatch for the DMABuf import. Device-level entry points are resolved through
58// vkGetDeviceProcAddr (itself obtained from the instance loader); vkGetPhysicalDeviceMemoryProperties stays on the
59// instance loader. The QRhi VkDevice is process-stable, so a single resolve serves every frame.
60struct VulkanImportFns
61{
62 PFN_vkCreateImage createImage = nullptr;
63 PFN_vkDestroyImage destroyImage = nullptr;
64 PFN_vkAllocateMemory allocateMemory = nullptr;
65 PFN_vkFreeMemory freeMemory = nullptr;
66 PFN_vkBindImageMemory bindImageMemory = nullptr;
67 PFN_vkGetImageMemoryRequirements getImageMemoryRequirements = nullptr;
68 PFN_vkGetMemoryFdPropertiesKHR getMemoryFdProperties = nullptr;
69 PFN_vkGetPhysicalDeviceMemoryProperties getPhysicalDeviceMemoryProperties = nullptr;
70
71 bool ok() const noexcept
72 {
73 return createImage && destroyImage && allocateMemory && freeMemory && bindImageMemory &&
74 getImageMemoryRequirements && getMemoryFdProperties && getPhysicalDeviceMemoryProperties;
75 }
76};
77
78const VulkanImportFns& resolveVulkanImportFns(QVulkanInstance& inst, VkDevice dev)
79{
80 static VulkanImportFns fns;
81 static std::once_flag once;
82 std::call_once(once, [&] {
83 const auto getDeviceProcAddr =
84 reinterpret_cast<PFN_vkGetDeviceProcAddr>(inst.getInstanceProcAddr("vkGetDeviceProcAddr"));
85 const auto dev_fn = [&](const char* name) -> PFN_vkVoidFunction {
86 return getDeviceProcAddr ? getDeviceProcAddr(dev, name) : nullptr;
87 };
88 fns.createImage = reinterpret_cast<PFN_vkCreateImage>(dev_fn("vkCreateImage"));
89 fns.destroyImage = reinterpret_cast<PFN_vkDestroyImage>(dev_fn("vkDestroyImage"));
90 fns.allocateMemory = reinterpret_cast<PFN_vkAllocateMemory>(dev_fn("vkAllocateMemory"));
91 fns.freeMemory = reinterpret_cast<PFN_vkFreeMemory>(dev_fn("vkFreeMemory"));
92 fns.bindImageMemory = reinterpret_cast<PFN_vkBindImageMemory>(dev_fn("vkBindImageMemory"));
93 fns.getImageMemoryRequirements =
94 reinterpret_cast<PFN_vkGetImageMemoryRequirements>(dev_fn("vkGetImageMemoryRequirements"));
95 fns.getMemoryFdProperties =
96 reinterpret_cast<PFN_vkGetMemoryFdPropertiesKHR>(dev_fn("vkGetMemoryFdPropertiesKHR"));
97 fns.getPhysicalDeviceMemoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
98 inst.getInstanceProcAddr("vkGetPhysicalDeviceMemoryProperties"));
99 });
100 return fns;
101}
102
103quint64 dmaMemoryBound(GstMemory* memory, const GstVideoInfo& videoInfo) noexcept
104{
105 gsize maxSize = 0;
106 const gsize size = memory ? gst_memory_get_sizes(memory, nullptr, &maxSize) : 0;
107 return std::max({static_cast<quint64>(GST_VIDEO_INFO_SIZE(&videoInfo)), static_cast<quint64>(size),
108 static_cast<quint64>(maxSize)});
109}
110
111bool validateVulkanPlaneLayouts(const GstVideoInfo& videoInfo, GstVideoMeta* vmeta, int planeCount, quint64 memoryBound,
112 VkSubresourceLayout* planeLayouts)
113{
114 const int width = GST_VIDEO_INFO_WIDTH(&videoInfo);
115 const int height = GST_VIDEO_INFO_HEIGHT(&videoInfo);
116 if (width <= 0 || height <= 0) {
117 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: invalid DMABuf dimensions" << width << "x" << height;
118 return false;
119 }
120
121 const int componentCount = GST_VIDEO_INFO_N_COMPONENTS(&videoInfo);
122 if (componentCount <= 0 || planeCount > componentCount) {
123 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: invalid DMABuf plane count" << planeCount << "components"
124 << componentCount;
125 return false;
126 }
127 if (vmeta && static_cast<int>(vmeta->n_planes) < planeCount) {
128 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: GstVideoMeta has too few planes" << vmeta->n_planes
129 << "expected" << planeCount;
130 return false;
131 }
132
133 constexpr quint64 kMaxVkDeviceSize = static_cast<quint64>((std::numeric_limits<VkDeviceSize>::max)());
134 for (int p = 0; p < planeCount; ++p) {
135 const gsize offset = vmeta ? vmeta->offset[p] : GST_VIDEO_INFO_PLANE_OFFSET(&videoInfo, p);
136 const gint stride = vmeta ? vmeta->stride[p] : GST_VIDEO_INFO_PLANE_STRIDE(&videoInfo, p);
137 const int planeWidth = GST_VIDEO_INFO_COMP_WIDTH(&videoInfo, p);
138 const int planeHeight = GST_VIDEO_INFO_COMP_HEIGHT(&videoInfo, p);
139 const int pixelStride = GST_VIDEO_INFO_COMP_PSTRIDE(&videoInfo, p);
140
141 if (stride <= 0 || planeWidth <= 0 || planeHeight <= 0 || pixelStride <= 0) {
142 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: invalid DMABuf plane layout"
143 << "plane" << p << "stride" << stride << "width" << planeWidth << "height"
144 << planeHeight << "pixelStride" << pixelStride;
145 return false;
146 }
147
148 const quint64 rowPitch = static_cast<quint64>(stride);
149 const quint64 minRowBytes = static_cast<quint64>(planeWidth) * static_cast<quint64>(pixelStride);
150 if (rowPitch < minRowBytes || offset > kMaxVkDeviceSize || rowPitch > kMaxVkDeviceSize) {
151 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: implausible DMABuf plane layout"
152 << "plane" << p << "offset" << static_cast<quint64>(offset) << "stride"
153 << stride << "minRowBytes" << minRowBytes;
154 return false;
155 }
156
157 if (rowPitch > std::numeric_limits<quint64>::max() / static_cast<quint64>(planeHeight)) {
158 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: DMABuf plane size overflow"
159 << "plane" << p << "stride" << stride << "height" << planeHeight;
160 return false;
161 }
162 const quint64 planeSize = rowPitch * static_cast<quint64>(planeHeight);
163 if (static_cast<quint64>(offset) > std::numeric_limits<quint64>::max() - planeSize) {
164 qCWarning(GstDmaBufVulkanLog)
165 << "Vulkan import: DMABuf plane end overflow"
166 << "plane" << p << "offset" << static_cast<quint64>(offset) << "planeSize" << planeSize;
167 return false;
168 }
169 const quint64 planeEnd = static_cast<quint64>(offset) + planeSize;
170 if (memoryBound > 0 && planeEnd > memoryBound) {
171 qCWarning(GstDmaBufVulkanLog) << "Vulkan import: DMABuf plane exceeds buffer bounds"
172 << "plane" << p << "end" << planeEnd << "bound" << memoryBound;
173 return false;
174 }
175
176 planeLayouts[p].offset = static_cast<VkDeviceSize>(offset);
177 planeLayouts[p].rowPitch = static_cast<VkDeviceSize>(rowPitch);
178 }
179 return true;
180}
181
182} // namespace
183
184namespace GstDmaBufVulkan {
185
186void resetLoggedState() noexcept
187{
188 s_loggedVulkanUnimpl.store(false, std::memory_order_release);
189}
190
191} // namespace GstDmaBufVulkan
192
193QVideoFrameTexturesUPtr GstDmaBufVideoBuffer::importVulkan(QRhi& rhi)
194{
195 // dmabuf-fd -> VkImage via VK_EXT_external_memory_dma_buf + VkImageDrmFormatModifierExplicitCreateInfoEXT, wrapped
196 // with QRhiTexture::createFrom. Every step is capability-gated; any miss returns fail() so the caller does the CPU
197 // upload. Single VkImage per buffer only (multi-plane YUV uses disjoint VkFormat planes); odd layouts fall back.
198 const auto* nh = static_cast<const QRhiVulkanNativeHandles*>(rhi.nativeHandles());
199 if (!nh || nh->dev == VK_NULL_HANDLE || nh->physDev == VK_NULL_HANDLE || !nh->inst) {
200 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl, "Vulkan import: native device handles unavailable");
201 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
202 }
203 const VkDevice dev = nh->dev;
204 // Resolved once per process (see VulkanImportFns); an instance-level lookup of device functions may return a
205 // trampoline or null.
206 const VulkanImportFns& fns = resolveVulkanImportFns(*nh->inst, dev);
207 if (!fns.ok()) {
208 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
209 "Vulkan import: required device functions (VK_EXT_external_memory_dma_buf) unavailable");
210 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
211 }
212 const auto pfnCreateImage = fns.createImage;
213 const auto pfnDestroyImage = fns.destroyImage;
214 const auto pfnAllocateMemory = fns.allocateMemory;
215 const auto pfnFreeMemory = fns.freeMemory;
216 const auto pfnBindImageMemory = fns.bindImageMemory;
217 const auto pfnGetMemReq = fns.getImageMemoryRequirements;
218 const auto pfnGetFdProps = fns.getMemoryFdProperties;
219 const auto pfnGetPhysMemProps = fns.getPhysicalDeviceMemoryProperties;
220
221 GstBuffer* buffer = gst_sample_get_buffer(_sample);
222 if (!buffer || gst_buffer_n_memory(buffer) != 1) {
223 // Multi-fd disjoint planes: out of scope for this conservative single-VkImage import.
224 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
225 "Vulkan import: multi-fd DMABuf not supported — CPU upload");
226 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
227 }
228 const int fourcc = GstHw::drmFourccForSingleFd(_videoInfo);
229 const VkFormat vkFormat = vkFormatForDrmFourcc(fourcc);
230 if (vkFormat == VK_FORMAT_UNDEFINED) {
231 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
232 "Vulkan import: no VkFormat for DMABuf layout — CPU upload");
233 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
234 }
235
236 // Pre-flight RHI format/size support before building the VkImage so an unsupported import demotes to CPU on a
237 // query.
238 if (!GstHwImportPreflight::preflightOrRecord(&rhi, HwVideoBufferPath::DmaBuf, _format.pixelFormat(),
239 _format.frameSize())) {
240 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
241 }
242
243 GstMemory* gstMemory = gst_buffer_peek_memory(buffer, 0);
244 const int fd = gst_dmabuf_memory_get_fd(gstMemory);
245 if (fd < 0) {
246 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
247 }
248 // memory-plane count == video-plane count only for the uncompressed modifiers this path accepts; a compressed
249 // modifier (e.g. CCS) adds metadata planes and would need the modifier's own plane count here.
250 const int planeCount = std::clamp(int(GST_VIDEO_INFO_N_PLANES(&_videoInfo)), 1, GstHw::kMaxPlanes);
251 GstVideoMeta* vmeta = gst_buffer_get_video_meta(buffer);
252 VkSubresourceLayout planeLayouts[GstHw::kMaxPlanes] = {};
253 if (!validateVulkanPlaneLayouts(_videoInfo, vmeta, planeCount, dmaMemoryBound(gstMemory, _videoInfo), planeLayouts)) {
254 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
255 }
256
257 VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {};
258 modInfo.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT;
259 modInfo.drmFormatModifier = _drmModifier;
260 modInfo.drmFormatModifierPlaneCount = static_cast<uint32_t>(planeCount);
261 modInfo.pPlaneLayouts = planeLayouts;
262
263 VkExternalMemoryImageCreateInfo extImg = {};
264 extImg.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
265 extImg.pNext = &modInfo;
266 extImg.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
267
268 VkImageCreateInfo imgInfo = {};
269 imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
270 imgInfo.pNext = &extImg;
271 imgInfo.imageType = VK_IMAGE_TYPE_2D;
272 imgInfo.format = vkFormat;
273 imgInfo.extent = {static_cast<uint32_t>(GST_VIDEO_INFO_WIDTH(&_videoInfo)),
274 static_cast<uint32_t>(GST_VIDEO_INFO_HEIGHT(&_videoInfo)), 1};
275 imgInfo.mipLevels = 1;
276 imgInfo.arrayLayers = 1;
277 imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
278 imgInfo.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT;
279 imgInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
280 imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
281 imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
282
283 VkImage image = VK_NULL_HANDLE;
284 if (pfnCreateImage(dev, &imgInfo, nullptr, &image) != VK_SUCCESS || image == VK_NULL_HANDLE) {
285 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl, "Vulkan import: vkCreateImage failed — CPU upload");
286 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
287 }
288 const auto destroyImage = qScopeGuard([&] {
289 if (image != VK_NULL_HANDLE)
290 pfnDestroyImage(dev, image, nullptr);
291 });
292
293 // dup the fd: Vulkan takes ownership of the imported fd on successful allocate.
294 const int dupFd = ::fcntl(fd, F_DUPFD_CLOEXEC, 0);
295 if (dupFd < 0) {
296 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
297 }
298 bool fdConsumed = false;
299 const auto closeFd = qScopeGuard([&] {
300 if (!fdConsumed && dupFd >= 0)
301 ::close(dupFd);
302 });
303
304 VkMemoryFdPropertiesKHR fdProps = {};
305 fdProps.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR;
306 if (pfnGetFdProps(dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, dupFd, &fdProps) != VK_SUCCESS) {
307 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
308 }
309
310 VkMemoryRequirements memReq = {};
311 pfnGetMemReq(dev, image, &memReq);
312 VkPhysicalDeviceMemoryProperties physMem = {};
313 pfnGetPhysMemProps(nh->physDev, &physMem);
314 uint32_t memTypeIndex = UINT32_MAX;
315 const uint32_t allowed = memReq.memoryTypeBits & fdProps.memoryTypeBits;
316 for (uint32_t i = 0; i < physMem.memoryTypeCount; ++i) {
317 if (allowed & (1u << i)) {
318 memTypeIndex = i;
319 break;
320 }
321 }
322 if (memTypeIndex == UINT32_MAX) {
323 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
324 }
325
326 VkImportMemoryFdInfoKHR importInfo = {};
327 importInfo.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR;
328 importInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
329 importInfo.fd = dupFd;
330
331 VkMemoryDedicatedAllocateInfo dedicated = {};
332 dedicated.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO;
333 dedicated.image = image;
334 dedicated.pNext = &importInfo;
335
336 VkMemoryAllocateInfo allocInfo = {};
337 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
338 allocInfo.pNext = &dedicated;
339 allocInfo.allocationSize = memReq.size;
340 allocInfo.memoryTypeIndex = memTypeIndex;
341
342 // VK_KHR_external_memory_fd transfers fd ownership to the implementation only on a SUCCESSFUL import; on failure
343 // the application still owns the fd and must close it. Disarm the close guard only after VK_SUCCESS.
344 VkDeviceMemory memory = VK_NULL_HANDLE;
345 if (pfnAllocateMemory(dev, &allocInfo, nullptr, &memory) != VK_SUCCESS || memory == VK_NULL_HANDLE) {
346 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
347 "Vulkan import: vkAllocateMemory failed — CPU upload");
348 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
349 }
350 fdConsumed = true;
351 const auto freeMemory = qScopeGuard([&] {
352 if (memory != VK_NULL_HANDLE)
353 pfnFreeMemory(dev, memory, nullptr);
354 });
355
356 if (const VkResult bindRes = pfnBindImageMemory(dev, image, memory, 0); bindRes != VK_SUCCESS) {
357 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
358 "Vulkan import: vkBindImageMemory failed (VkResult=" << bindRes << ") — CPU upload");
359 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
360 }
361
362 auto frameTextures =
363 std::make_unique<GstVulkanOwnedFrameTextures>(&rhi, _format.frameSize(), _format.pixelFormat(), image);
364 if (!frameTextures->valid()) {
365 QGC_HW_WARN_ONCE(GstDmaBufVulkanLog, s_loggedVulkanUnimpl,
366 "Vulkan import: QRhiTexture::createFrom(VkImage) failed");
367 return GstHwPathTelemetry::fail(HwVideoBufferPath::DmaBuf);
368 }
369 // Ownership of image/memory passes to the bundle; disarm the scope guards.
370 frameTextures->adoptVulkanResources(dev, image, memory, pfnDestroyImage, pfnFreeMemory);
371 image = VK_NULL_HANDLE;
372 memory = VK_NULL_HANDLE;
373 frameTextures->setSourceSample(takeSample());
374 static std::atomic<bool> s_loggedVulkanOk{false};
375 if (!s_loggedVulkanOk.exchange(true, std::memory_order_relaxed)) {
376 qCInfo(GstDmaBufVulkanLog) << "First Vulkan DMABuf zero-copy import success: format="
377 << int(_format.pixelFormat());
378 }
379 return frameTextures;
380}
381
382#endif // QGC_HAS_GST_DMABUF_GPU_PATH && QGC_HAS_GST_VULKAN_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)
constexpr int kMaxPlanes
Matches GST_VIDEO_MAX_PLANES (gst-video pins it at 4); single source of truth for every per-platform ...