MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxvk_ff_capture.cpp
Go to the documentation of this file.
1/**
2 * @file mxvk_ff_capture.cpp
3 * @brief Implementation of mxvk::VK_FF_Capture.
4 */
7
8#include <algorithm>
9#include <cstring>
10#include <format>
11#include <iostream>
12#ifdef MXVK_CUDA
13#include <cuda_runtime_api.h>
14#include <opencv2/cudaarithm.hpp>
15#ifdef MXVK_CUDA_NPP
16#include <nppi_color_conversion.h>
17#include <nppi_data_exchange_and_initialization.h>
18#endif
19#endif
20
21namespace mxvk {
22
23 namespace {
24 [[nodiscard]] double rationalToDouble(AVRational rational) {
25 if (rational.num <= 0 || rational.den <= 0) {
26 return 0.0;
27 }
28 return av_q2d(rational);
29 }
30
31#if defined(MXVK_CUDA) && defined(MXVK_CUDA_NPP)
32 [[nodiscard]] NppStreamContext makeNppStreamContext(cudaStream_t stream) {
33 NppStreamContext context{};
34 context.hStream = stream;
35 cudaGetDevice(&context.nCudaDeviceId);
36
37 cudaDeviceProp properties{};
38 cudaGetDeviceProperties(&properties, context.nCudaDeviceId);
39 context.nMultiProcessorCount = properties.multiProcessorCount;
40 context.nMaxThreadsPerMultiProcessor = properties.maxThreadsPerMultiProcessor;
41 context.nMaxThreadsPerBlock = properties.maxThreadsPerBlock;
42 context.nSharedMemPerBlock = properties.sharedMemPerBlock;
43
44 cudaDeviceGetAttribute(
45 &context.nCudaDevAttrComputeCapabilityMajor,
46 cudaDevAttrComputeCapabilityMajor,
47 context.nCudaDeviceId);
48 cudaDeviceGetAttribute(
49 &context.nCudaDevAttrComputeCapabilityMinor,
50 cudaDevAttrComputeCapabilityMinor,
51 context.nCudaDeviceId);
52 cudaStreamGetFlags(stream, &context.nStreamFlags);
53 return context;
54 }
55#endif
56 } // namespace
57
61
62 bool VK_FF_Capture::open(const std::string &filename) {
63 close();
64
65 if (avformat_open_input(&formatCtx, filename.c_str(), nullptr, nullptr) < 0) {
66 std::cout << std::format("mxvk_ff_capture: failed to open file: {}\n", filename);
67 close();
68 return false;
69 }
70
71 if (avformat_find_stream_info(formatCtx, nullptr) < 0) {
72 std::cout << std::format("mxvk_ff_capture: failed to read stream info: {}\n", filename);
73 close();
74 return false;
75 }
76
77 const int streamIndex = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
78 if (streamIndex < 0) {
79 std::cout << std::format("mxvk_ff_capture: no video stream found: {}\n", filename);
80 close();
81 return false;
82 }
83 videoStream = streamIndex;
84
85 AVStream *stream = formatCtx->streams[videoStream];
86 const AVCodecParameters *codecParams = stream->codecpar;
87 const AVCodec *decoder = avcodec_find_decoder(codecParams->codec_id);
88 if (decoder == nullptr) {
89 std::cout << "mxvk_ff_capture: no decoder found for video stream\n";
90 close();
91 return false;
92 }
93
94 codecCtx = avcodec_alloc_context3(decoder);
95 if (codecCtx == nullptr || avcodec_parameters_to_context(codecCtx, codecParams) < 0) {
96 std::cout << "mxvk_ff_capture: failed to create decoder context\n";
97 close();
98 return false;
99 }
100
101 codecCtx->opaque = this;
102 if (initHardwareDevice(decoder)) {
103 codecCtx->get_format = &VK_FF_Capture::chooseHwFormat;
104 codecCtx->hw_device_ctx = av_buffer_ref(hwDeviceCtx);
105 hardwareDecode = codecCtx->hw_device_ctx != nullptr;
106 }
107
108 if (avcodec_open2(codecCtx, decoder, nullptr) < 0) {
109 std::cout << "mxvk_ff_capture: failed to open decoder\n";
110 close();
111 return false;
112 }
113
114 packet = av_packet_alloc();
115 frame = av_frame_alloc();
116 swFrame = av_frame_alloc();
117 if (packet == nullptr || frame == nullptr || swFrame == nullptr) {
118 std::cout << "mxvk_ff_capture: failed to allocate decoder frames\n";
119 close();
120 return false;
121 }
122
123 frameWidth = codecCtx->width;
124 frameHeight = codecCtx->height;
125 frameFps = rationalToDouble(stream->avg_frame_rate);
126 if (frameFps <= 0.0) {
127 frameFps = rationalToDouble(stream->r_frame_rate);
128 }
129 if (frameFps <= 0.0) {
130 frameFps = 30.0;
131 }
132
133 std::cout << std::format(
134 "mxvk_ff_capture: opened {} ({}x{}, {:.3f} fps, decode={})\n",
135 filename,
136 frameWidth,
137 frameHeight,
138 frameFps,
139 hardwareDecode ? "cuda" : "software");
140 return true;
141 }
142
144 if (swsCtx != nullptr) {
145 sws_freeContext(swsCtx);
146 swsCtx = nullptr;
147 }
148 if (hwDeviceCtx != nullptr) {
149 av_buffer_unref(&hwDeviceCtx);
150 }
151 if (swFrame != nullptr) {
152 av_frame_free(&swFrame);
153 }
154 if (frame != nullptr) {
155 av_frame_free(&frame);
156 }
157 if (packet != nullptr) {
158 av_packet_free(&packet);
159 }
160 if (codecCtx != nullptr) {
161 avcodec_free_context(&codecCtx);
162 }
163 if (formatCtx != nullptr) {
164 avformat_close_input(&formatCtx);
165 }
166 videoStream = -1;
167 frameWidth = 0;
168 frameHeight = 0;
169 frameFps = 30.0;
170 hwPixFmt = AV_PIX_FMT_NONE;
171 hardwareDecode = false;
172 }
173
174 bool VK_FF_Capture::readRgba(std::vector<uint8_t> &rgba, int &width, int &height, int &pitch, bool flipY) {
175 if (!is_open()) {
176 return false;
177 }
178
179 if (!decodeNextFrame()) {
180 return false;
181 }
182 const bool converted = convertFrameToRgba(frame, rgba, width, height, pitch, flipY);
183 av_frame_unref(frame);
184 return converted;
185 }
186
187#ifdef MXVK_CUDA
188 bool VK_FF_Capture::readGpuRgba(cv::cuda::GpuMat &rgba, cv::cuda::Stream &stream, bool flipY) {
189 if (!is_open()) {
190 return false;
191 }
192
193 if (!decodeNextFrame()) {
194 return false;
195 }
196 const bool converted = convertFrameToGpuRgba(frame, rgba, stream, flipY);
197 av_frame_unref(frame);
198 return converted;
199 }
200#endif
201
202 bool VK_FF_Capture::decodeNextFrame() {
203 bool draining = false;
204 while (true) {
205 const int receiveResult = avcodec_receive_frame(codecCtx, frame);
206 if (receiveResult == 0) {
207 return true;
208 }
209 if (receiveResult == AVERROR_EOF) {
210 return false;
211 }
212 if (receiveResult != AVERROR(EAGAIN)) {
213 return false;
214 }
215 if (draining) {
216 return false;
217 }
218
219 while (true) {
220 const int readResult = av_read_frame(formatCtx, packet);
221 if (readResult < 0) {
222 draining = true;
223 avcodec_send_packet(codecCtx, nullptr);
224 break;
225 }
226
227 if (packet->stream_index == videoStream) {
228 const int sendResult = avcodec_send_packet(codecCtx, packet);
229 av_packet_unref(packet);
230 if (sendResult == 0 || sendResult == AVERROR(EAGAIN)) {
231 break;
232 }
233 return false;
234 }
235 av_packet_unref(packet);
236 }
237 }
238 }
239
240 AVPixelFormat VK_FF_Capture::chooseHwFormat(AVCodecContext *ctx, const AVPixelFormat *formats) {
241 const auto *capture = static_cast<const VK_FF_Capture *>(ctx->opaque);
242 if (capture == nullptr) {
243 return formats[0];
244 }
245 return capture->selectHwFormat(formats);
246 }
247
248 AVPixelFormat VK_FF_Capture::selectHwFormat(const AVPixelFormat *formats) const {
249 for (const AVPixelFormat *format = formats; *format != AV_PIX_FMT_NONE; ++format) {
250 if (*format == hwPixFmt) {
251 return *format;
252 }
253 }
254 std::cout << "mxvk_ff_capture: requested CUDA pixel format is unavailable; decoder will use software frames\n";
255 return formats[0];
256 }
257
258 bool VK_FF_Capture::initHardwareDevice(const AVCodec *decoder) {
259 const AVHWDeviceType deviceType = av_hwdevice_find_type_by_name("cuda");
260 if (deviceType == AV_HWDEVICE_TYPE_NONE) {
261 return false;
262 }
263
264 for (int index = 0;; ++index) {
265 const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, index);
266 if (config == nullptr) {
267 return false;
268 }
269 const bool hasDeviceCtx = (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) != 0;
270 if (hasDeviceCtx && config->device_type == deviceType) {
271 hwPixFmt = config->pix_fmt;
272 break;
273 }
274 }
275
276 if (av_hwdevice_ctx_create(&hwDeviceCtx, deviceType, nullptr, nullptr, 0) < 0) {
277 hwPixFmt = AV_PIX_FMT_NONE;
278 return false;
279 }
280
281 return true;
282 }
283
284 bool VK_FF_Capture::convertFrameToRgba(const AVFrame *decodedFrame, std::vector<uint8_t> &rgba, int &width, int &height, int &pitch, bool flipY) {
285 const AVFrame *sourceFrame = decodedFrame;
286 if (decodedFrame->format == hwPixFmt && hwPixFmt != AV_PIX_FMT_NONE) {
287 av_frame_unref(swFrame);
288 if (av_hwframe_transfer_data(swFrame, decodedFrame, 0) < 0) {
289 std::cout << "mxvk_ff_capture: failed to transfer CUDA decoded frame to host memory\n";
290 return false;
291 }
292 sourceFrame = swFrame;
293 }
294
295 width = sourceFrame->width;
296 height = sourceFrame->height;
297 pitch = width * 4;
298 if (width <= 0 || height <= 0) {
299 return false;
300 }
301
302 rgba.resize(static_cast<size_t>(pitch) * static_cast<size_t>(height));
303 uint8_t *dstData[4] = {rgba.data(), nullptr, nullptr, nullptr};
304 int dstLinesize[4] = {pitch, 0, 0, 0};
305
306 swsCtx = sws_getCachedContext(
307 swsCtx,
308 sourceFrame->width,
309 sourceFrame->height,
310 static_cast<AVPixelFormat>(sourceFrame->format),
311 width,
312 height,
313 AV_PIX_FMT_RGBA,
314 SWS_BILINEAR,
315 nullptr,
316 nullptr,
317 nullptr);
318 if (swsCtx == nullptr) {
319 return false;
320 }
321
322 const int scaledRows = sws_scale(swsCtx, sourceFrame->data, sourceFrame->linesize, 0, sourceFrame->height, dstData, dstLinesize);
323 if (scaledRows != height) {
324 return false;
325 }
326 if (flipY) {
327 flipRows(rgba, pitch);
328 }
329 return true;
330 }
331
332#ifdef MXVK_CUDA
333 bool VK_FF_Capture::convertFrameToGpuRgba(const AVFrame *decodedFrame, cv::cuda::GpuMat &rgba, cv::cuda::Stream &stream, bool flipY) {
334 if (decodedFrame->format == hwPixFmt && hwPixFmt != AV_PIX_FMT_NONE && decodedFrame->hw_frames_ctx != nullptr) {
335 const auto *framesContext = reinterpret_cast<const AVHWFramesContext *>(decodedFrame->hw_frames_ctx->data);
336 if (framesContext != nullptr && framesContext->sw_format == AV_PIX_FMT_NV12) {
337 const int width = decodedFrame->width;
338 const int height = decodedFrame->height;
339 if (width <= 0 || height <= 0 || decodedFrame->data[0] == nullptr || decodedFrame->data[1] == nullptr) {
340 return false;
341 }
342
343 gpuNv12.create(height + (height / 2), width, CV_8UC1);
344 cudaStream_t cudaStream = mxvk::cuda_stream_handle(stream);
345 cudaError_t result = cudaMemcpy2DAsync(
346 gpuNv12.ptr(),
347 gpuNv12.step,
348 decodedFrame->data[0],
349 static_cast<size_t>(decodedFrame->linesize[0]),
350 static_cast<size_t>(width),
351 static_cast<size_t>(height),
352 cudaMemcpyDeviceToDevice,
353 cudaStream);
354 if (result != cudaSuccess) {
355 std::cout << "mxvk_ff_capture: CUDA NV12 luma copy failed: " << cudaGetErrorString(result) << "\n";
356 return false;
357 }
358
359 result = cudaMemcpy2DAsync(
360 gpuNv12.ptr(height),
361 gpuNv12.step,
362 decodedFrame->data[1],
363 static_cast<size_t>(decodedFrame->linesize[1]),
364 static_cast<size_t>(width),
365 static_cast<size_t>(height / 2),
366 cudaMemcpyDeviceToDevice,
367 cudaStream);
368 if (result != cudaSuccess) {
369 std::cout << "mxvk_ff_capture: CUDA NV12 chroma copy failed: " << cudaGetErrorString(result) << "\n";
370 return false;
371 }
372
373#ifdef MXVK_CUDA_NPP
374 gpuRgb.create(height, width, CV_8UC3);
375 gpuRgba.create(height, width, CV_8UC4);
376
377 const Npp8u *srcPlanes[2] = {
378 static_cast<const Npp8u *>(gpuNv12.ptr()),
379 static_cast<const Npp8u *>(gpuNv12.ptr(height)),
380 };
381 const NppiSize roi{width, height};
382 const NppStreamContext nppContext = makeNppStreamContext(cudaStream);
383 NppStatus nppStatus = nppiNV12ToRGB_8u_P2C3R_Ctx(
384 srcPlanes,
385 static_cast<int>(gpuNv12.step),
386 static_cast<Npp8u *>(gpuRgb.ptr()),
387 static_cast<int>(gpuRgb.step),
388 roi,
389 nppContext);
390 if (nppStatus != NPP_SUCCESS) {
391 std::cout << "mxvk_ff_capture: NPP NV12 to RGB conversion failed: " << static_cast<int>(nppStatus) << "\n";
392 return false;
393 }
394
395 const int rgbaOrder[4] = {0, 1, 2, 3};
396 nppStatus = nppiSwapChannels_8u_C3C4R_Ctx(
397 static_cast<const Npp8u *>(gpuRgb.ptr()),
398 static_cast<int>(gpuRgb.step),
399 static_cast<Npp8u *>(gpuRgba.ptr()),
400 static_cast<int>(gpuRgba.step),
401 roi,
402 rgbaOrder,
403 255,
404 nppContext);
405 if (nppStatus != NPP_SUCCESS) {
406 std::cout << "mxvk_ff_capture: NPP RGB to RGBA conversion failed: " << static_cast<int>(nppStatus) << "\n";
407 return false;
408 }
409
410 if (flipY) {
411 cv::cuda::flip(gpuRgba, gpuFlippedRgba, 0, stream);
412 rgba = gpuFlippedRgba;
413 } else {
414 rgba = gpuRgba;
415 }
416 return true;
417#else
418 try {
419 cv::cuda::cvtColor(gpuNv12, gpuRgba, cv::COLOR_YUV2RGBA_NV12, 0, stream);
420 if (flipY) {
421 cv::cuda::flip(gpuRgba, gpuFlippedRgba, 0, stream);
422 rgba = gpuFlippedRgba;
423 } else {
424 rgba = gpuRgba;
425 }
426 return true;
427 } catch (const cv::Exception &e) {
428 std::cout << "mxvk_ff_capture: CUDA NV12 to RGBA conversion failed; falling back to host conversion: " << e.what() << "\n";
429 }
430#endif
431 }
432 }
433
434 std::vector<uint8_t> hostRgba;
435 int width = 0;
436 int height = 0;
437 int pitch = 0;
438 if (!convertFrameToRgba(decodedFrame, hostRgba, width, height, pitch, flipY) || pitch != width * 4) {
439 return false;
440 }
441 cv::Mat hostFrame(height, width, CV_8UC4, hostRgba.data(), static_cast<size_t>(pitch));
442 gpuRgba.upload(hostFrame, stream);
443 stream.waitForCompletion();
444 rgba = gpuRgba;
445 return true;
446 }
447#endif
448
449 void VK_FF_Capture::flipRows(std::vector<uint8_t> &rgba, int pitch) const {
450 std::vector<uint8_t> row(static_cast<size_t>(pitch));
451 for (int top = 0, bottom = frameHeight - 1; top < bottom; ++top, --bottom) {
452 auto *topPtr = rgba.data() + static_cast<size_t>(top) * static_cast<size_t>(pitch);
453 auto *bottomPtr = rgba.data() + static_cast<size_t>(bottom) * static_cast<size_t>(pitch);
454 std::memcpy(row.data(), topPtr, static_cast<size_t>(pitch));
455 std::memcpy(topPtr, bottomPtr, static_cast<size_t>(pitch));
456 std::memcpy(bottomPtr, row.data(), static_cast<size_t>(pitch));
457 }
458 }
459
460} // namespace mxvk
void close()
Close the active file and release decoder resources.
bool is_open() const
Check whether a decoder is open.
int width() const
Source width in pixels.
int height() const
Source height in pixels.
bool open(const std::string &filename)
Open a video file.
VK_FF_Capture()=default
Construct a closed capture source.
bool readRgba(std::vector< uint8_t > &rgba, int &width, int &height, int &pitch, bool flipY=false)
Decode the next frame as tightly packed RGBA8.
~VK_FF_Capture()
Close and release FFmpeg resources.
FFmpeg video-file capture with optional CUDA hardware decoding.
Small compatibility wrappers around OpenCV CUDA APIs.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30