forked from LizardByte/Sunshine
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcuda.cpp
More file actions
1331 lines (1083 loc) · 43.3 KB
/
Copy pathcuda.cpp
File metadata and controls
1331 lines (1083 loc) · 43.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @file src/platform/linux/cuda.cpp
* @brief Definitions for CUDA encoding.
*/
// standard includes
#include <bitset>
#include <fcntl.h>
#include <filesystem>
#include <thread>
// lib includes
#include <ffnvcodec/dynlink_loader.h>
#include <NvFBC.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/imgutils.h>
}
// local includes
#include "cuda.h"
#include "graphics.h"
#include "src/logging.h"
#include "src/utility.h"
#include "src/video.h"
#include "wayland.h"
/**
* @def SUNSHINE_STRINGVIEW_HELPER(x)
* @brief Macro for SUNSHINE STRINGVIEW HELPER.
*/
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
/**
* @def SUNSHINE_STRINGVIEW(x)
* @brief Macro for SUNSHINE STRINGVIEW.
*/
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
/**
* @def CU_CHECK(x, y)
* @brief Macro for CU CHECK.
*/
#define CU_CHECK(x, y) \
if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \
return -1
/**
* @def CU_CHECK_IGNORE(x, y)
* @brief Macro for CU CHECK IGNORE.
*/
#define CU_CHECK_IGNORE(x, y) \
check((x), SUNSHINE_STRINGVIEW(y ": "))
namespace fs = std::filesystem;
using namespace std::literals;
namespace cuda {
constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; ///< CUDA dev attr max threads per block.
constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39; ///< CUDA dev attr max threads per multi processor.
/**
* @brief Convert a CUDA result code into Sunshine's capture status.
*
* @param sv String view containing the text to inspect.
* @param name Human-readable name to assign.
* @param description Human-readable description used in log output.
*/
void pass_error(const std::string_view &sv, const char *name, const char *description) {
BOOST_LOG(error) << sv << name << ':' << description;
}
/**
* @brief Release a Core Foundation object when the wrapper is destroyed.
*
* @param cf Core Foundation object passed to the scoped releaser.
*/
void cff(CudaFunctions *cf) {
cuda_free_functions(&cf);
}
/**
* @brief Handle to a CUDA dynamic-library function table.
*/
using cdf_t = util::safe_ptr<CudaFunctions, cff>;
static cdf_t cdf;
inline static int check(CUresult result, const std::string_view &sv) {
if (result != CUDA_SUCCESS) {
const char *name;
const char *description;
cdf->cuGetErrorName(result, &name);
cdf->cuGetErrorString(result, &description);
BOOST_LOG(error) << sv << name << ':' << description;
return -1;
}
return 0;
}
/**
* @brief Release stream resources.
*
* @param stream CUDA stream or PipeWire stream involved in the operation.
*/
void freeStream(CUstream stream) {
CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream");
}
/**
* @brief Unregister a CUDA graphics resource if it is still registered.
*
* @param resource CUDA graphics resource being mapped or unmapped.
*/
void unregisterResource(CUgraphicsResource resource) {
CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource");
}
/**
* @brief CUDA graphics resource pointer released with `cuGraphicsUnregisterResource`.
*/
using registered_resource_t = util::safe_ptr<CUgraphicsResource_st, unregisterResource>;
/**
* @brief CUDA image wrapper that owns mapped graphics resources for one frame.
*/
class img_t: public platf::img_t {
public:
tex_t tex; ///< CUDA texture object used as the conversion source.
};
/**
* @brief Map CUDA graphics resources for use as an image.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
auto status = cuda_load_functions(&cdf, nullptr);
if (status) {
BOOST_LOG(error) << "Couldn't load cuda: "sv << status;
return -1;
}
CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda");
return 0;
}
/**
* @brief CUDA encode device that imports captured frames into CUDA memory.
*/
class cuda_t: public platf::avcodec_encode_device_t {
public:
/**
* @brief Initialize the CUDA device context used for frame conversion.
*
* @param in_width In width.
* @param in_height In height.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height) {
if (!cdf) {
BOOST_LOG(warning) << "cuda not initialized"sv;
return -1;
}
data = (void *) 0x1;
width = in_width;
height = in_height;
return 0;
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
this->hwframe.reset(frame);
this->frame = frame;
auto hwframe_ctx = (AVHWFramesContext *) hw_frames_ctx->data;
if (hwframe_ctx->sw_format != AV_PIX_FMT_NV12 && hwframe_ctx->sw_format != AV_PIX_FMT_YUV444P) {
BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12 and AV_PIX_FMT_YUV444P"sv;
return -1;
}
if (!frame->buf[0]) {
if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv;
return -1;
}
}
is_yuv444 = (hwframe_ctx->sw_format == AV_PIX_FMT_YUV444P);
auto cuda_ctx = (AVCUDADeviceContext *) hwframe_ctx->device_ctx->hwctx;
stream = make_stream();
if (!stream) {
return -1;
}
cuda_ctx->stream = stream.get();
auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4);
if (!sws_opt) {
return -1;
}
sws = std::move(*sws_opt);
linear_interpolation = width != frame->width || height != frame->height;
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
sws.apply_colorspace(colorspace);
auto tex = tex_t::make(height, width * 4);
if (!tex) {
return;
}
// The default green color is ugly.
// Update the background color
platf::img_t img;
img.width = width;
img.height = height;
img.pixel_pitch = 4;
img.row_pitch = img.width * img.pixel_pitch;
std::vector<std::uint8_t> image_data;
image_data.resize(img.row_pitch * img.height);
img.data = image_data.data();
if (sws.load_ram(img, tex->array)) {
return;
}
if (is_yuv444) {
sws.convert_yuv444(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], tex->texture.linear, stream.get(), {frame->width, frame->height, 0, 0});
} else {
sws.convert_nv12(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), {frame->width, frame->height, 0, 0});
}
}
/**
* @brief Select the CUDA texture object for the configured filtering mode.
*
* @param tex Texture resource used by the converter.
* @return CUDA texture object using linear or point sampling.
*/
cudaTextureObject_t tex_obj(const tex_t &tex) const {
return linear_interpolation ? tex.texture.linear : tex.texture.point;
}
stream_t stream; ///< CUDA stream used for asynchronous conversion work.
frame_t hwframe; ///< FFmpeg hardware frame backed by CUDA resources.
int height; ///< Frame or display height in pixels.
int width; ///< Frame or display width in pixels.
// When height and width don't change, it's not necessary to use linear interpolation
bool linear_interpolation; ///< Whether the CUDA converter uses linear interpolation.
bool is_yuv444; ///< Whether the CUDA converter outputs YUV 4:4:4.
sws_t sws; ///< Software scaler used for CUDA frame conversion fallback paths.
};
/**
* @brief CUDA encode device path that converts frames through system memory.
*/
class cuda_ram_t: public cuda_t {
public:
/**
* @brief Convert a captured frame through CUDA into system-memory encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (is_yuv444) {
return sws.load_ram(img, tex.array) || sws.convert_yuv444(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], tex_obj(tex), stream.get());
}
return sws.load_ram(img, tex.array) || sws.convert_nv12(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get());
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
if (cuda_t::set_frame(frame, hw_frames_ctx)) {
return -1;
}
auto tex_opt = tex_t::make(height, width * 4);
if (!tex_opt) {
return -1;
}
tex = std::move(*tex_opt);
return 0;
}
tex_t tex; ///< CUDA texture object used as the conversion source.
};
/**
* @brief CUDA encode device path that keeps converted frames in GPU memory.
*/
class cuda_vram_t: public cuda_t {
public:
/**
* @brief Convert a captured frame through CUDA into GPU encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (is_yuv444) {
return sws.convert_yuv444(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], tex_obj(((img_t *) &img)->tex), stream.get());
}
return sws.convert_nv12(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *) &img)->tex), stream.get());
}
};
/**
* @brief Opens the DRM device associated with the CUDA device index.
* @param index CUDA device index to open.
* @return File descriptor or -1 on failure.
*/
file_t open_drm_fd_for_cuda_device(int index) {
CUdevice device;
CU_CHECK(cdf->cuDeviceGet(&device, index), "Couldn't get CUDA device");
// There's no way to directly go from CUDA to a DRM device, so we'll
// use sysfs to look up the DRM device name from the PCI ID.
std::array<char, 13> pci_bus_id;
CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id.data(), pci_bus_id.size(), device), "Couldn't get CUDA device PCI bus ID");
BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id.data();
// Linux uses lowercase hexadecimal while CUDA uses uppercase
std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), [](char c) {
return std::tolower(c);
});
// Look for the name of the primary node in sysfs
try {
char sysfs_path[PATH_MAX];
std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data());
fs::path sysfs_dir {sysfs_path};
for (auto &entry : fs::directory_iterator {sysfs_dir}) {
auto file = entry.path().filename();
auto filestring = file.generic_string();
if (std::string_view {filestring}.substr(0, 4) != "card"sv) {
continue;
}
BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring;
fs::path dri_path {"/dev/dri"sv};
auto device_path = dri_path / file;
return platf::open_drm_card_fd(device_path);
}
} catch (const std::filesystem::filesystem_error &err) {
BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what();
}
BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id.data();
return -1;
}
/**
* @brief CUDA frame resources registered for interop conversion.
*/
struct cu_resources {
registered_resource_t y_res; ///< Y res.
registered_resource_t u_res; ///< U res.
registered_resource_t v_res; ///< V res.
registered_resource_t uv_res; ///< Uv res.
};
/**
* @brief OpenGL/CUDA interop resources used for GPU-side frame conversion.
*/
class gl_cuda_vram_t: public platf::avcodec_encode_device_t {
public:
/**
* @brief Initialize the GL->CUDA encoding device.
* @param in_width Width of captured frames.
* @param in_height Height of captured frames.
* @param offset_x Offset of content in captured frame.
* @param offset_y Offset of content in captured frame.
* @return 0 on success or -1 on failure.
*/
int init(int in_width, int in_height, int offset_x, int offset_y) {
// This must be non-zero to tell the video core that it's a hardware encoding device.
data = (void *) 0x1;
// TODO: Support more than one CUDA device
file = std::move(open_drm_fd_for_cuda_device(0));
if (file.el < 0) {
char string[1024];
BOOST_LOG(error) << "Couldn't open DRM FD for CUDA device: "sv << strerror_r(errno, string, sizeof(string));
return -1;
}
gbm.reset(gbm::create_device(file.el));
if (!gbm) {
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return -1;
}
display = egl::make_display(gbm.get());
if (!display) {
return -1;
}
auto ctx_opt = egl::make_ctx(display.get());
if (!ctx_opt) {
return -1;
}
ctx = std::move(*ctx_opt);
width = in_width;
height = in_height;
sequence = 0;
this->offset_x = offset_x;
this->offset_y = offset_y;
return 0;
}
/**
* @brief Initialize color conversion into target CUDA frame.
* @param frame Destination CUDA frame to write into.
* @param hw_frames_ctx_buf FFmpeg hardware frame context.
* @return 0 on success or -1 on failure.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override {
this->hwframe.reset(frame);
this->frame = frame;
auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;
if (hw_frames_ctx->sw_format != AV_PIX_FMT_NV12 && hw_frames_ctx->sw_format != AV_PIX_FMT_YUV444P && hw_frames_ctx->sw_format != AV_PIX_FMT_P010LE && hw_frames_ctx->sw_format != AV_PIX_FMT_YUV444P16LE) {
BOOST_LOG(error) << "cuda::gl_cuda_vram_t doesn't support any format other than AV_PIX_FMT_NV12, AV_PIX_FMT_P010LE, AV_PIX_FMT_YUV444P and AV_PIX_FMT_YUV444P16LE"sv;
return -1;
}
if (!frame->buf[0]) {
if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for NVENC_GL"sv;
return -1;
}
}
sw_format = hw_frames_ctx->sw_format;
is_yuv444 = (sw_format == AV_PIX_FMT_YUV444P || sw_format == AV_PIX_FMT_YUV444P16LE);
auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format, is_yuv444);
if (!sws_opt) {
return -1;
}
this->sws = std::move(*sws_opt);
if (is_yuv444) {
auto yuv444_opt = egl::create_yuv444_target(frame->width, frame->height, sw_format);
if (!yuv444_opt) {
return -1;
}
this->yuv444 = std::move(*yuv444_opt);
} else {
auto nv12_opt = egl::create_nv12_target(frame->width, frame->height, sw_format);
if (!nv12_opt) {
return -1;
}
this->nv12 = std::move(*nv12_opt);
}
auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx;
stream = make_stream();
if (!stream) {
return -1;
}
cuda_ctx->stream = stream.get();
if (is_yuv444) {
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&cu_res.y_res, yuv444->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register Y texture");
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&cu_res.u_res, yuv444->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register U texture");
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&cu_res.v_res, yuv444->tex[2], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register V texture");
} else {
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&cu_res.y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register Y plane texture");
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&cu_res.uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register UV plane texture");
}
return 0;
}
/**
* @brief Convert the captured image into the target CUDA frame.
* @param img Captured screen image.
* @return 0 on success or -1 on failure.
*/
int convert(platf::img_t &img) override {
auto &descriptor = (egl::img_descriptor_t &) img;
if (descriptor.sequence == 0) {
// For dummy images, use a blank RGB texture instead of importing a DMA-BUF
rgb = egl::create_blank(img);
} else if (descriptor.sequence > sequence) {
sequence = descriptor.sequence;
rgb = egl::rgb_t {};
auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
if (!rgb_opt) {
return -1;
}
rgb = std::move(*rgb_opt);
}
auto fmt_desc = av_pix_fmt_desc_get(sw_format);
sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0], is_yuv444);
if (is_yuv444) {
// Perform the color conversion and scaling in GL
sws.convert_yuv444(yuv444->buf);
// Map the GL textures to read for CUDA
std::array<CUgraphicsResource, 3> resources = {{cu_res.y_res.get(), cu_res.u_res.get(), cu_res.v_res.get()}};
CU_CHECK(cdf->cuGraphicsMapResources(resources.size(), resources.data(), stream.get()), "Couldn't map GL textures in CUDA");
// Copy from the GL textures to the target CUDA frame
for (int i = 0; i < 3; i++) {
CUDA_MEMCPY2D cpy = {};
cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY;
CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), "Couldn't get mapped plane array");
cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
cpy.dstDevice = (CUdeviceptr) frame->data[i];
cpy.dstPitch = frame->linesize[i];
cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step);
cpy.Height = frame->height;
CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame");
}
// Unmap the textures to allow modification from GL again
CU_CHECK(cdf->cuGraphicsUnmapResources(resources.size(), resources.data(), stream.get()), "Couldn't unmap GL textures from CUDA");
} else {
// Perform the color conversion and scaling in GL
sws.convert_nv12(nv12->buf);
// Map the GL textures to read for CUDA
std::array<CUgraphicsResource, 2> resources = {{cu_res.y_res.get(), cu_res.uv_res.get()}};
CU_CHECK(cdf->cuGraphicsMapResources(resources.size(), resources.data(), stream.get()), "Couldn't map GL textures in CUDA");
// Copy from the GL textures to the target CUDA frame
for (int i = 0; i < 2; i++) {
CUDA_MEMCPY2D cpy = {};
cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY;
CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), "Couldn't get mapped plane array");
cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
cpy.dstDevice = (CUdeviceptr) frame->data[i];
cpy.dstPitch = frame->linesize[i];
cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step) >> (i ? fmt_desc->log2_chroma_w : 0);
cpy.Height = frame->height >> (i ? fmt_desc->log2_chroma_h : 0);
CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame");
}
// Unmap the textures to allow modification from GL again
CU_CHECK(cdf->cuGraphicsUnmapResources(resources.size(), resources.data(), stream.get()), "Couldn't unmap GL textures from CUDA");
}
// Mapping the GL conversion targets into CUDA synchronizes the preceding
// GL draw that consumed the source DMA-BUF. It is now safe for PipeWire
// to return that producer-owned buffer to KWin for reuse.
descriptor.mark_capture_buffer_consumed();
return 0;
}
/**
* @brief Configures shader parameters for the specified colorspace.
*/
void apply_colorspace() override {
sws.apply_colorspace(colorspace, is_yuv444);
}
file_t file; ///< File descriptor for the imported DMA-BUF.
gbm::gbm_t gbm; ///< GBM device used for buffer allocation..
egl::display_t display; ///< EGL display used to import captured frames.
egl::ctx_t ctx; ///< EGL context used to import captured frames.
// This must be destroyed before display_t
stream_t stream; ///< CUDA stream used for asynchronous conversion work.
frame_t hwframe; ///< FFmpeg hardware frame backed by CUDA resources.
egl::sws_t sws; ///< Software scaler used for CUDA frame conversion fallback paths.
egl::nv12_t nv12; ///< EGL/OpenGL resources used for NV12 output frames.
egl::yuv444_t yuv444; ///< EGL/OpenGL resources used for YUV444 output frames.
AVPixelFormat sw_format; ///< FFmpeg software pixel format produced by conversion.
int height; ///< Frame or display height in pixels.
int width; ///< Frame or display width in pixels.
std::uint64_t sequence; ///< Capture sequence number associated with the frame.
egl::rgb_t rgb; ///< Imported RGB source image used before CUDA conversion.
cu_resources cu_res; ///< CUDA graphics resources registered for the current frame.
int offset_x; ///< Horizontal offset in physical pixels.
int offset_y; ///< Vertical offset in physical pixels.
bool is_yuv444; ///< Whether the CUDA converter outputs YUV 4:4:4.
};
/**
* @brief Create AVCodec encode device.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param vram Whether the image should use GPU memory instead of system memory.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, bool vram) {
if (init()) {
return nullptr;
}
std::unique_ptr<cuda_t> cuda;
if (vram) {
cuda = std::make_unique<cuda_vram_t>();
} else {
cuda = std::make_unique<cuda_ram_t>();
}
if (cuda->init(width, height)) {
return nullptr;
}
return cuda;
}
/**
* @brief Create a GL->CUDA encoding device for consuming captured dmabufs.
* @param width Width of captured frames.
* @param height Height of captured frames.
* @param offset_x Offset of content in captured frame.
* @param offset_y Offset of content in captured frame.
* @return FFmpeg encoding device context.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) {
if (init()) {
return nullptr;
}
auto cuda = std::make_unique<gl_cuda_vram_t>();
if (cuda->init(width, height, offset_x, offset_y)) {
return nullptr;
}
return cuda;
}
namespace nvfbc {
static PNVFBCCREATEINSTANCE createInstance {};
static NVFBC_API_FUNCTION_LIST func {NVFBC_VERSION};
static constexpr inline NVFBC_BOOL nv_bool(bool b) {
return b ? NVFBC_TRUE : NVFBC_FALSE;
}
static void *handle {nullptr};
/**
* @brief Load NvFBC and create the CUDA capture helper.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
static bool funcs_loaded = false;
if (funcs_loaded) {
return 0;
}
if (!handle) {
handle = dyn::handle({"libnvidia-fbc.so.1", "libnvidia-fbc.so"});
if (!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{(dyn::apiproc *) &createInstance, "NvFBCCreateInstance"},
};
if (dyn::load(handle, funcs)) {
dlclose(handle);
handle = nullptr;
return -1;
}
auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func);
if (status) {
BOOST_LOG(error) << "Unable to create NvFBC instance"sv;
dlclose(handle);
handle = nullptr;
return -1;
}
funcs_loaded = true;
return 0;
}
/**
* @brief NvFBC CUDA context selected for a capture session.
*/
class ctx_t {
public:
/**
* @brief Create an NvFBC session context for a native capture handle.
*
* @param handle Native library or object handle used by the operation.
*/
ctx_t(NVFBC_SESSION_HANDLE handle) {
NVFBC_BIND_CONTEXT_PARAMS params {NVFBC_BIND_CONTEXT_PARAMS_VER};
if (func.nvFBCBindContext(handle, ¶ms)) {
BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle);
}
this->handle = handle;
}
~ctx_t() {
NVFBC_RELEASE_CONTEXT_PARAMS params {NVFBC_RELEASE_CONTEXT_PARAMS_VER};
if (func.nvFBCReleaseContext(handle, ¶ms)) {
BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle);
}
}
NVFBC_SESSION_HANDLE handle; ///< NVIDIA FBC capture session handle.
};
/**
* @brief NvFBC dynamic-library handle and function table.
*/
class handle_t {
enum flag_e {
SESSION_HANDLE,
SESSION_CAPTURE,
MAX_FLAGS,
};
public:
handle_t() = default;
/**
* @brief Move an NvFBC API handle and its resolved function table.
*
* @param other Source object whose state is copied or moved into this object.
*/
handle_t(handle_t &&other):
handle_flags {other.handle_flags},
handle {other.handle} {
other.handle_flags.reset();
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference or value produced by the operator.
*/
handle_t &operator=(handle_t &&other) {
std::swap(handle_flags, other.handle_flags);
std::swap(handle, other.handle);
return *this;
}
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @return Created backend object, or null when creation fails.
*/
static std::optional<handle_t> make() {
NVFBC_CREATE_HANDLE_PARAMS params {NVFBC_CREATE_HANDLE_PARAMS_VER};
// Set privateData to allow NvFBC on consumer NVIDIA GPUs.
// Based on https://github.com/keylase/nvidia-patch/blob/3193b4b1cea91527bf09ea9b8db5aade6a3f3c0a/win/nvfbcwrp/nvfbcwrp_main.cpp#L23-L25 .
const unsigned int MAGIC_PRIVATE_DATA[4] = {0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA};
params.privateData = MAGIC_PRIVATE_DATA;
params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA);
handle_t handle;
auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms);
if (status) {
BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error();
return std::nullopt;
}
handle.handle_flags[SESSION_HANDLE] = true;
return handle;
}
/**
* @brief Read the last error string from the active NvFBC session.
*
* @return Human-readable NvFBC error string.
*/
const char *last_error() {
return func.nvFBCGetLastErrorStr(handle);
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
std::optional<NVFBC_GET_STATUS_PARAMS> status() {
NVFBC_GET_STATUS_PARAMS params {NVFBC_GET_STATUS_PARAMS_VER};
auto status = func.nvFBCGetStatus(handle, ¶ms);
if (status) {
BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error();
return std::nullopt;
}
return params;
}
/**
* @brief Run the capture loop for this backend.
*
* @param capture_params Capture params.
* @return Capture status reported to the streaming pipeline.
*/
int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) {
if (func.nvFBCCreateCaptureSession(handle, &capture_params)) {
BOOST_LOG(error) << "Failed to start capture session: "sv << last_error();
return -1;
}
handle_flags[SESSION_CAPTURE] = true;
NVFBC_TOCUDA_SETUP_PARAMS setup_params {
NVFBC_TOCUDA_SETUP_PARAMS_VER,
NVFBC_BUFFER_FORMAT_BGRA,
};
if (func.nvFBCToCudaSetUp(handle, &setup_params)) {
BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error();
return -1;
}
return 0;
}
/**
* @brief Release the NvFBC capture session and wait for capture work to stop.
*
* @return Stop status.
*/
int stop() {
if (!handle_flags[SESSION_CAPTURE]) {
return 0;
}
NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params {NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER};
if (func.nvFBCDestroyCaptureSession(handle, ¶ms)) {
BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error();
return -1;
}
handle_flags[SESSION_CAPTURE] = false;
return 0;
}
/**
* @brief Reset the object to its initial empty state.
*
* @return Reset status.
*/
int reset() {
if (!handle_flags[SESSION_HANDLE]) {
return 0;
}
stop();
NVFBC_DESTROY_HANDLE_PARAMS params {NVFBC_DESTROY_HANDLE_PARAMS_VER};
ctx_t ctx {handle};
if (func.nvFBCDestroyHandle(handle, ¶ms)) {
BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle);
}
handle_flags[SESSION_HANDLE] = false;
return 0;
}
~handle_t() {
reset();
}
std::bitset<MAX_FLAGS> handle_flags; ///< Handle flags.
NVFBC_SESSION_HANDLE handle; ///< NVIDIA FBC capture session handle.
};
/**
* @brief NvFBC display capture backend that produces CUDA frames.
*/
class display_t: public platf::display_t {
public:
/**
* @brief Initialize NvFBC capture for the selected display.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string_view &display_name, const ::video::config_t &config) {
auto handle = handle_t::make();
if (!handle) {
return -1;
}
ctx_t ctx {handle->handle};
auto status_params = handle->status();
if (!status_params) {
return -1;
}
int streamedMonitor = -1;
if (!display_name.empty()) {
if (status_params->bXRandRAvailable) {
auto monitor_nr = util::from_view(display_name);
if (monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) {
BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv;
} else {
streamedMonitor = monitor_nr;
}
} else {
BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv;
}
}
delay = ::video::capture_frame_interval(config);
capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER};
capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA;