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