Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qffmpeghwaccel_cuda.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtFFmpegMediaPluginImpl/private/qffmpeg_p.h>
7
8#include <QtMultimedia/private/qhwvideobuffer_p.h>
9#include <QtMultimedia/private/qmultimedia_ranges_p.h>
10#include <QtMultimedia/private/qmultimediautils_p.h>
11
12#include <QtGui/rhi/qrhi.h>
13#include <QtGui/rhi/qrhi_platform.h>
14
15#include <QtCore/qlibrary.h>
16#include <QtCore/qloggingcategory.h>
17#include <QtCore/qscopeguard.h>
18#include <QtCore/quuid.h>
19#include <QtCore/private/quniquehandle_types_p.h>
20
21#include <array>
22#include <optional>
23
24#if QT_CONFIG(opengl)
25# include <QtGui/qopenglcontext.h>
26# include <QtGui/qopengl.h>
27# include <QtGui/qopenglfunctions.h>
28# ifndef GL_RG16F
29# define GL_RG16F 0x822F
30# endif
31# ifndef GL_RG16
32# define GL_RG16 0x822C
33# endif
34# ifndef GL_R16
35# define GL_R16 0x822A
36# endif
37#endif
38
39#if QT_CONFIG(vulkan)
40# include <QtMultimedia/private/qmultimedia_vulkan_support_p.h>
41# include <QtGui/qvulkanfunctions.h>
42# include <QtGui/qvulkaninstance.h>
43# include <vulkan/vulkan.h>
44#endif
45
46#if QT_CONFIG(opengl) || QT_CONFIG(vulkan)
47# define QT_FFMPEG_HAS_CUDA_TEXTURE_INTEROP 1
48#endif
49
50#if QT_CONFIG(opengl) && (!QT_CONFIG(opengles2) || QT_CONFIG(opengles3))
51# define QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP 1
52#endif
53
54extern "C" {
55
56// dynlink_cuda.h declares the CUDA driver API without requiring a CUDA Toolkit install;
57// including it first also makes libavutil/hwcontext_cuda.h skip its own "<cuda.h>" fallback.
58// The header is vendored in src/3rdparty/nv-codec-headers, so it is always available.
59#include <ffnvcodec/dynlink_cuda.h>
60
61#include <libavutil/hwcontext.h>
62#include <libavutil/hwcontext_cuda.h>
63}
64
65QT_BEGIN_NAMESPACE
66
67using namespace Qt::StringLiterals;
68
69Q_STATIC_LOGGING_CATEGORY(qLHWAccelCUDA, "qt.multimedia.ffmpeg.hwaccelcuda")
70
71namespace QFFmpeg {
72
73template <typename T>
75
76#if QT_CONFIG(opengl) || QT_CONFIG(vulkan)
77
78namespace {
79
80class CudaDriverApi
81{
82public:
83 static const CudaDriverApi *get()
84 {
87 if (!api.isValid())
88 return std::nullopt;
89 return std::optional<CudaDriverApi>{ std::move(api) };
90 }();
91 return singleton ? &*singleton : nullptr;
92 }
93
95 bool popCurrent() const
96 {
97 CUcontext popped = nullptr;
99 }
100
106 tcuMemcpy2D_v2 *cuMemcpy2D_v2 = nullptr;
117
118private:
120 : m_library{
121 std::make_unique<QLibrary>(u"cuda"_s),
122 }
123 {
124 if (!m_library->load()) {
126 << "libcuda.so.1 could not be loaded:" << m_library->errorString();
127 m_library = nullptr;
128 return;
129 }
130
131# define RESOLVE(name)
132 name = reinterpret_cast<decltype(name)>(m_library->resolve(#name));
133 if (!name) {
134 qCDebug(qLHWAccelCUDA) << "libcuda.so.1 is missing symbol:" << #name;
135 m_library = nullptr;
136 return;
137 }
138
155# undef RESOLVE
156 }
157
158 bool isValid() const { return bool(m_library); }
160};
161
163{
164public:
166 {
167 if (!api.pushCurrent(ctx))
168 return std::nullopt;
169 return CudaContextGuard(api);
170 }
171
173 {
174 other.m_api = nullptr;
175 }
176
178
180 {
181 if (m_api)
182 m_api->popCurrent();
183 }
184
186
187private:
188 explicit CudaContextGuard(const CudaDriverApi &api) : m_api(&api) { }
189
190 const CudaDriverApi *m_api;
191};
192
193class CudaResult
194{
195public:
197
198private:
199 friend QDebug operator<<(QDebug debug, const CudaResult &r)
200 {
202 const char *errorString = nullptr;
204 debug.nospace().noquote() << "CUresult(0x" << Qt::hex << qToUnderlying(r.m_result)
205 << Qt::dec << ", "
206 << (errorString ? errorString : "unknown error") << ')';
207 return debug;
208 }
209
211 const CudaDriverApi &m_api;
212};
213
214// The layout of one plane of a CUDA-decoded pixel format, backend-agnostic: just enough to
215// compute plane sizes and the byte width of the CUDA memcpy.
217{
218 int sampleSize = 1; // bytes per sample: 1 for 8-bit formats, 2 for 10/16-bit-in-16-bit
219 int numChannels = 1; // 1 (luma) or 2 (interleaved chroma)
220 // The plane's size is the frame size divided by these factors, e.g. 2/2 for a 4:2:0
221 // chroma plane. Default to 1, not 0, since these are divisors.
222 int widthDivisor = 1;
223 int heightDivisor = 1;
224
225 int bytesPerTexel() const { return sampleSize * numChannels; }
226};
227
229{
230 int planeCount = 0;
232};
233
234// NVDEC only ever decodes into semi-planar (2-plane, chroma-subsampled) formats - never
235// fully-planar ones - so this table only needs to cover that family.
237{
238 switch (swFormat) {
239 case AV_PIX_FMT_NV12:
240 case AV_PIX_FMT_NV21:
241 return CudaFrameGeometry{
242 2,
243 { {
244 CudaPlaneGeometry{ 1, 1, 1, 1 },
245 CudaPlaneGeometry{ 1, 2, 2, 2 },
246 } },
247 };
248 case AV_PIX_FMT_P010:
249 case AV_PIX_FMT_P016:
250 return CudaFrameGeometry{
251 2,
252 { {
253 CudaPlaneGeometry{ 2, 1, 1, 1 },
254 CudaPlaneGeometry{ 2, 2, 2, 2 },
255 } },
256 };
257 default:
258 return std::nullopt;
259 }
260}
261
262// Exposes each plane's texture as a native handle: a GLuint on OpenGL, a VkImage on Vulkan.
264{
265public:
269 {
270 }
271
273 {
274 return plane < m_nPlanes ? m_handles[plane] : 0;
275 }
276
277private:
278 const int m_nPlanes;
280 const std::shared_ptr<void>
281 m_parentKeepAlive; // keeps the converter (and its GL/Vulkan resources) alive
282};
283
284// ---------------------------------------------------------------------------------------------
285// OpenGL backend
286// ---------------------------------------------------------------------------------------------
287
288# if QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
289
290struct GlPlaneFormat
291{
294 GLenum type;
295};
296
298{
300 if (geometry.numChannels == 1)
301 return GlPlaneFormat{
303 GLenum(GL_RED),
304 type,
305 };
306 return GlPlaneFormat{
308 GLenum(GL_RG),
309 type,
310 };
311}
312
313struct GlPlaneResource
314{
316
318 {
319 if (cudaResource && cudaApi)
321 if (texture)
323 cudaResource = nullptr;
324 texture = 0;
325 }
326
327 GLuint texture = 0;
329};
330
332{
333public:
335 {
336 if (!rhi)
337 return nullptr;
338
339 auto *api = CudaDriverApi::get();
340 if (!api) {
341 qCDebug(qLHWAccelCUDA) << "CUDA driver API is not available, disabling";
342 return nullptr;
343 }
344
346 }
347
350 {
351 }
352
354
357 {
359 return nullptr;
360
361 auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
363 if (!geometry) {
364 qWarning() << "CUDATextureConverter does not support sw_format" << framesCtx->sw_format;
365 return nullptr;
366 }
367
369 if (!deviceCtx)
370 return nullptr;
371 auto *cudaCtx = reinterpret_cast<AVCUDADeviceContext *>(deviceCtx->hwctx);
372
374 if (!guard) {
375 qWarning() << "Failed to push the CUDA context current; disabling";
376 rhi = nullptr;
377 return nullptr;
378 }
380
382 return nullptr;
383
385 for (int i = 0; i < geometry->planeCount; ++i)
387 const CUresult mapResult =
389 if (mapResult != CUDA_SUCCESS) {
390 qWarning() << "cuGraphicsMapResources failed:" << CudaResult(mapResult, *m_api);
391 return nullptr;
392 }
393
394 auto unmapGuard = qScopeGuard([&] {
396 });
397
399 for (int i = 0; i < geometry->planeCount; ++i) {
403
404 CUarray array = nullptr;
408 qWarning() << "cuGraphicsSubResourceGetMappedArray failed for plane" << i << ":"
410 return nullptr;
411 }
412
413 const CUDA_MEMCPY2D copy{
414 .srcXInBytes = 0,
415 .srcY = 0,
417 .srcHost = nullptr,
418 .srcDevice = reinterpret_cast<CUdeviceptr>(frame->data[i]),
419 .srcArray = nullptr,
421 .dstXInBytes = 0,
422 .dstY = 0,
424 .dstHost = nullptr,
425 .dstDevice = 0,
426 .dstArray = array,
427 .dstPitch = 0,
430 };
431
433 if (copyResult != CUDA_SUCCESS) {
434 qWarning() << "cuMemcpy2D_v2 failed for plane" << i << ":"
436 return nullptr;
437 }
438
440 }
441
444 }
445
446private:
448 {
449 auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
452 Q_ASSERT(maybeGeometry); // already validated by createTextureHandles
454
459 [](const auto &plane) {
460 return bool(plane);
461 });
462 if (canReuse)
463 return true;
464
468
469 auto *glContext =
470 static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())->context;
472
473 for (int i = 0; i < geometry.planeCount; ++i) {
478
487 planeSize.height(), 0, format.format, format.type, nullptr);
489
493 if (result != CUDA_SUCCESS) {
494 qWarning() << "cuGraphicsGLRegisterImage failed for plane" << i << ":"
495 << CudaResult(result, *m_api);
497 return false;
498 }
499
500 m_planes[i] = std::move(plane);
501 }
502
503 return true;
504 }
505
507 {
508 if (!rhi)
509 return;
510
513 : std::nullopt;
514
516 auto *glContext =
517 static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())->context;
519
520 for (auto &plane : m_planes) {
521 if (!plane)
522 continue;
523 plane->release(guard ? m_api : nullptr, functions);
524 plane.reset();
525 }
526 }
527
528 const CudaDriverApi *const m_api;
532 CUcontext m_cudaContext = nullptr;
533};
534
535# endif // QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
536
537// ---------------------------------------------------------------------------------------------
538// Vulkan backend
539// ---------------------------------------------------------------------------------------------
540
541# if QT_CONFIG(vulkan)
542
543// Pass a device to resolve a device-level function (e.g. of VK_KHR_external_memory_fd), which
544// yields nullptr if the extension is not enabled on that device. Without a device, an
545// instance-level function is resolved.
546template <typename Func>
549{
550 using namespace QtMultimediaPrivate;
553 if (!func)
554 qCDebug(qLHWAccelCUDA) << "Failed to resolve Vulkan function" << name;
555 return func;
556}
557
558// vkGetPhysicalDeviceProperties2 is core since Vulkan 1.1, with vkGetPhysicalDeviceProperties2KHR
559// as the (functionally identical) extension entry point on older API versions/instances.
561{
562 using namespace QtMultimediaPrivate;
565 inst, "vkGetPhysicalDeviceProperties2");
566 if (!func) {
568 inst, "vkGetPhysicalDeviceProperties2KHR");
569 }
570 return func;
571}
572
574{
575 return QUuid::fromRfc4122(QByteArrayView(reinterpret_cast<const char *>(cuUuid.bytes), 16));
576}
577
578// Resolves the Vulkan state/functions needed to export a plane's VkImage memory as a POSIX
579// fd (VK_KHR_external_memory_fd) for import into CUDA. Bound to one QRhi::Vulkan instance.
581{
582public:
584 : inst(handles.inst),
586 dev(handles.dev),
590 *handles.inst, "vkGetMemoryFdKHR", handles.dev)),
592 {
593 }
594
596 {
598 const auto *handles = static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles());
599 if (!handles || !handles->inst || !handles->dev || !handles->physDev)
600 return nullptr;
601
604 return nullptr;
605
608 << "VK_KHR_external_memory_fd or vkGetPhysicalDeviceProperties2 is either "
609 "unavailable or not enabled on this QRhi's VkDevice/VkInstance, disabling. "
610 "To use the CUDA texture interop with Vulkan, enable the device extensions "
611 "VK_KHR_external_memory and VK_KHR_external_memory_fd when creating the "
612 "QRhi, e.g. with QQuickGraphicsConfiguration::setDeviceExtensions() or "
613 "QRhiVulkanInitParams::deviceExtensions";
614 return nullptr;
615 }
616
617 return interop;
618 }
619
620 enum class MatchResult : uint8_t {
621 SameGpu,
623 Unknown, // Vulkan device UUID could not be queried
624 };
626 {
627 using namespace QtMultimediaPrivate;
628
631 .pNext = nullptr,
632 .deviceUUID = {},
633 .driverUUID = {},
634 .deviceLUID = {},
635 .deviceNodeMask = 0,
637 };
640 .pNext = &idProps,
641 .properties = {},
642 };
645 if (vkDeviceUuid.isNull())
646 return MatchResult::Unknown;
649 }
650
652 {
655 for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
656 const bool allowed = memoryTypeBits & (1u << i);
657 const bool deviceLocal =
659 if (allowed && deviceLocal)
660 return i;
661 }
662 return std::nullopt;
663 }
664
665 QVulkanInstance *const inst;
667 const VkDevice dev;
672};
673
675{
678};
679
681{
684 if (geometry.numChannels == 1)
685 return VulkanPlaneFormat{
687 cuFormat,
688 };
689 return VulkanPlaneFormat{
691 cuFormat,
692 };
693}
694
695// Vulkan interop (VK_KHR_external_memory_fd + cuImportExternalMemory).
697{
699 {
701 }
702
704 {
705 if (externalMemory && cudaApi)
707 if (memory != VK_NULL_HANDLE)
709 if (image != VK_NULL_HANDLE)
711 externalMemory = nullptr;
712 mipmappedArray = nullptr;
713 array = nullptr;
716 }
717
722 CUarray array = nullptr; // level-0 view of mipmappedArray, cached for the copy
723};
724
726{
728
729public:
731 {
732 if (!rhi)
733 return nullptr;
734
735 auto *api = CudaDriverApi::get();
736 if (!api) {
737 qCDebug(qLHWAccelCUDA) << "CUDA driver API is not available, disabling";
738 return nullptr;
739 }
740
742 if (!vulkanApi) {
743 qCDebug(qLHWAccelCUDA) << "CUDA-Vulkan interop is not available, disabling";
744 return nullptr;
745 }
746
748 }
749
753 {
754 }
755
757
760 {
762 return nullptr;
763
764 auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
766 if (!geometry) {
767 qWarning() << "CUDATextureConverter does not support sw_format" << framesCtx->sw_format;
768 return nullptr;
769 }
770
772 if (!deviceCtx)
773 return nullptr;
774 auto *cudaCtx = reinterpret_cast<AVCUDADeviceContext *>(deviceCtx->hwctx);
775
777 if (!guard) {
778 qWarning() << "Failed to push the CUDA context current; disabling";
779 rhi = nullptr;
780 return nullptr;
781 }
783
785 return nullptr;
786
788 for (int i = 0; i < geometry->planeCount; ++i) {
792
793 const CUDA_MEMCPY2D copy{
794 .srcXInBytes = 0,
795 .srcY = 0,
797 .srcHost = nullptr,
798 .srcDevice = reinterpret_cast<CUdeviceptr>(frame->data[i]),
799 .srcArray = nullptr,
801 .dstXInBytes = 0,
802 .dstY = 0,
804 .dstHost = nullptr,
805 .dstDevice = 0,
807 .dstPitch = 0,
810 };
811
813 if (copyResult != CUDA_SUCCESS) {
814 qWarning() << "cuMemcpy2D_v2 failed for plane" << i << ":"
816 return nullptr;
817 }
818
820 }
821
822 // No external-semaphore pipelining (see the class docstring): block until the
823 // copies actually land before handing the VkImages to the RHI. cuMemcpy2D_v2
824 // runs on the default stream, so synchronizing that stream is sufficient.
826 if (syncResult != CUDA_SUCCESS) {
827 qWarning() << "cuStreamSynchronize failed:" << CudaResult(syncResult, *m_api);
828 return nullptr;
829 }
830
833 }
834
835private:
837 {
838 auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
841 Q_ASSERT(maybeGeometry); // already validated by createTextureHandles
843
848 [](const auto &plane) {
849 return bool(plane);
850 });
851 if (canReuse)
852 return true;
853
857
859 CUdevice device = 0;
861 const bool ok = m_api->cuCtxGetDevice(&device) == CUDA_SUCCESS
866 qWarning() << "CUDATextureConverter: could not determine the QRhi's Vulkan "
867 "physical device UUID (missing "
868 "VK_KHR_get_physical_device_properties2 on the VkInstance?); "
869 "disabling";
871 qWarning() << "CUDATextureConverter: the CUDA device does not match the QRhi's "
872 "Vulkan physical device (e.g. a multi-GPU system); disabling";
873 }
874 }
876 return false;
877
878 for (int i = 0; i < geometry.planeCount; ++i) {
883
885 auto releaseGuard = qScopeGuard([&] {
887 });
888
891 .pNext = nullptr,
893 };
894
895 // TRANSFER_SRC is not needed by the RHI (it only samples this texture), but it
896 // costs nothing to declare and lets tests validate the copy via a readback.
900 .flags = 0,
903 .extent =
907 .depth = 1u,
908 },
909 .mipLevels = 1,
910 .arrayLayers = 1,
917 .pQueueFamilyIndices = nullptr,
919 };
920
922 &plane->image)
923 != VK_SUCCESS) {
924 qWarning() << "CUDATextureConverter: vkCreateImage failed for plane" << i;
925 return false;
926 }
927
930 &memReq);
931
934 if (!memoryTypeIndex) {
935 qWarning() << "CUDATextureConverter: no suitable Vulkan memory type for plane" << i;
936 return false;
937 }
938
941 .pNext = nullptr,
943 };
944
947 .pNext = &exportInfo,
950 };
951
953 &plane->memory)
954 != VK_SUCCESS) {
955 qWarning() << "CUDATextureConverter: vkAllocateMemory failed for plane" << i;
956 return false;
957 }
958
960 0)
961 != VK_SUCCESS) {
962 qWarning() << "CUDATextureConverter: vkBindImageMemory failed for plane" << i;
963 return false;
964 }
965
968 .pNext = nullptr,
969 .memory = plane->memory,
971 };
974 qWarning() << "CUDATextureConverter: vkGetMemoryFdKHR failed for plane" << i;
975 return false;
976 }
977
980 .handle = { .fd = fd.get() },
981 .size = memReq.size,
982 .flags = 0,
983 .reserved = {},
984 };
985 const CUresult importResult =
987 if (importResult != CUDA_SUCCESS) {
988 qWarning() << "CUDATextureConverter: cuImportExternalMemory failed for plane" << i
989 << ":" << CudaResult(importResult, *m_api);
990 return false;
991 }
992 std::ignore = fd.release(); // import succeeded, ownership transferred to CUDA
993
995 .offset = 0,
996 .arrayDesc =
1000 .Depth = 0,
1003 .Flags = 0,
1004 },
1005 .numLevels = 1,
1006 .reserved = {},
1007 };
1008
1012 qWarning() << "CUDATextureConverter: cuExternalMemoryGetMappedMipmappedArray "
1013 "failed for plane"
1014 << i << ":" << CudaResult(mappedArrayResult, *m_api);
1015 return false;
1016 }
1017
1021 qWarning() << "CUDATextureConverter: cuMipmappedArrayGetLevel failed for plane" << i
1022 << ":" << CudaResult(arrayLevelResult, *m_api);
1023 return false;
1024 }
1025
1027 m_planes[i] = std::move(plane);
1028 }
1029
1030 return true;
1031 }
1032
1034 {
1035 if (!rhi)
1036 return;
1037
1040 : std::nullopt;
1041
1042 for (auto &plane : m_planes) {
1043 if (!plane)
1044 continue;
1045 plane->release(guard ? m_api : nullptr, *m_vulkan);
1046 plane.reset();
1047 }
1048 }
1049
1050 const CudaDriverApi *const m_api;
1056 CUcontext m_cudaContext = nullptr;
1057};
1058
1059# endif // QT_CONFIG(vulkan)
1060
1061} // namespace
1062
1063#endif // QT_FFMPEG_HAS_CUDA_TEXTURE_INTEROP
1064
1065// ---------------------------------------------------------------------------------------------
1066// Factory
1067// ---------------------------------------------------------------------------------------------
1068
1069std::shared_ptr<TextureConverterBackend> CUDATextureConverterFactory::create(QRhi *rhi)
1070{
1071 if (rhi) {
1072 switch (rhi->backend()) {
1073#if QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
1074 case QRhi::OpenGLES2:
1075 return CUDAGLTextureConverter::create(rhi);
1076#endif
1077#if QT_CONFIG(vulkan)
1078 case QRhi::Vulkan:
1079 return CUDAVulkanTextureConverter::create(rhi);
1080#endif
1081 default:
1082 break;
1083 }
1084 }
1085
1086 return nullptr;
1087}
1088
1089} // namespace QFFmpeg
1090
1091QT_END_NAMESPACE
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)