MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxwrite.cpp
Go to the documentation of this file.
1#include "mxwrite.hpp"
2#include <algorithm>
3#include <cctype>
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7#include <iostream>
8#include <numeric>
9#include <string>
10#include <thread>
11#ifdef MXWRITE_HAS_CUDA_COPY
12#include <cuda_runtime.h>
13#endif
14extern "C" {
15#include <libavcodec/avcodec.h>
16#include <libavformat/avformat.h>
17#include <libavutil/imgutils.h>
18#include <libavutil/mathematics.h>
19#include <libavutil/opt.h>
20#include <libavutil/mastering_display_metadata.h>
21#include <libswscale/swscale.h>
22}
23
24namespace {
25
26// --- HDR helpers -----------------------------------------------------------
27// SMPTE ST.2084 (PQ) constants.
28constexpr float kPqM1 = 2610.0f / 16384.0f;
29constexpr float kPqM2 = (2523.0f / 4096.0f) * 128.0f;
30constexpr float kPqC1 = 3424.0f / 4096.0f;
31constexpr float kPqC2 = (2413.0f / 4096.0f) * 32.0f;
32constexpr float kPqC3 = (2392.0f / 4096.0f) * 32.0f;
33// SDR reference white as a fraction of PQ peak (100 nits / 10000 nits).
34constexpr float kSdrRefFraction = 100.0f / 10000.0f;
35
36inline float srgbEotf(float v) {
37 // sRGB non-linear -> linear light.
38 return (v <= 0.04045f) ? (v / 12.92f)
39 : std::pow((v + 0.055f) / 1.055f, 2.4f);
40}
41
42inline float pqOetf(float L) {
43 // L in [0,1] where 1.0 == 10000 nits; returns PQ code value in [0,1].
44 const float Lm = std::pow(std::max(0.0f, L), kPqM1);
45 const float num = kPqC1 + kPqC2 * Lm;
46 const float den = 1.0f + kPqC3 * Lm;
47 return std::pow(num / den, kPqM2);
48}
49
50inline uint16_t clamp10(float v) {
51 if (v < 0.0f) v = 0.0f;
52 if (v > 1023.0f) v = 1023.0f;
53 return static_cast<uint16_t>(v + 0.5f);
54}
55
56// Convert one RGBA8 row pair + 2 UV rows into BT.2020 PQ YUV420P10LE.
57// Assumes RGBA input is sRGB-gamma-encoded BT.709 SDR (which is what the
58// shader pipeline produces for HDR inputs after the 8-bit swscale path).
59// Output is limited-range 10-bit. Y: [64..940], UV: [64..960] centered at 512.
60void convertRgbaToBt2020PqYuv420p10(const uint8_t *rgba,
61 int src_stride_bytes,
62 uint16_t *y_plane, int y_stride_shorts,
63 uint16_t *u_plane, int u_stride_shorts,
64 uint16_t *v_plane, int v_stride_shorts,
65 int width, int height) {
66 // BT.2020 non-constant luminance RGB->YUV (limited range).
67 // E'Y = 0.2627*R + 0.6780*G + 0.0593*B
68 // E'Pb = (B - Y) / 1.8814
69 // E'Pr = (R - Y) / 1.4746
70 // Limited 10-bit: Y: 0..1 -> 64..940 (range 876), UV: -0.5..0.5 -> 64..960 (range 896, center 512).
71 constexpr float kKr = 0.2627f;
72 constexpr float kKg = 0.6780f;
73 constexpr float kKb = 0.0593f;
74 constexpr float kPbDiv = 1.0f / 1.8814f;
75 constexpr float kPrDiv = 1.0f / 1.4746f;
76
77 for (int y = 0; y < height; y += 2) {
78 const int y1 = std::min(y + 1, height - 1);
79 const uint8_t *row0 = rgba + y * src_stride_bytes;
80 const uint8_t *row1 = rgba + y1 * src_stride_bytes;
81 uint16_t *yr0 = y_plane + y * y_stride_shorts;
82 uint16_t *yr1 = y_plane + y1 * y_stride_shorts;
83 uint16_t *ur = u_plane + (y / 2) * u_stride_shorts;
84 uint16_t *vr = v_plane + (y / 2) * v_stride_shorts;
85
86 for (int x = 0; x < width; x += 2) {
87 const int x1 = std::min(x + 1, width - 1);
88
89 // Load 2x2 block of sRGB 8-bit pixels.
90 auto loadPq = [](const uint8_t *px,
91 float &Y, float &U, float &V) {
92 // sRGB 8-bit -> linear [0,1]
93 const float r = srgbEotf(px[0] * (1.0f / 255.0f));
94 const float g = srgbEotf(px[1] * (1.0f / 255.0f));
95 const float b = srgbEotf(px[2] * (1.0f / 255.0f));
96 // Scale SDR linear [0,1] (reference 100 nits) to PQ fractional.
97 const float rL = r * kSdrRefFraction;
98 const float gL = g * kSdrRefFraction;
99 const float bL = b * kSdrRefFraction;
100 // PQ encode per channel (RGB PQ).
101 const float rp = pqOetf(rL);
102 const float gp = pqOetf(gL);
103 const float bp = pqOetf(bL);
104 // BT.2020 RGB' -> YUV'.
105 Y = kKr * rp + kKg * gp + kKb * bp;
106 U = (bp - Y) * kPbDiv;
107 V = (rp - Y) * kPrDiv;
108 };
109
110 float Y00, U00, V00;
111 float Y01, U01, V01;
112 float Y10, U10, V10;
113 float Y11, U11, V11;
114 loadPq(row0 + x * 4, Y00, U00, V00);
115 loadPq(row0 + x1 * 4, Y01, U01, V01);
116 loadPq(row1 + x * 4, Y10, U10, V10);
117 loadPq(row1 + x1 * 4, Y11, U11, V11);
118
119 // Y: per-pixel, limited-range 10-bit.
120 yr0[x] = clamp10(Y00 * 876.0f + 64.0f);
121 yr0[x1] = clamp10(Y01 * 876.0f + 64.0f);
122 yr1[x] = clamp10(Y10 * 876.0f + 64.0f);
123 yr1[x1] = clamp10(Y11 * 876.0f + 64.0f);
124
125 // UV: 4:2:0 average of 2x2 block.
126 const float Uavg = 0.25f * (U00 + U01 + U10 + U11);
127 const float Vavg = 0.25f * (V00 + V01 + V10 + V11);
128 ur[x / 2] = clamp10(Uavg * 896.0f + 512.0f);
129 vr[x / 2] = clamp10(Vavg * 896.0f + 512.0f);
130 }
131 }
132}
133
134// Convert 16-bit RGBA (already BT.2020-primaries, PQ- or HLG-encoded) into
135// BT.2020 YUV420P10LE limited-range. No transfer conversion is applied here
136// because the GPU HDR encode pass already produced the non-linear signal.
137// @c rgba is tightly-packed 16-bit (8 bytes/pixel), @c src_stride_shorts is
138// the row stride in 16-bit samples (i.e. bytes/2).
140 int src_stride_shorts,
141 uint16_t *y_plane, int y_stride_shorts,
142 uint16_t *u_plane, int u_stride_shorts,
143 uint16_t *v_plane, int v_stride_shorts,
144 int width, int height) {
145 constexpr float kKr = 0.2627f;
146 constexpr float kKg = 0.6780f;
147 constexpr float kKb = 0.0593f;
148 constexpr float kPbDiv = 1.0f / 1.8814f;
149 constexpr float kPrDiv = 1.0f / 1.4746f;
150 constexpr float kInv65535 = 1.0f / 65535.0f;
151
152 for (int y = 0; y < height; y += 2) {
153 const int y1 = std::min(y + 1, height - 1);
154 const uint16_t *row0 = rgba + y * src_stride_shorts;
155 const uint16_t *row1 = rgba + y1 * src_stride_shorts;
156 uint16_t *yr0 = y_plane + y * y_stride_shorts;
157 uint16_t *yr1 = y_plane + y1 * y_stride_shorts;
158 uint16_t *ur = u_plane + (y / 2) * u_stride_shorts;
159 uint16_t *vr = v_plane + (y / 2) * v_stride_shorts;
160
161 for (int x = 0; x < width; x += 2) {
162 const int x1 = std::min(x + 1, width - 1);
163
164 auto load = [&](const uint16_t *px, float &Y, float &U, float &V) {
165 const float rp = px[0] * kInv65535;
166 const float gp = px[1] * kInv65535;
167 const float bp = px[2] * kInv65535;
168 Y = kKr * rp + kKg * gp + kKb * bp;
169 U = (bp - Y) * kPbDiv;
170 V = (rp - Y) * kPrDiv;
171 };
172
173 float Y00, U00, V00;
174 float Y01, U01, V01;
175 float Y10, U10, V10;
176 float Y11, U11, V11;
177 load(row0 + x * 4, Y00, U00, V00);
178 load(row0 + x1 * 4, Y01, U01, V01);
179 load(row1 + x * 4, Y10, U10, V10);
180 load(row1 + x1 * 4, Y11, U11, V11);
181
182 yr0[x] = clamp10(Y00 * 876.0f + 64.0f);
183 yr0[x1] = clamp10(Y01 * 876.0f + 64.0f);
184 yr1[x] = clamp10(Y10 * 876.0f + 64.0f);
185 yr1[x1] = clamp10(Y11 * 876.0f + 64.0f);
186
187 const float Uavg = 0.25f * (U00 + U01 + U10 + U11);
188 const float Vavg = 0.25f * (V00 + V01 + V10 + V11);
189 ur[x / 2] = clamp10(Uavg * 896.0f + 512.0f);
190 vr[x / 2] = clamp10(Vavg * 896.0f + 512.0f);
191 }
192 }
193}
194
195} // namespace
196
197
199
200bool is_format_supported(const char *filename) {
201 const char *ext = strrchr(filename, '.');
202 if (!ext)
203 return false;
204 // Lowercase the extension for case-insensitive comparison.
205 std::string lower_ext(ext);
206 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
207 [](unsigned char c) { return std::tolower(c); });
208 static const char *kSupported[] = {
209 ".mp4", ".mkv", ".mov", ".avi", ".m4v",
210 ".ts", ".mts", ".m2ts", ".mpg", ".mpeg",
211 ".flv", ".f4v", ".3gp", ".3g2", ".wmv",
212 ".asf", ".vob"
213 };
214 for (const char *s : kSupported) {
215 if (lower_ext == s) return true;
216 }
217 return false;
218}
219
220void cleanup_contexts(AVFormatContext *source_ctx,
221 AVFormatContext *dest_ctx,
222 AVFormatContext *output_ctx) {
223 if (source_ctx)
224 avformat_close_input(&source_ctx);
225 if (dest_ctx)
226 avformat_close_input(&dest_ctx);
227 if (output_ctx) {
228 if (!(output_ctx->oformat->flags & AVFMT_NOFILE))
229 avio_closep(&output_ctx->pb);
230 avformat_free_context(output_ctx);
231 }
232}
233
234void transfer_audio(std::string_view sourceAudioFile, std::string_view destVideoFile) {
235 std::lock_guard<std::mutex> lock(transfer_audio_mutex);
236 if (!is_format_supported(destVideoFile.data())) {
237 std::cerr << "Unsupported output format. Supported formats: .mp4, .mkv, .avi, .mov\n";
238 return;
239 }
240
241 AVFormatContext *source_ctx = nullptr, *dest_ctx = nullptr, *output_ctx = nullptr;
242 int source_audio_idx = -1, dest_video_idx = -1, dest_audio_idx = -1;
243 std::string temp_output = std::string(destVideoFile) + ".tmp";
244
245 if (avformat_open_input(&source_ctx, sourceAudioFile.data(), nullptr, nullptr) != 0 ||
246 avformat_open_input(&dest_ctx, destVideoFile.data(), nullptr, nullptr) != 0) {
247 std::cerr << "Failed to open input files\n";
248 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
249 return;
250 }
251
252 if (avformat_find_stream_info(source_ctx, nullptr) < 0 ||
253 avformat_find_stream_info(dest_ctx, nullptr) < 0) {
254 std::cerr << "Failed to find stream info\n";
255 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
256 return;
257 }
258
259 for (unsigned i = 0; i < source_ctx->nb_streams; ++i) {
260 if (source_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
261 source_audio_idx = i;
262 break;
263 }
264 }
265
266 for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) {
267 AVMediaType type = dest_ctx->streams[i]->codecpar->codec_type;
268 if (type == AVMEDIA_TYPE_VIDEO)
269 dest_video_idx = i;
270 else if (type == AVMEDIA_TYPE_AUDIO)
271 dest_audio_idx = i;
272 }
273
274 if (source_audio_idx == -1 || dest_video_idx == -1) {
275 std::cerr << "Required streams not found\n";
276 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
277 return;
278 }
279
280 const AVOutputFormat *output_fmt = av_guess_format(nullptr, destVideoFile.data(), nullptr);
281 if (!output_fmt) {
282 output_fmt = av_guess_format("mp4", nullptr, nullptr);
283 if (!output_fmt) {
284 std::cerr << "Failed to determine output format\n";
285 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
286 return;
287 }
288 }
289
290 if (avformat_alloc_output_context2(&output_ctx, output_fmt, nullptr, temp_output.c_str()) < 0) {
291 std::cerr << "Failed to create output context\n";
292 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
293 return;
294 }
295
296 const AVCodec *audio_codec = avcodec_find_decoder(source_ctx->streams[source_audio_idx]->codecpar->codec_id);
297 if (!audio_codec) {
298 std::cerr << "Failed to find audio decoder\n";
299 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
300 return;
301 }
302
303 for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) {
304 if (dest_ctx->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
305 continue;
306 }
307
308 AVStream *dest_stream = dest_ctx->streams[i];
309 AVStream *out_stream = avformat_new_stream(output_ctx, nullptr);
310 if (!out_stream) {
311 std::cerr << "Failed to create output stream\n";
312 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
313 return;
314 }
315
316 if (avcodec_parameters_copy(out_stream->codecpar, dest_stream->codecpar) < 0) {
317 std::cerr << "Failed to copy video parameters\n";
318 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
319 return;
320 }
321
322 out_stream->time_base = dest_stream->time_base;
323 out_stream->codecpar->codec_tag = 0;
324 }
325
326 AVStream *out_stream = avformat_new_stream(output_ctx, audio_codec);
327 if (!out_stream) {
328 std::cerr << "Failed to create audio stream\n";
329 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
330 return;
331 }
332
333 AVCodecParameters *source_params = source_ctx->streams[source_audio_idx]->codecpar;
334 if (avcodec_parameters_copy(out_stream->codecpar, source_params) < 0) {
335 std::cerr << "Failed to copy audio parameters\n";
336 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
337 return;
338 }
339
340 if (source_params->frame_size == 0) {
341 out_stream->codecpar->frame_size = 1024;
342 } else {
343 out_stream->codecpar->frame_size = source_params->frame_size;
344 }
345
346 out_stream->time_base = source_ctx->streams[source_audio_idx]->time_base;
347 out_stream->codecpar->codec_tag = 0;
348 dest_audio_idx = out_stream->index;
349
350 if (!(output_ctx->oformat->flags & AVFMT_NOFILE)) {
351 if (avio_open(&output_ctx->pb, temp_output.c_str(), AVIO_FLAG_WRITE) < 0) {
352 std::cerr << "Failed to open output file\n";
353 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
354 return;
355 }
356 }
357 if (avformat_write_header(output_ctx, nullptr) < 0) {
358 std::cerr << "Failed to write header\n";
359 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
360 return;
361 }
362 AVPacket packet;
363 while (av_read_frame(dest_ctx, &packet) >= 0) {
364 if (packet.stream_index == dest_audio_idx) {
365 av_packet_unref(&packet);
366 continue;
367 }
368
369 AVStream *in_stream = dest_ctx->streams[packet.stream_index];
370 AVStream *out_stream = output_ctx->streams[packet.stream_index];
371 av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base);
372
373 if (av_interleaved_write_frame(output_ctx, &packet) < 0) {
374 std::cerr << "Failed to write packet\n";
375 av_packet_unref(&packet);
376 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
377 return;
378 }
379 av_packet_unref(&packet);
380 }
381
382 int64_t video_duration_ts = 0;
383 {
384 AVStream *vid_stream = dest_ctx->streams[0];
385 if (vid_stream->duration > 0) {
386 video_duration_ts = av_rescale_q(vid_stream->duration, vid_stream->time_base, source_ctx->streams[source_audio_idx]->time_base);
387 } else if (dest_ctx->duration > 0) {
388 AVRational av_tb = {1, AV_TIME_BASE};
389 video_duration_ts = av_rescale_q(dest_ctx->duration, av_tb, source_ctx->streams[source_audio_idx]->time_base);
390 }
391 }
392
393 av_seek_frame(source_ctx, source_audio_idx, 0, AVSEEK_FLAG_BACKWARD);
394 while (av_read_frame(source_ctx, &packet) >= 0) {
395 if (packet.stream_index == source_audio_idx) {
396 if (video_duration_ts > 0 && packet.pts != AV_NOPTS_VALUE && packet.pts > video_duration_ts) {
397 av_packet_unref(&packet);
398 break;
399 }
400 AVStream *in_stream = source_ctx->streams[packet.stream_index];
401 AVStream *out_stream = output_ctx->streams[dest_audio_idx];
402 av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base);
403 packet.stream_index = dest_audio_idx;
404
405 if (av_interleaved_write_frame(output_ctx, &packet) < 0) {
406 std::cerr << "Failed to write audio packet\n";
407 av_packet_unref(&packet);
408 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
409 return;
410 }
411 }
412 av_packet_unref(&packet);
413 }
414 av_write_trailer(output_ctx);
415 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
416 std::remove(destVideoFile.data());
417 std::rename(temp_output.c_str(), destVideoFile.data());
418}
419
420void Writer::calculateFPSFraction(float fps, int &fps_num, int &fps_den) {
421 const float epsilon = 0.001f;
422 fps_den = 1001;
423 if (std::fabs(fps - 29.97f) < epsilon) {
424 fps_num = 30000;
425 fps_den = 1001;
426 } else if (std::fabs(fps - 59.94f) < epsilon) {
427 fps_num = 60000;
428 fps_den = 1001;
429 } else {
430 float precision = 1000.0f;
431 fps_num = static_cast<int>(std::round(fps * precision));
432 fps_den = static_cast<int>(precision);
433 int gcd = std::gcd(fps_num, fps_den);
434 fps_num /= gcd;
435 fps_den /= gcd;
436 }
437}
438
439namespace {
440
441// Map an x264-style preset name to an NVENC preset (p1..p7).
442// p1 = fastest/lowest quality, p7 = slowest/highest quality.
443const char *x264_preset_to_nvenc(const std::string &p) {
444 if (p == "ultrafast") return "p1";
445 if (p == "superfast") return "p2";
446 if (p == "veryfast" || p == "faster") return "p3";
447 if (p == "fast") return "p4";
448 if (p == "medium" || p.empty()) return "p5";
449 if (p == "slow" || p == "slower") return "p6";
450 if (p == "veryslow") return "p7";
451 // Allow passing NVENC preset names through directly.
452 return p.c_str();
453}
454
455bool is_valid_x264_preset(const std::string &p) {
456 static const char *presets[] = {
457 "ultrafast","superfast","veryfast","faster","fast",
458 "medium","slow","slower","veryslow","placebo"
459 };
460 for (const char *n : presets) if (p == n) return true;
461 return false;
462}
463
464std::string lowercase_ascii(std::string text) {
465 std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
466 return static_cast<char>(std::tolower(ch));
467 });
468 return text;
469}
470
471} // namespace
472
473bool Writer::open(const std::string &filename, int w, int h, float fps, const char *crf) {
474 std::lock_guard<std::mutex> lock(writer_mutex);
475 EncodeOptions opts;
476 if (crf && *crf) {
477 try { opts.crf = std::stoi(crf); } catch (...) {}
478 }
479 // Preserve legacy low-latency behaviour for old callers.
480 opts.preset = "ultrafast";
481 opts.tune = "zerolatency";
482 opts.realtime = true;
483 return openInternal(filename, w, h, fps, opts, false);
484}
485
486bool Writer::open(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts) {
487 std::lock_guard<std::mutex> lock(writer_mutex);
488 return openInternal(filename, w, h, fps, opts, false);
489}
490
491bool Writer::open_ts(const std::string &filename, int w, int h, float fps, const char *crf) {
492 std::lock_guard<std::mutex> lock(writer_mutex);
493 EncodeOptions opts;
494 if (crf && *crf) {
495 try { opts.crf = std::stoi(crf); } catch (...) {}
496 }
497 opts.preset = "ultrafast";
498 opts.tune = "zerolatency";
499 opts.realtime = true;
500 return openInternal(filename, w, h, fps, opts, true);
501}
502
503bool Writer::open_ts(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts) {
504 std::lock_guard<std::mutex> lock(writer_mutex);
505 return openInternal(filename, w, h, fps, opts, true);
506}
507
508bool Writer::initHardwareEncoding() {
509 if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0) < 0) {
510 return false;
511 }
512
513 codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
514 codec_ctx->pix_fmt = AV_PIX_FMT_CUDA;
515 codec_ctx->sw_pix_fmt = AV_PIX_FMT_RGBA;
516
517 hw_frames_ctx = av_hwframe_ctx_alloc(hw_device_ctx);
518 if (!hw_frames_ctx) {
519 return false;
520 }
521
522 auto *frames_ctx = reinterpret_cast<AVHWFramesContext *>(hw_frames_ctx->data);
523 frames_ctx->format = AV_PIX_FMT_CUDA;
524 frames_ctx->sw_format = AV_PIX_FMT_RGBA;
525 frames_ctx->width = width;
526 frames_ctx->height = height;
527 // Pool must comfortably exceed MAX_QUEUE_SIZE so av_hwframe_get_buffer()
528 // on the producer thread never becomes the throttle. A few extra slots
529 // cover frames currently in-flight inside the encoder.
530 frames_ctx->initial_pool_size = static_cast<int>(MAX_QUEUE_SIZE) + 8;
531
532 if (av_hwframe_ctx_init(hw_frames_ctx) < 0) {
533 return false;
534 }
535
536 codec_ctx->hw_frames_ctx = av_buffer_ref(hw_frames_ctx);
537
538 upload_sw_frame = av_frame_alloc();
539 if (!upload_sw_frame) {
540 return false;
541 }
542 upload_sw_frame->format = AV_PIX_FMT_RGBA;
543 upload_sw_frame->width = width;
544 upload_sw_frame->height = height;
545 if (av_frame_get_buffer(upload_sw_frame, 32) < 0) {
546 return false;
547 }
548
549#ifdef MXWRITE_HAS_CUDA_COPY
550 // Non-blocking stream so device-to-device uploads from write_cuda_rgba()
551 // do not serialise against the renderer's CUDA work on the default stream.
552 if (!cuda_upload_stream) {
553 if (cudaStreamCreateWithFlags(&cuda_upload_stream, cudaStreamNonBlocking) != cudaSuccess) {
554 cuda_upload_stream = nullptr;
555 }
556 }
557#endif
558
559 return true;
560}
561
562bool Writer::openInternal(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts, bool ts_mode) {
563 avformat_network_init();
564 av_log_set_level(AV_LOG_ERROR);
565 opened = false;
566 stop_requested = false;
567 frame_count = 0;
568 last_duration = 0.0;
569 block_when_full.store(opts.block_when_full, std::memory_order_relaxed);
570
571 while (!encode_queue.empty()) {
572 releaseFrame(encode_queue.front());
573 encode_queue.pop();
574 }
575
576 // Pass nullptr for format_name so libavformat picks the container based
577 // on the filename extension (mp4, mkv, mov, avi...).
578 if (avformat_alloc_output_context2(&format_ctx, nullptr, nullptr, filename.c_str()) < 0) {
579 std::cerr << "Could not allocate output context.\n";
580 return false;
581 }
582
583 width = w;
584 height = h;
585 hdr_output = opts.hdr.enabled;
586 hdr_info = opts.hdr;
587
588 // ---- HDR (HEVC Main10 + BT.2020/PQ) path ------------------------------
589 // Short-circuits the normal SDR codec selection when opts.hdr.enabled is
590 // true. Forces software libx265 + YUV420P10LE + PQ metadata, writes the
591 // color tags and mastering/content-light side data, and bypasses NVENC.
592 if (hdr_output) {
593 const AVCodec *hdr_codec = avcodec_find_encoder_by_name("libx265");
594 if (!hdr_codec) {
595 std::cerr << "MXWrite: HDR output requested but libx265 encoder not available.\n";
596 avformat_free_context(format_ctx);
597 format_ctx = nullptr;
598 return false;
599 }
600
601 stream = avformat_new_stream(format_ctx, hdr_codec);
602 if (!stream) {
603 std::cerr << "MXWrite: could not create HDR stream.\n";
604 avformat_free_context(format_ctx);
605 format_ctx = nullptr;
606 return false;
607 }
608
609 calculateFPSFraction(fps, fps_num, fps_den);
610 AVRational tb_hdr = {fps_den, fps_num};
611 stream->time_base = tb_hdr;
612
613 codec_ctx = avcodec_alloc_context3(hdr_codec);
614 if (!codec_ctx) {
615 std::cerr << "MXWrite: could not allocate HDR codec context.\n";
616 avformat_free_context(format_ctx);
617 format_ctx = nullptr;
618 return false;
619 }
620
621 codec_ctx->width = width;
622 codec_ctx->height = height;
623 codec_ctx->time_base = stream->time_base;
624 codec_ctx->framerate = AVRational{fps_num, fps_den};
625 codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P10LE;
626 codec_ctx->profile = AV_PROFILE_HEVC_MAIN_10;
627 codec_ctx->bits_per_raw_sample = 10;
628 codec_ctx->gop_size = 30;
629 codec_ctx->max_b_frames = 0;
630 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
631 codec_ctx->thread_type = FF_THREAD_SLICE;
632 codec_ctx->delay = 0;
633
634 // Tag the stream with BT.2020 + PQ (or whatever the input used).
635 codec_ctx->color_primaries = static_cast<AVColorPrimaries>(
636 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
637 codec_ctx->color_trc = static_cast<AVColorTransferCharacteristic>(
638 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
639 codec_ctx->colorspace = static_cast<AVColorSpace>(
640 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
641 codec_ctx->color_range = static_cast<AVColorRange>(
642 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
643 codec_ctx->chroma_sample_location = AVCHROMA_LOC_LEFT;
644
645 // Encoder options: Main10, matching x265-params for color volume.
646 std::string preset_hdr = opts.preset.empty() ? std::string("medium") : opts.preset;
647 av_opt_set(codec_ctx->priv_data, "preset", preset_hdr.c_str(), 0);
648 int crf_val_hdr = opts.crf;
649 if (crf_val_hdr < 0) crf_val_hdr = 0;
650 if (crf_val_hdr > 51) crf_val_hdr = 51;
651 const std::string crf_hdr = std::to_string(crf_val_hdr);
652 av_opt_set(codec_ctx->priv_data, "crf", crf_hdr.c_str(), 0);
653
654 // x265 params: colorprim, transfer, colormatrix, range, hdr flag.
655 // These drive the stream VUI + SEI so players recognise the file as HDR.
656 std::string x265_params = "profile=main10:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:range=limited:repeat-headers=1";
657 // When HLG transfer is requested, swap transfer + mark hlg.
658 if (codec_ctx->color_trc == AVCOL_TRC_ARIB_STD_B67) {
659 x265_params = "profile=main10:colorprim=bt2020:transfer=arib-std-b67:colormatrix=bt2020nc:range=limited:repeat-headers=1";
660 }
661 av_opt_set(codec_ctx->priv_data, "x265-params", x265_params.c_str(), 0);
662
663 time_base = tb_hdr;
664 if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
665 codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
666 }
667 if (avcodec_open2(codec_ctx, hdr_codec, nullptr) < 0) {
668 std::cerr << "MXWrite: could not open libx265 for HDR output.\n";
669 avcodec_free_context(&codec_ctx);
670 avformat_free_context(format_ctx);
671 format_ctx = nullptr;
672 return false;
673 }
674 if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
675 std::cerr << "MXWrite: could not copy HDR codec parameters.\n";
676 avcodec_free_context(&codec_ctx);
677 avformat_free_context(format_ctx);
678 format_ctx = nullptr;
679 return false;
680 }
681
682 // Attach mastering-display / content-light side data to the stream
683 // codec parameters. Uses the modern AVCodecParameters coded_side_data
684 // API. Failures are logged but non-fatal.
685 auto attach_side = [&](AVPacketSideDataType type,
686 const std::vector<uint8_t> &payload) {
687 if (payload.empty()) return;
688 uint8_t *buf = static_cast<uint8_t *>(av_malloc(payload.size()));
689 if (!buf) return;
690 std::memcpy(buf, payload.data(), payload.size());
691 const AVPacketSideData *added = av_packet_side_data_add(
692 &stream->codecpar->coded_side_data,
693 &stream->codecpar->nb_coded_side_data,
694 type,
695 buf,
696 payload.size(),
697 0);
698 if (!added) {
699 av_free(buf);
700 std::cerr << "MXWrite: failed to attach HDR side data (type " << (int)type << ").\n";
701 }
702 };
703 attach_side(AV_PKT_DATA_MASTERING_DISPLAY_METADATA, hdr_info.mastering_display);
704 attach_side(AV_PKT_DATA_CONTENT_LIGHT_LEVEL, hdr_info.content_light);
705
706 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
707 if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
708 std::cerr << "MXWrite: could not open HDR output file: " << filename << "\n";
709 avcodec_free_context(&codec_ctx);
710 avformat_free_context(format_ctx);
711 format_ctx = nullptr;
712 return false;
713 }
714 }
715 if (avformat_write_header(format_ctx, nullptr) < 0) {
716 std::cerr << "MXWrite: error writing HDR MP4 header.\n";
717 avio_closep(&format_ctx->pb);
718 avcodec_free_context(&codec_ctx);
719 avformat_free_context(format_ctx);
720 format_ctx = nullptr;
721 return false;
722 }
723
724 // Allocate the 10-bit YUV staging frame used by encodeAndWriteFrame.
725 frame10 = av_frame_alloc();
726 if (!frame10) {
727 std::cerr << "MXWrite: could not allocate YUV420P10LE frame.\n";
728 avio_closep(&format_ctx->pb);
729 avcodec_free_context(&codec_ctx);
730 avformat_free_context(format_ctx);
731 format_ctx = nullptr;
732 return false;
733 }
734 frame10->format = AV_PIX_FMT_YUV420P10LE;
735 frame10->width = width;
736 frame10->height = height;
737 if (av_frame_get_buffer(frame10, 32) < 0) {
738 std::cerr << "MXWrite: could not allocate YUV420P10LE buffer.\n";
739 av_frame_free(&frame10);
740 avio_closep(&format_ctx->pb);
741 avcodec_free_context(&codec_ctx);
742 avformat_free_context(format_ctx);
743 format_ctx = nullptr;
744 return false;
745 }
746
747 opened = true;
748 use_hw_encode = false;
749 recordingStart = std::chrono::steady_clock::now();
750 startEncoderThread();
751 std::cout << "MXWrite: HDR output active (libx265 Main10, BT.2020, "
752 << (codec_ctx->color_trc == AVCOL_TRC_ARIB_STD_B67 ? "HLG" : "PQ")
753 << ")\n";
754 return true;
755 }
756 // ---- End HDR path -----------------------------------------------------
757
758 const bool is_high_res = (width > 3840 || height > 2160);
759 const std::string codec_pref = lowercase_ascii(opts.codec);
760 const bool explicit_hevc_nvenc = (codec_pref == "hevc_nvenc" || codec_pref == "h265_nvenc");
761 const bool explicit_h264_nvenc = (codec_pref == "h264_nvenc");
762 const bool use_hevc_codec = explicit_hevc_nvenc || (!explicit_h264_nvenc && is_high_res);
763 const char *hw_codec_name = use_hevc_codec ? "hevc_nvenc" : "h264_nvenc";
764 const AVCodecID sw_codec_id = use_hevc_codec ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264;
765
766 // Codec selection based on user preference.
767 const AVCodec *codec = nullptr;
768 bool wants_hw = false;
769 if (codec_pref == "software" || codec_pref == "x264" || codec_pref == "cpu") {
770 codec = avcodec_find_encoder(sw_codec_id);
771 wants_hw = false;
772 } else {
773 // "auto" or "nvenc" keeps the resolution-based default; concrete
774 // names like "hevc_nvenc" and "h264_nvenc" select that NVENC codec.
775 codec = avcodec_find_encoder_by_name(hw_codec_name);
776 wants_hw = (codec != nullptr);
777 if (!codec) {
778 if (codec_pref == "nvenc" || explicit_hevc_nvenc || explicit_h264_nvenc) {
779 std::cerr << "MXWrite: NVENC requested but " << hw_codec_name
780 << " not available; falling back to software.\n";
781 }
782 codec = avcodec_find_encoder(sw_codec_id);
783 }
784 }
785
786 if (!codec) {
787 std::cerr << "Could not find " << (use_hevc_codec ? "H.265" : "H.264") << " encoder.\n";
788 avformat_free_context(format_ctx);
789 format_ctx = nullptr;
790 return false;
791 }
792
793 // Validate / sanitise preset and CRF.
794 std::string preset = opts.preset.empty() ? std::string("medium") : opts.preset;
795 if (!is_valid_x264_preset(preset)) {
796 // Accept unknown names; forward as-is. If empty, medium.
797 }
798 int crf_val = opts.crf;
799 if (crf_val < 0) crf_val = 0;
800 if (crf_val > 51) crf_val = 51;
801 const std::string crf_str = std::to_string(crf_val);
802
803 stream = avformat_new_stream(format_ctx, codec);
804 if (!stream) {
805 std::cerr << "Could not create new stream.\n";
806 avformat_free_context(format_ctx);
807 format_ctx = nullptr;
808 return false;
809 }
810
811 calculateFPSFraction(fps, fps_num, fps_den);
812
813 AVRational tb = {fps_den, fps_num};
814 stream->time_base = tb;
815
816 codec_ctx = avcodec_alloc_context3(codec);
817 if (!codec_ctx) {
818 std::cerr << "Could not allocate codec context.\n";
819 avformat_free_context(format_ctx);
820 format_ctx = nullptr;
821 return false;
822 }
823
824 codec_ctx->width = width;
825 codec_ctx->height = height;
826 codec_ctx->time_base = stream->time_base;
827 codec_ctx->framerate = AVRational{fps_num, fps_den};
828 codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
829 codec_ctx->gop_size = 30;
830 codec_ctx->max_b_frames = 0;
831 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
832 // Frame threading scales much better than slice threading for x264 when
833 // latency is not a concern; switch only to slice threading in realtime/ts.
834 if (ts_mode || opts.realtime) {
835 codec_ctx->thread_type = FF_THREAD_SLICE;
836 codec_ctx->slices = 4;
837 } else {
838 codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
839 }
840 codec_ctx->delay = 0;
841
842 if (ts_mode || opts.realtime) {
843 codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
844 }
845
846 if (wants_hw) {
847 const char *nv_preset = x264_preset_to_nvenc(preset);
848 av_opt_set(codec_ctx->priv_data, "preset", nv_preset, 0);
849 // NVENC "tune": hq (high quality), ll (low latency), ull (ultra low latency), lossless.
850 const char *nv_tune = opts.realtime ? "ll" : "hq";
851 av_opt_set(codec_ctx->priv_data, "tune", nv_tune, 0);
852 av_opt_set(codec_ctx->priv_data, "rc", "vbr", 0);
853 av_opt_set(codec_ctx->priv_data, "cq", crf_str.c_str(), 0);
854 if (opts.realtime) {
855 av_opt_set(codec_ctx->priv_data, "zerolatency", "1", 0);
856 }
857 if (use_hevc_codec) {
858 av_opt_set(codec_ctx->priv_data, "tier", "high", 0);
859 }
860
861 if (initHardwareEncoding()) {
862 use_hw_encode = true;
863 std::cout << "MXWrite: hardware encoder selected (" << hw_codec_name << ")\n";
864 } else {
865 std::cerr << "MXWrite: " << hw_codec_name << " present but CUDA context failed, falling back to software encoder\n";
866 av_buffer_unref(&hw_frames_ctx);
867 av_buffer_unref(&hw_device_ctx);
868 avcodec_free_context(&codec_ctx);
869
870 codec = avcodec_find_encoder(sw_codec_id);
871 if (!codec) {
872 std::cerr << "Could not find software fallback encoder.\n";
873 avformat_free_context(format_ctx);
874 format_ctx = nullptr;
875 return false;
876 }
877
878 codec_ctx = avcodec_alloc_context3(codec);
879 if (!codec_ctx) {
880 std::cerr << "Could not allocate fallback codec context.\n";
881 avformat_free_context(format_ctx);
882 format_ctx = nullptr;
883 return false;
884 }
885
886 codec_ctx->width = width;
887 codec_ctx->height = height;
888 codec_ctx->time_base = stream->time_base;
889 codec_ctx->framerate = AVRational{fps_num, fps_den};
890 codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
891 codec_ctx->gop_size = 30;
892 codec_ctx->max_b_frames = 0;
893 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
894 if (ts_mode || opts.realtime) {
895 codec_ctx->thread_type = FF_THREAD_SLICE;
896 codec_ctx->slices = 4;
897 } else {
898 codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
899 }
900 codec_ctx->delay = 0;
901
902 if (ts_mode || opts.realtime) {
903 codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
904 }
905 }
906 }
907
908 if (!use_hw_encode) {
909 av_opt_set(codec_ctx->priv_data, "preset", preset.c_str(), 0);
910 // Apply tune: realtime forces zerolatency; otherwise honour user value.
911 std::string tune = opts.realtime ? std::string("zerolatency") : opts.tune;
912 if (!tune.empty() && tune != "none") {
913 av_opt_set(codec_ctx->priv_data, "tune", tune.c_str(), 0);
914 }
915 av_opt_set(codec_ctx->priv_data, "crf", crf_str.c_str(), 0);
916 if (opts.realtime && sw_codec_id == AV_CODEC_ID_H264) {
917 // Legacy low-latency parameters kept for realtime path to avoid
918 // pipeline stalls during live capture.
919 av_opt_set(codec_ctx->priv_data, "x264-params", "bframes=0:ref=1:me=dia:subme=0", 0);
920 av_opt_set(codec_ctx->priv_data, "force_cfr", "1", 0);
921 }
922 }
923
924 time_base = tb;
925
926 if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
927 codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
928 }
929 if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
930 std::cerr << "Could not open codec.\n";
931 avcodec_free_context(&codec_ctx);
932 avformat_free_context(format_ctx);
933 format_ctx = nullptr;
934 return false;
935 }
936 if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
937 std::cerr << "Could not copy codec parameters.\n";
938 avcodec_free_context(&codec_ctx);
939 avformat_free_context(format_ctx);
940 format_ctx = nullptr;
941 return false;
942 }
943 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
944 if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
945 std::cerr << "Could not open output file: " << filename << "\n";
946 avcodec_free_context(&codec_ctx);
947 avformat_free_context(format_ctx);
948 format_ctx = nullptr;
949 return false;
950 }
951 }
952 if (avformat_write_header(format_ctx, nullptr) < 0) {
953 std::cerr << "Error writing MP4 header.\n";
954 avio_closep(&format_ctx->pb);
955 avcodec_free_context(&codec_ctx);
956 avformat_free_context(format_ctx);
957 format_ctx = nullptr;
958 return false;
959 }
960
961 if (!use_hw_encode) {
962 frameYUV = av_frame_alloc();
963 if (!frameYUV) {
964 std::cerr << "Could not allocate YUV frame.\n";
965 avio_closep(&format_ctx->pb);
966 avcodec_free_context(&codec_ctx);
967 avformat_free_context(format_ctx);
968 format_ctx = nullptr;
969 return false;
970 }
971 frameYUV->format = AV_PIX_FMT_YUV420P;
972 frameYUV->width = width;
973 frameYUV->height = height;
974 if (av_frame_get_buffer(frameYUV, 32) < 0) {
975 std::cerr << "Could not allocate frame buffer for YUV frame.\n";
976 av_frame_free(&frameYUV);
977 avio_closep(&format_ctx->pb);
978 avcodec_free_context(&codec_ctx);
979 avformat_free_context(format_ctx);
980 format_ctx = nullptr;
981 return false;
982 }
983
984 sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
985 if (!sws_ctx) {
986 std::cerr << "Could not initialize conversion context.\n";
987 av_frame_free(&frameYUV);
988 avio_closep(&format_ctx->pb);
989 avcodec_free_context(&codec_ctx);
990 avformat_free_context(format_ctx);
991 format_ctx = nullptr;
992 return false;
993 }
994 }
995
996 opened = true;
997 recordingStart = std::chrono::steady_clock::now();
998 startEncoderThread();
999 return true;
1000}
1001
1002void Writer::write(void *rgba_buffer) {
1003 if (!rgba_buffer) {
1004 return;
1005 }
1006
1007 {
1008 std::lock_guard<std::mutex> lock(writer_mutex);
1009 if (!opened) {
1010 return;
1011 }
1012 }
1013
1014 AVFrame *queued_frame = av_frame_alloc();
1015 if (!queued_frame) {
1016 std::cerr << "Writer: failed to allocate queued frame\n";
1017 return;
1018 }
1019
1020 if (use_hw_encode) {
1021 queued_frame->format = AV_PIX_FMT_CUDA;
1022 queued_frame->width = width;
1023 queued_frame->height = height;
1024 if (av_hwframe_get_buffer(hw_frames_ctx, queued_frame, 0) < 0) {
1025 std::cerr << "Writer: failed to allocate CUDA frame from hardware pool\n";
1026 releaseFrame(queued_frame);
1027 return;
1028 }
1029
1030 if (av_frame_make_writable(upload_sw_frame) < 0) {
1031 std::cerr << "Writer: software upload frame not writable\n";
1032 releaseFrame(queued_frame);
1033 return;
1034 }
1035
1036 const auto *src = static_cast<const uint8_t *>(rgba_buffer);
1037 for (int y = 0; y < height; ++y) {
1038 std::memcpy(upload_sw_frame->data[0] + static_cast<size_t>(y) * upload_sw_frame->linesize[0],
1039 src + static_cast<size_t>(y) * static_cast<size_t>(width) * 4,
1040 static_cast<size_t>(width) * 4);
1041 }
1042
1043 if (av_hwframe_transfer_data(queued_frame, upload_sw_frame, 0) < 0) {
1044 std::cerr << "Writer: failed to transfer RGBA system frame to CUDA frame\n";
1045 releaseFrame(queued_frame);
1046 return;
1047 }
1048 } else {
1049 queued_frame->format = AV_PIX_FMT_RGBA;
1050 queued_frame->width = width;
1051 queued_frame->height = height;
1052 if (av_frame_get_buffer(queued_frame, 32) < 0) {
1053 std::cerr << "Writer: failed to allocate queued RGBA frame buffer\n";
1054 releaseFrame(queued_frame);
1055 return;
1056 }
1057 if (av_frame_make_writable(queued_frame) < 0) {
1058 std::cerr << "Writer: queued RGBA frame not writable\n";
1059 releaseFrame(queued_frame);
1060 return;
1061 }
1062
1063 const auto *src = static_cast<const uint8_t *>(rgba_buffer);
1064 for (int y = 0; y < height; ++y) {
1065 std::memcpy(queued_frame->data[0] + static_cast<size_t>(y) * queued_frame->linesize[0],
1066 src + static_cast<size_t>(y) * static_cast<size_t>(width) * 4,
1067 static_cast<size_t>(width) * 4);
1068 }
1069 }
1070
1071 {
1072 std::unique_lock<std::mutex> lock(queue_mutex);
1073 if (block_when_full.load(std::memory_order_relaxed)) {
1074 if (encode_queue.size() >= MAX_QUEUE_SIZE) {
1075 // In no-drop mode, once the queue reaches capacity we wait
1076 // until the encoder thread drains it, then continue.
1077 queue_cv.wait(lock, [this] {
1078 return stop_requested || encode_queue.empty();
1079 });
1080 }
1081 if (stop_requested) {
1082 releaseFrame(queued_frame);
1083 return;
1084 }
1085 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
1086 static int drop_counter = 0;
1087 if (++drop_counter % 30 == 0) {
1088 std::cerr << "Writer: dropped " << drop_counter << " SDR frames (encoder queue full)\n";
1089 }
1090 releaseFrame(queued_frame);
1091 return;
1092 }
1093 queued_frame->pts = frame_count++;
1094 encode_queue.push(queued_frame);
1095 }
1096
1097 queue_cv.notify_one();
1098}
1099
1100void Writer::write_hdr_rgba16(void *rgba16_buffer) {
1101 if (!rgba16_buffer) {
1102 return;
1103 }
1104
1105 {
1106 std::lock_guard<std::mutex> lock(writer_mutex);
1107 if (!opened) {
1108 return;
1109 }
1110 if (!hdr_output) {
1111 std::cerr << "Writer: write_hdr_rgba16 called but writer not in HDR mode\n";
1112 return;
1113 }
1114 }
1115
1116 AVFrame *queued_frame = av_frame_alloc();
1117 if (!queued_frame) {
1118 std::cerr << "Writer: failed to allocate queued HDR frame\n";
1119 return;
1120 }
1121 queued_frame->format = AV_PIX_FMT_YUV420P10LE;
1122 queued_frame->width = width;
1123 queued_frame->height = height;
1124 if (av_frame_get_buffer(queued_frame, 32) < 0) {
1125 std::cerr << "Writer: failed to allocate YUV420P10 buffer\n";
1126 releaseFrame(queued_frame);
1127 return;
1128 }
1129 if (av_frame_make_writable(queued_frame) < 0) {
1130 std::cerr << "Writer: queued HDR frame not writable\n";
1131 releaseFrame(queued_frame);
1132 return;
1133 }
1134
1135 // Convert the already-PQ/HLG-encoded 16-bit BT.2020 RGBA into the
1136 // 10-bit limited-range YUV420 plane that libx265 Main10 expects.
1137 convertBt2020Rgba16EncodedToYuv420p10(
1138 reinterpret_cast<const uint16_t *>(rgba16_buffer),
1139 width * 4,
1140 reinterpret_cast<uint16_t *>(queued_frame->data[0]),
1141 queued_frame->linesize[0] / 2,
1142 reinterpret_cast<uint16_t *>(queued_frame->data[1]),
1143 queued_frame->linesize[1] / 2,
1144 reinterpret_cast<uint16_t *>(queued_frame->data[2]),
1145 queued_frame->linesize[2] / 2,
1146 width,
1147 height);
1148
1149 queued_frame->color_primaries = static_cast<AVColorPrimaries>(
1150 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
1151 queued_frame->color_trc = static_cast<AVColorTransferCharacteristic>(
1152 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
1153 queued_frame->colorspace = static_cast<AVColorSpace>(
1154 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
1155 queued_frame->color_range = static_cast<AVColorRange>(
1156 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
1157
1158 {
1159 std::unique_lock<std::mutex> lock(queue_mutex);
1160 if (block_when_full.load(std::memory_order_relaxed)) {
1161 if (encode_queue.size() >= MAX_QUEUE_SIZE) {
1162 queue_cv.wait(lock, [this] {
1163 return stop_requested || encode_queue.empty();
1164 });
1165 }
1166 if (stop_requested) {
1167 releaseFrame(queued_frame);
1168 return;
1169 }
1170 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
1171 static int drop_counter = 0;
1172 if (++drop_counter % 30 == 0) {
1173 std::cerr << "Writer: dropped " << drop_counter << " HDR frames (encoder queue full)\n";
1174 }
1175 releaseFrame(queued_frame);
1176 return;
1177 }
1178 queued_frame->pts = frame_count++;
1179 encode_queue.push(queued_frame);
1180 }
1181
1182 queue_cv.notify_one();
1183}
1184
1185bool Writer::write_cuda_rgba(void *cuda_rgba_buffer, int src_stride, [[maybe_unused]] bool bottom_up) {
1186 if (!cuda_rgba_buffer || src_stride <= 0) {
1187 return false;
1188 }
1189
1190 {
1191 std::lock_guard<std::mutex> lock(writer_mutex);
1192 if (!opened || !use_hw_encode) {
1193 return false;
1194 }
1195 }
1196
1197 AVFrame *queued_frame = av_frame_alloc();
1198 if (!queued_frame) {
1199 std::cerr << "Writer: failed to allocate queued CUDA frame\n";
1200 return false;
1201 }
1202
1203 queued_frame->format = AV_PIX_FMT_CUDA;
1204 queued_frame->width = width;
1205 queued_frame->height = height;
1206
1207 if (av_hwframe_get_buffer(hw_frames_ctx, queued_frame, 0) < 0) {
1208 std::cerr << "Writer: failed to allocate CUDA frame from hardware pool\n";
1209 releaseFrame(queued_frame);
1210 return false;
1211 }
1212
1213#ifdef MXWRITE_HAS_CUDA_COPY
1214 cudaStream_t stream = cuda_upload_stream;
1215 const bool use_async_stream = (stream != nullptr);
1216 if (!bottom_up) {
1217 const auto copy_err = use_async_stream
1218 ? cudaMemcpy2DAsync(
1219 queued_frame->data[0],
1220 static_cast<size_t>(queued_frame->linesize[0]),
1221 cuda_rgba_buffer,
1222 static_cast<size_t>(src_stride),
1223 static_cast<size_t>(width) * 4,
1224 static_cast<size_t>(height),
1225 cudaMemcpyDeviceToDevice,
1226 stream)
1227 : cudaMemcpy2D(
1228 queued_frame->data[0],
1229 static_cast<size_t>(queued_frame->linesize[0]),
1230 cuda_rgba_buffer,
1231 static_cast<size_t>(src_stride),
1232 static_cast<size_t>(width) * 4,
1233 static_cast<size_t>(height),
1234 cudaMemcpyDeviceToDevice);
1235
1236 if (copy_err != cudaSuccess) {
1237 std::cerr << "Writer: cudaMemcpy2D device upload failed: " << cudaGetErrorString(copy_err) << "\n";
1238 releaseFrame(queued_frame);
1239 return false;
1240 }
1241 } else {
1242 // Flip vertically by issuing one async row copy per destination row
1243 // onto a single stream — kept asynchronous so launch overhead overlaps
1244 // and the producer thread only blocks once at cudaStreamSynchronize.
1245 auto *src_base = static_cast<unsigned char *>(cuda_rgba_buffer);
1246 auto *dst_base = queued_frame->data[0];
1247 const size_t row_bytes = static_cast<size_t>(width) * 4;
1248
1249 for (int y = 0; y < height; ++y) {
1250 auto *src_row = src_base + static_cast<size_t>(height - 1 - y) * static_cast<size_t>(src_stride);
1251 auto *dst_row = dst_base + static_cast<size_t>(y) * static_cast<size_t>(queued_frame->linesize[0]);
1252 const auto row_copy_err = use_async_stream
1253 ? cudaMemcpyAsync(dst_row, src_row, row_bytes, cudaMemcpyDeviceToDevice, stream)
1254 : cudaMemcpy(dst_row, src_row, row_bytes, cudaMemcpyDeviceToDevice);
1255 if (row_copy_err != cudaSuccess) {
1256 std::cerr << "Writer: cudaMemcpy row upload failed: " << cudaGetErrorString(row_copy_err) << "\n";
1257 releaseFrame(queued_frame);
1258 return false;
1259 }
1260 }
1261 }
1262 // Single synchronisation point — NVENC requires the data to be ready when
1263 // avcodec_send_frame() reads it, and the encoder thread is decoupled by
1264 // the queue so this sync only serialises this one producer call.
1265 if (use_async_stream) {
1266 const auto sync_err = cudaStreamSynchronize(stream);
1267 if (sync_err != cudaSuccess) {
1268 std::cerr << "Writer: cudaStreamSynchronize failed: " << cudaGetErrorString(sync_err) << "\n";
1269 releaseFrame(queued_frame);
1270 return false;
1271 }
1272 }
1273#else
1274 std::cerr << "Writer: CUDA copy support disabled at build time\n";
1275 releaseFrame(queued_frame);
1276 return false;
1277#endif
1278
1279 {
1280 std::unique_lock<std::mutex> lock(queue_mutex);
1281 if (block_when_full.load(std::memory_order_relaxed)) {
1282 if (encode_queue.size() >= MAX_QUEUE_SIZE) {
1283 queue_cv.wait(lock, [this] {
1284 return stop_requested || encode_queue.empty();
1285 });
1286 }
1287 if (stop_requested) {
1288 releaseFrame(queued_frame);
1289 return false;
1290 }
1291 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
1292 static int drop_counter = 0;
1293 if (++drop_counter % 30 == 0) {
1294 std::cerr << "Writer: dropped " << drop_counter << " frames (encoder queue full)\n";
1295 }
1296 releaseFrame(queued_frame);
1297 // Return TRUE so the producer does NOT fall back to the slow CPU
1298 // write() path — we already "handled" the frame (by dropping it).
1299 // Falling back would double-process every frame and double the drops.
1300 queue_cv.notify_one();
1301 return true;
1302 }
1303 queued_frame->pts = frame_count++;
1304 encode_queue.push(queued_frame);
1305 }
1306
1307 queue_cv.notify_one();
1308 return true;
1309}
1310
1311void Writer::write_ts(void *rgba_buffer) {
1312 write(rgba_buffer);
1313}
1314
1315void Writer::startEncoderThread() {
1316 stop_requested = false;
1317 encode_thread = std::jthread([this](std::stop_token st) {
1318 encodeLoop(st);
1319 });
1320}
1321
1322void Writer::stopEncoderThread() {
1323 {
1324 std::lock_guard<std::mutex> lock(queue_mutex);
1325 stop_requested = true;
1326 }
1327 queue_cv.notify_all();
1328
1329 if (encode_thread.joinable()) {
1330 encode_thread.request_stop();
1331 encode_thread.join();
1332 }
1333}
1334
1335void Writer::releaseFrame(AVFrame *f) {
1336 if (!f) {
1337 return;
1338 }
1339 av_frame_free(&f);
1340}
1341
1342void Writer::drainEncoderPackets() {
1343 AVPacket *pkt = av_packet_alloc();
1344 if (!pkt) {
1345 std::cerr << "Writer: failed to allocate packet\n";
1346 return;
1347 }
1348
1349 while (true) {
1350 int ret = avcodec_receive_packet(codec_ctx, pkt);
1351 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
1352 break;
1353 }
1354 if (ret < 0) {
1355 std::cerr << "Writer: error receiving packet: " << ret << "\n";
1356 break;
1357 }
1358
1359 av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
1360 pkt->stream_index = stream->index;
1361
1362 if (av_interleaved_write_frame(format_ctx, pkt) < 0) {
1363 std::cerr << "Writer: error writing frame\n";
1364 av_packet_unref(pkt);
1365 break;
1366 }
1367 av_packet_unref(pkt);
1368 }
1369
1370 av_packet_free(&pkt);
1371}
1372
1373void Writer::encodeAndWriteFrame(AVFrame *in_frame) {
1374 if (!in_frame) {
1375 return;
1376 }
1377
1378 AVFrame *encode_frame = in_frame;
1379 if (hdr_output) {
1380 if (in_frame->format == AV_PIX_FMT_YUV420P10LE) {
1381 // Frame has already been converted to BT.2020 PQ YUV420P10LE
1382 // by write_hdr_rgba16(). Use directly.
1383 encode_frame = in_frame;
1384 } else {
1385 // in_frame is RGBA 8-bit from the shader pipeline. Convert to BT.2020
1386 // PQ YUV420P10LE in frame10 and submit that instead.
1387 if (av_frame_make_writable(frame10) < 0) {
1388 std::cerr << "Writer: HDR frame not writable\n";
1389 return;
1390 }
1392 in_frame->data[0],
1393 in_frame->linesize[0],
1394 reinterpret_cast<uint16_t *>(frame10->data[0]),
1395 frame10->linesize[0] / 2,
1396 reinterpret_cast<uint16_t *>(frame10->data[1]),
1397 frame10->linesize[1] / 2,
1398 reinterpret_cast<uint16_t *>(frame10->data[2]),
1399 frame10->linesize[2] / 2,
1400 width,
1401 height);
1402 frame10->pts = in_frame->pts;
1403 frame10->color_primaries = static_cast<AVColorPrimaries>(
1404 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
1405 frame10->color_trc = static_cast<AVColorTransferCharacteristic>(
1406 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
1407 frame10->colorspace = static_cast<AVColorSpace>(
1408 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
1409 frame10->color_range = static_cast<AVColorRange>(
1410 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
1411 encode_frame = frame10;
1412 }
1413 } else if (!use_hw_encode) {
1414 const uint8_t *src_data[1] = {in_frame->data[0]};
1415 int src_linesize[1] = {in_frame->linesize[0]};
1416 sws_scale(sws_ctx, src_data, src_linesize, 0, height, frameYUV->data, frameYUV->linesize);
1417 frameYUV->pts = in_frame->pts;
1418 encode_frame = frameYUV;
1419 }
1420
1421 int ret = avcodec_send_frame(codec_ctx, encode_frame);
1422 if (ret == AVERROR(EAGAIN)) {
1423 // Encoder output queue is full; drain and retry this frame once.
1424 drainEncoderPackets();
1425 ret = avcodec_send_frame(codec_ctx, encode_frame);
1426 }
1427 if (ret < 0) {
1428 std::cerr << "Writer: error sending frame to encoder: " << ret << "\n";
1429 return;
1430 }
1431
1432 drainEncoderPackets();
1433}
1434
1435void Writer::encodeLoop(std::stop_token stop_token) {
1436 while (true) {
1437 AVFrame *frame = nullptr;
1438 {
1439 std::unique_lock<std::mutex> lock(queue_mutex);
1440 queue_cv.wait(lock, [this, &stop_token]() {
1441 return stop_requested || stop_token.stop_requested() || !encode_queue.empty();
1442 });
1443
1444 if ((stop_requested || stop_token.stop_requested()) && encode_queue.empty()) {
1445 break;
1446 }
1447
1448 frame = encode_queue.front();
1449 encode_queue.pop();
1450 }
1451 // Wake any producer blocked in write() waiting for queue space.
1452 queue_cv.notify_one();
1453
1454 encodeAndWriteFrame(frame);
1455 releaseFrame(frame);
1456 };
1457
1458 if (codec_ctx) {
1459 const int flush_ret = avcodec_send_frame(codec_ctx, nullptr);
1460 if (flush_ret >= 0) {
1461 drainEncoderPackets();
1462 }
1463 }
1464}
1466 std::lock_guard<std::mutex> lock(writer_mutex);
1467 if (!opened) {
1468 return;
1469 }
1470
1471 stopEncoderThread();
1472
1473 if (stream && stream->duration > 0) {
1474 last_duration = static_cast<double>(stream->duration) * av_q2d(stream->time_base);
1475 } else if (fps_num > 0 && fps_den > 0) {
1476 last_duration = static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num);
1477 }
1478
1479 av_write_trailer(format_ctx);
1480
1481 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
1482 avio_closep(&format_ctx->pb);
1483 }
1484
1485 av_frame_free(&frameRGBA);
1486 av_frame_free(&frameYUV);
1487 av_frame_free(&frame10);
1488 sws_freeContext(sws_ctx);
1489 av_frame_free(&upload_sw_frame);
1490#ifdef MXWRITE_HAS_CUDA_COPY
1491 // Destroy stream before tearing down FFmpeg CUDA device/frames contexts.
1492 if (cuda_upload_stream) {
1493 cudaStreamSynchronize(cuda_upload_stream);
1494 cudaStreamDestroy(cuda_upload_stream);
1495 cuda_upload_stream = nullptr;
1496 }
1497#endif
1498 avcodec_free_context(&codec_ctx);
1499 av_buffer_unref(&hw_frames_ctx);
1500 av_buffer_unref(&hw_device_ctx);
1501 avformat_free_context(format_ctx);
1502
1503
1504 while (!encode_queue.empty()) {
1505 releaseFrame(encode_queue.front());
1506 encode_queue.pop();
1507 }
1508 opened = false;
1509 format_ctx = nullptr;
1510 codec_ctx = nullptr;
1511 sws_ctx = nullptr;
1512 frameRGBA = nullptr;
1513 frameYUV = nullptr;
1514 frame10 = nullptr;
1515 upload_sw_frame = nullptr;
1516 use_hw_encode = false;
1517 hdr_output = false;
1518 stop_requested = false;
1519}
1520
1521double Writer::get_duration() const {
1522 if (!opened && last_duration > 0.0) {
1523 return last_duration;
1524 }
1525 if (stream && stream->duration > 0) {
1526 return static_cast<double>(stream->duration) * av_q2d(stream->time_base);
1527 }
1528 if (fps_num > 0 && fps_den > 0) {
1529 return static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num);
1530 }
1531 return 0.0;
1532}
bool write_cuda_rgba(void *cuda_rgba_buffer, int src_stride, bool bottom_up=false)
Queue a CUDA RGBA frame for encoding.
Definition mxwrite.cpp:1185
bool open(const std::string &filename, int width, int height, float fps, const char *crf)
Open an output file using the legacy CRF string interface.
Definition mxwrite.cpp:473
void write(void *rgba_buffer)
Queue a host RGBA frame for immediate-mode encoding.
Definition mxwrite.cpp:1002
bool open_ts(const std::string &filename, int width, int height, float fps, const char *crf)
Open a timestamp-based output stream using the legacy CRF string interface.
Definition mxwrite.cpp:491
double get_duration() const
Return the encoded duration in seconds.
Definition mxwrite.cpp:1521
void close()
Close the writer and flush pending packets.
Definition mxwrite.cpp:1465
void write_hdr_rgba16(void *rgba16_buffer)
Write a 16-bit RGBA frame that is already PQ- or HLG-encoded in BT.2020 primaries (8 bytes/pixel: R16...
Definition mxwrite.cpp:1100
void write_ts(void *rgba_buffer)
Queue a host RGBA frame using capture timestamps.
Definition mxwrite.cpp:1311
std::mutex transfer_audio_mutex
Definition mxwrite.cpp:198
void transfer_audio(std::string_view sourceAudioFile, std::string_view destVideoFile)
Copy audio from one video file to another.
Definition mxwrite.cpp:234
bool is_format_supported(const char *filename)
Definition mxwrite.cpp:200
void cleanup_contexts(AVFormatContext *source_ctx, AVFormatContext *dest_ctx, AVFormatContext *output_ctx)
Free FFmpeg format contexts used during transfer operations.
Definition mxwrite.cpp:220
FFmpeg-based video writer used by MXWrite.
std::string lowercase_ascii(std::string text)
Definition mxwrite.cpp:464
void convertRgbaToBt2020PqYuv420p10(const uint8_t *rgba, int src_stride_bytes, uint16_t *y_plane, int y_stride_shorts, uint16_t *u_plane, int u_stride_shorts, uint16_t *v_plane, int v_stride_shorts, int width, int height)
Definition mxwrite.cpp:60
bool is_valid_x264_preset(const std::string &p)
Definition mxwrite.cpp:455
void convertBt2020Rgba16EncodedToYuv420p10(const uint16_t *rgba, int src_stride_shorts, uint16_t *y_plane, int y_stride_shorts, uint16_t *u_plane, int u_stride_shorts, uint16_t *v_plane, int v_stride_shorts, int width, int height)
Definition mxwrite.cpp:139
constexpr float kSdrRefFraction
Definition mxwrite.cpp:34
const char * x264_preset_to_nvenc(const std::string &p)
Definition mxwrite.cpp:443
bool enabled
Enables the HDR output path.
Definition mxwrite.hpp:83
User-configurable video encoder quality options.
Definition mxwrite.hpp:56
std::string codec
Encoder selection policy or concrete NVENC codec.
Definition mxwrite.hpp:60
std::string tune
Optional tuning mode.
Definition mxwrite.hpp:58
bool realtime
Enable low-latency settings.
Definition mxwrite.hpp:61
struct EncodeOptions::HdrInfo hdr
int crf
Constant Rate Factor.
Definition mxwrite.hpp:59
bool block_when_full
Block producer threads instead of dropping when the encoder queue is full.
Definition mxwrite.hpp:62
std::string preset
Encoder preset name.
Definition mxwrite.hpp:57