Loading...
Searching...
No Matches
HWCUDA.hpp
1#pragma once
2
3#include <score/gfx/Vulkan.hpp>
4
5// CUDA driver-API types + dlopen'd function table (shared with the
6// CudaInterop consumer — see Gfx/Graph/interop/CudaFunctions.hpp).
9#include <Gfx/Graph/VulkanVideoDevice.hpp>
10
11extern "C" {
12#if __has_include(<libavutil/hwcontext_cuda.h>)
13// CUDA_VERSION=0 keeps libavutil from pulling in the real <cuda.h>; the
14// types it needs (CUcontext, CUstream, ...) are provided by CudaFunctions.hpp.
15#define CUDA_VERSION 0
16#include <libavutil/hwcontext_cuda.h>
17#define SCORE_HAS_CUDA_HWCONTEXT 1
18#endif
19}
20
21#if defined(SCORE_HAS_CUDA_HWCONTEXT) && QT_HAS_VULKAN && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
22
23#include <Gfx/Graph/decoders/ColorSpace.hpp>
24#include <Gfx/Graph/decoders/GPUVideoDecoder.hpp>
25#include <Gfx/Graph/decoders/NV12.hpp>
26#include <Gfx/Graph/decoders/P010.hpp>
27#include <Video/GpuFormats.hpp>
28
29#include <QtGui/private/qrhivulkan_p.h>
30#include <qvulkanfunctions.h>
31#include <vulkan/vulkan.h>
32
33#if defined(_WIN32)
34#include <windows.h>
35#ifndef VK_USE_PLATFORM_WIN32_KHR
36#define VK_USE_PLATFORM_WIN32_KHR
37#endif
38#include <vulkan/vulkan_win32.h>
39#else
40#include <dlfcn.h>
41#include <unistd.h>
42#endif
43
44extern "C" {
45#include <libavformat/avformat.h>
46#include <libavutil/hwcontext.h>
47}
48
49namespace score::gfx
50{
51
72struct HWCudaVulkanDecoder : GPUVideoDecoder
73{
74 Video::ImageFormat& decoder;
75 PixelFormatInfo m_fmt;
76
77 // Vulkan handles (borrowed from QRhi). m_qInst is the route through
78 // which vkinterop::VulkanCtx resolves device/instance function tables.
79 VkDevice m_dev{VK_NULL_HANDLE};
80 VkPhysicalDevice m_physDev{VK_NULL_HANDLE};
81 QVulkanInstance* m_qInst{};
82
83 // CUDA context and stream (from FFmpeg's AVCUDADeviceContext)
84 CUcontext m_cuCtx{};
85 CUstream m_cuStream{};
86
87 // Dynamically loaded CUDA functions
88 CudaFunctions m_cu;
89
90 // Per-plane resources (created once, reused every frame)
91 struct PlaneResources
92 {
93 VkImage image{VK_NULL_HANDLE};
94 VkDeviceMemory memory{VK_NULL_HANDLE};
95 CUexternalMemory cuExtMem{};
96 CUmipmappedArray cuMipArray{};
97 CUarray cuArray{};
98 VkDeviceSize memSize{};
99 };
100 PlaneResources m_planes[2]{}; // 0=Y, 1=UV
101
102 bool m_interopReady{false};
103
104 // ------------------------------------------------------------------
105
106 static bool isAvailable(QRhi& rhi, AVBufferRef* hwDeviceCtx)
107 {
108 if(rhi.backend() != QRhi::Vulkan)
109 return false;
110 auto* nh = static_cast<const QRhiVulkanNativeHandles*>(rhi.nativeHandles());
111 if(!nh || !nh->dev || !nh->physDev || !nh->inst)
112 return false;
113 // Through the device: the export (vkinterop::exportMemoryHandle) resolves it the same way.
114#if defined(_WIN32)
115 if(!vulkanDeviceCommand(rhi, "vkGetMemoryWin32HandleKHR"))
116 return false;
117#else
118 if(!vulkanDeviceCommand(rhi, "vkGetMemoryFdKHR"))
119 return false;
120#endif
121 if(!hwDeviceCtx)
122 return false;
123
124 // Verify it's actually a CUDA device context
125 auto* devCtx = reinterpret_cast<AVHWDeviceContext*>(hwDeviceCtx->data);
126 if(devCtx->type != AV_HWDEVICE_TYPE_CUDA)
127 return false;
128
129 // Check that CUDA driver supports external memory
130 CudaFunctions probe;
131 return probe.load();
132 }
133
134 explicit HWCudaVulkanDecoder(
135 Video::ImageFormat& d, QRhi& rhi, AVBufferRef* hwDeviceCtx,
136 PixelFormatInfo fmt)
137 : decoder{d}
138 , m_fmt{fmt}
139 {
140 auto* nh = static_cast<const QRhiVulkanNativeHandles*>(rhi.nativeHandles());
141 m_dev = nh->dev;
142 m_physDev = nh->physDev;
143 m_qInst = nh->inst;
144
145 // Extract CUDA context and stream from FFmpeg's device context
146 auto* devCtx = reinterpret_cast<AVHWDeviceContext*>(hwDeviceCtx->data);
147 auto* cudaDevCtx = static_cast<AVCUDADeviceContext*>(devCtx->hwctx);
148 m_cuCtx = cudaDevCtx->cuda_ctx;
149 m_cuStream = cudaDevCtx->stream;
150
151 m_cu.load();
152 }
153
154 ~HWCudaVulkanDecoder() override { cleanup(); }
155
156 void cleanup()
157 {
158 if(m_cuCtx && m_cu.ctxPush)
159 {
160 m_cu.ctxPush(m_cuCtx);
161 for(auto& p : m_planes)
162 {
163 if(p.cuMipArray)
164 m_cu.destroyMipArray(p.cuMipArray);
165 if(p.cuExtMem)
166 m_cu.destroyExtMem(p.cuExtMem);
167 p.cuArray = {};
168 p.cuMipArray = {};
169 p.cuExtMem = {};
170 }
171 CUcontext dummy{};
172 m_cu.ctxPop(&dummy);
173 }
174
175 vkinterop::VulkanCtx vctx{
176 m_qInst ? m_qInst->vkInstance() : VK_NULL_HANDLE, m_physDev, m_dev,
177 m_qInst};
178 for(auto& p : m_planes)
179 {
180 vkinterop::ExternalImage img{p.image, p.memory, p.memSize};
181 vkinterop::destroyExternal(vctx, img);
182 p.image = VK_NULL_HANDLE;
183 p.memory = VK_NULL_HANDLE;
184 }
185
186 m_interopReady = false;
187 }
188
189 // ------------------------------------------------------------------
190 // init — create exportable Vulkan textures, import into CUDA
191 // ------------------------------------------------------------------
192
193 std::pair<QShader, QShader> init(RenderList& r) override
194 {
195 auto& rhi = *r.state.rhi;
196 const auto w = decoder.width, h = decoder.height;
197
198 if(m_fmt.is10bit())
199 {
200 // P010: R16 (Y) + RG16 (UV)
201 {
202 auto tex = rhi.newTexture(QRhiTexture::R16, {w, h}, 1, QRhiTexture::Flag{});
203 tex->create();
204 auto sampler = rhi.newSampler(
205 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
206 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
207 sampler->create();
208 samplers.push_back({sampler, tex});
209 }
210 {
211 auto tex
212 = rhi.newTexture(QRhiTexture::RG16, {w / 2, h / 2}, 1, QRhiTexture::Flag{});
213 tex->create();
214 auto sampler = rhi.newSampler(
215 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
216 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
217 sampler->create();
218 samplers.push_back({sampler, tex});
219 }
220
221 // Setup Vulkan→CUDA interop for both planes
222 if(!setupPlane(0, VK_FORMAT_R16_UNORM, w, h, 1, 2)
223 || !setupPlane(1, VK_FORMAT_R16G16_UNORM, w / 2, h / 2, 2, 2))
224 {
225 qDebug() << "HWCudaVulkanDecoder: interop setup failed";
226 failed = true;
227 cleanup();
228 // setupPlane may already have re-pointed a sampler texture at a
229 // VkImage cleanup() just destroyed (createFrom). Recreate each
230 // texture with its own QRhi-owned storage so the material keeps
231 // binding valid images (rendering black instead of faulting).
232 for(auto& s : samplers)
233 if(s.texture)
234 s.texture->create();
235 }
236
238 r.state, vertexShader(),
239 QString(P010Decoder::frag).arg("").arg(colorMatrix(decoder)));
240 }
241 else
242 {
243 // NV12: R8 (Y) + RG8 (UV)
244 {
245 auto tex = rhi.newTexture(QRhiTexture::R8, {w, h}, 1, QRhiTexture::Flag{});
246 tex->create();
247 auto sampler = rhi.newSampler(
248 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
249 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
250 sampler->create();
251 samplers.push_back({sampler, tex});
252 }
253 {
254 auto tex
255 = rhi.newTexture(QRhiTexture::RG8, {w / 2, h / 2}, 1, QRhiTexture::Flag{});
256 tex->create();
257 auto sampler = rhi.newSampler(
258 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
259 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
260 sampler->create();
261 samplers.push_back({sampler, tex});
262 }
263
264 // Setup Vulkan→CUDA interop for both planes
265 if(!setupPlane(0, VK_FORMAT_R8_UNORM, w, h, 1, 1)
266 || !setupPlane(1, VK_FORMAT_R8G8_UNORM, w / 2, h / 2, 2, 1))
267 {
268 qDebug() << "HWCudaVulkanDecoder: interop setup failed";
269 failed = true;
270 cleanup();
271 // setupPlane may already have re-pointed a sampler texture at a
272 // VkImage cleanup() just destroyed (createFrom). Recreate each
273 // texture with its own QRhi-owned storage so the material keeps
274 // binding valid images (rendering black instead of faulting).
275 for(auto& s : samplers)
276 if(s.texture)
277 s.texture->create();
278 }
279
280 QString frag = NV12Decoder::nv12_filter_prologue;
281 frag += " vec3 yuv = vec3(y, u, v);\n";
282 frag += NV12Decoder::nv12_filter_epilogue;
284 r.state, vertexShader(), frag.arg("").arg(colorMatrix(decoder)));
285 }
286 }
287
288 // ------------------------------------------------------------------
289 // exec — GPU-to-GPU copy from NVDEC output to Vulkan texture
290 // ------------------------------------------------------------------
291
292 void exec(RenderList& r, QRhiResourceUpdateBatch& res, AVFrame& frame) override
293 {
294#if LIBAVUTIL_VERSION_MAJOR >= 57
295 if(!m_interopReady)
296 return;
297
298 if(!Video::formatIsHardwareDecoded(static_cast<AVPixelFormat>(frame.format)))
299 return;
300
301 const int w = decoder.width;
302 const int h = decoder.height;
303 const int bpc = m_fmt.is10bit() ? 2 : 1; // bytes per component
304
305 m_cu.ctxPush(m_cuCtx);
306
307 // Y plane: frame->data[0] is CUdeviceptr
308 {
309 CUDA_MEMCPY2D cpy{};
310 cpy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
311 cpy.srcDevice = reinterpret_cast<CUdeviceptr>(frame.data[0]);
312 cpy.srcPitch = static_cast<size_t>(frame.linesize[0]);
313 cpy.dstMemoryType = CU_MEMORYTYPE_ARRAY;
314 cpy.dstArray = m_planes[0].cuArray;
315 cpy.WidthInBytes = static_cast<size_t>(w * 1 * bpc); // 1 channel
316 cpy.Height = static_cast<size_t>(h);
317 m_cu.memcpy2DAsync(&cpy, m_cuStream);
318 }
319
320 // UV plane: frame->data[1] is CUdeviceptr
321 {
322 CUDA_MEMCPY2D cpy{};
323 cpy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
324 cpy.srcDevice = reinterpret_cast<CUdeviceptr>(frame.data[1]);
325 cpy.srcPitch = static_cast<size_t>(frame.linesize[1]);
326 cpy.dstMemoryType = CU_MEMORYTYPE_ARRAY;
327 cpy.dstArray = m_planes[1].cuArray;
328 cpy.WidthInBytes = static_cast<size_t>((w / 2) * 2 * bpc); // 2 channels
329 cpy.Height = static_cast<size_t>(h / 2);
330 m_cu.memcpy2DAsync(&cpy, m_cuStream);
331 }
332
333 // Wait for copies to complete before Vulkan reads the textures
334 m_cu.streamSync(m_cuStream);
335
336 CUcontext dummy{};
337 m_cu.ctxPop(&dummy);
338
339 // Tell Qt RHI the images were written externally so it inserts a barrier.
340 // VK_IMAGE_LAYOUT_GENERAL (1) → SHADER_READ_ONLY_OPTIMAL transition
341 // preserves content and acts as a memory barrier.
342 samplers[0].texture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL);
343 samplers[1].texture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL);
344#endif
345 }
346
347private:
348 // ------------------------------------------------------------------
349 // Setup one plane: exportable VkImage → fd → CUDA external memory
350 // ------------------------------------------------------------------
351 bool setupPlane(
352 int idx, VkFormat vkFmt, int w, int h,
353 int numChannels, int bytesPerChannel)
354 {
355 auto& plane = m_planes[idx];
356
357#if defined(_WIN32)
358 constexpr auto kHandleType
359 = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT;
360 constexpr auto kCudaHandleType
361 = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32;
362#else
363 constexpr auto kHandleType
364 = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
365 constexpr auto kCudaHandleType
366 = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD;
367#endif
368
369 vkinterop::VulkanCtx vctx{m_qInst->vkInstance(), m_physDev, m_dev, m_qInst};
370
371 // --- Create exportable VkImage + bound memory in one helper call. ---
372 vkinterop::ExternalImageDesc imgDesc{};
373 imgDesc.format = vkFmt;
374 imgDesc.extent
375 = {static_cast<uint32_t>(w), static_cast<uint32_t>(h), 1u};
376 imgDesc.usage
377 = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
378 imgDesc.tiling = VK_IMAGE_TILING_OPTIMAL;
379 imgDesc.handleType = kHandleType;
380 imgDesc.dedicated = true;
381 imgDesc.preferDeviceLocal = true;
382
383 auto extImg = vkinterop::createExportableImage(vctx, imgDesc);
384 if(!extImg)
385 return false;
386
387 plane.image = extImg->image;
388 plane.memory = extImg->memory;
389 plane.memSize = extImg->size;
390
391 // --- Export memory as fd / HANDLE. ---
392 auto exported = vkinterop::exportMemoryHandle(vctx, plane.memory, kHandleType);
393 if(!exported)
394 {
395 vkinterop::ExternalImage tmp{plane.image, plane.memory, plane.memSize};
396 vkinterop::destroyExternal(vctx, tmp);
397 plane.image = VK_NULL_HANDLE;
398 plane.memory = VK_NULL_HANDLE;
399 return false;
400 }
401
402 // --- Import into CUDA using FFmpeg's CUcontext (not score's own — we
403 // deliberately do NOT use cuda_interop_import_vulkan_image here because
404 // the CUarray must live in the same context as NVDEC's decoder
405 // output for cuMemcpy2DAsync in exec()).
406 m_cu.ctxPush(m_cuCtx);
407
409 memDesc.type = kCudaHandleType;
410#if defined(_WIN32)
411 memDesc.handle.win32.handle = exported->handle;
412#else
413 memDesc.handle.fd = exported->fd;
414#endif
415 memDesc.size = plane.memSize;
416
417 if(m_cu.importExtMem(&plane.cuExtMem, &memDesc) != CUDA_SUCCESS)
418 {
419#if defined(_WIN32)
420 CloseHandle(exported->handle);
421#else
422 ::close(exported->fd); // ownership did not transfer; we must close
423#endif
424 CUcontext dummy{};
425 m_cu.ctxPop(&dummy);
426 return false;
427 }
428#if defined(_WIN32)
429 // CUDA does NOT take ownership of Win32 handles — close after import.
430 CloseHandle(exported->handle);
431#else
432 // fd ownership transferred to CUDA per Vulkan / CUDA spec.
433#endif
434
435 // --- Map to a CUDA mipmapped array; expose level 0 as the memcpy
436 // destination used by exec().
438 mipDesc.offset = 0;
439 mipDesc.arrayDesc.Width = static_cast<size_t>(w);
440 mipDesc.arrayDesc.Height = static_cast<size_t>(h);
441 mipDesc.arrayDesc.Depth = 0; // 2D
442 mipDesc.arrayDesc.Format = (bytesPerChannel == 2)
443 ? CU_AD_FORMAT_UNSIGNED_INT16
444 : CU_AD_FORMAT_UNSIGNED_INT8;
445 mipDesc.arrayDesc.NumChannels = static_cast<unsigned int>(numChannels);
446 mipDesc.arrayDesc.Flags = 0;
447 mipDesc.numLevels = 1;
448
449 if(m_cu.getMapArray(&plane.cuMipArray, plane.cuExtMem, &mipDesc)
450 != CUDA_SUCCESS)
451 {
452 CUcontext dummy{};
453 m_cu.ctxPop(&dummy);
454 return false;
455 }
456 if(m_cu.getLevel(&plane.cuArray, plane.cuMipArray, 0) != CUDA_SUCCESS)
457 {
458 CUcontext dummy{};
459 m_cu.ctxPop(&dummy);
460 return false;
461 }
462
463 CUcontext dummy{};
464 m_cu.ctxPop(&dummy);
465
466 // --- Wrap VkImage in QRhiTexture (non-owning); layout=GENERAL since
467 // CUDA writes externally.
468 samplers[idx].texture->createFrom(QRhiTexture::NativeTexture{
469 quint64(plane.image), VK_IMAGE_LAYOUT_GENERAL});
470
471 m_interopReady = (idx == 1); // Ready after both planes are set up
472 return true;
473 }
474};
475
476} // namespace score::gfx
477
478#endif // SCORE_HAS_CUDA_HWCONTEXT && QT_HAS_VULKAN && QT_VERSION >= 6.6
Shared dlopen'd CUDA driver-API table for score-plugin-gfx.
Vulkan external-memory image/buffer create + export + import helpers.
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
std::pair< QShader, QShader > makeShaders(const RenderState &v, QString vert, QString frag, int multiViewCount)
Get a pair of compiled vertex / fragment shaders from GLSL 4.5 sources.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1707
Definition CudaFunctions.hpp:235
Definition CudaFunctions.hpp:271
Definition CudaFunctions.hpp:279
Definition VideoInterface.hpp:29