Revision control

+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+ #include "qffmpeghwaccel_cuda_p.h"
+
+ #include <QtFFmpegMediaPluginImpl/private/qffmpeg_p.h>
+
+ #include <QtMultimedia/private/qhwvideobuffer_p.h>
+ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
+ #include <QtMultimedia/private/qmultimediautils_p.h>
+
+ #include <QtGui/rhi/qrhi.h>
+ #include <QtGui/rhi/qrhi_platform.h>
+
+ #include <QtCore/qlibrary.h>
+ #include <QtCore/qloggingcategory.h>
+ #include <QtCore/qscopeguard.h>
+ #include <QtCore/quuid.h>
+ #include <QtCore/private/quniquehandle_types_p.h>
+
+ #include <array>
+ #include <optional>
+
+ #if QT_CONFIG(opengl)
+ # include <QtGui/qopenglcontext.h>
+ # include <QtGui/qopengl.h>
+ # include <QtGui/qopenglfunctions.h>
+ # ifndef GL_RG16F
+ # define GL_RG16F 0x822F
+ # endif
+ # ifndef GL_RG16
+ # define GL_RG16 0x822C
+ # endif
+ # ifndef GL_R16
+ # define GL_R16 0x822A
+ # endif
+ #endif
+
+ #if QT_CONFIG(vulkan)
+ # include <QtMultimedia/private/qmultimedia_vulkan_support_p.h>
+ # include <QtGui/qvulkanfunctions.h>
+ # include <QtGui/qvulkaninstance.h>
+ # include <vulkan/vulkan.h>
+ #endif
+
+ #if QT_CONFIG(opengl) || QT_CONFIG(vulkan)
+ # define QT_FFMPEG_HAS_CUDA_TEXTURE_INTEROP 1
+ #endif
+
+ #if QT_CONFIG(opengl) && (!QT_CONFIG(opengles2) || QT_CONFIG(opengles3))
+ # define QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP 1
+ #endif
+
+ extern "C" {
+
+ // dynlink_cuda.h declares the CUDA driver API without requiring a CUDA Toolkit install;
+ // including it first also makes libavutil/hwcontext_cuda.h skip its own "<cuda.h>" fallback.
+ // The header is vendored in src/3rdparty/nv-codec-headers, so it is always available.
+ #include <ffnvcodec/dynlink_cuda.h>
+
+ #include <libavutil/hwcontext.h>
+ #include <libavutil/hwcontext_cuda.h>
+ }
+
+ QT_BEGIN_NAMESPACE
+
+ using namespace Qt::StringLiterals;
+
+ Q_STATIC_LOGGING_CATEGORY(qLHWAccelCUDA, "qt.multimedia.ffmpeg.hwaccelcuda")
+
+ namespace QFFmpeg {
+
+ template <typename T>
+ using PerPlaneArray = std::array<T, QVideoTextureHelper::TextureDescription::maxPlanes>;
+
+ #if QT_CONFIG(opengl) || QT_CONFIG(vulkan)
+
+ namespace {
+
+ class CudaDriverApi
+ {
+ public:
+ static const CudaDriverApi *get()
+ {
+ static std::optional<CudaDriverApi> singleton = []() -> std::optional<CudaDriverApi> {
+ CudaDriverApi api;
+ if (!api.isValid())
+ return std::nullopt;
+ return std::optional<CudaDriverApi>{ std::move(api) };
+ }();
+ return singleton ? &*singleton : nullptr;
+ }
+
+ bool pushCurrent(CUcontext ctx) const { return cuCtxPushCurrent_v2(ctx) == CUDA_SUCCESS; }
+ bool popCurrent() const
+ {
+ CUcontext popped = nullptr;
+ return cuCtxPopCurrent_v2(&popped) == CUDA_SUCCESS;
+ }
+
+ tcuGraphicsGLRegisterImage *cuGraphicsGLRegisterImage = nullptr;
+ tcuGraphicsUnregisterResource *cuGraphicsUnregisterResource = nullptr;
+ tcuGraphicsMapResources *cuGraphicsMapResources = nullptr;
+ tcuGraphicsUnmapResources *cuGraphicsUnmapResources = nullptr;
+ tcuGraphicsSubResourceGetMappedArray *cuGraphicsSubResourceGetMappedArray = nullptr;
+ tcuMemcpy2D_v2 *cuMemcpy2D_v2 = nullptr;
+ tcuCtxPushCurrent_v2 *cuCtxPushCurrent_v2 = nullptr;
+ tcuCtxPopCurrent_v2 *cuCtxPopCurrent_v2 = nullptr;
+ tcuCtxGetDevice *cuCtxGetDevice = nullptr;
+ tcuStreamSynchronize *cuStreamSynchronize = nullptr;
+ tcuDeviceGetUuid *cuDeviceGetUuid = nullptr;
+ tcuImportExternalMemory *cuImportExternalMemory = nullptr;
+ tcuDestroyExternalMemory *cuDestroyExternalMemory = nullptr;
+ tcuExternalMemoryGetMappedMipmappedArray *cuExternalMemoryGetMappedMipmappedArray = nullptr;
+ tcuMipmappedArrayGetLevel *cuMipmappedArrayGetLevel = nullptr;
+ tcuGetErrorString *cuGetErrorString = nullptr;
+
+ private:
+ CudaDriverApi()
+ : m_library{
+ std::make_unique<QLibrary>(u"cuda"_s),
+ }
+ {
+ if (!m_library->load()) {
+ qCDebug(qLHWAccelCUDA)
+ << "libcuda.so.1 could not be loaded:" << m_library->errorString();
+ m_library = nullptr;
+ return;
+ }
+
+ # define RESOLVE(name) \
+ name = reinterpret_cast<decltype(name)>(m_library->resolve(#name)); \
+ if (!name) { \
+ qCDebug(qLHWAccelCUDA) << "libcuda.so.1 is missing symbol:" << #name; \
+ m_library = nullptr; \
+ return; \
+ }
+
+ RESOLVE(cuGraphicsGLRegisterImage)
+ RESOLVE(cuGraphicsUnregisterResource)
+ RESOLVE(cuGraphicsMapResources)
+ RESOLVE(cuGraphicsUnmapResources)
+ RESOLVE(cuGraphicsSubResourceGetMappedArray)
+ RESOLVE(cuMemcpy2D_v2)
+ RESOLVE(cuCtxPushCurrent_v2)
+ RESOLVE(cuCtxPopCurrent_v2)
+ RESOLVE(cuCtxGetDevice)
+ RESOLVE(cuStreamSynchronize)
+ RESOLVE(cuDeviceGetUuid)
+ RESOLVE(cuImportExternalMemory)
+ RESOLVE(cuDestroyExternalMemory)
+ RESOLVE(cuExternalMemoryGetMappedMipmappedArray)
+ RESOLVE(cuMipmappedArrayGetLevel)
+ RESOLVE(cuGetErrorString)
+ # undef RESOLVE
+ }
+
+ bool isValid() const { return bool(m_library); }
+ std::unique_ptr<QLibrary> m_library;
+ };
+
+ class CudaContextGuard
+ {
+ public:
+ static std::optional<CudaContextGuard> create(const CudaDriverApi &api, CUcontext ctx)
+ {
+ if (!api.pushCurrent(ctx))
+ return std::nullopt;
+ return CudaContextGuard(api);
+ }
+
+ CudaContextGuard(CudaContextGuard &&other) noexcept : m_api(other.m_api)
+ {
+ other.m_api = nullptr;
+ }
+
+ CudaContextGuard &operator=(CudaContextGuard &&) = delete;
+
+ ~CudaContextGuard()
+ {
+ if (m_api)
+ m_api->popCurrent();
+ }
+
+ Q_DISABLE_COPY(CudaContextGuard)
+
+ private:
+ explicit CudaContextGuard(const CudaDriverApi &api) : m_api(&api) { }
+
+ const CudaDriverApi *m_api;
+ };
+
+ class CudaResult
+ {
+ public:
+ CudaResult(CUresult result, const CudaDriverApi &api) : m_result(result), m_api(api) { }
+
+ private:
+ friend QDebug operator<<(QDebug debug, const CudaResult &r)
+ {
+ const QDebugStateSaver saver(debug);
+ const char *errorString = nullptr;
+ r.m_api.cuGetErrorString(r.m_result, &errorString);
+ debug.nospace().noquote() << "CUresult(0x" << Qt::hex << qToUnderlying(r.m_result)
+ << Qt::dec << ", "
+ << (errorString ? errorString : "unknown error") << ')';
+ return debug;
+ }
+
+ CUresult m_result;
+ const CudaDriverApi &m_api;
+ };
+
+ // The layout of one plane of a CUDA-decoded pixel format, backend-agnostic: just enough to
+ // compute plane sizes and the byte width of the CUDA memcpy.
+ struct CudaPlaneGeometry
+ {
+ int sampleSize = 1; // bytes per sample: 1 for 8-bit formats, 2 for 10/16-bit-in-16-bit
+ int numChannels = 1; // 1 (luma) or 2 (interleaved chroma)
+ // The plane's size is the frame size divided by these factors, e.g. 2/2 for a 4:2:0
+ // chroma plane. Default to 1, not 0, since these are divisors.
+ int widthDivisor = 1;
+ int heightDivisor = 1;
+
+ int bytesPerTexel() const { return sampleSize * numChannels; }
+ };
+
+ struct CudaFrameGeometry
+ {
+ int planeCount = 0;
+ PerPlaneArray<CudaPlaneGeometry> planes{};
+ };
+
+ // NVDEC only ever decodes into semi-planar (2-plane, chroma-subsampled) formats - never
+ // fully-planar ones - so this table only needs to cover that family.
+ std::optional<CudaFrameGeometry> cudaFrameGeometry(AVPixelFormat swFormat)
+ {
+ switch (swFormat) {
+ case AV_PIX_FMT_NV12:
+ case AV_PIX_FMT_NV21:
+ return CudaFrameGeometry{
+ 2,
+ { {
+ CudaPlaneGeometry{ 1, 1, 1, 1 },
+ CudaPlaneGeometry{ 1, 2, 2, 2 },
+ } },
+ };
+ case AV_PIX_FMT_P010:
+ case AV_PIX_FMT_P016:
+ return CudaFrameGeometry{
+ 2,
+ { {
+ CudaPlaneGeometry{ 2, 1, 1, 1 },
+ CudaPlaneGeometry{ 2, 2, 2, 2 },
+ } },
+ };
+ default:
+ return std::nullopt;
+ }
+ }
+
+ // Exposes each plane's texture as a native handle: a GLuint on OpenGL, a VkImage on Vulkan.
+ class CudaTextureHandles final : public QVideoFrameTexturesHandles
+ {
+ public:
+ CudaTextureHandles(int nPlanes, PerPlaneArray<quint64> handles,
+ std::shared_ptr<void> parentKeepAlive)
+ : m_nPlanes(nPlanes), m_handles(handles), m_parentKeepAlive(std::move(parentKeepAlive))
+ {
+ }
+
+ quint64 textureHandle(QRhi &, int plane) override
+ {
+ return plane < m_nPlanes ? m_handles[plane] : 0;
+ }
+
+ private:
+ const int m_nPlanes;
+ const PerPlaneArray<quint64> m_handles;
+ const std::shared_ptr<void>
+ m_parentKeepAlive; // keeps the converter (and its GL/Vulkan resources) alive
+ };
+
+ // ---------------------------------------------------------------------------------------------
+ // OpenGL backend
+ // ---------------------------------------------------------------------------------------------
+
+ # if QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
+
+ struct GlPlaneFormat
+ {
+ GLenum internalFormat;
+ GLenum format;
+ GLenum type;
+ };
+
+ GlPlaneFormat glPlaneFormat(const CudaPlaneGeometry &geometry)
+ {
+ const GLenum type = geometry.sampleSize == 1 ? GL_UNSIGNED_BYTE : GL_UNSIGNED_SHORT;
+ if (geometry.numChannels == 1)
+ return GlPlaneFormat{
+ GLenum(geometry.sampleSize == 1 ? GL_R8 : GL_R16),
+ GLenum(GL_RED),
+ type,
+ };
+ return GlPlaneFormat{
+ GLenum(geometry.sampleSize == 1 ? GL_RG8 : GL_RG16),
+ GLenum(GL_RG),
+ type,
+ };
+ }
+
+ struct GlPlaneResource
+ {
+ ~GlPlaneResource() { Q_ASSERT(texture == 0 && !cudaResource); }
+
+ void release(const CudaDriverApi *cudaApi, QOpenGLFunctions &functions)
+ {
+ if (cudaResource && cudaApi)
+ cudaApi->cuGraphicsUnregisterResource(cudaResource);
+ if (texture)
+ functions.glDeleteTextures(1, &texture);
+ cudaResource = nullptr;
+ texture = 0;
+ }
+
+ GLuint texture = 0;
+ CUgraphicsResource cudaResource = nullptr;
+ };
+
+ class CUDAGLTextureConverter final : public TextureConverterBackend
+ {
+ public:
+ static std::shared_ptr<TextureConverterBackend> create(QRhi *rhi)
+ {
+ if (!rhi)
+ return nullptr;
+
+ auto *api = CudaDriverApi::get();
+ if (!api) {
+ qCDebug(qLHWAccelCUDA) << "CUDA driver API is not available, disabling";
+ return nullptr;
+ }
+
+ return std::make_shared<CUDAGLTextureConverter>(rhi, api);
+ }
+
+ explicit CUDAGLTextureConverter(QRhi *rhi, const CudaDriverApi *api)
+ : TextureConverterBackend(rhi), m_api{ api }
+ {
+ }
+
+ ~CUDAGLTextureConverter() override { releasePlaneResources(); }
+
+ QVideoFrameTexturesHandlesUPtr
+ createTextureHandles(AVFrame *frame, QVideoFrameTexturesHandlesUPtr /*oldHandles*/) override
+ {
+ if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx)
+ return nullptr;
+
+ auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
+ const std::optional<CudaFrameGeometry> geometry = cudaFrameGeometry(framesCtx->sw_format);
+ if (!geometry) {
+ qWarning() << "CUDATextureConverter does not support sw_format" << framesCtx->sw_format;
+ return nullptr;
+ }
+
+ auto *deviceCtx = avFrameDeviceContext(frame);
+ if (!deviceCtx)
+ return nullptr;
+ auto *cudaCtx = reinterpret_cast<AVCUDADeviceContext *>(deviceCtx->hwctx);
+
+ std::optional<CudaContextGuard> guard = CudaContextGuard::create(*m_api, cudaCtx->cuda_ctx);
+ if (!guard) {
+ qWarning() << "Failed to push the CUDA context current; disabling";
+ rhi = nullptr;
+ return nullptr;
+ }
+ m_cudaContext = cudaCtx->cuda_ctx;
+
+ if (!ensurePlaneResources(frame))
+ return nullptr;
+
+ PerPlaneArray<CUgraphicsResource> resources{};
+ for (int i = 0; i < geometry->planeCount; ++i)
+ resources[i] = m_planes[i]->cudaResource;
+ const CUresult mapResult =
+ m_api->cuGraphicsMapResources(geometry->planeCount, resources.data(), nullptr);
+ if (mapResult != CUDA_SUCCESS) {
+ qWarning() << "cuGraphicsMapResources failed:" << CudaResult(mapResult, *m_api);
+ return nullptr;
+ }
+
+ auto unmapGuard = qScopeGuard([&] {
+ m_api->cuGraphicsUnmapResources(geometry->planeCount, resources.data(), nullptr);
+ });
+
+ PerPlaneArray<quint64> handles{};
+ for (int i = 0; i < geometry->planeCount; ++i) {
+ const CudaPlaneGeometry &planeGeometry = geometry->planes[i];
+ const QSize planeSize(frame->width / planeGeometry.widthDivisor,
+ frame->height / planeGeometry.heightDivisor);
+
+ CUarray array = nullptr;
+ const CUresult subResourceResult =
+ m_api->cuGraphicsSubResourceGetMappedArray(&array, resources[i], 0, 0);
+ if (subResourceResult != CUDA_SUCCESS) {
+ qWarning() << "cuGraphicsSubResourceGetMappedArray failed for plane" << i << ":"
+ << CudaResult(subResourceResult, *m_api);
+ return nullptr;
+ }
+
+ const CUDA_MEMCPY2D copy{
+ .srcXInBytes = 0,
+ .srcY = 0,
+ .srcMemoryType = CU_MEMORYTYPE_DEVICE,
+ .srcHost = nullptr,
+ .srcDevice = reinterpret_cast<CUdeviceptr>(frame->data[i]),
+ .srcArray = nullptr,
+ .srcPitch = size_t(frame->linesize[i]),
+ .dstXInBytes = 0,
+ .dstY = 0,
+ .dstMemoryType = CU_MEMORYTYPE_ARRAY,
+ .dstHost = nullptr,
+ .dstDevice = 0,
+ .dstArray = array,
+ .dstPitch = 0,
+ .WidthInBytes = size_t(planeSize.width()) * size_t(planeGeometry.bytesPerTexel()),
+ .Height = size_t(planeSize.height()),
+ };
+
+ const CUresult copyResult = m_api->cuMemcpy2D_v2(&copy);
+ if (copyResult != CUDA_SUCCESS) {
+ qWarning() << "cuMemcpy2D_v2 failed for plane" << i << ":"
+ << CudaResult(copyResult, *m_api);
+ return nullptr;
+ }
+
+ handles[i] = quint64(m_planes[i]->texture);
+ }
+
+ return std::make_unique<CudaTextureHandles>(geometry->planeCount, handles,
+ shared_from_this());
+ }
+
+ private:
+ bool ensurePlaneResources(AVFrame *frame)
+ {
+ auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
+ const std::optional<CudaFrameGeometry> maybeGeometry =
+ cudaFrameGeometry(framesCtx->sw_format);
+ Q_ASSERT(maybeGeometry); // already validated by createTextureHandles
+ const CudaFrameGeometry &geometry = *maybeGeometry;
+
+ const QSize frameSize(frame->width, frame->height);
+ const bool canReuse = frameSize == m_planeResourcesSize
+ && framesCtx->sw_format == m_planeResourcesFormat
+ && ranges::all_of(QSpan(m_planes.data(), geometry.planeCount),
+ [](const auto &plane) {
+ return bool(plane);
+ });
+ if (canReuse)
+ return true;
+
+ releasePlaneResources();
+ m_planeResourcesSize = frameSize;
+ m_planeResourcesFormat = framesCtx->sw_format;
+
+ auto *glContext =
+ static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())->context;
+ QOpenGLFunctions functions(glContext);
+
+ for (int i = 0; i < geometry.planeCount; ++i) {
+ const CudaPlaneGeometry &planeGeometry = geometry.planes[i];
+ const QSize planeSize(m_planeResourcesSize.width() / planeGeometry.widthDivisor,
+ m_planeResourcesSize.height() / planeGeometry.heightDivisor);
+ const GlPlaneFormat format = glPlaneFormat(planeGeometry);
+
+ auto plane = std::make_unique<GlPlaneResource>();
+ functions.glGenTextures(1, &plane->texture);
+ functions.glBindTexture(GL_TEXTURE_2D, plane->texture);
+ functions.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ functions.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ functions.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ functions.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ functions.glTexImage2D(GL_TEXTURE_2D, 0, format.internalFormat, planeSize.width(),
+ planeSize.height(), 0, format.format, format.type, nullptr);
+ functions.glBindTexture(GL_TEXTURE_2D, 0);
+
+ const CUresult result = m_api->cuGraphicsGLRegisterImage(
+ &plane->cudaResource, plane->texture, GL_TEXTURE_2D,
+ CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
+ if (result != CUDA_SUCCESS) {
+ qWarning() << "cuGraphicsGLRegisterImage failed for plane" << i << ":"
+ << CudaResult(result, *m_api);
+ plane->release(m_api, functions);
+ return false;
+ }
+
+ m_planes[i] = std::move(plane);
+ }
+
+ return true;
+ }
+
+ void releasePlaneResources()
+ {
+ if (!rhi)
+ return;
+
+ std::optional<CudaContextGuard> guard = m_cudaContext && m_api
+ ? CudaContextGuard::create(*m_api, m_cudaContext)
+ : std::nullopt;
+
+ rhi->makeThreadLocalNativeContextCurrent();
+ auto *glContext =
+ static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())->context;
+ QOpenGLFunctions functions(glContext);
+
+ for (auto &plane : m_planes) {
+ if (!plane)
+ continue;
+ plane->release(guard ? m_api : nullptr, functions);
+ plane.reset();
+ }
+ }
+
+ const CudaDriverApi *const m_api;
+ PerPlaneArray<std::unique_ptr<GlPlaneResource>> m_planes;
+ QSize m_planeResourcesSize;
+ AVPixelFormat m_planeResourcesFormat = AV_PIX_FMT_NONE;
+ CUcontext m_cudaContext = nullptr;
+ };
+
+ # endif // QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
+
+ // ---------------------------------------------------------------------------------------------
+ // Vulkan backend
+ // ---------------------------------------------------------------------------------------------
+
+ # if QT_CONFIG(vulkan)
+
+ // Pass a device to resolve a device-level function (e.g. of VK_KHR_external_memory_fd), which
+ // yields nullptr if the extension is not enabled on that device. Without a device, an
+ // instance-level function is resolved.
+ template <typename Func>
+ Func resolveVulkanFunctionOrWarn(QVulkanInstance &inst, const char *name,
+ VkDevice dev = VK_NULL_HANDLE)
+ {
+ using namespace QtMultimediaPrivate;
+ Func func = dev ? resolveVulkanDeviceFunction<Func>(inst, dev, name)
+ : resolveVulkanFunction<Func>(inst, name);
+ if (!func)
+ qCDebug(qLHWAccelCUDA) << "Failed to resolve Vulkan function" << name;
+ return func;
+ }
+
+ // vkGetPhysicalDeviceProperties2 is core since Vulkan 1.1, with vkGetPhysicalDeviceProperties2KHR
+ // as the (functionally identical) extension entry point on older API versions/instances.
+ PFN_vkGetPhysicalDeviceProperties2 resolveVkGetPhysicalDeviceProperties2(QVulkanInstance &inst)
+ {
+ using namespace QtMultimediaPrivate;
+ PFN_vkGetPhysicalDeviceProperties2 func =
+ resolveVulkanFunction<PFN_vkGetPhysicalDeviceProperties2>(
+ inst, "vkGetPhysicalDeviceProperties2");
+ if (!func) {
+ func = resolveVulkanFunctionOrWarn<PFN_vkGetPhysicalDeviceProperties2>(
+ inst, "vkGetPhysicalDeviceProperties2KHR");
+ }
+ return func;
+ }
+
+ QUuid uuidFromCUuuid(const CUuuid &cuUuid)
+ {
+ return QUuid::fromRfc4122(QByteArrayView(reinterpret_cast<const char *>(cuUuid.bytes), 16));
+ }
+
+ // Resolves the Vulkan state/functions needed to export a plane's VkImage memory as a POSIX
+ // fd (VK_KHR_external_memory_fd) for import into CUDA. Bound to one QRhi::Vulkan instance.
+ class CudaVulkanInterop
+ {
+ public:
+ explicit CudaVulkanInterop(const QRhiVulkanNativeHandles &handles)
+ : inst(handles.inst),
+ physDev(handles.physDev),
+ dev(handles.dev),
+ deviceFuncs(handles.inst->deviceFunctions(handles.dev)),
+ instFuncs(handles.inst->functions()),
+ vkGetMemoryFdKHR(resolveVulkanFunctionOrWarn<PFN_vkGetMemoryFdKHR>(
+ *handles.inst, "vkGetMemoryFdKHR", handles.dev)),
+ vkGetPhysicalDeviceProperties2(resolveVkGetPhysicalDeviceProperties2(*handles.inst))
+ {
+ }
+
+ static std::unique_ptr<CudaVulkanInterop> create(QRhi *rhi)
+ {
+ Q_ASSERT(rhi->backend() == QRhi::Vulkan);
+ const auto *handles = static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles());
+ if (!handles || !handles->inst || !handles->dev || !handles->physDev)
+ return nullptr;
+
+ auto interop = std::make_unique<CudaVulkanInterop>(*handles);
+ if (!interop->deviceFuncs || !interop->instFuncs)
+ return nullptr;
+
+ if (!interop->vkGetMemoryFdKHR || !interop->vkGetPhysicalDeviceProperties2) {
+ qCDebug(qLHWAccelCUDA)
+ << "VK_KHR_external_memory_fd or vkGetPhysicalDeviceProperties2 is either "
+ "unavailable or not enabled on this QRhi's VkDevice/VkInstance, disabling. "
+ "To use the CUDA texture interop with Vulkan, enable the device extensions "
+ "VK_KHR_external_memory and VK_KHR_external_memory_fd when creating the "
+ "QRhi, e.g. with QQuickGraphicsConfiguration::setDeviceExtensions() or "
+ "QRhiVulkanInitParams::deviceExtensions";
+ return nullptr;
+ }
+
+ return interop;
+ }
+
+ enum class MatchResult : uint8_t {
+ SameGpu,
+ DifferentGpu,
+ Unknown, // Vulkan device UUID could not be queried
+ };
+ MatchResult matchesCudaDevice(const CUuuid &cudaUuid) const
+ {
+ using namespace QtMultimediaPrivate;
+
+ VkPhysicalDeviceIDProperties idProps{
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES,
+ .pNext = nullptr,
+ .deviceUUID = {},
+ .driverUUID = {},
+ .deviceLUID = {},
+ .deviceNodeMask = 0,
+ .deviceLUIDValid = VK_FALSE,
+ };
+ VkPhysicalDeviceProperties2 props2{
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
+ .pNext = &idProps,
+ .properties = {},
+ };
+ vkGetPhysicalDeviceProperties2(physDev, &props2);
+ const QUuid vkDeviceUuid = uuidFromVkUuid(idProps.deviceUUID);
+ if (vkDeviceUuid.isNull())
+ return MatchResult::Unknown;
+ const QUuid cuDeviceUuid = uuidFromCUuuid(cudaUuid);
+ return (vkDeviceUuid == cuDeviceUuid) ? MatchResult::SameGpu : MatchResult::DifferentGpu;
+ }
+
+ std::optional<uint32_t> findDeviceLocalMemoryTypeIndex(uint32_t memoryTypeBits) const
+ {
+ VkPhysicalDeviceMemoryProperties memProps{};
+ instFuncs->vkGetPhysicalDeviceMemoryProperties(physDev, &memProps);
+ for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
+ const bool allowed = memoryTypeBits & (1u << i);
+ const bool deviceLocal =
+ memProps.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
+ if (allowed && deviceLocal)
+ return i;
+ }
+ return std::nullopt;
+ }
+
+ QVulkanInstance *const inst;
+ const VkPhysicalDevice physDev;
+ const VkDevice dev;
+ QVulkanDeviceFunctions *const deviceFuncs;
+ QVulkanFunctions *const instFuncs;
+ const PFN_vkGetMemoryFdKHR vkGetMemoryFdKHR;
+ const PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2;
+ };
+
+ struct VulkanPlaneFormat
+ {
+ VkFormat vkFormat;
+ CUarray_format cuArrayFormat;
+ };
+
+ VulkanPlaneFormat vulkanPlaneFormat(const CudaPlaneGeometry &geometry)
+ {
+ const CUarray_format cuFormat =
+ geometry.sampleSize == 1 ? CU_AD_FORMAT_UNSIGNED_INT8 : CU_AD_FORMAT_UNSIGNED_INT16;
+ if (geometry.numChannels == 1)
+ return VulkanPlaneFormat{
+ geometry.sampleSize == 1 ? VK_FORMAT_R8_UNORM : VK_FORMAT_R16_UNORM,
+ cuFormat,
+ };
+ return VulkanPlaneFormat{
+ geometry.sampleSize == 1 ? VK_FORMAT_R8G8_UNORM : VK_FORMAT_R16G16_UNORM,
+ cuFormat,
+ };
+ }
+
+ // Vulkan interop (VK_KHR_external_memory_fd + cuImportExternalMemory).
+ struct VulkanPlaneResource
+ {
+ ~VulkanPlaneResource()
+ {
+ Q_ASSERT(image == VK_NULL_HANDLE && memory == VK_NULL_HANDLE && !externalMemory);
+ }
+
+ void release(const CudaDriverApi *cudaApi, CudaVulkanInterop &vulkan)
+ {
+ if (externalMemory && cudaApi)
+ cudaApi->cuDestroyExternalMemory(externalMemory);
+ if (memory != VK_NULL_HANDLE)
+ vulkan.deviceFuncs->vkFreeMemory(vulkan.dev, memory, nullptr);
+ if (image != VK_NULL_HANDLE)
+ vulkan.deviceFuncs->vkDestroyImage(vulkan.dev, image, nullptr);
+ externalMemory = nullptr;
+ mipmappedArray = nullptr;
+ array = nullptr;
+ memory = VK_NULL_HANDLE;
+ image = VK_NULL_HANDLE;
+ }
+
+ VkImage image = VK_NULL_HANDLE;
+ VkDeviceMemory memory = VK_NULL_HANDLE;
+ CUexternalMemory externalMemory = nullptr;
+ CUmipmappedArray mipmappedArray = nullptr;
+ CUarray array = nullptr; // level-0 view of mipmappedArray, cached for the copy
+ };
+
+ class CUDAVulkanTextureConverter final : public TextureConverterBackend
+ {
+ using MatchResult = CudaVulkanInterop::MatchResult;
+
+ public:
+ static std::shared_ptr<TextureConverterBackend> create(QRhi *rhi)
+ {
+ if (!rhi)
+ return nullptr;
+
+ auto *api = CudaDriverApi::get();
+ if (!api) {
+ qCDebug(qLHWAccelCUDA) << "CUDA driver API is not available, disabling";
+ return nullptr;
+ }
+
+ auto vulkanApi = CudaVulkanInterop::create(rhi);
+ if (!vulkanApi) {
+ qCDebug(qLHWAccelCUDA) << "CUDA-Vulkan interop is not available, disabling";
+ return nullptr;
+ }
+
+ return std::make_shared<CUDAVulkanTextureConverter>(rhi, api, std::move(vulkanApi));
+ }
+
+ explicit CUDAVulkanTextureConverter(QRhi *rhi, const CudaDriverApi *api,
+ std::unique_ptr<CudaVulkanInterop> vulkanApi)
+ : TextureConverterBackend(rhi), m_api{ api }, m_vulkan{ std::move(vulkanApi) }
+ {
+ }
+
+ ~CUDAVulkanTextureConverter() override { releasePlaneResources(); }
+
+ QVideoFrameTexturesHandlesUPtr
+ createTextureHandles(AVFrame *frame, QVideoFrameTexturesHandlesUPtr /*oldHandles*/) override
+ {
+ if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx)
+ return nullptr;
+
+ auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
+ const std::optional<CudaFrameGeometry> geometry = cudaFrameGeometry(framesCtx->sw_format);
+ if (!geometry) {
+ qWarning() << "CUDATextureConverter does not support sw_format" << framesCtx->sw_format;
+ return nullptr;
+ }
+
+ auto *deviceCtx = avFrameDeviceContext(frame);
+ if (!deviceCtx)
+ return nullptr;
+ auto *cudaCtx = reinterpret_cast<AVCUDADeviceContext *>(deviceCtx->hwctx);
+
+ std::optional<CudaContextGuard> guard = CudaContextGuard::create(*m_api, cudaCtx->cuda_ctx);
+ if (!guard) {
+ qWarning() << "Failed to push the CUDA context current; disabling";
+ rhi = nullptr;
+ return nullptr;
+ }
+ m_cudaContext = cudaCtx->cuda_ctx;
+
+ if (!ensurePlaneResources(frame))
+ return nullptr;
+
+ PerPlaneArray<quint64> handles{};
+ for (int i = 0; i < geometry->planeCount; ++i) {
+ const CudaPlaneGeometry &planeGeometry = geometry->planes[i];
+ const QSize planeSize(frame->width / planeGeometry.widthDivisor,
+ frame->height / planeGeometry.heightDivisor);
+
+ const CUDA_MEMCPY2D copy{
+ .srcXInBytes = 0,
+ .srcY = 0,
+ .srcMemoryType = CU_MEMORYTYPE_DEVICE,
+ .srcHost = nullptr,
+ .srcDevice = reinterpret_cast<CUdeviceptr>(frame->data[i]),
+ .srcArray = nullptr,
+ .srcPitch = size_t(frame->linesize[i]),
+ .dstXInBytes = 0,
+ .dstY = 0,
+ .dstMemoryType = CU_MEMORYTYPE_ARRAY,
+ .dstHost = nullptr,
+ .dstDevice = 0,
+ .dstArray = m_planes[i]->array,
+ .dstPitch = 0,
+ .WidthInBytes = size_t(planeSize.width()) * size_t(planeGeometry.bytesPerTexel()),
+ .Height = size_t(planeSize.height()),
+ };
+
+ const CUresult copyResult = m_api->cuMemcpy2D_v2(&copy);
+ if (copyResult != CUDA_SUCCESS) {
+ qWarning() << "cuMemcpy2D_v2 failed for plane" << i << ":"
+ << CudaResult(copyResult, *m_api);
+ return nullptr;
+ }
+
+ handles[i] = quint64(m_planes[i]->image);
+ }
+
+ // No external-semaphore pipelining (see the class docstring): block until the
+ // copies actually land before handing the VkImages to the RHI. cuMemcpy2D_v2
+ // runs on the default stream, so synchronizing that stream is sufficient.
+ const CUresult syncResult = m_api->cuStreamSynchronize(nullptr);
+ if (syncResult != CUDA_SUCCESS) {
+ qWarning() << "cuStreamSynchronize failed:" << CudaResult(syncResult, *m_api);
+ return nullptr;
+ }
+
+ return std::make_unique<CudaTextureHandles>(geometry->planeCount, handles,
+ shared_from_this());
+ }
+
+ private:
+ bool ensurePlaneResources(AVFrame *frame)
+ {
+ auto *framesCtx = reinterpret_cast<AVHWFramesContext *>(frame->hw_frames_ctx->data);
+ const std::optional<CudaFrameGeometry> maybeGeometry =
+ cudaFrameGeometry(framesCtx->sw_format);
+ Q_ASSERT(maybeGeometry); // already validated by createTextureHandles
+ const CudaFrameGeometry &geometry = *maybeGeometry;
+
+ const QSize frameSize(frame->width, frame->height);
+ const bool canReuse = frameSize == m_planeResourcesSize
+ && framesCtx->sw_format == m_planeResourcesFormat
+ && ranges::all_of(QSpan(m_planes.data(), geometry.planeCount),
+ [](const auto &plane) {
+ return bool(plane);
+ });
+ if (canReuse)
+ return true;
+
+ releasePlaneResources();
+ m_planeResourcesSize = frameSize;
+ m_planeResourcesFormat = framesCtx->sw_format;
+
+ if (m_vulkanDeviceMatches == MatchResult::Unknown) {
+ CUdevice device = 0;
+ CUuuid cudaUuid{};
+ const bool ok = m_api->cuCtxGetDevice(&device) == CUDA_SUCCESS
+ && m_api->cuDeviceGetUuid(&cudaUuid, device) == CUDA_SUCCESS;
+ m_vulkanDeviceMatches =
+ ok ? m_vulkan->matchesCudaDevice(cudaUuid) : MatchResult::Unknown;
+ if (m_vulkanDeviceMatches == MatchResult::Unknown) {
+ qWarning() << "CUDATextureConverter: could not determine the QRhi's Vulkan "
+ "physical device UUID (missing "
+ "VK_KHR_get_physical_device_properties2 on the VkInstance?); "
+ "disabling";
+ } else if (m_vulkanDeviceMatches == MatchResult::DifferentGpu) {
+ qWarning() << "CUDATextureConverter: the CUDA device does not match the QRhi's "
+ "Vulkan physical device (e.g. a multi-GPU system); disabling";
+ }
+ }
+ if (m_vulkanDeviceMatches != MatchResult::SameGpu)
+ return false;
+
+ for (int i = 0; i < geometry.planeCount; ++i) {
+ const CudaPlaneGeometry &planeGeometry = geometry.planes[i];
+ const QSize planeSize(m_planeResourcesSize.width() / planeGeometry.widthDivisor,
+ m_planeResourcesSize.height() / planeGeometry.heightDivisor);
+ const VulkanPlaneFormat format = vulkanPlaneFormat(planeGeometry);
+
+ auto plane = std::make_unique<VulkanPlaneResource>();
+ auto releaseGuard = qScopeGuard([&] {
+ plane->release(m_api, *m_vulkan);
+ });
+
+ const VkExternalMemoryImageCreateInfo extImageInfo{
+ .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT,
+ };
+
+ // TRANSFER_SRC is not needed by the RHI (it only samples this texture), but it
+ // costs nothing to declare and lets tests validate the copy via a readback.
+ const VkImageCreateInfo imageInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = &extImageInfo,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = format.vkFormat,
+ .extent =
+ VkExtent3D{
+ .width = uint32_t(planeSize.width()),
+ .height = uint32_t(planeSize.height()),
+ .depth = 1u,
+ },
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
+ | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ };
+
+ if (m_vulkan->deviceFuncs->vkCreateImage(m_vulkan->dev, &imageInfo, nullptr,
+ &plane->image)
+ != VK_SUCCESS) {
+ qWarning() << "CUDATextureConverter: vkCreateImage failed for plane" << i;
+ return false;
+ }
+
+ VkMemoryRequirements memReq{};
+ m_vulkan->deviceFuncs->vkGetImageMemoryRequirements(m_vulkan->dev, plane->image,
+ &memReq);
+
+ const std::optional<uint32_t> memoryTypeIndex =
+ m_vulkan->findDeviceLocalMemoryTypeIndex(memReq.memoryTypeBits);
+ if (!memoryTypeIndex) {
+ qWarning() << "CUDATextureConverter: no suitable Vulkan memory type for plane" << i;
+ return false;
+ }
+
+ const VkExportMemoryAllocateInfo exportInfo{
+ .sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO,
+ .pNext = nullptr,
+ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT,
+ };
+
+ const VkMemoryAllocateInfo allocInfo{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ .pNext = &exportInfo,
+ .allocationSize = memReq.size,
+ .memoryTypeIndex = *memoryTypeIndex,
+ };
+
+ if (m_vulkan->deviceFuncs->vkAllocateMemory(m_vulkan->dev, &allocInfo, nullptr,
+ &plane->memory)
+ != VK_SUCCESS) {
+ qWarning() << "CUDATextureConverter: vkAllocateMemory failed for plane" << i;
+ return false;
+ }
+
+ if (m_vulkan->deviceFuncs->vkBindImageMemory(m_vulkan->dev, plane->image, plane->memory,
+ 0)
+ != VK_SUCCESS) {
+ qWarning() << "CUDATextureConverter: vkBindImageMemory failed for plane" << i;
+ return false;
+ }
+
+ const VkMemoryGetFdInfoKHR getFdInfo{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR,
+ .pNext = nullptr,
+ .memory = plane->memory,
+ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT,
+ };
+ QUniqueFileDescriptorHandle fd;
+ if (m_vulkan->vkGetMemoryFdKHR(m_vulkan->dev, &getFdInfo, &fd) != VK_SUCCESS) {
+ qWarning() << "CUDATextureConverter: vkGetMemoryFdKHR failed for plane" << i;
+ return false;
+ }
+
+ const CUDA_EXTERNAL_MEMORY_HANDLE_DESC extMemDesc{
+ .type = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD,
+ .handle = { .fd = fd.get() },
+ .size = memReq.size,
+ .flags = 0,
+ .reserved = {},
+ };
+ const CUresult importResult =
+ m_api->cuImportExternalMemory(&plane->externalMemory, &extMemDesc);
+ if (importResult != CUDA_SUCCESS) {
+ qWarning() << "CUDATextureConverter: cuImportExternalMemory failed for plane" << i
+ << ":" << CudaResult(importResult, *m_api);
+ return false;
+ }
+ std::ignore = fd.release(); // import succeeded, ownership transferred to CUDA
+
+ const CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC arrayDesc{
+ .offset = 0,
+ .arrayDesc =
+ CUDA_ARRAY3D_DESCRIPTOR{
+ .Width = size_t(planeSize.width()),
+ .Height = size_t(planeSize.height()),
+ .Depth = 0,
+ .Format = format.cuArrayFormat,
+ .NumChannels = unsigned(planeGeometry.numChannels),
+ .Flags = 0,
+ },
+ .numLevels = 1,
+ .reserved = {},
+ };
+
+ const CUresult mappedArrayResult = m_api->cuExternalMemoryGetMappedMipmappedArray(
+ &plane->mipmappedArray, plane->externalMemory, &arrayDesc);
+ if (mappedArrayResult != CUDA_SUCCESS) {
+ qWarning() << "CUDATextureConverter: cuExternalMemoryGetMappedMipmappedArray "
+ "failed for plane"
+ << i << ":" << CudaResult(mappedArrayResult, *m_api);
+ return false;
+ }
+
+ const CUresult arrayLevelResult =
+ m_api->cuMipmappedArrayGetLevel(&plane->array, plane->mipmappedArray, 0);
+ if (arrayLevelResult != CUDA_SUCCESS) {
+ qWarning() << "CUDATextureConverter: cuMipmappedArrayGetLevel failed for plane" << i
+ << ":" << CudaResult(arrayLevelResult, *m_api);
+ return false;
+ }
+
+ releaseGuard.dismiss();
+ m_planes[i] = std::move(plane);
+ }
+
+ return true;
+ }
+
+ void releasePlaneResources()
+ {
+ if (!rhi)
+ return;
+
+ std::optional<CudaContextGuard> guard = m_cudaContext && m_api
+ ? CudaContextGuard::create(*m_api, m_cudaContext)
+ : std::nullopt;
+
+ for (auto &plane : m_planes) {
+ if (!plane)
+ continue;
+ plane->release(guard ? m_api : nullptr, *m_vulkan);
+ plane.reset();
+ }
+ }
+
+ const CudaDriverApi *const m_api;
+ const std::unique_ptr<CudaVulkanInterop> m_vulkan;
+ MatchResult m_vulkanDeviceMatches = MatchResult::Unknown;
+ PerPlaneArray<std::unique_ptr<VulkanPlaneResource>> m_planes;
+ QSize m_planeResourcesSize;
+ AVPixelFormat m_planeResourcesFormat = AV_PIX_FMT_NONE;
+ CUcontext m_cudaContext = nullptr;
+ };
+
+ # endif // QT_CONFIG(vulkan)
+
+ } // namespace
+
+ #endif // QT_FFMPEG_HAS_CUDA_TEXTURE_INTEROP
+
+ // ---------------------------------------------------------------------------------------------
+ // Factory
+ // ---------------------------------------------------------------------------------------------
+
+ std::shared_ptr<TextureConverterBackend> CUDATextureConverterFactory::create(QRhi *rhi)
+ {
+ if (rhi) {
+ switch (rhi->backend()) {
+ #if QT_FFMPEG_HAS_CUDA_GL_TEXTURE_INTEROP
+ case QRhi::OpenGLES2:
+ return CUDAGLTextureConverter::create(rhi);
+ #endif
+ #if QT_CONFIG(vulkan)
+ case QRhi::Vulkan:
+ return CUDAVulkanTextureConverter::create(rhi);
+ #endif
+ default:
+ break;
+ }
+ }
+
+ return nullptr;
+ }
+
+ } // namespace QFFmpeg
+
+ QT_END_NAMESPACE