diff options
Diffstat (limited to 'src')
42 files changed, 18749 insertions, 0 deletions
diff --git a/src/args_parser.c b/src/args_parser.c new file mode 100644 index 0000000..ed89205 --- /dev/null +++ b/src/args_parser.c @@ -0,0 +1,702 @@ +#include "../include/args_parser.h" +#include "../include/defs.h" +#include "../include/egl.h" +#include "../include/window/window.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <limits.h> +#include <assert.h> +#include <errno.h> +#include <libgen.h> +#include <sys/stat.h> + +#ifndef GSR_VERSION +#define GSR_VERSION "unknown" +#endif + +static const ArgEnum video_codec_enums[] = { + { .name = "auto", .value = GSR_VIDEO_CODEC_AUTO }, + { .name = "h264", .value = GSR_VIDEO_CODEC_H264 }, + { .name = "h265", .value = GSR_VIDEO_CODEC_HEVC }, + { .name = "hevc", .value = GSR_VIDEO_CODEC_HEVC }, + { .name = "hevc_hdr", .value = GSR_VIDEO_CODEC_HEVC_HDR }, + { .name = "hevc_10bit", .value = GSR_VIDEO_CODEC_HEVC_10BIT }, + { .name = "av1", .value = GSR_VIDEO_CODEC_AV1 }, + { .name = "av1_hdr", .value = GSR_VIDEO_CODEC_AV1_HDR }, + { .name = "av1_10bit", .value = GSR_VIDEO_CODEC_AV1_10BIT }, + { .name = "vp8", .value = GSR_VIDEO_CODEC_VP8 }, + { .name = "vp9", .value = GSR_VIDEO_CODEC_VP9 }, +}; + +static const ArgEnum audio_codec_enums[] = { + { .name = "opus", .value = GSR_AUDIO_CODEC_OPUS }, + { .name = "aac", .value = GSR_AUDIO_CODEC_AAC }, + { .name = "flac", .value = GSR_AUDIO_CODEC_FLAC }, +}; + +static const ArgEnum video_encoder_enums[] = { + { .name = "gpu", .value = GSR_VIDEO_ENCODER_HW_GPU }, + { .name = "cpu", .value = GSR_VIDEO_ENCODER_HW_CPU }, +}; + +static const ArgEnum pixel_format_enums[] = { + { .name = "yuv420", .value = GSR_PIXEL_FORMAT_YUV420 }, + { .name = "yuv444", .value = GSR_PIXEL_FORMAT_YUV444 }, +}; + +static const ArgEnum framerate_mode_enums[] = { + { .name = "vfr", .value = GSR_FRAMERATE_MODE_VARIABLE }, + { .name = "cfr", .value = GSR_FRAMERATE_MODE_CONSTANT }, + { .name = "content", .value = GSR_FRAMERATE_MODE_CONTENT }, +}; + +static const ArgEnum bitrate_mode_enums[] = { + { .name = "auto", .value = GSR_BITRATE_MODE_AUTO }, + { .name = "qp", .value = GSR_BITRATE_MODE_QP }, + { .name = "cbr", .value = GSR_BITRATE_MODE_CBR }, + { .name = "vbr", .value = GSR_BITRATE_MODE_VBR }, +}; + +static const ArgEnum color_range_enums[] = { + { .name = "limited", .value = GSR_COLOR_RANGE_LIMITED }, + { .name = "full", .value = GSR_COLOR_RANGE_FULL }, +}; + +static const ArgEnum tune_enums[] = { + { .name = "performance", .value = GSR_TUNE_PERFORMANCE }, + { .name = "quality", .value = GSR_TUNE_QUALITY }, +}; + +static const ArgEnum replay_storage_enums[] = { + { .name = "ram", .value = GSR_REPLAY_STORAGE_RAM }, + { .name = "disk", .value = GSR_REPLAY_STORAGE_DISK }, +}; + +static void arg_deinit(Arg *arg) { + if(arg->values) { + free(arg->values); + arg->values = NULL; + } +} + +static bool arg_append_value(Arg *arg, const char *value) { + if(arg->num_values + 1 >= arg->capacity_num_values) { + const int new_capacity_num_values = arg->capacity_num_values == 0 ? 4 : arg->capacity_num_values*2; + void *new_data = realloc(arg->values, new_capacity_num_values * sizeof(const char*)); + if(!new_data) + return false; + + arg->values = new_data; + arg->capacity_num_values = new_capacity_num_values; + } + + arg->values[arg->num_values] = value; + ++arg->num_values; + return true; +} + +static bool arg_get_enum_value_by_name(const Arg *arg, const char *name, int *enum_value) { + assert(arg->type == ARG_TYPE_ENUM); + assert(arg->enum_values); + for(int i = 0; i < arg->num_enum_values; ++i) { + if(strcmp(arg->enum_values[i].name, name) == 0) { + *enum_value = arg->enum_values[i].value; + return true; + } + } + return false; +} + +static void arg_print_expected_enum_names(const Arg *arg) { + assert(arg->type == ARG_TYPE_ENUM); + assert(arg->enum_values); + for(int i = 0; i < arg->num_enum_values; ++i) { + if(i > 0) { + if(i == arg->num_enum_values -1) + fprintf(stderr, " or "); + else + fprintf(stderr, ", "); + } + fprintf(stderr, "'%s'", arg->enum_values[i].name); + } +} + +static Arg* args_get_by_key(Arg *args, int num_args, const char *key) { + for(int i = 0; i < num_args; ++i) { + if(strcmp(args[i].key, key) == 0) + return &args[i]; + } + return NULL; +} + +static const char* args_get_value_by_key(Arg *args, int num_args, const char *key) { + for(int i = 0; i < num_args; ++i) { + if(strcmp(args[i].key, key) == 0) { + if(args[i].num_values == 0) + return NULL; + else + return args[i].values[0]; + } + } + return NULL; +} + +static bool args_get_boolean_by_key(Arg *args, int num_args, const char *key, bool default_value) { + Arg *arg = args_get_by_key(args, num_args, key); + assert(arg); + if(arg->num_values == 0) { + return default_value; + } else { + assert(arg->type == ARG_TYPE_BOOLEAN); + return arg->typed_value.boolean; + } +} + +static int args_get_enum_by_key(Arg *args, int num_args, const char *key, int default_value) { + Arg *arg = args_get_by_key(args, num_args, key); + assert(arg); + if(arg->num_values == 0) { + return default_value; + } else { + assert(arg->type == ARG_TYPE_ENUM); + return arg->typed_value.enum_value; + } +} + +static int64_t args_get_i64_by_key(Arg *args, int num_args, const char *key, int64_t default_value) { + Arg *arg = args_get_by_key(args, num_args, key); + assert(arg); + if(arg->num_values == 0) { + return default_value; + } else { + assert(arg->type == ARG_TYPE_I64); + return arg->typed_value.i64_value; + } +} + +static double args_get_double_by_key(Arg *args, int num_args, const char *key, double default_value) { + Arg *arg = args_get_by_key(args, num_args, key); + assert(arg); + if(arg->num_values == 0) { + return default_value; + } else { + assert(arg->type == ARG_TYPE_DOUBLE); + return arg->typed_value.d_value; + } +} + +static void usage_header(void) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder"; + printf("usage: %s -w <window_id|monitor|focused|portal|region|v4l2_device_path> [-c <container_format>] [-s WxH] [-region WxH+X+Y] [-f <fps>] [-a <audio_input>] " + "[-q <quality>] [-r <replay_buffer_size_sec>] [-replay-storage ram|disk] [-restart-replay-on-save yes|no] " + "[-k h264|hevc|av1|vp8|vp9|hevc_hdr|av1_hdr|hevc_10bit|av1_10bit] [-ac aac|opus|flac] [-ab <bitrate>] [-oc yes|no] [-fm cfr|vfr|content] " + "[-bm auto|qp|vbr|cbr] [-cr limited|full] [-tune performance|quality] [-df yes|no] [-sc <script_path>] [-p <plugin_path>] " + "[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] " + "[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [--list-capture-options [card_path]] [--list-audio-devices] " + "[--list-application-audio] [--list-v4l2-devices] [-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name); + fflush(stdout); +} + +static void usage_full(void) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + usage_header(); + printf("\n"); + printf("NOTES:\n"); + if(inside_flatpak) + printf(" Run \"man /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/man/man1/gpu-screen-recorder.1\" to open the man page for GPU Screen Recorder to see an explanation for each option and examples\n"); + else + printf(" Run \"man gpu-screen-recorder.1\" to open the man page for GPU Screen Recorder to see an explanation for each option and examples\n"); + fflush(stdout); +} + +static void usage(void) { + usage_header(); +} + +// TODO: Does this match all livestreaming cases? +static bool is_livestream_path(const char *str) { + const int len = strlen(str); + if((len >= 7 && memcmp(str, "http://", 7) == 0) || (len >= 8 && memcmp(str, "https://", 8) == 0)) + return true; + else if((len >= 7 && memcmp(str, "rtmp://", 7) == 0) || (len >= 8 && memcmp(str, "rtmps://", 8) == 0)) + return true; + else if((len >= 7 && memcmp(str, "rtsp://", 7) == 0)) + return true; + else if((len >= 6 && memcmp(str, "srt://", 6) == 0)) + return true; + else if((len >= 6 && memcmp(str, "tcp://", 6) == 0)) + return true; + else if((len >= 6 && memcmp(str, "udp://", 6) == 0)) + return true; + else + return false; +} + +static bool args_parser_set_values(args_parser *self) { + self->video_encoder = (gsr_video_encoder_hardware)args_get_enum_by_key(self->args, NUM_ARGS, "-encoder", GSR_VIDEO_ENCODER_HW_GPU); + self->pixel_format = (gsr_pixel_format)args_get_enum_by_key(self->args, NUM_ARGS, "-pixfmt", GSR_PIXEL_FORMAT_YUV420); + self->framerate_mode = (gsr_framerate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-fm", GSR_FRAMERATE_MODE_VARIABLE); + self->color_range = (gsr_color_range)args_get_enum_by_key(self->args, NUM_ARGS, "-cr", GSR_COLOR_RANGE_LIMITED); + self->tune = (gsr_tune)args_get_enum_by_key(self->args, NUM_ARGS, "-tune", GSR_TUNE_PERFORMANCE); + self->video_codec = (gsr_video_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-k", GSR_VIDEO_CODEC_AUTO); + self->audio_codec = (gsr_audio_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-ac", GSR_AUDIO_CODEC_OPUS); + self->bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO); + self->replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM); + + self->capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w"); + self->verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true); + self->gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false); + self->record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true); + self->date_folders = args_get_boolean_by_key(self->args, NUM_ARGS, "-df", false); + self->restore_portal_session = args_get_boolean_by_key(self->args, NUM_ARGS, "-restore-portal-session", false); + self->restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false); + self->overclock = args_get_boolean_by_key(self->args, NUM_ARGS, "-oc", false); + self->fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); + + self->audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); + self->audio_bitrate *= 1000LL; + + self->keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); + + if(self->audio_codec == GSR_AUDIO_CODEC_FLAC) { + fprintf(stderr, "gsr warning: flac audio codec is temporary disabled, using opus audio codec instead\n"); + self->audio_codec = GSR_AUDIO_CODEC_OPUS; + } + + self->portal_session_token_filepath = args_get_value_by_key(self->args, NUM_ARGS, "-portal-session-token-filepath"); + if(self->portal_session_token_filepath) { + int len = strlen(self->portal_session_token_filepath); + if(len > 0 && self->portal_session_token_filepath[len - 1] == '/') { + fprintf(stderr, "gsr error: -portal-session-token-filepath should be a path to a file but it ends with a /: %s\n", self->portal_session_token_filepath); + return false; + } + } + + self->recording_saved_script = args_get_value_by_key(self->args, NUM_ARGS, "-sc"); + if(self->recording_saved_script) { + struct stat buf; + if(stat(self->recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { + fprintf(stderr, "gsr error: Script \"%s\" either doesn't exist or it's not a file\n", self->recording_saved_script); + usage(); + return false; + } + + if(!(buf.st_mode & S_IXUSR)) { + fprintf(stderr, "gsr error: Script \"%s\" is not executable\n", self->recording_saved_script); + usage(); + return false; + } + } + + const char *quality_str = args_get_value_by_key(self->args, NUM_ARGS, "-q"); + self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + self->video_bitrate = 0; + + if(self->bitrate_mode == GSR_BITRATE_MODE_CBR) { + if(!quality_str) { + fprintf(stderr, "gsr error: option '-q' is required when using '-bm cbr' option\n"); + usage(); + return false; + } + + if(sscanf(quality_str, "%" PRIi64, &self->video_bitrate) != 1) { + fprintf(stderr, "gsr error: -q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value\n", quality_str); + usage(); + return false; + } + + if(self->video_bitrate < 0) { + fprintf(stderr, "gsr error: -q is expected to be 0 or larger, got %" PRIi64 "\n", self->video_bitrate); + usage(); + return false; + } + + self->video_bitrate *= 1000LL; + } else { + if(!quality_str) + quality_str = "very_high"; + + if(strcmp(quality_str, "medium") == 0) { + self->video_quality = GSR_VIDEO_QUALITY_MEDIUM; + } else if(strcmp(quality_str, "high") == 0) { + self->video_quality = GSR_VIDEO_QUALITY_HIGH; + } else if(strcmp(quality_str, "very_high") == 0) { + self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + } else if(strcmp(quality_str, "ultra") == 0) { + self->video_quality = GSR_VIDEO_QUALITY_ULTRA; + } else { + fprintf(stderr, "gsr error: -q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'\n", quality_str); + usage(); + return false; + } + } + + self->output_resolution = (vec2i){0, 0}; + + const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s"); + if(output_resolution_str) { + if(sscanf(output_resolution_str, "%dx%d", &self->output_resolution.x, &self->output_resolution.y) != 2) { + fprintf(stderr, "gsr error: invalid value for option -s '%s', expected a value in format WxH\n", output_resolution_str); + usage(); + return false; + } + + if(self->output_resolution.x < 0 || self->output_resolution.y < 0) { + fprintf(stderr, "gsr error: invalid value for option -s '%s', expected width and height to be greater or equal to 0\n", output_resolution_str); + usage(); + return false; + } + } + + self->region_size = (vec2i){0, 0}; + self->region_position = (vec2i){0, 0}; + const char *region_str = args_get_value_by_key(self->args, NUM_ARGS, "-region"); + if(region_str) { + if(sscanf(region_str, "%dx%d+%d+%d", &self->region_size.x, &self->region_size.y, &self->region_position.x, &self->region_position.y) != 4) { + fprintf(stderr, "gsr error: invalid value for option -region '%s', expected a value in format WxH+X+Y\n", region_str); + usage(); + return false; + } + + if(self->region_size.x < 0 || self->region_size.y < 0) { + fprintf(stderr, "gsr error: invalid value for option -region '%s', expected width and height to be greater or equal to 0\n", region_str); + usage(); + return false; + } + } + + self->fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60); + self->replay_buffer_size_secs = args_get_i64_by_key(self->args, NUM_ARGS, "-r", -1); + if(self->replay_buffer_size_secs != -1) + self->replay_buffer_size_secs += (int64_t)(self->keyint + 0.5); // Add a few seconds to account of lost packets because of non-keyframe packets skipped + + self->container_format = args_get_value_by_key(self->args, NUM_ARGS, "-c"); + if(self->container_format && strcmp(self->container_format, "mkv") == 0) + self->container_format = "matroska"; + + const bool is_replaying = self->replay_buffer_size_secs != -1; + self->is_livestream = false; + self->filename = args_get_value_by_key(self->args, NUM_ARGS, "-o"); + if(self->filename) { + self->is_livestream = is_livestream_path(self->filename); + if(self->is_livestream) { + if(is_replaying) { + fprintf(stderr, "gsr error: replay mode is not applicable to live streaming\n"); + return false; + } + } else { + if(!is_replaying) { + char directory_buf[PATH_MAX]; + snprintf(directory_buf, sizeof(directory_buf), "%s", self->filename); + char *directory = dirname(directory_buf); + if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) { + if(create_directory_recursive(directory) != 0) { + fprintf(stderr, "gsr error: failed to create directory for output file: %s\n", self->filename); + return false; + } + } + } else { + if(!self->container_format) { + fprintf(stderr, "gsr error: option -c is required when using option -r\n"); + usage(); + return false; + } + + struct stat buf; + if(stat(self->filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { + fprintf(stderr, "gsr error: File \"%s\" exists but it's not a directory\n", self->filename); + usage(); + return false; + } + } + } + } else { + if(!is_replaying) { + self->filename = "/dev/stdout"; + } else { + fprintf(stderr, "gsr error: Option -o is required when using option -r\n"); + usage(); + return false; + } + + if(!self->container_format) { + fprintf(stderr, "gsr error: option -c is required when not using option -o\n"); + usage(); + return false; + } + } + + self->is_output_piped = strcmp(self->filename, "/dev/stdout") == 0; + self->low_latency_recording = self->is_livestream || self->is_output_piped; + + self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); + + if(self->is_livestream && self->recording_saved_script) { + fprintf(stderr, "gsr warning: live stream detected, -sc script is ignored\n"); + self->recording_saved_script = NULL; + } + + return true; +} + +bool args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *arg_handlers, void *userdata) { + assert(arg_handlers); + memset(self, 0, sizeof(*self)); + + if(argc <= 1) { + usage_full(); + return false; + } + + if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { + usage_full(); + return false; + } + + if(argc == 2 && strcmp(argv[1], "--info") == 0) { + arg_handlers->info(userdata); + return true; + } + + if(argc == 2 && strcmp(argv[1], "--list-audio-devices") == 0) { + arg_handlers->list_audio_devices(userdata); + return true; + } + + if(argc == 2 && strcmp(argv[1], "--list-application-audio") == 0) { + arg_handlers->list_application_audio(userdata); + return true; + } + + if(argc == 2 && strcmp(argv[1], "--list-v4l2-devices") == 0) { + arg_handlers->list_v4l2_devices(userdata); + return true; + } + + if(strcmp(argv[1], "--list-capture-options") == 0) { + if(argc == 2) { + arg_handlers->list_capture_options(NULL, userdata); + return true; + } else if(argc == 3 || argc == 4) { + const char *card_path = argv[2]; + arg_handlers->list_capture_options(card_path, userdata); + return true; + } else { + fprintf(stderr, "gsr error: expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)\n"); + return false; + } + } + + if(argc == 2 && strcmp(argv[1], "--version") == 0) { + arg_handlers->version(userdata); + return true; + } + + int arg_index = 0; + self->args[arg_index++] = (Arg){ .key = "-w", .optional = false, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-c", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-f", .optional = true, .list = false, .type = ARG_TYPE_I64, .integer_value_min = 1, .integer_value_max = 1000 }; + self->args[arg_index++] = (Arg){ .key = "-s", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-region", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-a", .optional = true, .list = true, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-q", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-o", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-ro", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-r", .optional = true, .list = false, .type = ARG_TYPE_I64, .integer_value_min = 2, .integer_value_max = 86400 }; + self->args[arg_index++] = (Arg){ .key = "-restart-replay-on-save", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-k", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = video_codec_enums, .num_enum_values = sizeof(video_codec_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-ac", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = audio_codec_enums, .num_enum_values = sizeof(audio_codec_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-ab", .optional = true, .list = false, .type = ARG_TYPE_I64, .integer_value_min = 0, .integer_value_max = 50000 }; + self->args[arg_index++] = (Arg){ .key = "-oc", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-fm", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = framerate_mode_enums, .num_enum_values = sizeof(framerate_mode_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-bm", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = bitrate_mode_enums, .num_enum_values = sizeof(bitrate_mode_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-pixfmt", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = pixel_format_enums, .num_enum_values = sizeof(pixel_format_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-v", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-gl-debug", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-df", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-sc", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-cr", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = color_range_enums, .num_enum_values = sizeof(color_range_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-tune", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = tune_enums, .num_enum_values = sizeof(tune_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-cursor", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-keyint", .optional = true, .list = false, .type = ARG_TYPE_DOUBLE, .integer_value_min = 0, .integer_value_max = 500 }; + self->args[arg_index++] = (Arg){ .key = "-restore-portal-session", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-portal-session-token-filepath", .optional = true, .list = false, .type = ARG_TYPE_STRING }; + self->args[arg_index++] = (Arg){ .key = "-encoder", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = video_encoder_enums, .num_enum_values = sizeof(video_encoder_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-fallback-cpu-encoding", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-replay-storage", .optional = true, .list = false, .type = ARG_TYPE_ENUM, .enum_values = replay_storage_enums, .num_enum_values = sizeof(replay_storage_enums)/sizeof(ArgEnum) }; + self->args[arg_index++] = (Arg){ .key = "-p", .optional = true, .list = true, .type = ARG_TYPE_STRING }; + assert(arg_index == NUM_ARGS); + + for(int i = 1; i < argc; i += 2) { + const char *arg_name = argv[i]; + Arg *arg = args_get_by_key(self->args, NUM_ARGS, arg_name); + if(!arg) { + fprintf(stderr, "gsr error: invalid argument '%s'\n", arg_name); + usage(); + return false; + } + + if(arg->num_values > 0 && !arg->list) { + fprintf(stderr, "gsr error: expected argument '%s' to only be specified once\n", arg_name); + usage(); + return false; + } + + if(i + 1 >= argc) { + fprintf(stderr, "gsr error: missing value for argument '%s'\n", arg_name); + usage(); + return false; + } + + const char *arg_value = argv[i + 1]; + switch(arg->type) { + case ARG_TYPE_STRING: { + break; + } + case ARG_TYPE_BOOLEAN: { + if(strcmp(arg_value, "yes") == 0) { + arg->typed_value.boolean = true; + } else if(strcmp(arg_value, "no") == 0) { + arg->typed_value.boolean = false; + } else { + fprintf(stderr, "gsr error: %s should either be 'yes' or 'no', got: '%s'\n", arg_name, arg_value); + usage(); + return false; + } + break; + } + case ARG_TYPE_ENUM: { + if(!arg_get_enum_value_by_name(arg, arg_value, &arg->typed_value.enum_value)) { + fprintf(stderr, "gsr error: %s should either be ", arg_name); + arg_print_expected_enum_names(arg); + fprintf(stderr, ", got: '%s'\n", arg_value); + usage(); + return false; + } + break; + } + case ARG_TYPE_I64: { + if(sscanf(arg_value, "%" PRIi64, &arg->typed_value.i64_value) != 1) { + fprintf(stderr, "gsr error: %s argument \"%s\" is not an integer\n", arg_name, arg_value); + usage(); + return false; + } + + if(arg->typed_value.i64_value < arg->integer_value_min) { + fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_min, arg->typed_value.i64_value); + usage(); + return false; + } + + if(arg->typed_value.i64_value > arg->integer_value_max) { + fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_max, arg->typed_value.i64_value); + usage(); + return false; + } + break; + } + case ARG_TYPE_DOUBLE: { + if(sscanf(arg_value, "%lf", &arg->typed_value.d_value) != 1) { + fprintf(stderr, "gsr error: %s argument \"%s\" is not an floating-point number\n", arg_name, arg_value); + usage(); + return false; + } + + if(arg->typed_value.d_value < arg->integer_value_min) { + fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_min, arg->typed_value.d_value); + usage(); + return false; + } + + if(arg->typed_value.d_value > arg->integer_value_max) { + fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_max, arg->typed_value.d_value); + usage(); + return false; + } + break; + } + } + + if(!arg_append_value(arg, arg_value)) { + fprintf(stderr, "gsr error: failed to append argument, out of memory\n"); + return false; + } + } + + for(int i = 0; i < NUM_ARGS; ++i) { + const Arg *arg = &self->args[i]; + if(!arg->optional && arg->num_values == 0) { + fprintf(stderr, "gsr error: missing argument '%s'\n", arg->key); + usage(); + return false; + } + } + + return args_parser_set_values(self); +} + +void args_parser_deinit(args_parser *self) { + for(int i = 0; i < NUM_ARGS; ++i) { + arg_deinit(&self->args[i]); + } +} + +bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl) { + const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; + + if(self->bitrate_mode == (gsr_bitrate_mode)GSR_BITRATE_MODE_AUTO) { + // QP is broken on steam deck, see https://github.com/ValveSoftware/SteamOS/issues/1609 + self->bitrate_mode = egl->gpu_info.is_steam_deck ? GSR_BITRATE_MODE_VBR : GSR_BITRATE_MODE_QP; + } + + if(egl->gpu_info.is_steam_deck && self->bitrate_mode == GSR_BITRATE_MODE_QP) { + fprintf(stderr, "gsr warning: qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead\n"); + self->bitrate_mode = GSR_BITRATE_MODE_VBR; + } + + if(self->video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->bitrate_mode == GSR_BITRATE_MODE_VBR) { + fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); + self->bitrate_mode = GSR_BITRATE_MODE_QP; + } + + if(egl->gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && self->overclock) { + fprintf(stderr, "gsr info: overclock option has no effect on amd/intel, ignoring option\n"); + self->overclock = false; + } + + if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && self->overclock && wayland) { + fprintf(stderr, "gsr info: overclocking is not possible on nvidia on wayland, ignoring option\n"); + self->overclock = false; + } + + if(egl->gpu_info.is_steam_deck) { + fprintf(stderr, "gsr warning: steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\n" + "If you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD.\n"); + } + + self->very_old_gpu = false; + if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl->gpu_info.gpu_version != 0 && egl->gpu_info.gpu_version < 900) { + fprintf(stderr, "gsr info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n"); + self->very_old_gpu = true; + } + + if(video_codec_is_hdr(self->video_codec) && !wayland) { + fprintf(stderr, "gsr error: hdr video codec option %s is not available on X11\n", video_codec_to_string(self->video_codec)); + usage(); + return false; + } + + return true; +} + +void args_parser_print_usage(void) { + usage(); +} + +Arg* args_parser_get_arg(args_parser *self, const char *arg_name) { + return args_get_by_key(self->args, NUM_ARGS, arg_name); +} diff --git a/src/capture/capture.c b/src/capture/capture.c new file mode 100644 index 0000000..bc95300 --- /dev/null +++ b/src/capture/capture.c @@ -0,0 +1,53 @@ +#include "../../include/capture/capture.h" +#include <assert.h> + +int gsr_capture_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + assert(!cap->started); + int res = cap->start(cap, capture_metadata); + if(res == 0) + cap->started = true; + + return res; +} + +void gsr_capture_tick(gsr_capture *cap) { + assert(cap->started); + if(cap->tick) + cap->tick(cap); +} + +void gsr_capture_on_event(gsr_capture *cap, gsr_egl *egl) { + if(cap->on_event) + cap->on_event(cap, egl); +} + +bool gsr_capture_should_stop(gsr_capture *cap, bool *err) { + assert(cap->started); + if(cap->should_stop) + return cap->should_stop(cap, err); + else + return false; +} + +int gsr_capture_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + assert(cap->started); + return cap->capture(cap, capture_metadata, color_conversion); +} + +bool gsr_capture_uses_external_image(gsr_capture *cap) { + if(cap->uses_external_image) + return cap->uses_external_image(cap); + else + return false; +} + +bool gsr_capture_set_hdr_metadata(gsr_capture *cap, AVMasteringDisplayMetadata *mastering_display_metadata, AVContentLightMetadata *light_metadata) { + if(cap->set_hdr_metadata) + return cap->set_hdr_metadata(cap, mastering_display_metadata, light_metadata); + else + return false; +} + +void gsr_capture_destroy(gsr_capture *cap) { + cap->destroy(cap); +} diff --git a/src/capture/kms.c b/src/capture/kms.c new file mode 100644 index 0000000..4f2eb7d --- /dev/null +++ b/src/capture/kms.c @@ -0,0 +1,738 @@ +#include "../../include/capture/kms.h" +#include "../../include/utils.h" +#include "../../include/color_conversion.h" +#include "../../include/cursor.h" +#include "../../include/window/window.h" + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> + +#include <xf86drm.h> +#include <drm_fourcc.h> + +#include <libavutil/mastering_display_metadata.h> + +#define FIND_CRTC_BY_NAME_TIMEOUT_SECONDS 2.0 + +#define HDMI_STATIC_METADATA_TYPE1 0 +#define HDMI_EOTF_SMPTE_ST2084 2 + +#define MAX_CONNECTOR_IDS 32 + +typedef struct { + uint32_t connector_ids[MAX_CONNECTOR_IDS]; + int num_connector_ids; +} MonitorId; + +typedef struct { + gsr_capture_kms_params params; + + vec2i capture_pos; + vec2i capture_size; + MonitorId monitor_id; + + gsr_monitor_rotation monitor_rotation; + + unsigned int input_texture_id; + unsigned int external_input_texture_id; + unsigned int cursor_texture_id; + + bool no_modifiers_fallback; + bool external_texture_fallback; + + struct hdr_output_metadata hdr_metadata; + bool hdr_metadata_set; + + bool is_x11; + + //int drm_fd; + //uint64_t prev_sequence; + //bool damaged; + + vec2i prev_target_pos; + vec2i prev_plane_size; + + double last_time_monitor_check; + + bool capture_is_combined_plane; + gsr_kms_response_item *drm_fd; + vec2i output_size; + vec2i target_pos; +} gsr_capture_kms; + +static void gsr_capture_kms_stop(gsr_capture_kms *self) { + if(self->input_texture_id) { + self->params.egl->glDeleteTextures(1, &self->input_texture_id); + self->input_texture_id = 0; + } + + if(self->external_input_texture_id) { + self->params.egl->glDeleteTextures(1, &self->external_input_texture_id); + self->external_input_texture_id = 0; + } + + if(self->cursor_texture_id) { + self->params.egl->glDeleteTextures(1, &self->cursor_texture_id); + self->cursor_texture_id = 0; + } + + // if(self->drm_fd > 0) { + // close(self->drm_fd); + // self->drm_fd = -1; + // } +} + +static int max_int(int a, int b) { + return a > b ? a : b; +} + +static void gsr_capture_kms_create_input_texture_ids(gsr_capture_kms *self) { + self->params.egl->glGenTextures(1, &self->input_texture_id); + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->input_texture_id); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + + self->params.egl->glGenTextures(1, &self->external_input_texture_id); + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->external_input_texture_id); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + + const bool cursor_texture_id_is_external = self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA; + const int cursor_texture_id_target = cursor_texture_id_is_external ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + + self->params.egl->glGenTextures(1, &self->cursor_texture_id); + self->params.egl->glBindTexture(cursor_texture_id_target, self->cursor_texture_id); + self->params.egl->glTexParameteri(cursor_texture_id_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(cursor_texture_id_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(cursor_texture_id_target, 0); +} + +/* TODO: On monitor reconfiguration, find monitor x, y, width and height again. Do the same for nvfbc. */ + +typedef struct { + MonitorId *monitor_id; + const char *monitor_to_capture; + int monitor_to_capture_len; + int num_monitors; +} MonitorCallbackUserdata; + +static void monitor_callback(const gsr_monitor *monitor, void *userdata) { + MonitorCallbackUserdata *monitor_callback_userdata = userdata; + ++monitor_callback_userdata->num_monitors; + + if(monitor_callback_userdata->monitor_to_capture_len != monitor->name_len || memcmp(monitor_callback_userdata->monitor_to_capture, monitor->name, monitor->name_len) != 0) + return; + + if(monitor_callback_userdata->monitor_id->num_connector_ids < MAX_CONNECTOR_IDS) { + monitor_callback_userdata->monitor_id->connector_ids[monitor_callback_userdata->monitor_id->num_connector_ids] = monitor->connector_id; + ++monitor_callback_userdata->monitor_id->num_connector_ids; + } + + if(monitor_callback_userdata->monitor_id->num_connector_ids == MAX_CONNECTOR_IDS) + fprintf(stderr, "gsr warning: reached max connector ids\n"); +} + +static vec2i rotate_capture_size_if_rotated(gsr_capture_kms *self, vec2i capture_size) { + if(self->monitor_rotation == GSR_MONITOR_ROT_90 || self->monitor_rotation == GSR_MONITOR_ROT_270) { + int tmp_x = capture_size.x; + capture_size.x = capture_size.y; + capture_size.y = tmp_x; + } + return capture_size; +} + +static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_kms *self = cap->priv; + + gsr_capture_kms_create_input_texture_ids(self); + + gsr_monitor monitor; + self->monitor_id.num_connector_ids = 0; + + self->is_x11 = gsr_window_get_display_server(self->params.egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = self->is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + + MonitorCallbackUserdata monitor_callback_userdata = { + &self->monitor_id, + self->params.display_to_capture, strlen(self->params.display_to_capture), + 0, + }; + for_each_active_monitor_output(self->params.egl->window, self->params.egl->card_path, connection_type, monitor_callback, &monitor_callback_userdata); + + if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { + fprintf(stderr, "gsr error: gsr_capture_kms_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_capture_kms_stop(self); + return -1; + } + + monitor.name = self->params.display_to_capture; + vec2i monitor_position = {0, 0}; + drm_monitor_get_display_server_data(self->params.egl->window, &monitor, &self->monitor_rotation, &monitor_position); + + self->capture_pos = monitor.pos; + /* Monitor size is already rotated on x11 when the monitor is rotated, no need to apply it ourselves */ + if(self->is_x11) + self->capture_size = monitor.size; + else + self->capture_size = rotate_capture_size_if_rotated(self, monitor.size); + + if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) { + self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution); + capture_metadata->video_size = self->params.output_resolution; + } else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) { + capture_metadata->video_size = self->params.region_size; + } else { + capture_metadata->video_size = self->capture_size; + } + + self->last_time_monitor_check = clock_get_monotonic_seconds(); + return 0; +} + +// TODO: This is disabled for now because we want to be able to record at a framerate higher than the monitor framerate +// static void gsr_capture_kms_tick(gsr_capture *cap) { +// gsr_capture_kms *self = cap->priv; + +// if(self->drm_fd <= 0) +// self->drm_fd = open(self->params.egl->card_path, O_RDONLY); + +// if(self->drm_fd <= 0) +// return; + +// uint64_t sequence = 0; +// uint64_t ns = 0; +// if(drmCrtcGetSequence(self->drm_fd, 79, &sequence, &ns) != 0) +// return; + +// if(sequence != self->prev_sequence) { +// self->prev_sequence = sequence; +// self->damaged = true; +// } +// } + +static gsr_kms_response_item* find_drm_by_connector_id(gsr_kms_response *kms_response, uint32_t connector_id) { + for(int i = 0; i < kms_response->num_items; ++i) { + if(kms_response->items[i].connector_id == connector_id && !kms_response->items[i].is_cursor) + return &kms_response->items[i]; + } + return NULL; +} + +static gsr_kms_response_item* find_largest_drm(gsr_kms_response *kms_response) { + if(kms_response->num_items == 0) + return NULL; + + int64_t largest_size = 0; + gsr_kms_response_item *largest_drm = &kms_response->items[0]; + for(int i = 0; i < kms_response->num_items; ++i) { + const int64_t size = (int64_t)kms_response->items[i].width * (int64_t)kms_response->items[i].height; + if(size > largest_size && !kms_response->items[i].is_cursor) { + largest_size = size; + largest_drm = &kms_response->items[i]; + } + } + return largest_drm; +} + +static gsr_kms_response_item* find_cursor_drm(gsr_kms_response *kms_response, uint32_t connector_id) { + gsr_kms_response_item *cursor_drm = NULL; + for(int i = 0; i < kms_response->num_items; ++i) { + if(kms_response->items[i].is_cursor) { + cursor_drm = &kms_response->items[i]; + if(kms_response->items[i].connector_id == connector_id) + break; + } + } + return cursor_drm; +} + +static bool hdr_metadata_is_supported_format(const struct hdr_output_metadata *hdr_metadata) { + return hdr_metadata->metadata_type == HDMI_STATIC_METADATA_TYPE1 && + hdr_metadata->hdmi_metadata_type1.metadata_type == HDMI_STATIC_METADATA_TYPE1 && + hdr_metadata->hdmi_metadata_type1.eotf == HDMI_EOTF_SMPTE_ST2084; +} + +// TODO: Check if this hdr data can be changed after the call to av_packet_side_data_add +static void gsr_kms_set_hdr_metadata(gsr_capture_kms *self, const gsr_kms_response_item *drm_fd) { + if(self->hdr_metadata_set) + return; + + self->hdr_metadata_set = true; + self->hdr_metadata = drm_fd->hdr_metadata; +} + +static vec2i swap_vec2i(vec2i value) { + int tmp = value.x; + value.x = value.y; + value.y = tmp; + return value; +} + +static EGLImage gsr_capture_kms_create_egl_image(gsr_capture_kms *self, const gsr_kms_response_item *drm_fd, const int *fds, const uint32_t *offsets, const uint32_t *pitches, const uint64_t *modifiers, bool use_modifiers) { + intptr_t img_attr[44]; + setup_dma_buf_attrs(img_attr, drm_fd->pixel_format, drm_fd->width, drm_fd->height, fds, offsets, pitches, modifiers, drm_fd->num_dma_bufs, use_modifiers); + while(self->params.egl->eglGetError() != EGL_SUCCESS){} + EGLImage image = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr); + if(!image || self->params.egl->eglGetError() != EGL_SUCCESS) { + if(image) + self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); + return NULL; + } + return image; +} + +static EGLImage gsr_capture_kms_create_egl_image_with_fallback(gsr_capture_kms *self, const gsr_kms_response_item *drm_fd) { + // TODO: This causes a crash sometimes on steam deck, why? is it a driver bug? a vaapi pure version doesn't cause a crash. + // Even ffmpeg kmsgrab causes this crash. The error is: + // amdgpu: Failed to allocate a buffer: + // amdgpu: size : 28508160 bytes + // amdgpu: alignment : 2097152 bytes + // amdgpu: domains : 4 + // amdgpu: flags : 4 + // amdgpu: Failed to allocate a buffer: + // amdgpu: size : 28508160 bytes + // amdgpu: alignment : 2097152 bytes + // amdgpu: domains : 4 + // amdgpu: flags : 4 + // EE ../jupiter-mesa/src/gallium/drivers/radeonsi/radeon_vcn_enc.c:516 radeon_create_encoder UVD - Can't create CPB buffer. + // [hevc_vaapi @ 0x55ea72b09840] Failed to upload encode parameters: 2 (resource allocation failed). + // [hevc_vaapi @ 0x55ea72b09840] Encode failed: -5. + // Error: avcodec_send_frame failed, error: Input/output error + // Assertion pic->display_order == pic->encode_order failed at libavcodec/vaapi_encode_h265.c:765 + // kms server info: kms client shutdown, shutting down the server + + int fds[GSR_KMS_MAX_DMA_BUFS]; + uint32_t offsets[GSR_KMS_MAX_DMA_BUFS]; + uint32_t pitches[GSR_KMS_MAX_DMA_BUFS]; + uint64_t modifiers[GSR_KMS_MAX_DMA_BUFS]; + + for(int i = 0; i < drm_fd->num_dma_bufs; ++i) { + fds[i] = drm_fd->dma_buf[i].fd; + offsets[i] = drm_fd->dma_buf[i].offset; + pitches[i] = drm_fd->dma_buf[i].pitch; + modifiers[i] = drm_fd->modifier; + } + + EGLImage image = NULL; + if(self->no_modifiers_fallback) { + image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, false); + } else { + image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, true); + if(!image) { + fprintf(stderr, "gsr error: gsr_capture_kms_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + self->no_modifiers_fallback = true; + image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, false); + } + } + return image; +} + +static bool gsr_capture_kms_bind_image_to_texture(gsr_capture_kms *self, EGLImage image, unsigned int texture_id, bool external_texture) { + const int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + while(self->params.egl->glGetError() != 0){} + self->params.egl->glBindTexture(texture_target, texture_id); + self->params.egl->glEGLImageTargetTexture2DOES(texture_target, image); + const bool success = self->params.egl->glGetError() == 0; + self->params.egl->glBindTexture(texture_target, 0); + return success; +} + +static void gsr_capture_kms_bind_image_to_input_texture_with_fallback(gsr_capture_kms *self, EGLImage image) { + if(self->external_texture_fallback) { + gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); + } else { + if(!gsr_capture_kms_bind_image_to_texture(self, image, self->input_texture_id, false)) { + fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to bind image to texture, trying with external texture\n"); + self->external_texture_fallback = true; + gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); + } + } +} + +static gsr_kms_response_item* find_monitor_drm(gsr_capture_kms *self, bool *capture_is_combined_plane) { + *capture_is_combined_plane = false; + gsr_kms_response_item *drm_fd = NULL; + + for(int i = 0; i < self->monitor_id.num_connector_ids; ++i) { + drm_fd = find_drm_by_connector_id(self->params.kms_response, self->monitor_id.connector_ids[i]); + if(drm_fd) + break; + } + + // Will never happen on wayland unless the target monitor has been disconnected + if(!drm_fd && self->is_x11) { + drm_fd = find_largest_drm(self->params.kms_response); + *capture_is_combined_plane = true; + } + + return drm_fd; +} + +static gsr_kms_response_item* find_cursor_drm_if_on_monitor(gsr_capture_kms *self, uint32_t monitor_connector_id, bool capture_is_combined_plane) { + gsr_kms_response_item *cursor_drm_fd = find_cursor_drm(self->params.kms_response, monitor_connector_id); + if(!capture_is_combined_plane && cursor_drm_fd && cursor_drm_fd->connector_id != monitor_connector_id) + cursor_drm_fd = NULL; + return cursor_drm_fd; +} + +static gsr_monitor_rotation kms_rotation_to_gsr_monitor_rotation(gsr_kms_rotation rotation) { + // Right now both enums have the same values + return (gsr_monitor_rotation)rotation; +} + +static int remainder_int(int a, int b) { + return a - (a / b) * b; +} + +static gsr_monitor_rotation sub_rotations(gsr_monitor_rotation rot1, gsr_monitor_rotation rot2) { + return remainder_int(rot1 - rot2, 4); +} + +static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata, const gsr_kms_response_item *cursor_drm_fd, vec2i target_pos, vec2i output_size, vec2i framebuffer_size) { + const vec2d scale = { + self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x, + self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y + }; + + const bool cursor_texture_id_is_external = self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA; + const vec2i cursor_size = {cursor_drm_fd->width, cursor_drm_fd->height}; + + const gsr_monitor_rotation cursor_plane_rotation = kms_rotation_to_gsr_monitor_rotation(cursor_drm_fd->rotation); + const gsr_monitor_rotation rotation = sub_rotations(self->monitor_rotation, cursor_plane_rotation); + + vec2i cursor_pos = {cursor_drm_fd->x, cursor_drm_fd->y}; + switch(rotation) { + case GSR_MONITOR_ROT_0: + break; + case GSR_MONITOR_ROT_90: + cursor_pos = swap_vec2i(cursor_pos); + cursor_pos.x = framebuffer_size.x - cursor_pos.x; + // TODO: Remove this horrible hack + cursor_pos.x -= cursor_size.x; + break; + case GSR_MONITOR_ROT_180: + cursor_pos.x = framebuffer_size.x - cursor_pos.x; + cursor_pos.y = framebuffer_size.y - cursor_pos.y; + // TODO: Remove this horrible hack + cursor_pos.x -= cursor_size.x; + cursor_pos.y -= cursor_size.y; + break; + case GSR_MONITOR_ROT_270: + cursor_pos = swap_vec2i(cursor_pos); + cursor_pos.y = framebuffer_size.y - cursor_pos.y; + // TODO: Remove this horrible hack + cursor_pos.y -= cursor_size.y; + break; + } + + cursor_pos.x -= self->params.region_position.x; + cursor_pos.y -= self->params.region_position.y; + + cursor_pos.x *= scale.x; + cursor_pos.y *= scale.y; + + cursor_pos.x += target_pos.x; + cursor_pos.y += target_pos.y; + + int fds[GSR_KMS_MAX_DMA_BUFS]; + uint32_t offsets[GSR_KMS_MAX_DMA_BUFS]; + uint32_t pitches[GSR_KMS_MAX_DMA_BUFS]; + uint64_t modifiers[GSR_KMS_MAX_DMA_BUFS]; + + for(int i = 0; i < cursor_drm_fd->num_dma_bufs; ++i) { + fds[i] = cursor_drm_fd->dma_buf[i].fd; + offsets[i] = cursor_drm_fd->dma_buf[i].offset; + pitches[i] = cursor_drm_fd->dma_buf[i].pitch; + modifiers[i] = cursor_drm_fd->modifier; + } + + intptr_t img_attr_cursor[44]; + setup_dma_buf_attrs(img_attr_cursor, cursor_drm_fd->pixel_format, cursor_drm_fd->width, cursor_drm_fd->height, + fds, offsets, pitches, modifiers, cursor_drm_fd->num_dma_bufs, true); + + EGLImage cursor_image = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr_cursor); + const int target = cursor_texture_id_is_external ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + self->params.egl->glBindTexture(target, self->cursor_texture_id); + self->params.egl->glEGLImageTargetTexture2DOES(target, cursor_image); + self->params.egl->glBindTexture(target, 0); + + if(cursor_image) + self->params.egl->eglDestroyImage(self->params.egl->egl_display, cursor_image); + + self->params.egl->glEnable(GL_SCISSOR_TEST); + self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y); + + gsr_color_conversion_draw(color_conversion, self->cursor_texture_id, + cursor_pos, (vec2i){cursor_size.x * scale.x, cursor_size.y * scale.y}, + (vec2i){0, 0}, cursor_size, cursor_size, + gsr_monitor_rotation_to_rotation(rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, cursor_texture_id_is_external); + + self->params.egl->glDisable(GL_SCISSOR_TEST); +} + +static void render_x11_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata, vec2i capture_pos, vec2i target_pos, vec2i output_size) { + if(!self->params.x11_cursor->visible) + return; + + const vec2d scale = { + self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x, + self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y + }; + + const vec2i cursor_pos = { + target_pos.x + (self->params.x11_cursor->position.x - self->params.x11_cursor->hotspot.x - capture_pos.x) * scale.x, + target_pos.y + (self->params.x11_cursor->position.y - self->params.x11_cursor->hotspot.y - capture_pos.y) * scale.y + }; + + self->params.egl->glEnable(GL_SCISSOR_TEST); + self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y); + + gsr_color_conversion_draw(color_conversion, self->params.x11_cursor->texture_id, + cursor_pos, (vec2i){self->params.x11_cursor->size.x * scale.x, self->params.x11_cursor->size.y * scale.y}, + (vec2i){0, 0}, self->params.x11_cursor->size, self->params.x11_cursor->size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + self->params.egl->glDisable(GL_SCISSOR_TEST); +} + +static void gsr_capture_kms_update_capture_size_change(gsr_capture_kms *self, gsr_color_conversion *color_conversion, vec2i target_pos, const gsr_kms_response_item *drm_fd) { + if(target_pos.x != self->prev_target_pos.x || target_pos.y != self->prev_target_pos.y || drm_fd->src_w != self->prev_plane_size.x || drm_fd->src_h != self->prev_plane_size.y) { + self->prev_target_pos = target_pos; + self->prev_plane_size = self->capture_size; + color_conversion->schedule_clear = true; + } +} + +static void gsr_capture_kms_update_connector_ids(gsr_capture_kms *self) { + const double now = clock_get_monotonic_seconds(); + if(now - self->last_time_monitor_check < FIND_CRTC_BY_NAME_TIMEOUT_SECONDS) + return; + + self->last_time_monitor_check = now; + /* TODO: Assume for now that there is only 1 framebuffer for all monitors and it doesn't change */ + if(self->is_x11) + return; + + self->monitor_id.num_connector_ids = 0; + const gsr_connection_type connection_type = self->is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + // MonitorCallbackUserdata monitor_callback_userdata = { + // &self->monitor_id, + // self->params.display_to_capture, strlen(self->params.display_to_capture), + // 0, + // }; + // for_each_active_monitor_output(self->params.egl->window, self->params.egl->card_path, connection_type, monitor_callback, &monitor_callback_userdata); + + gsr_monitor monitor; + if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { + fprintf(stderr, "gsr error: gsr_capture_kms_update_connector_ids: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + return; + } + + self->monitor_id.num_connector_ids = 1; + self->monitor_id.connector_ids[0] = monitor.connector_id; + + monitor.name = self->params.display_to_capture; + vec2i monitor_position = {0, 0}; + // TODO: This is cached. We need it updated. + drm_monitor_get_display_server_data(self->params.egl->window, &monitor, &self->monitor_rotation, &monitor_position); + + self->capture_pos = monitor.pos; + /* Monitor size is already rotated on x11 when the monitor is rotated, no need to apply it ourselves */ + if(self->is_x11) + self->capture_size = monitor.size; + else + self->capture_size = rotate_capture_size_if_rotated(self, monitor.size); +} + +static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + gsr_capture_kms *self = cap->priv; + + if(self->params.kms_response->num_items == 0) { + static bool error_shown = false; + if(!error_shown) { + error_shown = true; + fprintf(stderr, "gsr error: gsr_capture_kms_pre_capture: no drm found, capture will fail\n"); + } + return; + } + + gsr_capture_kms_update_connector_ids(self); + + self->capture_is_combined_plane = false; + self->drm_fd = find_monitor_drm(self, &self->capture_is_combined_plane); + if(!self->drm_fd) + return; + + if(self->drm_fd->has_hdr_metadata && self->params.hdr && hdr_metadata_is_supported_format(&self->drm_fd->hdr_metadata)) + gsr_kms_set_hdr_metadata(self, self->drm_fd); + + self->capture_size = rotate_capture_size_if_rotated(self, (vec2i){ self->drm_fd->src_w, self->drm_fd->src_h }); + if(self->params.region_size.x > 0 && self->params.region_size.y > 0) + self->capture_size = self->params.region_size; + + self->output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size); + self->target_pos = gsr_capture_get_target_position(self->output_size, capture_metadata); + gsr_capture_kms_update_capture_size_change(self, color_conversion, self->target_pos, self->drm_fd); +} + +static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + (void)capture_metadata; + gsr_capture_kms *self = cap->priv; + + if(self->params.kms_response->num_items == 0) + return -1; + + vec2i capture_pos = self->capture_pos; + if(!self->capture_is_combined_plane) + capture_pos = (vec2i){self->drm_fd->x, self->drm_fd->y}; + + capture_pos.x += self->params.region_position.x; + capture_pos.y += self->params.region_position.y; + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + EGLImage image = gsr_capture_kms_create_egl_image_with_fallback(self, self->drm_fd); + if(image) { + gsr_capture_kms_bind_image_to_input_texture_with_fallback(self, image); + self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); + } + + const gsr_monitor_rotation plane_rotation = kms_rotation_to_gsr_monitor_rotation(self->drm_fd->rotation); + const gsr_monitor_rotation rotation = self->capture_is_combined_plane ? GSR_MONITOR_ROT_0 : sub_rotations(self->monitor_rotation, plane_rotation); + + gsr_color_conversion_draw(color_conversion, self->external_texture_fallback ? self->external_input_texture_id : self->input_texture_id, + self->target_pos, self->output_size, + capture_pos, self->capture_size, (vec2i){ self->drm_fd->width, self->drm_fd->height }, + gsr_monitor_rotation_to_rotation(rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->external_texture_fallback); + + if(self->params.record_cursor) { + gsr_kms_response_item *cursor_drm_fd = find_cursor_drm_if_on_monitor(self, self->drm_fd->connector_id, self->capture_is_combined_plane); + // The cursor is handled by x11 on x11 instead of using the cursor drm plane because on prime systems with a dedicated nvidia gpu + // the cursor plane is not available when the cursor is on the monitor controlled by the nvidia device. + // TODO: This doesn't work properly with software cursor on x11 since it will draw the x11 cursor on top of the cursor already in the framebuffer. + // Detect if software cursor is used on x11 somehow. + if(self->is_x11) { + vec2i cursor_monitor_offset = self->capture_pos; + cursor_monitor_offset.x += self->params.region_position.x; + cursor_monitor_offset.y += self->params.region_position.y; + render_x11_cursor(self, color_conversion, capture_metadata, cursor_monitor_offset, self->target_pos, self->output_size); + } else if(cursor_drm_fd) { + const vec2i framebuffer_size = rotate_capture_size_if_rotated(self, (vec2i){ self->drm_fd->src_w, self->drm_fd->src_h }); + render_drm_cursor(self, color_conversion, capture_metadata, cursor_drm_fd, self->target_pos, self->output_size, framebuffer_size); + } + } + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + return 0; +} + +static bool gsr_capture_kms_should_stop(gsr_capture *cap, bool *err) { + (void)cap; + if(err) + *err = false; + return false; +} + +static bool gsr_capture_kms_uses_external_image(gsr_capture *cap) { + (void)cap; + return true; +} + +static bool gsr_capture_kms_set_hdr_metadata(gsr_capture *cap, AVMasteringDisplayMetadata *mastering_display_metadata, AVContentLightMetadata *light_metadata) { + gsr_capture_kms *self = cap->priv; + + if(!self->hdr_metadata_set) + return false; + + light_metadata->MaxCLL = self->hdr_metadata.hdmi_metadata_type1.max_cll; + light_metadata->MaxFALL = self->hdr_metadata.hdmi_metadata_type1.max_fall; + + for(int i = 0; i < 3; ++i) { + mastering_display_metadata->display_primaries[i][0] = av_make_q(self->hdr_metadata.hdmi_metadata_type1.display_primaries[i].x, 50000); + mastering_display_metadata->display_primaries[i][1] = av_make_q(self->hdr_metadata.hdmi_metadata_type1.display_primaries[i].y, 50000); + } + + mastering_display_metadata->white_point[0] = av_make_q(self->hdr_metadata.hdmi_metadata_type1.white_point.x, 50000); + mastering_display_metadata->white_point[1] = av_make_q(self->hdr_metadata.hdmi_metadata_type1.white_point.y, 50000); + + mastering_display_metadata->min_luminance = av_make_q(self->hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance, 10000); + mastering_display_metadata->max_luminance = av_make_q(self->hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance, 1); + + mastering_display_metadata->has_primaries = true; + mastering_display_metadata->has_luminance = true; + + return true; +} + +// static bool gsr_capture_kms_is_damaged(gsr_capture *cap) { +// gsr_capture_kms *self = cap->priv; +// return self->damaged; +// } + +// static void gsr_capture_kms_clear_damage(gsr_capture *cap) { +// gsr_capture_kms *self = cap->priv; +// self->damaged = false; +// } + +static void gsr_capture_kms_destroy(gsr_capture *cap) { + gsr_capture_kms *self = cap->priv; + if(cap->priv) { + gsr_capture_kms_stop(self); + free((void*)self->params.display_to_capture); + self->params.display_to_capture = NULL; + free(cap->priv); + cap->priv = NULL; + } + free(cap); +} + +gsr_capture* gsr_capture_kms_create(const gsr_capture_kms_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_kms_create params is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_kms *cap_kms = calloc(1, sizeof(gsr_capture_kms)); + if(!cap_kms) { + free(cap); + return NULL; + } + + const char *display_to_capture = strdup(params->display_to_capture); + if(!display_to_capture) { + free(cap); + free(cap_kms); + return NULL; + } + + cap_kms->params = *params; + cap_kms->params.display_to_capture = display_to_capture; + + *cap = (gsr_capture) { + .start = gsr_capture_kms_start, + //.tick = gsr_capture_kms_tick, + .should_stop = gsr_capture_kms_should_stop, + .pre_capture = gsr_capture_kms_pre_capture, + .capture = gsr_capture_kms_capture, + .uses_external_image = gsr_capture_kms_uses_external_image, + .set_hdr_metadata = gsr_capture_kms_set_hdr_metadata, + //.is_damaged = gsr_capture_kms_is_damaged, + //.clear_damage = gsr_capture_kms_clear_damage, + .destroy = gsr_capture_kms_destroy, + .priv = cap_kms + }; + + return cap; +} diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c new file mode 100644 index 0000000..144f88a --- /dev/null +++ b/src/capture/nvfbc.c @@ -0,0 +1,457 @@ +#include "../../include/capture/nvfbc.h" +#include "../../external/NvFBC.h" +#include "../../include/egl.h" +#include "../../include/utils.h" +#include "../../include/color_conversion.h" +#include "../../include/window/window.h" + +#include <dlfcn.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <math.h> +#include <assert.h> + +#include <X11/Xlib.h> +#include <X11/extensions/Xrandr.h> + +typedef struct { + gsr_capture_nvfbc_params params; + void *library; + + NVFBC_SESSION_HANDLE nv_fbc_handle; + PNVFBCCREATEINSTANCE nv_fbc_create_instance; + NVFBC_API_FUNCTION_LIST nv_fbc_function_list; + bool fbc_handle_created; + bool capture_session_created; + + NVFBC_TOGL_SETUP_PARAMS setup_params; + + bool supports_direct_cursor; + uint32_t width, height; + NVFBC_TRACKING_TYPE tracking_type; + uint32_t output_id; + uint32_t tracking_width, tracking_height; + bool nvfbc_needs_recreate; + double nvfbc_dead_start; +} gsr_capture_nvfbc; + +static int max_int(int a, int b) { + return a > b ? a : b; +} + +/* Returns 0 on failure */ +static uint32_t get_output_id_from_display_name(NVFBC_RANDR_OUTPUT_INFO *outputs, uint32_t num_outputs, const char *display_name, uint32_t *width, uint32_t *height) { + if(!outputs) + return 0; + + for(uint32_t i = 0; i < num_outputs; ++i) { + if(strcmp(outputs[i].name, display_name) == 0) { + *width = outputs[i].trackedBox.w; + *height = outputs[i].trackedBox.h; + return outputs[i].dwId; + } + } + + return 0; +} + +static bool version_at_least(int major, int minor, int expected_major, int expected_minor) { + return major > expected_major || (major == expected_major && minor >= expected_minor); +} + +static bool version_less_than(int major, int minor, int expected_major, int expected_minor) { + return major < expected_major || (major == expected_major && minor < expected_minor); +} + +static void set_func_ptr(void **dst, void *src) { + *dst = src; +} + +static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) { + gsr_capture_nvfbc *self = cap->priv; + + dlerror(); /* clear */ + void *lib = dlopen("libnvidia-fbc.so.1", RTLD_LAZY); + if(!lib) { + fprintf(stderr, "gsr error: failed to load libnvidia-fbc.so.1, error: %s\n", dlerror()); + return false; + } + + set_func_ptr((void**)&self->nv_fbc_create_instance, dlsym(lib, "NvFBCCreateInstance")); + if(!self->nv_fbc_create_instance) { + fprintf(stderr, "gsr error: unable to resolve symbol 'NvFBCCreateInstance'\n"); + dlclose(lib); + return false; + } + + memset(&self->nv_fbc_function_list, 0, sizeof(self->nv_fbc_function_list)); + self->nv_fbc_function_list.dwVersion = NVFBC_VERSION; + NVFBCSTATUS status = self->nv_fbc_create_instance(&self->nv_fbc_function_list); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: failed to create NvFBC instance (status: %d)\n", status); + dlclose(lib); + return false; + } + + self->library = lib; + return true; +} + +static void gsr_capture_nvfbc_destroy_session(gsr_capture_nvfbc *self) { + if(self->fbc_handle_created && self->capture_session_created) { + NVFBC_DESTROY_CAPTURE_SESSION_PARAMS destroy_capture_params; + memset(&destroy_capture_params, 0, sizeof(destroy_capture_params)); + destroy_capture_params.dwVersion = NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER; + self->nv_fbc_function_list.nvFBCDestroyCaptureSession(self->nv_fbc_handle, &destroy_capture_params); + self->capture_session_created = false; + } +} + +static void gsr_capture_nvfbc_destroy_handle(gsr_capture_nvfbc *self) { + if(self->fbc_handle_created) { + NVFBC_DESTROY_HANDLE_PARAMS destroy_params; + memset(&destroy_params, 0, sizeof(destroy_params)); + destroy_params.dwVersion = NVFBC_DESTROY_HANDLE_PARAMS_VER; + self->nv_fbc_function_list.nvFBCDestroyHandle(self->nv_fbc_handle, &destroy_params); + self->fbc_handle_created = false; + self->nv_fbc_handle = 0; + } +} + +static void gsr_capture_nvfbc_destroy_session_and_handle(gsr_capture_nvfbc *self) { + gsr_capture_nvfbc_destroy_session(self); + gsr_capture_nvfbc_destroy_handle(self); +} + +static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { + NVFBCSTATUS status; + + NVFBC_CREATE_HANDLE_PARAMS create_params; + memset(&create_params, 0, sizeof(create_params)); + create_params.dwVersion = NVFBC_CREATE_HANDLE_PARAMS_VER; + create_params.bExternallyManagedContext = NVFBC_TRUE; + create_params.glxCtx = self->params.egl->glx_context; + create_params.glxFBConfig = self->params.egl->glx_fb_config; + + status = self->nv_fbc_function_list.nvFBCCreateHandle(&self->nv_fbc_handle, &create_params); + if(status != NVFBC_SUCCESS) { + // Reverse engineering for interoperability + const uint8_t enable_key[] = { 0xac, 0x10, 0xc9, 0x2e, 0xa5, 0xe6, 0x87, 0x4f, 0x8f, 0x4b, 0xf4, 0x61, 0xf8, 0x56, 0x27, 0xe9 }; + create_params.privateData = enable_key; + create_params.privateDataSize = 16; + + status = self->nv_fbc_function_list.nvFBCCreateHandle(&self->nv_fbc_handle, &create_params); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + goto error_cleanup; + } + } + self->fbc_handle_created = true; + + NVFBC_GET_STATUS_PARAMS status_params; + memset(&status_params, 0, sizeof(status_params)); + status_params.dwVersion = NVFBC_GET_STATUS_PARAMS_VER; + + status = self->nv_fbc_function_list.nvFBCGetStatus(self->nv_fbc_handle, &status_params); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + goto error_cleanup; + } + + if(status_params.bCanCreateNow == NVFBC_FALSE) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system\n"); + goto error_cleanup; + } + + assert(gsr_window_get_display_server(self->params.egl->window) == GSR_DISPLAY_SERVER_X11); + Display *display = gsr_window_get_display(self->params.egl->window); + + self->tracking_width = XWidthOfScreen(DefaultScreenOfDisplay(display)); + self->tracking_height = XHeightOfScreen(DefaultScreenOfDisplay(display)); + self->tracking_type = strcmp(self->params.display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT; + if(self->tracking_type == NVFBC_TRACKING_OUTPUT) { + if(!status_params.bXRandRAvailable) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the xrandr extension is not available\n"); + goto error_cleanup; + } + + if(status_params.bInModeset) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record\n"); + goto error_cleanup; + } + + self->output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, self->params.display_to_capture, &self->tracking_width, &self->tracking_height); + if(self->output_id == 0) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: display '%s' not found\n", self->params.display_to_capture); + goto error_cleanup; + } + } + + self->width = self->tracking_width; + self->height = self->tracking_height; + return 0; + + error_cleanup: + gsr_capture_nvfbc_destroy_session_and_handle(self); + return -1; +} + +static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *self) { + NVFBC_CREATE_CAPTURE_SESSION_PARAMS create_capture_params; + memset(&create_capture_params, 0, sizeof(create_capture_params)); + create_capture_params.dwVersion = NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER; + create_capture_params.eCaptureType = NVFBC_CAPTURE_TO_GL; + create_capture_params.bWithCursor = (!self->params.direct_capture || self->supports_direct_cursor) ? NVFBC_TRUE : NVFBC_FALSE; + if(!self->params.record_cursor) + create_capture_params.bWithCursor = false; + create_capture_params.eTrackingType = self->tracking_type; + create_capture_params.dwSamplingRateMs = (uint32_t)ceilf(1000.0f / (float)self->params.fps); + create_capture_params.bAllowDirectCapture = self->params.direct_capture ? NVFBC_TRUE : NVFBC_FALSE; + create_capture_params.bPushModel = self->params.direct_capture ? NVFBC_TRUE : NVFBC_FALSE; + create_capture_params.bDisableAutoModesetRecovery = true; + if(self->tracking_type == NVFBC_TRACKING_OUTPUT) + create_capture_params.dwOutputId = self->output_id; + + NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCCreateCaptureSession(self->nv_fbc_handle, &create_capture_params); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + return -1; + } + self->capture_session_created = true; + + memset(&self->setup_params, 0, sizeof(self->setup_params)); + self->setup_params.dwVersion = NVFBC_TOGL_SETUP_PARAMS_VER; + self->setup_params.eBufferFormat = NVFBC_BUFFER_FORMAT_BGRA; + + status = self->nv_fbc_function_list.nvFBCToGLSetUp(self->nv_fbc_handle, &self->setup_params); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_capture_nvfbc_destroy_session(self); + return -1; + } + + return 0; +} + +static void gsr_capture_nvfbc_stop(gsr_capture_nvfbc *self) { + gsr_capture_nvfbc_destroy_session_and_handle(self); + if(self->library) { + dlclose(self->library); + self->library = NULL; + } + if(self->params.display_to_capture) { + free((void*)self->params.display_to_capture); + self->params.display_to_capture = NULL; + } +} + +static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_nvfbc *self = cap->priv; + + if(!gsr_capture_nvfbc_load_library(cap)) + return -1; + + self->supports_direct_cursor = false; + int driver_major_version = 0; + int driver_minor_version = 0; + if(self->params.direct_capture && get_nvidia_driver_version(&driver_major_version, &driver_minor_version)) { + fprintf(stderr, "gsr info: detected nvidia version: %d.%d\n", driver_major_version, driver_minor_version); + + // TODO: + if(version_at_least(driver_major_version, driver_minor_version, 515, 57) && version_less_than(driver_major_version, driver_minor_version, 520, 56)) { + self->params.direct_capture = false; + fprintf(stderr, "gsr warning: \"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead.\n"); + } + + // TODO: + // Cursor capture disabled because moving the cursor doesn't update capture rate to monitor hz and instead captures at 10-30 hz + /* + if(direct_capture) { + if(version_at_least(driver_major_version, driver_minor_version, 515, 57)) + self->supports_direct_cursor = true; + else + fprintf(stderr, "gsr info: capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead.\n"); + } + */ + } + + if(gsr_capture_nvfbc_setup_handle(self) != 0) { + goto error_cleanup; + } + + if(gsr_capture_nvfbc_setup_session(self) != 0) { + goto error_cleanup; + } + + capture_metadata->video_size.x = self->tracking_width; + capture_metadata->video_size.y = self->tracking_height; + + if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) { + self->params.output_resolution = scale_keep_aspect_ratio(capture_metadata->video_size, self->params.output_resolution); + capture_metadata->video_size = self->params.output_resolution; + } else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) { + capture_metadata->video_size = self->params.region_size; + } + + return 0; + + error_cleanup: + gsr_capture_nvfbc_stop(self); + return -1; +} + +static bool gsr_capture_nvfbc_is_capture_monitor_connected(gsr_capture_nvfbc *self) { + Display *dpy = gsr_window_get_display(self->params.egl->window); + int num_monitors = 0; + XRRMonitorInfo *monitors = XRRGetMonitors(dpy, DefaultRootWindow(dpy), True, &num_monitors); + if(!monitors) + return false; + + bool capture_monitor_connected = false; + if(strcmp(self->params.display_to_capture, "screen") == 0) { + capture_monitor_connected = num_monitors > 0; + } else { + for(int i = 0; i < num_monitors; ++i) { + char *monitor_name = XGetAtomName(dpy, monitors[i].name); + if(!monitor_name) + continue; + + if(strcmp(monitor_name, self->params.display_to_capture) == 0) { + capture_monitor_connected = true; + XFree(monitor_name); + break; + } + XFree(monitor_name); + } + } + + XRRFreeMonitors(monitors); + return capture_monitor_connected; +} + +static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + gsr_capture_nvfbc *self = cap->priv; + + const double nvfbc_recreate_retry_time_seconds = 1.0; + if(self->nvfbc_needs_recreate) { + const double now = clock_get_monotonic_seconds(); + if(now - self->nvfbc_dead_start >= nvfbc_recreate_retry_time_seconds) { + self->nvfbc_dead_start = now; + /* + Do not attempt to recreate the nvfbc session if the monitor isn't turned on/connected. + This is to predict if the nvfbc session create below will fail since if it fails it leaks an x11 display (a bug in the nvidia driver). + */ + if(!gsr_capture_nvfbc_is_capture_monitor_connected(self)) + return 0; + + gsr_capture_nvfbc_destroy_session_and_handle(self); + + if(gsr_capture_nvfbc_setup_handle(self) != 0) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + return -1; + } + + if(gsr_capture_nvfbc_setup_session(self) != 0) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + return -1; + } + + fprintf(stderr, "gsr info: gsr_capture_nvfbc_capture: recreated nvfbc session after modeset recovery\n"); + self->nvfbc_needs_recreate = false; + } else { + return 0; + } + } + + vec2i frame_size = (vec2i){self->width, self->height}; + const vec2i original_frame_size = frame_size; + if(self->params.region_size.x > 0 && self->params.region_size.y > 0) + frame_size = self->params.region_size; + + const vec2i output_size = scale_keep_aspect_ratio(frame_size, capture_metadata->recording_size); + const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata); + + NVFBC_FRAME_GRAB_INFO frame_info; + memset(&frame_info, 0, sizeof(frame_info)); + + NVFBC_TOGL_GRAB_FRAME_PARAMS grab_params; + memset(&grab_params, 0, sizeof(grab_params)); + grab_params.dwVersion = NVFBC_TOGL_GRAB_FRAME_PARAMS_VER; + grab_params.dwFlags = NVFBC_TOGL_GRAB_FLAGS_NOWAIT | NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH; // TODO: Remove NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH + grab_params.pFrameGrabInfo = &frame_info; + grab_params.dwTimeoutMs = 0; + + NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCToGLGrabFrame(self->nv_fbc_handle, &grab_params); + if(status != NVFBC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds); + self->nvfbc_needs_recreate = true; + self->nvfbc_dead_start = clock_get_monotonic_seconds(); + return 0; + } + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + gsr_color_conversion_draw(color_conversion, self->setup_params.dwTextures[grab_params.dwTextureIndex], + target_pos, (vec2i){output_size.x, output_size.y}, + self->params.region_position, frame_size, original_frame_size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_BGR, false); + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + return 0; +} + +static void gsr_capture_nvfbc_destroy(gsr_capture *cap) { + gsr_capture_nvfbc *self = cap->priv; + gsr_capture_nvfbc_stop(self); + free(cap->priv); + free(cap); +} + +gsr_capture* gsr_capture_nvfbc_create(const gsr_capture_nvfbc_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params is NULL\n"); + return NULL; + } + + if(!params->display_to_capture) { + fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params.display_to_capture is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_nvfbc *cap_nvfbc = calloc(1, sizeof(gsr_capture_nvfbc)); + if(!cap_nvfbc) { + free(cap); + return NULL; + } + + const char *display_to_capture = strdup(params->display_to_capture); + if(!display_to_capture) { + free(cap); + free(cap_nvfbc); + return NULL; + } + + cap_nvfbc->params = *params; + cap_nvfbc->params.display_to_capture = display_to_capture; + cap_nvfbc->params.fps = max_int(cap_nvfbc->params.fps, 1); + + *cap = (gsr_capture) { + .start = gsr_capture_nvfbc_start, + .tick = NULL, + .should_stop = NULL, + .capture = gsr_capture_nvfbc_capture, + .uses_external_image = NULL, + .destroy = gsr_capture_nvfbc_destroy, + .priv = cap_nvfbc + }; + + return cap; +} diff --git a/src/capture/portal.c b/src/capture/portal.c new file mode 100644 index 0000000..c6f058d --- /dev/null +++ b/src/capture/portal.c @@ -0,0 +1,482 @@ +#include "../../include/capture/portal.h" +#include "../../include/color_conversion.h" +#include "../../include/egl.h" +#include "../../include/utils.h" +#include "../../include/dbus.h" +#include "../../include/pipewire_video.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <limits.h> +#include <assert.h> + +#define PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE 60 + +typedef enum { + PORTAL_CAPTURE_SETUP_IDLE, + PORTAL_CAPTURE_SETUP_IN_PROGRESS, + PORTAL_CAPTURE_SETUP_FINISHED, + PORTAL_CAPTURE_SETUP_FAILED +} gsr_portal_capture_setup_state; + +typedef struct { + gsr_capture_portal_params params; + + gsr_texture_map texture_map; + + gsr_dbus dbus; + char *session_handle; + + gsr_pipewire_video pipewire; + vec2i capture_size; + + gsr_map_texture_output pipewire_data; + + bool should_stop; + bool stop_is_error; + bool do_capture; +} gsr_capture_portal; + +static void gsr_capture_portal_cleanup_plane_fds(gsr_capture_portal *self) { + for(int i = 0; i < self->pipewire_data.num_dmabuf_data; ++i) { + if(self->pipewire_data.dmabuf_data[i].fd > 0) { + close(self->pipewire_data.dmabuf_data[i].fd); + self->pipewire_data.dmabuf_data[i].fd = 0; + } + } + self->pipewire_data.num_dmabuf_data = 0; +} + +static void gsr_capture_portal_stop(gsr_capture_portal *self) { + if(self->texture_map.texture_id) { + self->params.egl->glDeleteTextures(1, &self->texture_map.texture_id); + self->texture_map.texture_id = 0; + } + + if(self->texture_map.external_texture_id) { + self->params.egl->glDeleteTextures(1, &self->texture_map.external_texture_id); + self->texture_map.external_texture_id = 0; + } + + if(self->texture_map.cursor_texture_id) { + self->params.egl->glDeleteTextures(1, &self->texture_map.cursor_texture_id); + self->texture_map.cursor_texture_id = 0; + } + + gsr_capture_portal_cleanup_plane_fds(self); + gsr_pipewire_video_deinit(&self->pipewire); + gsr_dbus_deinit(&self->dbus); +} + +static void gsr_capture_portal_create_input_textures(gsr_capture_portal *self) { + self->params.egl->glGenTextures(1, &self->texture_map.texture_id); + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_map.texture_id); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + + self->params.egl->glGenTextures(1, &self->texture_map.external_texture_id); + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_map.external_texture_id); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + + self->params.egl->glGenTextures(1, &self->texture_map.cursor_texture_id); + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_map.cursor_texture_id); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); +} + +static void get_default_gpu_screen_recorder_restore_token_path(char *buffer, size_t buffer_size) { + const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + if(xdg_config_home) { + snprintf(buffer, buffer_size, "%s/gpu-screen-recorder/restore_token", xdg_config_home); + } else { + const char *home = getenv("HOME"); + if(!home) + home = "/tmp"; + snprintf(buffer, buffer_size, "%s/.config/gpu-screen-recorder/restore_token", home); + } +} + +static bool create_directory_to_file(const char *filepath) { + char dir[PATH_MAX]; + dir[0] = '\0'; + + const char *split = strrchr(filepath, '/'); + if(!split) /* Assuming it's the current directory (for example if filepath is "restore_token"), which doesn't need to be created */ + return true; + + snprintf(dir, sizeof(dir), "%.*s", (int)(split - filepath), filepath); + if(create_directory_recursive(dir) != 0) { + fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create directory (%s) for restore token\n", dir); + return false; + } + return true; +} + +static void gsr_capture_portal_save_restore_token(const char *restore_token, const char *portal_session_token_filepath) { + char restore_token_path[PATH_MAX]; + restore_token_path[0] = '\0'; + if(portal_session_token_filepath) + snprintf(restore_token_path, sizeof(restore_token_path), "%s", portal_session_token_filepath); + else + get_default_gpu_screen_recorder_restore_token_path(restore_token_path, sizeof(restore_token_path)); + + if(!create_directory_to_file(restore_token_path)) + return; + + FILE *f = fopen(restore_token_path, "wb"); + if(!f) { + fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create restore token file (%s)\n", restore_token_path); + return; + } + + const int restore_token_len = strlen(restore_token); + if((long)fwrite(restore_token, 1, restore_token_len, f) != restore_token_len) { + fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to write restore token to file (%s)\n", restore_token_path); + fclose(f); + return; + } + + fprintf(stderr, "gsr info: gsr_capture_portal_save_restore_token: saved restore token to cache (%s)\n", restore_token); + fclose(f); +} + +static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t buffer_size, const char *portal_session_token_filepath) { + assert(buffer_size > 0); + buffer[0] = '\0'; + + char restore_token_path[PATH_MAX]; + restore_token_path[0] = '\0'; + if(portal_session_token_filepath) + snprintf(restore_token_path, sizeof(restore_token_path), "%s", portal_session_token_filepath); + else + get_default_gpu_screen_recorder_restore_token_path(restore_token_path, sizeof(restore_token_path)); + + FILE *f = fopen(restore_token_path, "rb"); + if(!f) { + fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: no restore token found in cache or failed to load (%s)\n", restore_token_path); + return; + } + + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + if(file_size > 0 && file_size < 1024 && file_size < (long)buffer_size && (long)fread(buffer, 1, file_size, f) != file_size) { + buffer[0] = '\0'; + fprintf(stderr, "gsr warning: gsr_capture_portal_get_restore_token_from_cache: failed to read restore token (%s)\n", restore_token_path); + fclose(f); + return; + } + + if(file_size > 0 && file_size < (long)buffer_size) + buffer[file_size] = '\0'; + + fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: read cached restore token (%s)\n", buffer); + fclose(f); +} + +static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire_fd, uint32_t *pipewire_node) { + *pipewire_fd = 0; + *pipewire_node = 0; + int response_status = 0; + + char restore_token[1024]; + restore_token[0] = '\0'; + if(self->params.restore_portal_session) + gsr_capture_portal_get_restore_token_from_cache(restore_token, sizeof(restore_token), self->params.portal_session_token_filepath); + + if(!gsr_dbus_init(&self->dbus, restore_token)) + return -1; + + fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: CreateSession\n"); + response_status = gsr_dbus_screencast_create_session(&self->dbus, &self->session_handle); + if(response_status != 0) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: CreateSession failed\n"); + return response_status; + } + + fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: SelectSources\n"); + response_status = gsr_dbus_screencast_select_sources(&self->dbus, self->session_handle, GSR_PORTAL_CAPTURE_TYPE_ALL, self->params.record_cursor ? GSR_PORTAL_CURSOR_MODE_EMBEDDED : GSR_PORTAL_CURSOR_MODE_HIDDEN); + if(response_status != 0) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: SelectSources failed\n"); + return response_status; + } + + fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: Start\n"); + response_status = gsr_dbus_screencast_start(&self->dbus, self->session_handle, pipewire_node); + if(response_status != 0) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: Start failed\n"); + return response_status; + } + + const char *screencast_restore_token = gsr_dbus_screencast_get_restore_token(&self->dbus); + if(screencast_restore_token) + gsr_capture_portal_save_restore_token(screencast_restore_token, self->params.portal_session_token_filepath); + + fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: OpenPipeWireRemote\n"); + if(!gsr_dbus_screencast_open_pipewire_remote(&self->dbus, self->session_handle, pipewire_fd)) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: OpenPipeWireRemote failed\n"); + return -1; + } + + fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: desktop portal setup finished\n"); + return 0; +} + +static bool gsr_capture_portal_get_frame_dimensions(gsr_capture_portal *self) { + fprintf(stderr, "gsr info: gsr_capture_portal_start: waiting for pipewire negotiation\n"); + + const double start_time = clock_get_monotonic_seconds(); + while(clock_get_monotonic_seconds() - start_time < 5.0) { + if(gsr_pipewire_video_map_texture(&self->pipewire, self->texture_map, &self->pipewire_data)) { + self->capture_size.x = self->pipewire_data.region.width; + self->capture_size.y = self->pipewire_data.region.height; + fprintf(stderr, "gsr info: gsr_capture_portal_start: pipewire negotiation finished\n"); + return true; + } + usleep(30 * 1000); /* 30 milliseconds */ + } + + fprintf(stderr, "gsr info: gsr_capture_portal_start: timed out waiting for pipewire negotiation (5 seconds)\n"); + return false; +} + +static int gsr_capture_portal_setup(gsr_capture_portal *self, int fps) { + gsr_capture_portal_create_input_textures(self); + + int pipewire_fd = 0; + uint32_t pipewire_node = 0; + const int response_status = gsr_capture_portal_setup_dbus(self, &pipewire_fd, &pipewire_node); + if(response_status != 0) { + // Response status values: + // 0: Success, the request is carried out + // 1: The user cancelled the interaction + // 2: The user interaction was ended in some other way + // Response status value 2 happens usually if there was some kind of error in the desktop portal on the system + if(response_status == 2) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. Either you Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system\n"); + return 50; + } else if(response_status == 1) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. It seems like desktop portal capture was canceled by the user.\n"); + return PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE; + } else { + return -1; + } + } + + fprintf(stderr, "gsr info: gsr_capture_portal_setup: setting up pipewire\n"); + /* TODO: support hdr when pipewire supports it */ + /* gsr_pipewire closes the pipewire fd, even on failure */ + if(!gsr_pipewire_video_init(&self->pipewire, pipewire_fd, pipewire_node, fps, self->params.record_cursor, self->params.egl)) { + fprintf(stderr, "gsr error: gsr_capture_portal_setup: failed to setup pipewire with fd: %d, node: %" PRIu32 "\n", pipewire_fd, pipewire_node); + return -1; + } + fprintf(stderr, "gsr info: gsr_capture_portal_setup: pipewire setup finished\n"); + + if(!gsr_capture_portal_get_frame_dimensions(self)) + return -1; + + return 0; +} + +static int gsr_capture_portal_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_portal *self = cap->priv; + + const int result = gsr_capture_portal_setup(self, capture_metadata->fps); + if(result != 0) { + gsr_capture_portal_stop(self); + return result; + } + + if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) { + capture_metadata->video_size = self->capture_size; + } else { + self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution); + capture_metadata->video_size = self->params.output_resolution; + } + + return 0; +} + +static int max_int(int a, int b) { + return a > b ? a : b; +} + +static bool gsr_capture_portal_capture_has_synchronous_task(gsr_capture *cap) { + gsr_capture_portal *self = cap->priv; + return gsr_pipewire_video_should_restart(&self->pipewire); +} + +static bool fourcc_has_alpha(uint32_t fourcc) { + const uint8_t *p = (const uint8_t*)&fourcc; + for(int i = 0; i < 4; ++i) { + if(p[i] == 'A') + return true; + } + return false; +} + +static void gsr_capture_portal_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + gsr_capture_portal *self = cap->priv; + self->do_capture = false; + + if(self->should_stop) + return; + + if(gsr_pipewire_video_should_restart(&self->pipewire)) { + fprintf(stderr, "gsr info: gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again\n"); + gsr_capture_portal_stop(self); + const int result = gsr_capture_portal_setup(self, capture_metadata->fps); + if(result != 0) { + self->stop_is_error = result != PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE; + self->should_stop = true; + } + return; + } + + /* TODO: Handle formats other than RGB(A) */ + if(self->pipewire_data.num_dmabuf_data == 0) { + if(gsr_pipewire_video_map_texture(&self->pipewire, self->texture_map, &self->pipewire_data)) { + if(self->pipewire_data.region.width != self->capture_size.x || self->pipewire_data.region.height != self->capture_size.y) { + self->capture_size.x = self->pipewire_data.region.width; + self->capture_size.y = self->pipewire_data.region.height; + color_conversion->schedule_clear = true; + } + } else { + return; + } + } + + const bool fourcc_alpha = fourcc_has_alpha(self->pipewire_data.fourcc); + if(fourcc_alpha) + color_conversion->schedule_clear = true; + + self->do_capture = true; +} + +static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + (void)color_conversion; + gsr_capture_portal *self = cap->priv; + + if(self->should_stop || !self->do_capture) + return -1; + + const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size); + const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata); + + const vec2i actual_texture_size = {self->pipewire_data.texture_width, self->pipewire_data.texture_height}; + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + // TODO: Handle region crop + + gsr_color_conversion_draw(color_conversion, self->pipewire_data.using_external_image ? self->texture_map.external_texture_id : self->texture_map.texture_id, + target_pos, output_size, + (vec2i){self->pipewire_data.region.x, self->pipewire_data.region.y}, (vec2i){self->pipewire_data.region.width, self->pipewire_data.region.height}, actual_texture_size, + gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->pipewire_data.using_external_image); + + if(self->params.record_cursor && self->texture_map.cursor_texture_id > 0 && self->pipewire_data.cursor_region.width > 0) { + const vec2d scale = { + self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x, + self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y + }; + + const vec2i cursor_pos = { + target_pos.x + (self->pipewire_data.cursor_region.x * scale.x), + target_pos.y + (self->pipewire_data.cursor_region.y * scale.y) + }; + + self->params.egl->glEnable(GL_SCISSOR_TEST); + self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y); + + gsr_color_conversion_draw(color_conversion, self->texture_map.cursor_texture_id, + (vec2i){cursor_pos.x, cursor_pos.y}, + (vec2i){self->pipewire_data.cursor_region.width * scale.x, self->pipewire_data.cursor_region.height * scale.y}, + (vec2i){0, 0}, + (vec2i){self->pipewire_data.cursor_region.width, self->pipewire_data.cursor_region.height}, + (vec2i){self->pipewire_data.cursor_region.width, self->pipewire_data.cursor_region.height}, + gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + self->params.egl->glDisable(GL_SCISSOR_TEST); + } + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + gsr_capture_portal_cleanup_plane_fds(self); + + return 0; +} + +static bool gsr_capture_portal_uses_external_image(gsr_capture *cap) { + (void)cap; + return true; +} + +static bool gsr_capture_portal_should_stop(gsr_capture *cap, bool *err) { + gsr_capture_portal *self = cap->priv; + if(err) + *err = self->stop_is_error; + return self->should_stop; +} + +static bool gsr_capture_portal_is_damaged(gsr_capture *cap) { + gsr_capture_portal *self = cap->priv; + return gsr_pipewire_video_is_damaged(&self->pipewire); +} + +static void gsr_capture_portal_clear_damage(gsr_capture *cap) { + gsr_capture_portal *self = cap->priv; + gsr_pipewire_video_clear_damage(&self->pipewire); +} + +static void gsr_capture_portal_destroy(gsr_capture *cap) { + gsr_capture_portal *self = cap->priv; + if(cap->priv) { + gsr_capture_portal_stop(self); + free(cap->priv); + cap->priv = NULL; + } + free(cap); +} + +gsr_capture* gsr_capture_portal_create(const gsr_capture_portal_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_portal_create params is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_portal *cap_portal = calloc(1, sizeof(gsr_capture_portal)); + if(!cap_portal) { + free(cap); + return NULL; + } + + cap_portal->params = *params; + + *cap = (gsr_capture) { + .start = gsr_capture_portal_start, + .tick = NULL, + .should_stop = gsr_capture_portal_should_stop, + .capture_has_synchronous_task = gsr_capture_portal_capture_has_synchronous_task, + .pre_capture = gsr_capture_portal_pre_capture, + .capture = gsr_capture_portal_capture, + .uses_external_image = gsr_capture_portal_uses_external_image, + .is_damaged = gsr_capture_portal_is_damaged, + .clear_damage = gsr_capture_portal_clear_damage, + .destroy = gsr_capture_portal_destroy, + .priv = cap_portal + }; + + return cap; +} diff --git a/src/capture/v4l2.c b/src/capture/v4l2.c new file mode 100644 index 0000000..18c726e --- /dev/null +++ b/src/capture/v4l2.c @@ -0,0 +1,684 @@ +#include "../../include/capture/v4l2.h" +#include "../../include/color_conversion.h" +#include "../../include/egl.h" +#include "../../include/utils.h" + +#include <dlfcn.h> +#include <fcntl.h> +#include <unistd.h> +#include <poll.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <linux/videodev2.h> +#include <linux/dma-buf.h> +#include <drm_fourcc.h> + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <assert.h> + +#define TJPF_RGB 0 +#define TJPF_RGBA 7 +#define TJFLAG_FASTDCT 2048 + +#define NUM_BUFFERS 2 +#define NUM_PBOS 2 + +typedef void* tjhandle; +typedef tjhandle (*FUNC_tjInitDecompress)(void); +typedef int (*FUNC_tjDestroy)(tjhandle handle); +typedef int (*FUNC_tjDecompressHeader2)(tjhandle handle, + unsigned char *jpegBuf, unsigned long jpegSize, + int *width, int *height, int *jpegSubsamp); +typedef int (*FUNC_tjDecompress2)(tjhandle handle, const unsigned char *jpegBuf, + unsigned long jpegSize, unsigned char *dstBuf, + int width, int pitch, int height, int pixelFormat, + int flags); +typedef char* (*FUNC_tjGetErrorStr2)(tjhandle handle); + +typedef enum { + V4L2_BUFFER_TYPE_DMABUF, + V4L2_BUFFER_TYPE_MMAP +} v4l2_buffer_type; + +typedef struct { + gsr_capture_v4l2_params params; + vec2i capture_size; + + bool should_stop; + bool stop_is_error; + + int fd; + int dmabuf_fd[NUM_BUFFERS]; + EGLImage dma_image[NUM_BUFFERS]; + unsigned int texture_id[NUM_BUFFERS]; + unsigned int prev_texture_index; + bool got_first_frame; + + void *dmabuf_map[NUM_BUFFERS]; + size_t dmabuf_size[NUM_BUFFERS]; + unsigned int pbos[NUM_PBOS]; + unsigned int pbo_index; + + v4l2_buffer_type buffer_type; + + void *libturbojpeg_lib; + FUNC_tjInitDecompress tjInitDecompress; + FUNC_tjDestroy tjDestroy; + FUNC_tjDecompressHeader2 tjDecompressHeader2; + FUNC_tjDecompress2 tjDecompress2; + FUNC_tjGetErrorStr2 tjGetErrorStr2; + tjhandle jpeg_decompressor; + + double capture_start_time; + + bool yuyv_conversion_fallback; +} gsr_capture_v4l2; + +static int xioctl(int fd, unsigned long request, void *arg) { + int r; + + do { + r = ioctl(fd, request, arg); + } while (-1 == r && EINTR == errno); + + return r; +} + +static void gsr_capture_v4l2_stop(gsr_capture_v4l2 *self) { + self->params.egl->glDeleteBuffers(NUM_PBOS, self->pbos); + for(int i = 0; i < NUM_PBOS; ++i) { + self->pbos[i] = 0; + } + + self->params.egl->glDeleteTextures(NUM_BUFFERS, self->texture_id); + for(int i = 0; i < NUM_BUFFERS; ++i) { + self->texture_id[i] = 0; + } + + for(int i = 0; i < NUM_BUFFERS; ++i) { + if(self->dmabuf_map[i]) { + munmap(self->dmabuf_map[i], self->dmabuf_size[i]); + self->dmabuf_map[i] = NULL; + } + + if(self->dma_image[i]) { + self->params.egl->eglDestroyImage(self->params.egl->egl_display, self->dma_image[i]); + self->dma_image[i] = NULL; + } + + if(self->dmabuf_fd[i] > 0) { + close(self->dmabuf_fd[i]); + self->dmabuf_fd[i] = 0; + } + } + + if(self->fd > 0) { + xioctl(self->fd, VIDIOC_STREAMOFF, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE}); + close(self->fd); + self->fd = 0; + } + + if(self->jpeg_decompressor) { + self->tjDestroy(self->jpeg_decompressor); + self->jpeg_decompressor = NULL; + } + + if(self->libturbojpeg_lib) { + dlclose(self->libturbojpeg_lib); + self->libturbojpeg_lib = NULL; + } +} + +static void gsr_capture_v4l2_reset_cropping(gsr_capture_v4l2 *self) { + struct v4l2_cropcap cropcap = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE + }; + if(xioctl(self->fd, VIDIOC_CROPCAP, &cropcap) == 0) { + struct v4l2_crop crop = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .c = cropcap.defrect /* reset to default */ + }; + + if(xioctl(self->fd, VIDIOC_S_CROP, &crop) == -1) { + switch (errno) { + case EINVAL: + /* Cropping not supported. */ + break; + default: + /* Errors ignored. */ + break; + } + } + } else { + /* Errors ignored. */ + } +} + +gsr_capture_v4l2_supported_pixfmts gsr_capture_v4l2_get_supported_pixfmts(int fd) { + gsr_capture_v4l2_supported_pixfmts result = {0}; + + struct v4l2_fmtdesc fmt = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE + }; + while(xioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) { + //fprintf(stderr, "fmt: %d, desc: %s, flags: %d\n", fmt.pixelformat, fmt.description, fmt.flags); + switch(fmt.pixelformat) { + case V4L2_PIX_FMT_YUYV: + result.yuyv = true; + break; + case V4L2_PIX_FMT_MJPEG: + result.mjpeg = true; + break; + } + ++fmt.index; + } + + return result; +} + +static uint32_t gsr_pixfmt_to_v4l2_pixfmt(gsr_capture_v4l2_pixfmt pixfmt) { + switch(pixfmt) { + case GSR_CAPTURE_V4L2_PIXFMT_AUTO: + assert(false); + break; + case GSR_CAPTURE_V4L2_PIXFMT_YUYV: + return V4L2_PIX_FMT_YUYV; + case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: + return V4L2_PIX_FMT_MJPEG; + } + assert(false); + return V4L2_PIX_FMT_YUYV; +} + +static bool gsr_capture_v4l2_validate_pixfmt(gsr_capture_v4l2 *self, const gsr_capture_v4l2_supported_pixfmts supported_pixfmts) { + switch(self->params.pixfmt) { + case GSR_CAPTURE_V4L2_PIXFMT_AUTO: { + if(supported_pixfmts.yuyv) { + self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; + } else if(supported_pixfmts.mjpeg) { + self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; + } else { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about\n", self->params.device_path); + return false; + } + break; + } + case GSR_CAPTURE_V4L2_PIXFMT_YUYV: { + if(!supported_pixfmts.yuyv) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with -pixfmt mjpeg or -pixfmt auto instead\n", self->params.device_path); + return false; + } + break; + } + case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: { + if(!supported_pixfmts.mjpeg) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with -pixfmt yuyv or -pixfmt auto instead\n", self->params.device_path); + return false; + } + break; + } + } + return true; +} + +static bool gsr_capture_v4l2_create_pbos(gsr_capture_v4l2 *self, int width, int height) { + self->pbo_index = 0; + + self->params.egl->glGenBuffers(NUM_PBOS, self->pbos); + for(int i = 0; i < NUM_PBOS; ++i) { + if(self->pbos[i] == 0) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects\n"); + return false; + } + + self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[i]); + self->params.egl->glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4, 0, GL_DYNAMIC_DRAW); + } + + self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + return true; +} + +static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l2_format *fmt) { + switch(self->params.pixfmt) { + case GSR_CAPTURE_V4L2_PIXFMT_AUTO: { + assert(false); + return false; + } + case GSR_CAPTURE_V4L2_PIXFMT_YUYV: { + self->params.egl->glGenTextures(NUM_BUFFERS, self->texture_id); + + for(int i = 0; i < NUM_BUFFERS; ++i) { + self->dma_image[i] = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, (intptr_t[]) { + EGL_WIDTH, fmt->fmt.pix.width, + EGL_HEIGHT, fmt->fmt.pix.height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_YUYV, + EGL_DMA_BUF_PLANE0_FD_EXT, self->dmabuf_fd[i], + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, fmt->fmt.pix.bytesperline, + EGL_NONE + }); + + if(!self->dma_image[i]) { + self->yuyv_conversion_fallback = true; + self->dma_image[i] = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, (intptr_t[]) { + EGL_WIDTH, fmt->fmt.pix.width, + EGL_HEIGHT, fmt->fmt.pix.height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_RG88, + EGL_DMA_BUF_PLANE0_FD_EXT, self->dmabuf_fd[i], + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, fmt->fmt.pix.bytesperline, + EGL_NONE + }); + + if(!self->dma_image[i]) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d\n", self->params.egl->eglGetError()); + return false; + } + } + + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_id[i]); + self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, self->dma_image[i]); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + if(self->texture_id[i] == 0) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + return false; + } + } + + self->buffer_type = V4L2_BUFFER_TYPE_DMABUF; + break; + } + case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: { + for(int i = 0; i < NUM_BUFFERS; ++i) { + self->dmabuf_size[i] = fmt->fmt.pix.sizeimage; + self->dmabuf_map[i] = mmap(NULL, fmt->fmt.pix.sizeimage, PROT_READ, MAP_SHARED, self->dmabuf_fd[i], 0); + if(self->dmabuf_map[i] == MAP_FAILED) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: mmap failed, error: %s\n", strerror(errno)); + return false; + } + + // GL_RGBA is intentionally used here instead of GL_RGB, because the performance is much better when using glTexSubImage2D (22% cpu usage compared to 38% cpu usage) + self->texture_id[i] = gl_create_texture(self->params.egl, fmt->fmt.pix.width, fmt->fmt.pix.height, GL_RGBA8, GL_RGBA, GL_LINEAR); + if(self->texture_id[i] == 0) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + return false; + } + } + + if(!gsr_capture_v4l2_create_pbos(self, fmt->fmt.pix.width, fmt->fmt.pix.height)) + return false; + + self->buffer_type = V4L2_BUFFER_TYPE_MMAP; + break; + } + } + return true; +} + +static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { + self->fd = open(self->params.device_path, O_RDWR | O_NONBLOCK); + if(self->fd < 0) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to open %s, error: %s\n", self->params.device_path, strerror(errno)); + return -1; + } + + struct v4l2_capability cap = {0}; + if(xioctl(self->fd, VIDIOC_QUERYCAP, &cap) == -1) { + if(EINVAL == errno) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a v4l2 device\n", self->params.device_path); + return -1; + } else { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s\n", strerror(errno)); + return -1; + } + } + + if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a video capture device\n", self->params.device_path); + return -1; + } + + if(!(cap.capabilities & V4L2_CAP_STREAMING)) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support streaming i/o\n", self->params.device_path); + return -1; + } + + gsr_capture_v4l2_reset_cropping(self); + + const gsr_capture_v4l2_supported_pixfmts supported_pixfmts = gsr_capture_v4l2_get_supported_pixfmts(self->fd); + if(!gsr_capture_v4l2_validate_pixfmt(self, supported_pixfmts)) + return -1; + + if(self->params.pixfmt == GSR_CAPTURE_V4L2_PIXFMT_MJPEG) { + dlerror(); /* clear */ + self->libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY); + if(!self->libturbojpeg_lib) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s\n", dlerror()); + return -1; + } + + self->tjInitDecompress = (FUNC_tjInitDecompress)dlsym(self->libturbojpeg_lib, "tjInitDecompress"); + self->tjDestroy = (FUNC_tjDestroy)dlsym(self->libturbojpeg_lib, "tjDestroy"); + self->tjDecompressHeader2 = (FUNC_tjDecompressHeader2)dlsym(self->libturbojpeg_lib, "tjDecompressHeader2"); + self->tjDecompress2 = (FUNC_tjDecompress2)dlsym(self->libturbojpeg_lib, "tjDecompress2"); + self->tjGetErrorStr2 = (FUNC_tjGetErrorStr2)dlsym(self->libturbojpeg_lib, "tjGetErrorStr2"); + + if(!self->tjInitDecompress || !self->tjDestroy || !self->tjDecompressHeader2 || !self->tjDecompress2 || !self->tjGetErrorStr2) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated\n"); + return -1; + } + + self->jpeg_decompressor = self->tjInitDecompress(); + if(!self->jpeg_decompressor) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to create jpeg decompressor\n"); + return -1; + } + } + + const uint32_t v4l2_pixfmt = gsr_pixfmt_to_v4l2_pixfmt(self->params.pixfmt); + struct v4l2_format fmt = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .fmt.pix.pixelformat = v4l2_pixfmt + }; + if(xioctl(self->fd, VIDIOC_S_FMT, &fmt) == -1) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s\n", strerror(errno)); + return -1; + } + + if(fmt.fmt.pix.pixelformat != v4l2_pixfmt) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s\n", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno)); + return -1; + } + + self->capture_size.x = fmt.fmt.pix.width; + self->capture_size.y = fmt.fmt.pix.height; + + struct v4l2_requestbuffers reqbuf = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .memory = V4L2_MEMORY_MMAP, + .count = NUM_BUFFERS + }; + if(xioctl(self->fd, VIDIOC_REQBUFS, &reqbuf) == -1) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s\n", strerror(errno)); + return -1; + } + + for(int i = 0; i < NUM_BUFFERS; ++i) { + struct v4l2_exportbuffer expbuf = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .index = i, + .flags = O_RDONLY + }; + if(xioctl(self->fd, VIDIOC_EXPBUF, &expbuf) == -1) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s\n", strerror(errno)); + return -1; + } + self->dmabuf_fd[i] = expbuf.fd; + } + + if(!gsr_capture_v4l2_map_buffer(self, &fmt)) + return -1; + + for(int i = 0; i < NUM_BUFFERS; ++i) { + struct v4l2_buffer buf = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .index = i, + .memory = V4L2_MEMORY_MMAP + }; + xioctl(self->fd, VIDIOC_QBUF, &buf); + } + + if(xioctl(self->fd, VIDIOC_STREAMON, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE})) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s\n", strerror(errno)); + return -1; + } + + fprintf(stderr, "gsr info: gsr_capture_v4l2_create: waiting for camera %s to be ready\n", self->params.device_path); + return 0; +} + +static int gsr_capture_v4l2_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_v4l2 *self = cap->priv; + + const int result = gsr_capture_v4l2_setup(self); + if(result != 0) { + gsr_capture_v4l2_stop(self); + return result; + } + + if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) { + capture_metadata->video_size = self->capture_size; + } else { + self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution); + capture_metadata->video_size = self->params.output_resolution; + } + + self->capture_start_time = clock_get_monotonic_seconds(); + return 0; +} + +static void gsr_capture_v4l2_tick(gsr_capture *cap) { + gsr_capture_v4l2 *self = cap->priv; + if(!self->got_first_frame && !self->should_stop) { + const double timeout_sec = 5.0; + if(clock_get_monotonic_seconds() - self->capture_start_time >= timeout_sec) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: didn't receive camera data in %f seconds\n", timeout_sec); + self->should_stop = true; + self->stop_is_error = true; + } + } +} + +static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, const struct v4l2_buffer *buf) { + int jpeg_subsamp = 0; + int jpeg_width = 0; + int jpeg_height = 0; + if(self->tjDecompressHeader2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, &jpeg_width, &jpeg_height, &jpeg_subsamp) != 0) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + return; + } + + if(jpeg_width != self->capture_size.x || jpeg_height != self->capture_size.y) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d\n", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height); + return; + } + + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_id[buf->index]); + + self->pbo_index = (self->pbo_index + 1) % NUM_PBOS; + const unsigned int next_pbo_index = (self->pbo_index + 1) % NUM_PBOS; + + self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[self->pbo_index]); + self->params.egl->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self->capture_size.x, self->capture_size.y, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + + self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[next_pbo_index]); + self->params.egl->glBufferData(GL_PIXEL_UNPACK_BUFFER, self->capture_size.x * self->capture_size.y * 4, 0, GL_DYNAMIC_DRAW); + + void *mapped_buffer = self->params.egl->glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, self->capture_size.x * self->capture_size.y * 4, GL_MAP_WRITE_BIT); + if(mapped_buffer) { + if(self->tjDecompress2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, mapped_buffer, jpeg_width, 0, jpeg_height, TJPF_RGBA, TJFLAG_FASTDCT) != 0) + fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + self->params.egl->glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + } + + self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); +} + +static int gsr_capture_v4l2_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + (void)color_conversion; + gsr_capture_v4l2 *self = cap->priv; + + struct v4l2_buffer buf = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .memory = V4L2_MEMORY_MMAP + }; + + xioctl(self->fd, VIDIOC_DQBUF, &buf); + unsigned int texture_index = buf.index; + + if(buf.bytesused > 0 && !(buf.flags & V4L2_BUF_FLAG_ERROR)) { + if(!self->got_first_frame) + fprintf(stderr, "gsr info: gsr_capture_v4l2_capture: camera %s is now ready\n", self->params.device_path); + self->got_first_frame = true; + + switch(self->buffer_type) { + case V4L2_BUFFER_TYPE_DMABUF: { + //self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_id); + //self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, self->dma_image[buf.index]); + //self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + break; + } + case V4L2_BUFFER_TYPE_MMAP: { + //xioctl(self->dmabuf_fd[buf.index], DMA_BUF_IOCTL_SYNC, &(struct dma_buf_sync){ .flags = DMA_BUF_SYNC_START }); + gsr_capture_v4l2_decode_jpeg_to_texture(self, &buf); + //xioctl(self->dmabuf_fd[buf.index], DMA_BUF_IOCTL_SYNC, &(struct dma_buf_sync){ .flags = DMA_BUF_SYNC_END }); + break; + } + } + + self->prev_texture_index = buf.index; + } else { + texture_index = self->prev_texture_index; + } + xioctl(self->fd, VIDIOC_QBUF, &buf); + + const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size); + const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata); + + self->params.egl->glFlush(); + if(self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + self->params.egl->glFinish(); + + if(self->buffer_type == V4L2_BUFFER_TYPE_DMABUF) { + gsr_color_conversion_draw(color_conversion, self->texture_id[texture_index], + target_pos, output_size, + (vec2i){0, 0}, self->capture_size, self->capture_size, + GSR_ROT_0, capture_metadata->flip, self->yuyv_conversion_fallback ? GSR_SOURCE_COLOR_YUYV : GSR_SOURCE_COLOR_RGB, true); + } else { + gsr_color_conversion_draw(color_conversion, self->texture_id[texture_index], + target_pos, output_size, + (vec2i){0, 0}, self->capture_size, self->capture_size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + } + + return self->got_first_frame ? 0 : -1; +} + +static bool gsr_capture_v4l2_uses_external_image(gsr_capture *cap) { + (void)cap; + return true; +} + +static bool gsr_capture_v4l2_should_stop(gsr_capture *cap, bool *err) { + gsr_capture_v4l2 *self = cap->priv; + if(err) + *err = self->stop_is_error; + return self->should_stop; +} + +static bool gsr_capture_v4l2_is_damaged(gsr_capture *cap) { + gsr_capture_v4l2 *self = cap->priv; + struct pollfd poll_data = { + .fd = self->fd, + .events = POLLIN, + .revents = 0 + }; + return poll(&poll_data, 1, 0) > 0 && (poll_data.revents & POLLIN); +} + +static void gsr_capture_v4l2_clear_damage(gsr_capture *cap) { + gsr_capture_v4l2 *self = cap->priv; + (void)self; +} + +static void gsr_capture_v4l2_destroy(gsr_capture *cap) { + gsr_capture_v4l2 *self = cap->priv; + if(cap->priv) { + gsr_capture_v4l2_stop(self); + free(cap->priv); + cap->priv = NULL; + } + free(cap); +} + +gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_v4l2_create params is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_v4l2 *cap_camera = calloc(1, sizeof(gsr_capture_v4l2)); + if(!cap_camera) { + free(cap); + return NULL; + } + + cap_camera->params = *params; + + *cap = (gsr_capture) { + .start = gsr_capture_v4l2_start, + .tick = gsr_capture_v4l2_tick, + .should_stop = gsr_capture_v4l2_should_stop, + .capture = gsr_capture_v4l2_capture, + .uses_external_image = gsr_capture_v4l2_uses_external_image, + .is_damaged = gsr_capture_v4l2_is_damaged, + .clear_damage = gsr_capture_v4l2_clear_damage, + .destroy = gsr_capture_v4l2_destroy, + .priv = cap_camera + }; + + return cap; +} + +void gsr_capture_v4l2_list_devices(v4l2_devices_query_callback callback, void *userdata) { + void *libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY); + const bool has_libturbojpeg_lib = libturbojpeg_lib != NULL; + if(libturbojpeg_lib) + dlclose(libturbojpeg_lib); + + char v4l2_device_path[128]; + for(int i = 0; i < 8; ++i) { + snprintf(v4l2_device_path, sizeof(v4l2_device_path), "/dev/video%d", i); + + const int fd = open(v4l2_device_path, O_RDWR | O_NONBLOCK); + if(fd < 0) + continue; + + struct v4l2_capability cap = {0}; + if(xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) + goto next; + + if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) + goto next; + + if(!(cap.capabilities & V4L2_CAP_STREAMING)) + goto next; + + struct v4l2_format fmt = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE + }; + if(xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) + goto next; + + gsr_capture_v4l2_supported_pixfmts supported_pixfmts = gsr_capture_v4l2_get_supported_pixfmts(fd); + if(!has_libturbojpeg_lib) + supported_pixfmts.mjpeg = false; + + if(supported_pixfmts.yuyv || supported_pixfmts.mjpeg) + callback(v4l2_device_path, supported_pixfmts, (vec2i){ fmt.fmt.pix.width, fmt.fmt.pix.height }, userdata); + + next: + close(fd); + } +} diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c new file mode 100644 index 0000000..8539b77 --- /dev/null +++ b/src/capture/xcomposite.c @@ -0,0 +1,324 @@ +#include "../../include/capture/xcomposite.h" +#include "../../include/window_texture.h" +#include "../../include/utils.h" +#include "../../include/cursor.h" +#include "../../include/color_conversion.h" +#include "../../include/window/window.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> + +#include <X11/Xlib.h> + +typedef struct { + gsr_capture_xcomposite_params params; + Display *display; + + bool should_stop; + bool stop_is_error; + bool window_resized; + bool follow_focused_initialized; + bool init_new_window; + + Window window; + vec2i window_pos; + vec2i window_size; + vec2i texture_size; + double window_resize_timer; + + WindowTexture window_texture; + + Atom net_active_window_atom; + + bool clear_background; +} gsr_capture_xcomposite; + +static void gsr_capture_xcomposite_stop(gsr_capture_xcomposite *self) { + window_texture_deinit(&self->window_texture); +} + +static int max_int(int a, int b) { + return a > b ? a : b; +} + +static Window get_focused_window(Display *display, Atom net_active_window_atom) { + Atom type; + int format = 0; + unsigned long num_items = 0; + unsigned long bytes_after = 0; + unsigned char *properties = NULL; + if(XGetWindowProperty(display, DefaultRootWindow(display), net_active_window_atom, 0, 1024, False, AnyPropertyType, &type, &format, &num_items, &bytes_after, &properties) == Success && properties) { + Window focused_window = *(unsigned long*)properties; + XFree(properties); + return focused_window; + } + return None; +} + +static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_xcomposite *self = cap->priv; + + if(self->params.follow_focused) { + self->net_active_window_atom = XInternAtom(self->display, "_NET_ACTIVE_WINDOW", False); + if(!self->net_active_window_atom) { + fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom\n"); + return -1; + } + self->window = get_focused_window(self->display, self->net_active_window_atom); + } else { + self->window = self->params.window; + } + + /* TODO: Do these in tick, and allow error if follow_focused */ + + XWindowAttributes attr; + if(!XGetWindowAttributes(self->display, self->window, &attr) && !self->params.follow_focused) { + fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: invalid window id: %lu\n", self->window); + return -1; + } + + self->window_pos.x = attr.x; + self->window_pos.y = attr.y; + + self->window_size.x = max_int(attr.width, 0); + self->window_size.y = max_int(attr.height, 0); + + if(self->params.follow_focused) + XSelectInput(self->display, DefaultRootWindow(self->display), PropertyChangeMask); + + // TODO: Get select and add these on top of it and then restore at the end. Also do the same in other xcomposite + XSelectInput(self->display, self->window, StructureNotifyMask | ExposureMask); + + if(window_texture_init(&self->window_texture, self->display, self->window, self->params.egl) != 0 && !self->params.follow_focused) { + fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: failed to get window texture for window %ld\n", (long)self->window); + return -1; + } + + self->texture_size.x = self->window_texture.window_width; + self->texture_size.y = self->window_texture.window_height; + + if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) { + capture_metadata->video_size = self->texture_size; + } else { + capture_metadata->video_size = self->params.output_resolution; + } + + self->window_resize_timer = clock_get_monotonic_seconds(); + return 0; +} + +static void gsr_capture_xcomposite_tick(gsr_capture *cap) { + gsr_capture_xcomposite *self = cap->priv; + + if(self->params.follow_focused && !self->follow_focused_initialized) { + self->init_new_window = true; + } + + if(self->init_new_window) { + self->init_new_window = false; + Window focused_window = get_focused_window(self->display, self->net_active_window_atom); + if(focused_window != self->window || !self->follow_focused_initialized) { + self->follow_focused_initialized = true; + XSelectInput(self->display, self->window, 0); + self->window = focused_window; + XSelectInput(self->display, self->window, StructureNotifyMask | ExposureMask); + + XWindowAttributes attr; + attr.width = 0; + attr.height = 0; + if(!XGetWindowAttributes(self->display, self->window, &attr)) + fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window); + + self->window_pos.x = attr.x; + self->window_pos.y = attr.y; + + self->window_size.x = max_int(attr.width, 0); + self->window_size.y = max_int(attr.height, 0); + + window_texture_deinit(&self->window_texture); + window_texture_init(&self->window_texture, self->display, self->window, self->params.egl); // TODO: Do not do the below window_texture_on_resize after this + + self->texture_size.x = self->window_texture.window_width; + self->texture_size.y = self->window_texture.window_height; + + self->window_resized = false; + self->clear_background = true; + } + } + + const double window_resize_timeout = 1.0; // 1 second + if(self->window_resized && clock_get_monotonic_seconds() - self->window_resize_timer >= window_resize_timeout) { + self->window_resized = false; + + if(window_texture_on_resize(&self->window_texture) != 0) { + fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick: window_texture_on_resize failed\n"); + //self->should_stop = true; + //self->stop_is_error = true; + return; + } + + self->texture_size.x = self->window_texture.window_width; + self->texture_size.y = self->window_texture.window_height; + + self->clear_background = true; + } +} + +static void gsr_capture_xcomposite_on_event(gsr_capture *cap, gsr_egl *egl) { + gsr_capture_xcomposite *self = cap->priv; + XEvent *xev = gsr_window_get_event_data(egl->window); + switch(xev->type) { + case DestroyNotify: { + /* Window died (when not following focused window), so we stop recording */ + if(!self->params.follow_focused && xev->xdestroywindow.window == self->window) { + self->should_stop = true; + self->stop_is_error = false; + } + break; + } + case Expose: { + /* Requires window texture recreate */ + if(xev->xexpose.count == 0 && xev->xexpose.window == self->window) { + self->window_resize_timer = clock_get_monotonic_seconds(); + self->window_resized = true; + } + break; + } + case ConfigureNotify: { + self->window_pos.x = xev->xconfigure.x; + self->window_pos.y = xev->xconfigure.y; + + /* Window resized */ + if(xev->xconfigure.window == self->window && (xev->xconfigure.width != self->window_size.x || xev->xconfigure.height != self->window_size.y)) { + self->window_size.x = max_int(xev->xconfigure.width, 0); + self->window_size.y = max_int(xev->xconfigure.height, 0); + self->window_resize_timer = clock_get_monotonic_seconds(); + self->window_resized = true; + } + break; + } + case PropertyNotify: { + /* Focused window changed */ + if(self->params.follow_focused && xev->xproperty.atom == self->net_active_window_atom) { + self->init_new_window = true; + } + break; + } + } +} + +static bool gsr_capture_xcomposite_should_stop(gsr_capture *cap, bool *err) { + gsr_capture_xcomposite *self = cap->priv; + if(self->should_stop) { + if(err) + *err = self->stop_is_error; + return true; + } + + if(err) + *err = false; + return false; +} + +static void gsr_capture_xcomposite_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + (void)capture_metadata; + gsr_capture_xcomposite *self = cap->priv; + + if(self->clear_background) { + self->clear_background = false; + color_conversion->schedule_clear = true; + } +} + +static int gsr_capture_xcomposite_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + gsr_capture_xcomposite *self = cap->priv; + + const vec2i output_size = scale_keep_aspect_ratio(self->texture_size, capture_metadata->recording_size); + const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata); + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + gsr_color_conversion_draw(color_conversion, window_texture_get_opengl_texture_id(&self->window_texture), + target_pos, output_size, + (vec2i){0, 0}, self->texture_size, self->texture_size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + if(self->params.record_cursor && self->params.cursor->visible) { + const vec2d scale = { + self->texture_size.x == 0 ? 0 : (double)output_size.x / (double)self->texture_size.x, + self->texture_size.y == 0 ? 0 : (double)output_size.y / (double)self->texture_size.y + }; + + const vec2i cursor_pos = { + target_pos.x + (self->params.cursor->position.x - self->params.cursor->hotspot.x - self->window_pos.x) * scale.x, + target_pos.y + (self->params.cursor->position.y - self->params.cursor->hotspot.y - self->window_pos.y) * scale.y + }; + + self->params.egl->glEnable(GL_SCISSOR_TEST); + self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y); + + gsr_color_conversion_draw(color_conversion, self->params.cursor->texture_id, + cursor_pos, (vec2i){self->params.cursor->size.x * scale.x, self->params.cursor->size.y * scale.y}, + (vec2i){0, 0}, self->params.cursor->size, self->params.cursor->size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + self->params.egl->glDisable(GL_SCISSOR_TEST); + } + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); + + return 0; +} + +static uint64_t gsr_capture_xcomposite_get_window_id(gsr_capture *cap) { + gsr_capture_xcomposite *self = cap->priv; + return self->window; +} + +static void gsr_capture_xcomposite_destroy(gsr_capture *cap) { + if(cap->priv) { + gsr_capture_xcomposite_stop(cap->priv); + free(cap->priv); + cap->priv = NULL; + } + free(cap); +} + +gsr_capture* gsr_capture_xcomposite_create(const gsr_capture_xcomposite_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_xcomposite_create params is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_xcomposite *cap_xcomp = calloc(1, sizeof(gsr_capture_xcomposite)); + if(!cap_xcomp) { + free(cap); + return NULL; + } + + cap_xcomp->params = *params; + cap_xcomp->display = gsr_window_get_display(params->egl->window); + + *cap = (gsr_capture) { + .start = gsr_capture_xcomposite_start, + .on_event = gsr_capture_xcomposite_on_event, + .tick = gsr_capture_xcomposite_tick, + .should_stop = gsr_capture_xcomposite_should_stop, + .pre_capture = gsr_capture_xcomposite_pre_capture, + .capture = gsr_capture_xcomposite_capture, + .uses_external_image = NULL, + .get_window_id = gsr_capture_xcomposite_get_window_id, + .destroy = gsr_capture_xcomposite_destroy, + .priv = cap_xcomp + }; + + return cap; +} diff --git a/src/capture/ximage.c b/src/capture/ximage.c new file mode 100644 index 0000000..b0cf364 --- /dev/null +++ b/src/capture/ximage.c @@ -0,0 +1,226 @@ +#include "../../include/capture/ximage.h" +#include "../../include/utils.h" +#include "../../include/cursor.h" +#include "../../include/color_conversion.h" +#include "../../include/window/window.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> + +#include <X11/Xlib.h> + +/* TODO: update when monitors are reconfigured */ + +typedef struct { + gsr_capture_ximage_params params; + Display *display; + gsr_monitor monitor; + vec2i capture_pos; + vec2i capture_size; + unsigned int texture_id; + Window root_window; +} gsr_capture_ximage; + +static void gsr_capture_ximage_stop(gsr_capture_ximage *self) { + if(self->texture_id) { + self->params.egl->glDeleteTextures(1, &self->texture_id); + self->texture_id = 0; + } +} + +static int max_int(int a, int b) { + return a > b ? a : b; +} + +static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) { + gsr_capture_ximage *self = cap->priv; + self->root_window = DefaultRootWindow(self->display); + + if(!get_monitor_by_name(self->params.egl, GSR_CONNECTION_X11, self->params.display_to_capture, &self->monitor)) { + fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_capture_ximage_stop(self); + return -1; + } + + self->capture_pos = self->monitor.pos; + self->capture_size = self->monitor.size; + + if(self->params.region_size.x > 0 && self->params.region_size.y > 0) + self->capture_size = self->params.region_size; + + if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) { + self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution); + capture_metadata->video_size = self->params.output_resolution; + } else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) { + capture_metadata->video_size = self->params.region_size; + } else { + capture_metadata->video_size = self->capture_size; + } + + self->texture_id = gl_create_texture(self->params.egl, self->capture_size.x, self->capture_size.y, GL_RGB8, GL_RGB, GL_LINEAR); + if(self->texture_id == 0) { + fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to create texture\n"); + gsr_capture_ximage_stop(self); + return -1; + } + + return 0; +} + +static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x, int y, int width, int height) { + const int max_width = XWidthOfScreen(DefaultScreenOfDisplay(self->display)); + const int max_height = XHeightOfScreen(DefaultScreenOfDisplay(self->display)); + + if(x < 0) + x = 0; + else if(x >= max_width) + x = max_width - 1; + + if(y < 0) + y = 0; + else if(y >= max_height) + y = max_height - 1; + + if(width < 0) + width = 0; + else if(x + width >= max_width) + width = max_width - x; + + if(height < 0) + height = 0; + else if(y + height >= max_height) + height = max_height - y; + + XImage *image = XGetImage(self->display, self->root_window, x, y, width, height, AllPlanes, ZPixmap); + if(!image) { + fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: XGetImage failed\n"); + return false; + } + + bool success = false; + uint8_t *image_data = malloc(image->width * image->height * 3); + if(!image_data) { + fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: failed to allocate image data\n"); + goto done; + } + + for(int y = 0; y < image->height; ++y) { + for(int x = 0; x < image->width; ++x) { + unsigned long pixel = XGetPixel(image, x, y); + unsigned char red = (pixel & image->red_mask) >> 16; + unsigned char green = (pixel & image->green_mask) >> 8; + unsigned char blue = pixel & image->blue_mask; + + const size_t texture_data_index = (x + y * image->width) * 3; + image_data[texture_data_index + 0] = red; + image_data[texture_data_index + 1] = green; + image_data[texture_data_index + 2] = blue; + } + } + + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_id); + // TODO: Change to GL_RGBA for better performance? image_data needs alpha then as well + self->params.egl->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height, GL_RGB, GL_UNSIGNED_BYTE, image_data); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + success = true; + + done: + free(image_data); + XDestroyImage(image); + return success; +} + +static int gsr_capture_ximage_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) { + gsr_capture_ximage *self = cap->priv; + + const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size); + const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata); + gsr_capture_ximage_upload_to_texture(self, self->capture_pos.x + self->params.region_position.x, self->capture_pos.y + self->params.region_position.y, self->capture_size.x, self->capture_size.y); + + gsr_color_conversion_draw(color_conversion, self->texture_id, + target_pos, output_size, + (vec2i){0, 0}, self->capture_size, self->capture_size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + if(self->params.record_cursor && self->params.cursor->visible) { + const vec2d scale = { + self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x, + self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y + }; + + const vec2i cursor_pos = { + target_pos.x + (self->params.cursor->position.x - self->params.cursor->hotspot.x) * scale.x - self->capture_pos.x - self->params.region_position.x, + target_pos.y + (self->params.cursor->position.y - self->params.cursor->hotspot.y) * scale.y - self->capture_pos.y - self->params.region_position.y + }; + + self->params.egl->glEnable(GL_SCISSOR_TEST); + self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y); + + gsr_color_conversion_draw(color_conversion, self->params.cursor->texture_id, + cursor_pos, (vec2i){self->params.cursor->size.x * scale.x, self->params.cursor->size.y * scale.y}, + (vec2i){0, 0}, self->params.cursor->size, self->params.cursor->size, + GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false); + + self->params.egl->glDisable(GL_SCISSOR_TEST); + } + + self->params.egl->glFlush(); + self->params.egl->glFinish(); + + return 0; +} + +static void gsr_capture_ximage_destroy(gsr_capture *cap) { + gsr_capture_ximage *self = cap->priv; + if(cap->priv) { + gsr_capture_ximage_stop(self); + free((void*)self->params.display_to_capture); + self->params.display_to_capture = NULL; + free(self); + cap->priv = NULL; + } + free(cap); +} + +gsr_capture* gsr_capture_ximage_create(const gsr_capture_ximage_params *params) { + if(!params) { + fprintf(stderr, "gsr error: gsr_capture_ximage_create params is NULL\n"); + return NULL; + } + + gsr_capture *cap = calloc(1, sizeof(gsr_capture)); + if(!cap) + return NULL; + + gsr_capture_ximage *cap_ximage = calloc(1, sizeof(gsr_capture_ximage)); + if(!cap_ximage) { + free(cap); + return NULL; + } + + const char *display_to_capture = strdup(params->display_to_capture); + if(!display_to_capture) { + free(cap); + free(cap_ximage); + return NULL; + } + + cap_ximage->params = *params; + cap_ximage->display = gsr_window_get_display(params->egl->window); + cap_ximage->params.display_to_capture = display_to_capture; + + *cap = (gsr_capture) { + .start = gsr_capture_ximage_start, + .tick = NULL, + .should_stop = NULL, + .capture = gsr_capture_ximage_capture, + .uses_external_image = NULL, + .get_window_id = NULL, + .destroy = gsr_capture_ximage_destroy, + .priv = cap_ximage + }; + + return cap; +} diff --git a/src/codec_query/nvenc.c b/src/codec_query/nvenc.c new file mode 100644 index 0000000..5e52807 --- /dev/null +++ b/src/codec_query/nvenc.c @@ -0,0 +1,259 @@ +#include "../../include/codec_query/nvenc.h" +#include "../../include/cuda.h" +#include "../../external/nvEncodeAPI.h" + +#include <dlfcn.h> +#include <stdio.h> +#include <string.h> + +#define NVENCAPI_MAJOR_VERSION_470 11 +#define NVENCAPI_MINOR_VERSION_470 1 +#define NVENCAPI_VERSION_470 (NVENCAPI_MAJOR_VERSION_470 | (NVENCAPI_MINOR_VERSION_470 << 24)) +#define NVENCAPI_STRUCT_VERSION_CUSTOM(nvenc_api_version, struct_version) ((uint32_t)(nvenc_api_version) | ((struct_version)<<16) | (0x7 << 28)) + +static void* open_nvenc_library(void) { + dlerror(); /* clear */ + void *lib = dlopen("libnvidia-encode.so.1", RTLD_LAZY); + if(!lib) { + lib = dlopen("libnvidia-encode.so", RTLD_LAZY); + if(!lib) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc failed: failed to load libnvidia-encode.so/libnvidia-encode.so.1, error: %s\n", dlerror()); + return NULL; + } + } + return lib; +} + +static bool profile_is_h264(const GUID *profile_guid) { + const GUID *h264_guids[] = { + &NV_ENC_H264_PROFILE_BASELINE_GUID, + &NV_ENC_H264_PROFILE_MAIN_GUID, + &NV_ENC_H264_PROFILE_HIGH_GUID, + &NV_ENC_H264_PROFILE_PROGRESSIVE_HIGH_GUID, + &NV_ENC_H264_PROFILE_CONSTRAINED_HIGH_GUID + }; + + for(int i = 0; i < 5; ++i) { + if(memcmp(profile_guid, h264_guids[i], sizeof(GUID)) == 0) + return true; + } + + return false; +} + +static bool profile_is_hevc(const GUID *profile_guid) { + const GUID *h264_guids[] = { + &NV_ENC_HEVC_PROFILE_MAIN_GUID, + }; + + for(int i = 0; i < 1; ++i) { + if(memcmp(profile_guid, h264_guids[i], sizeof(GUID)) == 0) + return true; + } + + return false; +} + +static bool profile_is_hevc_10bit(const GUID *profile_guid) { + const GUID *h264_guids[] = { + &NV_ENC_HEVC_PROFILE_MAIN10_GUID, + }; + + for(int i = 0; i < 1; ++i) { + if(memcmp(profile_guid, h264_guids[i], sizeof(GUID)) == 0) + return true; + } + + return false; +} + +static bool profile_is_av1(const GUID *profile_guid) { + const GUID *h264_guids[] = { + &NV_ENC_AV1_PROFILE_MAIN_GUID, + }; + + for(int i = 0; i < 1; ++i) { + if(memcmp(profile_guid, h264_guids[i], sizeof(GUID)) == 0) + return true; + } + + return false; +} + +/* Returns 0 on error */ +static int nvenc_get_encoding_capability(const NV_ENCODE_API_FUNCTION_LIST *function_list, void *nvenc_encoder, const GUID *encode_guid, uint32_t nvenc_api_version, NV_ENC_CAPS cap) { + NV_ENC_CAPS_PARAM param = { + .version = NVENCAPI_STRUCT_VERSION_CUSTOM(nvenc_api_version, 1), + .capsToQuery = cap + }; + + int value = 0; + if(function_list->nvEncGetEncodeCaps(nvenc_encoder, *encode_guid, ¶m, &value) != NV_ENC_SUCCESS) + return 0; + + return value; +} + +static vec2i encoder_get_max_resolution(const NV_ENCODE_API_FUNCTION_LIST *function_list, void *nvenc_encoder, const GUID *encode_guid, uint32_t nvenc_api_version) { + return (vec2i){ + .x = nvenc_get_encoding_capability(function_list, nvenc_encoder, encode_guid, nvenc_api_version, NV_ENC_CAPS_WIDTH_MAX), + .y = nvenc_get_encoding_capability(function_list, nvenc_encoder, encode_guid, nvenc_api_version, NV_ENC_CAPS_HEIGHT_MAX), + }; +} + +static bool encoder_get_supported_profiles(const NV_ENCODE_API_FUNCTION_LIST *function_list, void *nvenc_encoder, const GUID *encoder_guid, gsr_supported_video_codecs *supported_video_codecs, uint32_t nvenc_api_version) { + bool success = false; + GUID *profile_guids = NULL; + + uint32_t profile_guid_count = 0; + if(function_list->nvEncGetEncodeProfileGUIDCount(nvenc_encoder, *encoder_guid, &profile_guid_count) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + goto fail; + } + + if(profile_guid_count == 0) + goto fail; + + profile_guids = calloc(profile_guid_count, sizeof(GUID)); + if(!profile_guids) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)profile_guid_count); + goto fail; + } + + if(function_list->nvEncGetEncodeProfileGUIDs(nvenc_encoder, *encoder_guid, profile_guids, profile_guid_count, &profile_guid_count) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + goto fail; + } + + const vec2i max_resolution = encoder_get_max_resolution(function_list, nvenc_encoder, encoder_guid, nvenc_api_version); + for(uint32_t i = 0; i < profile_guid_count; ++i) { + if(profile_is_h264(&profile_guids[i])) { + supported_video_codecs->h264 = (gsr_supported_video_codec){ true, false, max_resolution }; + } else if(profile_is_hevc(&profile_guids[i])) { + supported_video_codecs->hevc = (gsr_supported_video_codec){ true, false, max_resolution }; + } else if(profile_is_hevc_10bit(&profile_guids[i])) { + supported_video_codecs->hevc_hdr = (gsr_supported_video_codec){ true, false, max_resolution }; + supported_video_codecs->hevc_10bit = (gsr_supported_video_codec){ true, false, max_resolution }; + } else if(profile_is_av1(&profile_guids[i])) { + supported_video_codecs->av1 = (gsr_supported_video_codec){ true, false, max_resolution }; + supported_video_codecs->av1_hdr = (gsr_supported_video_codec){ true, false, max_resolution }; + supported_video_codecs->av1_10bit = (gsr_supported_video_codec){ true, false, max_resolution }; + } + } + + success = true; + fail: + + if(profile_guids) + free(profile_guids); + + return success; +} + +static bool get_supported_video_codecs(const NV_ENCODE_API_FUNCTION_LIST *function_list, void *nvenc_encoder, gsr_supported_video_codecs *supported_video_codecs, uint32_t nvenc_api_version) { + bool success = false; + GUID *encoder_guids = NULL; + *supported_video_codecs = (gsr_supported_video_codecs){0}; + + uint32_t encode_guid_count = 0; + if(function_list->nvEncGetEncodeGUIDCount(nvenc_encoder, &encode_guid_count) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + goto fail; + } + + if(encode_guid_count == 0) + goto fail; + + encoder_guids = calloc(encode_guid_count, sizeof(GUID)); + if(!encoder_guids) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)encode_guid_count); + goto fail; + } + + if(function_list->nvEncGetEncodeGUIDs(nvenc_encoder, encoder_guids, encode_guid_count, &encode_guid_count) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + goto fail; + } + + for(uint32_t i = 0; i < encode_guid_count; ++i) { + encoder_get_supported_profiles(function_list, nvenc_encoder, &encoder_guids[i], supported_video_codecs, nvenc_api_version); + } + + success = true; + fail: + + if(encoder_guids) + free(encoder_guids); + + return success; +} + +bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_codecs, bool cleanup) { + memset(video_codecs, 0, sizeof(*video_codecs)); + + bool success = false; + void *nvenc_lib = NULL; + void *nvenc_encoder = NULL; + gsr_cuda cuda; + memset(&cuda, 0, sizeof(cuda)); + + if(!gsr_cuda_load(&cuda, NULL, false)) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to load cuda\n"); + goto done; + } + + nvenc_lib = open_nvenc_library(); + if(!nvenc_lib) + goto done; + + typedef NVENCSTATUS NVENCAPI (*FUNC_NvEncodeAPICreateInstance)(NV_ENCODE_API_FUNCTION_LIST *functionList); + FUNC_NvEncodeAPICreateInstance nvEncodeAPICreateInstance = (FUNC_NvEncodeAPICreateInstance)dlsym(nvenc_lib, "NvEncodeAPICreateInstance"); + if(!nvEncodeAPICreateInstance) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to find NvEncodeAPICreateInstance in libnvidia-encode.so\n"); + goto done; + } + + NV_ENCODE_API_FUNCTION_LIST function_list; + memset(&function_list, 0, sizeof(function_list)); + function_list.version = NVENCAPI_STRUCT_VERSION(2); + if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance failed\n"); + goto done; + } + + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS params; + memset(¶ms, 0, sizeof(params)); + params.version = NVENCAPI_STRUCT_VERSION(1); + params.deviceType = NV_ENC_DEVICE_TYPE_CUDA; + params.device = cuda.cu_ctx; + params.apiVersion = NVENCAPI_VERSION; + if(function_list.nvEncOpenEncodeSessionEx(¶ms, &nvenc_encoder) != NV_ENC_SUCCESS) { + // Old nvidia gpus dont support the new nvenc api (which is required for av1). + // In such cases fallback to old api version if possible and try again. + function_list.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 2); + if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance (retry) failed\n"); + goto done; + } + + params.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 1); + params.apiVersion = NVENCAPI_VERSION_470; + if(function_list.nvEncOpenEncodeSessionEx(¶ms, &nvenc_encoder) != NV_ENC_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed\n"); + goto done; + } + } + + success = get_supported_video_codecs(&function_list, nvenc_encoder, video_codecs, params.apiVersion); + + done: + if(cleanup) { + if(nvenc_encoder) + function_list.nvEncDestroyEncoder(nvenc_encoder); + if(nvenc_lib) + dlclose(nvenc_lib); + gsr_cuda_unload(&cuda); + } + + return success; +} diff --git a/src/codec_query/vaapi.c b/src/codec_query/vaapi.c new file mode 100644 index 0000000..0943450 --- /dev/null +++ b/src/codec_query/vaapi.c @@ -0,0 +1,236 @@ +#include "../../include/codec_query/vaapi.h" +#include "../../include/utils.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> + +#include <va/va.h> +#include <va/va_drm.h> + +static bool profile_is_h264(VAProfile profile) { + switch(profile) { + case 5: // VAProfileH264Baseline + case VAProfileH264Main: + case VAProfileH264High: + case VAProfileH264ConstrainedBaseline: + return true; + default: + return false; + } +} + +static bool profile_is_hevc_8bit(VAProfile profile) { + switch(profile) { + case VAProfileHEVCMain: + return true; + default: + return false; + } +} + +static bool profile_is_hevc_10bit(VAProfile profile) { + switch(profile) { + case VAProfileHEVCMain10: + //case VAProfileHEVCMain12: + //case VAProfileHEVCMain422_10: + //case VAProfileHEVCMain422_12: + //case VAProfileHEVCMain444: + //case VAProfileHEVCMain444_10: + //case VAProfileHEVCMain444_12: + return true; + default: + return false; + } +} + +static bool profile_is_av1(VAProfile profile) { + switch(profile) { + case VAProfileAV1Profile0: + case VAProfileAV1Profile1: + return true; + default: + return false; + } +} + +static bool profile_is_vp8(VAProfile profile) { + switch(profile) { + case VAProfileVP8Version0_3: + return true; + default: + return false; + } +} + +static bool profile_is_vp9(VAProfile profile) { + switch(profile) { + case VAProfileVP9Profile0: + case VAProfileVP9Profile1: + case VAProfileVP9Profile2: + case VAProfileVP9Profile3: + return true; + default: + return false; + } +} + +/* Returns 0, 0 on error */ +static vec2i profile_entrypoint_get_max_resolution(VADisplay va_dpy, VAProfile profile, VAEntrypoint entrypoint) { + VAConfigAttrib attribs[2] = { + { + .type = VAConfigAttribMaxPictureWidth, + }, + { + .type = VAConfigAttribMaxPictureHeight, + } + }; + if(vaGetConfigAttributes(va_dpy, profile, entrypoint, attribs, 2) != VA_STATUS_SUCCESS || (attribs[0].value & VA_ATTRIB_NOT_SUPPORTED) || (attribs[1].value & VA_ATTRIB_NOT_SUPPORTED)) + return (vec2i){0, 0}; + + return (vec2i){ attribs[0].value, attribs[1].value }; +} + +/* Returns 0 on error or if none is supported */ +static VAEntrypoint profile_get_video_encoding_entrypoint(VADisplay va_dpy, VAProfile profile) { + int num_entrypoints = vaMaxNumEntrypoints(va_dpy); + if(num_entrypoints <= 0) + return 0; + + VAEntrypoint *entrypoint_list = calloc(num_entrypoints, sizeof(VAEntrypoint)); + if(!entrypoint_list) + return 0; + + int encoding_entrypoint_index = -1; + int lower_power_entrypoint_index = -1; + if(vaQueryConfigEntrypoints(va_dpy, profile, entrypoint_list, &num_entrypoints) == VA_STATUS_SUCCESS) { + for(int i = 0; i < num_entrypoints; ++i) { + if(entrypoint_list[i] == VAEntrypointEncSlice) + encoding_entrypoint_index = i; + else if(entrypoint_list[i] == VAEntrypointEncSliceLP) + lower_power_entrypoint_index = i; + } + } + + VAEntrypoint encoding_entrypoint = 0; + if(encoding_entrypoint_index != -1) + encoding_entrypoint = entrypoint_list[encoding_entrypoint_index]; + else if(lower_power_entrypoint_index != -1) + encoding_entrypoint = entrypoint_list[lower_power_entrypoint_index]; + + free(entrypoint_list); + return encoding_entrypoint; +} + +static bool get_supported_video_codecs(VADisplay va_dpy, gsr_supported_video_codecs *video_codecs, bool cleanup) { + *video_codecs = (gsr_supported_video_codecs){0}; + bool success = false; + VAProfile *profile_list = NULL; + + vaSetInfoCallback(va_dpy, NULL, NULL); + + int va_major = 0; + int va_minor = 0; + if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: vaInitialize failed\n"); + return false; + } + + int num_profiles = vaMaxNumProfiles(va_dpy); + if(num_profiles <= 0) + goto fail; + + profile_list = calloc(num_profiles, sizeof(VAProfile)); + if(!profile_list || vaQueryConfigProfiles(va_dpy, profile_list, &num_profiles) != VA_STATUS_SUCCESS) + goto fail; + + for(int i = 0; i < num_profiles; ++i) { + if(profile_is_h264(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->h264 = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } else if(profile_is_hevc_8bit(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->hevc = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } else if(profile_is_hevc_10bit(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->hevc_hdr = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + video_codecs->hevc_10bit = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } else if(profile_is_av1(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->av1 = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + video_codecs->av1_hdr = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + video_codecs->av1_10bit = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } else if(profile_is_vp8(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->vp8 = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } else if(profile_is_vp9(profile_list[i])) { + const VAEntrypoint encoding_entrypoint = profile_get_video_encoding_entrypoint(va_dpy, profile_list[i]); + if(encoding_entrypoint != 0) { + const vec2i max_resolution = profile_entrypoint_get_max_resolution(va_dpy, profile_list[i], encoding_entrypoint); + video_codecs->vp9 = (gsr_supported_video_codec){ true, encoding_entrypoint == VAEntrypointEncSliceLP, max_resolution }; + } + } + } + + success = true; + fail: + if(profile_list) + free(profile_list); + + if(cleanup) + vaTerminate(va_dpy); + + return success; +} + +bool gsr_get_supported_video_codecs_vaapi(gsr_supported_video_codecs *video_codecs, const char *card_path, bool cleanup) { + memset(video_codecs, 0, sizeof(*video_codecs)); + bool success = false; + int drm_fd = -1; + + char render_path[128]; + if(!gsr_card_path_get_render_path(card_path, render_path)) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + goto done; + } + + drm_fd = open(render_path, O_RDWR); + if(drm_fd == -1) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to open device %s\n", render_path); + goto done; + } + + VADisplay va_dpy = vaGetDisplayDRM(drm_fd); + if(va_dpy) { + if(!get_supported_video_codecs(va_dpy, video_codecs, cleanup)) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to query supported video codecs for device %s\n", render_path); + goto done; + } + success = true; + } + + done: + if(cleanup) { + if(drm_fd > 0) + close(drm_fd); + } + + return success; +} diff --git a/src/codec_query/vulkan.c b/src/codec_query/vulkan.c new file mode 100644 index 0000000..15dd98b --- /dev/null +++ b/src/codec_query/vulkan.c @@ -0,0 +1,156 @@ +#include "../../include/codec_query/vulkan.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <xf86drm.h> +#define VK_NO_PROTOTYPES +//#include <vulkan/vulkan.h> + +#define MAX_PHYSICAL_DEVICES 32 + +static const char *required_device_extensions[] = { + "VK_KHR_external_memory_fd", + "VK_KHR_external_semaphore_fd", + "VK_KHR_video_encode_queue", + "VK_KHR_video_queue", + "VK_KHR_video_maintenance1", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_external_memory_host", + "VK_EXT_image_drm_format_modifier" +}; +static int num_required_device_extensions = 8; + +bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_codecs, const char *card_path, bool cleanup) { + memset(video_codecs, 0, sizeof(*video_codecs)); +#if 0 + bool success = false; + VkInstance instance = NULL; + VkPhysicalDevice physical_devices[MAX_PHYSICAL_DEVICES]; + VkDevice device = NULL; + VkExtensionProperties *device_extensions = NULL; + + const VkApplicationInfo app_info = { + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName = "GPU Screen Recorder", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "GPU Screen Recorder", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_3, + }; + + const VkInstanceCreateInfo instance_create_info = { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &app_info + }; + + if(vkCreateInstance(&instance_create_info, NULL, &instance) != VK_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkCreateInstance failed\n"); + goto done; + } + + uint32_t num_devices = 0; + if(vkEnumeratePhysicalDevices(instance, &num_devices, NULL) != VK_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (query num devices) failed\n"); + goto done; + } + + if(num_devices == 0) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: no vulkan capable device found\n"); + goto done; + } + + if(num_devices > MAX_PHYSICAL_DEVICES) + num_devices = MAX_PHYSICAL_DEVICES; + + if(vkEnumeratePhysicalDevices(instance, &num_devices, physical_devices) != VK_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (get data) failed\n"); + goto done; + } + + VkPhysicalDevice physical_device = NULL; + char device_card_path[128]; + for(uint32_t i = 0; i < num_devices; ++i) { + VkPhysicalDeviceDrmPropertiesEXT device_drm_properties = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT + }; + + VkPhysicalDeviceProperties2 device_properties = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &device_drm_properties + }; + vkGetPhysicalDeviceProperties2(physical_devices[i], &device_properties); + + if(!device_drm_properties.hasPrimary) + continue; + + snprintf(device_card_path, sizeof(device_card_path), DRM_DEV_NAME, DRM_DIR_NAME, (int)device_drm_properties.primaryMinor); + if(strcmp(device_card_path, card_path) == 0) { + physical_device = physical_devices[i]; + break; + } + } + + if(!physical_device) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to find a vulkan device that matches opengl device %s\n", card_path); + goto done; + } + + const VkDeviceCreateInfo device_create_info = { + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .enabledExtensionCount = num_required_device_extensions, + .ppEnabledExtensionNames = required_device_extensions + }; + + if(vkCreateDevice(physical_device, &device_create_info, NULL, &device) != VK_SUCCESS) { + //fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkCreateDevice failed. Device %s likely doesn't support vulkan video encoding\n", card_path); + goto done; + } + + uint32_t num_device_extensions = 0; + if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, NULL) != VK_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (query num device extensions) failed\n"); + goto done; + } + + device_extensions = calloc(num_device_extensions, sizeof(VkExtensionProperties)); + if(!device_extensions) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to allocate %d device extensions\n", num_device_extensions); + goto done; + } + + if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, device_extensions) != VK_SUCCESS) { + fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (get data) failed\n"); + goto done; + } + + for(uint32_t i = 0; i < num_device_extensions; ++i) { + if(strcmp(device_extensions[i].extensionName, "VK_KHR_video_encode_h264") == 0) { + video_codecs->h264 = true; + } else if(strcmp(device_extensions[i].extensionName, "VK_KHR_video_encode_h265") == 0) { + // TODO: Verify if 10bit and hdr are actually supported + video_codecs->hevc = true; + video_codecs->hevc_10bit = true; + video_codecs->hevc_hdr = true; + } + } + + success = true; + + done: + if(cleanup) { + if(device) + vkDestroyDevice(device, NULL); + if(instance) + vkDestroyInstance(instance, NULL); + } + if(device_extensions) + free(device_extensions); + return success; +#else + // TODO: Low power query + video_codecs->h264 = (gsr_supported_video_codec){ true, false }; + video_codecs->hevc = (gsr_supported_video_codec){ true, false }; + return true; +#endif +} diff --git a/src/color_conversion.c b/src/color_conversion.c new file mode 100644 index 0000000..22a1378 --- /dev/null +++ b/src/color_conversion.c @@ -0,0 +1,948 @@ +#include "../include/color_conversion.h" +#include "../include/egl.h" +#include <stdio.h> +#include <string.h> +#include <assert.h> + +#define GRAPHICS_SHADER_INDEX_Y 0 +#define GRAPHICS_SHADER_INDEX_UV 1 +#define GRAPHICS_SHADER_INDEX_Y_EXTERNAL 2 +#define GRAPHICS_SHADER_INDEX_UV_EXTERNAL 3 +#define GRAPHICS_SHADER_INDEX_RGB 4 +#define GRAPHICS_SHADER_INDEX_RGB_EXTERNAL 5 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_Y 6 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_UV 7 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL 8 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL 9 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_RGB 10 +#define GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL 11 + +/* https://en.wikipedia.org/wiki/YCbCr, see study/color_space_transform_matrix.png */ + +/* ITU-R BT2020, full */ +/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-2-201510-I!!PDF-E.pdf */ +#define RGB_TO_P010_FULL "const mat4 RGBtoYUV = mat4(0.262700, -0.139630, 0.500000, 0.000000,\n" \ + " 0.678000, -0.360370, -0.459786, 0.000000,\n" \ + " 0.059300, 0.500000, -0.040214, 0.000000,\n" \ + " 0.000000, 0.500000, 0.500000, 1.000000);\n" + +/* ITU-R BT2020, limited (full multiplied by (235-16)/255, adding 16/255 to luma) */ +#define RGB_TO_P010_LIMITED "const mat4 RGBtoYUV = mat4(0.225613, -0.119918, 0.429412, 0.000000,\n" \ + " 0.582282, -0.309494, -0.394875, 0.000000,\n" \ + " 0.050928, 0.429412, -0.034537, 0.000000,\n" \ + " 0.062745, 0.500000, 0.500000, 1.000000);\n" + +/* ITU-R BT709, full, custom values: 0.2110 0.7110 0.0710 */ +/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf */ +#define RGB_TO_NV12_FULL "const mat4 RGBtoYUV = mat4(0.211000, -0.113563, 0.500000, 0.000000,\n" \ + " 0.711000, -0.382670, -0.450570, 0.000000,\n" \ + " 0.071000, 0.500000, -0.044994, 0.000000,\n" \ + " 0.000000, 0.500000, 0.500000, 1.000000);\n" + +/* ITU-R BT709, limited, custom values: 0.2100 0.7100 0.0700 (full multiplied by (235-16)/255, adding 16/255 to luma) */ +#define RGB_TO_NV12_LIMITED "const mat4 RGBtoYUV = mat4(0.180353, -0.096964, 0.429412, 0.000000,\n" \ + " 0.609765, -0.327830, -0.385927, 0.000000,\n" \ + " 0.060118, 0.429412, -0.038049, 0.000000,\n" \ + " 0.062745, 0.500000, 0.500000, 1.000000);\n" + +static const char* color_format_range_get_transform_matrix(gsr_destination_color color_format, gsr_color_range color_range) { + switch(color_format) { + case GSR_DESTINATION_COLOR_NV12: { + switch(color_range) { + case GSR_COLOR_RANGE_LIMITED: + return RGB_TO_NV12_LIMITED; + case GSR_COLOR_RANGE_FULL: + return RGB_TO_NV12_FULL; + } + break; + } + case GSR_DESTINATION_COLOR_P010: { + switch(color_range) { + case GSR_COLOR_RANGE_LIMITED: + return RGB_TO_P010_LIMITED; + case GSR_COLOR_RANGE_FULL: + return RGB_TO_P010_FULL; + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: + return ""; + default: + return NULL; + } + return NULL; +} + +static int load_graphics_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) { + const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range); + + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n" + "} \n"); + + const char *main_code = + " vec4 pixel = texture(tex1, texcoords_out); \n" + " FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x; \n" + " FragColor.w = pixel.a; \n"; + + char fragment_shader[2048]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "%s" + "void main() \n" + "{ \n" + "%s" + "} \n", color_transform_matrix, main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "%s" + "void main() \n" + "{ \n" + "%s" + "} \n", color_transform_matrix, main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static unsigned int load_graphics_shader_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) { + const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range); + + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = (vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0)) * vec4(0.5, 0.5, 1.0, 1.0) - vec4(0.5, 0.5, 0.0, 0.0); \n" + "} \n"); + + const char *main_code = + " vec4 pixel = texture(tex1, texcoords_out); \n" + " FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n" + " FragColor.w = pixel.a; \n"; + + char fragment_shader[2048]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "%s" + "void main() \n" + "{ \n" + "%s" + "} \n", color_transform_matrix, main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "%s" + "void main() \n" + "{ \n" + "%s" + "} \n", color_transform_matrix, main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static unsigned int load_graphics_shader_rgb(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture) { + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n" + "} \n"); + + const char *main_code = + " vec4 pixel = texture(tex1, texcoords_out); \n" + " FragColor = pixel; \n"; + + char fragment_shader[2048]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static int load_graphics_shader_yuyv_to_y(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture) { + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n" + "} \n"); + + const char *main_code = + " vec4 pixel = texture(tex1, texcoords_out); \n" + " FragColor.x = pixel.r; \n" + " FragColor.w = 1.0; \n"; + + char fragment_shader[2048]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static unsigned int load_graphics_shader_yuyv_to_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture) { + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = (vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0)) * vec4(0.5, 0.5, 1.0, 1.0) - vec4(0.5, 0.5, 0.0, 0.0); \n" + "} \n"); + + const char *main_code = + " vec2 resolution = vec2(textureSize(tex1, 0));\n" + " ivec2 uv = ivec2(texcoords_out * resolution);\n" + " float u = 0.0;\n" + " float v = 0.0;\n" + " vec4 this_color = texelFetch(tex1, uv, 0);\n" + " if((uv.x & 1) == 0) {\n" + " vec2 next_color = texelFetch(tex1, uv + ivec2(1, 0), 0).rg;\n" + " u = this_color.g;\n" + " v = next_color.g;\n" + " } else {\n" + " vec2 prev_color = texelFetch(tex1, uv - ivec2(1, 0), 0).rg;\n" + " u = prev_color.g;\n" + " v = this_color.g;\n" + " }\n" + " FragColor.rg = vec2(u, v);\n" + " FragColor.w = 1.0;\n"; + + char fragment_shader[2048]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static unsigned int load_graphics_shader_yuyv_to_rgb(gsr_shader *shader, gsr_egl *egl, gsr_color_graphics_uniforms *uniforms, bool external_texture) { + char vertex_shader[2048]; + snprintf(vertex_shader, sizeof(vertex_shader), + "#version 300 es \n" + "in vec2 pos; \n" + "in vec2 texcoords; \n" + "out vec2 texcoords_out; \n" + "uniform vec2 offset; \n" + "uniform float rotation; \n" + "uniform mat2 rotation_matrix; \n" + "void main() \n" + "{ \n" + " texcoords_out = vec2(texcoords.x - 0.5, texcoords.y - 0.5) * rotation_matrix + vec2(0.5, 0.5); \n" + " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n" + "} \n"); + + const char *main_code = + " vec2 resolution = vec2(textureSize(tex1, 0));\n" + " ivec2 uv = ivec2(texcoords_out * resolution);\n" + " float y = 0.0;\n" + " float u = 0.0;\n" + " float v = 0.0;\n" + " vec4 this_color = texelFetch(tex1, uv, 0);\n" + " if((uv.x & 1) == 0) {\n" + " vec2 next_color = texelFetch(tex1, uv + ivec2(1, 0), 0).rg;\n" + " y = this_color.r;\n" + " u = this_color.g;\n" + " v = next_color.g;\n" + " } else {\n" + " vec2 prev_color = texelFetch(tex1, uv - ivec2(1, 0), 0).rg;\n" + " y = this_color.r;\n" + " u = prev_color.g;\n" + " v = this_color.g;\n" + " }\n" + " FragColor = vec4(\n" + " y + 1.4065 * (v - 0.5),\n" + " y - 0.3455 * (u - 0.5) - 0.7169 * (v - 0.5),\n" + " y + 1.1790 * (u - 0.5),\n" + " 1.0);\n"; + + char fragment_shader[4096]; + if(external_texture) { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "#extension GL_OES_EGL_image_external : enable \n" + "#extension GL_OES_EGL_image_external_essl3 : require \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform samplerExternalOES tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } else { + snprintf(fragment_shader, sizeof(fragment_shader), + "#version 300 es \n" + "precision highp float; \n" + "in vec2 texcoords_out; \n" + "uniform sampler2D tex1; \n" + "out vec4 FragColor; \n" + "void main() \n" + "{ \n" + "%s" + "} \n", main_code); + } + + if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0) + return -1; + + gsr_shader_bind_attribute_location(shader, "pos", 0); + gsr_shader_bind_attribute_location(shader, "texcoords", 1); + uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset"); + uniforms->rotation_matrix = egl->glGetUniformLocation(shader->program_id, "rotation_matrix"); + return 0; +} + +static int load_framebuffers(gsr_color_conversion *self) { + /* TODO: Only generate the necessary amount of framebuffers (self->params.num_destination_textures) */ + const unsigned int draw_buffer = GL_COLOR_ATTACHMENT0; + self->params.egl->glGenFramebuffers(GSR_COLOR_CONVERSION_MAX_FRAMEBUFFERS, self->framebuffers); + + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[0], 0); + self->params.egl->glDrawBuffers(1, &draw_buffer); + if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for Y\n"); + goto err; + } + + if(self->params.num_destination_textures > 1) { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]); + self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[1], 0); + self->params.egl->glDrawBuffers(1, &draw_buffer); + if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for UV\n"); + goto err; + } + } + + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + return 0; + + err: + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + return -1; +} + +static int create_vertices(gsr_color_conversion *self) { + self->params.egl->glGenVertexArrays(1, &self->vertex_array_object_id); + self->params.egl->glBindVertexArray(self->vertex_array_object_id); + + self->params.egl->glGenBuffers(1, &self->vertex_buffer_object_id); + self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id); + self->params.egl->glBufferData(GL_ARRAY_BUFFER, 24 * sizeof(float), NULL, GL_DYNAMIC_DRAW); + + self->params.egl->glEnableVertexAttribArray(0); + self->params.egl->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); + + self->params.egl->glEnableVertexAttribArray(1); + self->params.egl->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + + self->params.egl->glBindVertexArray(0); + return 0; +} + +static bool gsr_color_conversion_load_graphics_shaders(gsr_color_conversion *self) { + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y], self->params.destination_color, self->params.color_range, false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader\n"); + return false; + } + + if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV], self->params.destination_color, self->params.color_range, false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader\n"); + return false; + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB], false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], false) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader\n"); + return false; + } + break; + } + } + return true; +} + +static bool gsr_color_conversion_load_external_graphics_shaders(gsr_color_conversion *self) { + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.destination_color, self->params.color_range, true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader (external)\n"); + return false; + } + + if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.destination_color, self->params.color_range, true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader (external)\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader (external)\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader (external)\n"); + return false; + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader (external)\n"); + return false; + } + + if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], true) != 0) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader (external)\n"); + return false; + } + break; + } + } + return true; +} + +int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params) { + assert(params); + assert(params->egl); + memset(self, 0, sizeof(*self)); + self->params.egl = params->egl; + self->params = *params; + + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + if(self->params.num_destination_textures != 2) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)\n", self->params.num_destination_textures); + goto err; + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + if(self->params.num_destination_textures != 1) { + fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 1 destination textures for destination color RGB8, got %d destination texture(s)\n", self->params.num_destination_textures); + goto err; + } + break; + } + } + + if(!gsr_color_conversion_load_graphics_shaders(self)) + goto err; + + if(self->params.load_external_image_shader) { + if(!gsr_color_conversion_load_external_graphics_shaders(self)) + goto err; + } + + if(load_framebuffers(self) != 0) + goto err; + + if(create_vertices(self) != 0) + goto err; + + return 0; + + err: + gsr_color_conversion_deinit(self); + return -1; +} + +void gsr_color_conversion_deinit(gsr_color_conversion *self) { + if(!self->params.egl) + return; + + if(self->vertex_buffer_object_id) { + self->params.egl->glDeleteBuffers(1, &self->vertex_buffer_object_id); + self->vertex_buffer_object_id = 0; + } + + if(self->vertex_array_object_id) { + self->params.egl->glDeleteVertexArrays(1, &self->vertex_array_object_id); + self->vertex_array_object_id = 0; + } + + self->params.egl->glDeleteFramebuffers(GSR_COLOR_CONVERSION_MAX_FRAMEBUFFERS, self->framebuffers); + for(int i = 0; i < GSR_COLOR_CONVERSION_MAX_FRAMEBUFFERS; ++i) { + self->framebuffers[i] = 0; + } + + for(int i = 0; i < GSR_COLOR_CONVERSION_MAX_GRAPHICS_SHADERS; ++i) { + gsr_shader_deinit(&self->graphics_shaders[i]); + } + + self->params.egl = NULL; +} + +static void gsr_color_conversion_apply_rotation(gsr_rotation rotation, float rotation_matrix[2][2]) { + /* + rotation_matrix[0][0] = cos(angle); + rotation_matrix[0][1] = -sin(angle); + rotation_matrix[1][0] = sin(angle); + rotation_matrix[1][1] = cos(angle); + The manual matrix code below is the same as this code above, but without floating-point errors. + This is done to remove any blurring caused by these floating-point errors. + */ + switch(rotation) { + case GSR_ROT_0: + rotation_matrix[0][0] = 1.0f; + rotation_matrix[0][1] = 0.0f; + rotation_matrix[1][0] = 0.0f; + rotation_matrix[1][1] = 1.0f; + break; + case GSR_ROT_90: + rotation_matrix[0][0] = 0.0f; + rotation_matrix[0][1] = -1.0f; + rotation_matrix[1][0] = 1.0f; + rotation_matrix[1][1] = 0.0f; + break; + case GSR_ROT_180: + rotation_matrix[0][0] = -1.0f; + rotation_matrix[0][1] = 0.0f; + rotation_matrix[1][0] = 0.0f; + rotation_matrix[1][1] = -1.0f; + break; + case GSR_ROT_270: + rotation_matrix[0][0] = 0.0f; + rotation_matrix[0][1] = 1.0f; + rotation_matrix[1][0] = -1.0f; + rotation_matrix[1][1] = 0.0f; + break; + } +} + +static void gsr_color_conversion_swizzle_texture_source(gsr_color_conversion *self, unsigned int texture_target, gsr_source_color source_color) { + if(source_color == GSR_SOURCE_COLOR_BGR) { + const int swizzle_mask[] = { GL_BLUE, GL_GREEN, GL_RED, 1 }; + self->params.egl->glTexParameteriv(texture_target, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask); + } +} + +static void gsr_color_conversion_swizzle_reset(gsr_color_conversion *self, unsigned int texture_target, gsr_source_color source_color) { + if(source_color == GSR_SOURCE_COLOR_BGR) { + const int swizzle_mask[] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA }; + self->params.egl->glTexParameteriv(texture_target, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask); + } +} + +static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsigned int texture_id, bool external_texture, gsr_rotation rotation, gsr_flip flip, float rotation_matrix[2][2], vec2i source_position, vec2i source_size, vec2i destination_pos, vec2i texture_size, vec2f scale, gsr_source_color source_color) { + if(source_size.x == 0 || source_size.y == 0) + return; + + const vec2i dest_texture_size = self->params.destination_textures_size[0]; + const unsigned int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + + if(rotation == GSR_ROT_90 || rotation == GSR_ROT_270) { + const float tmp = texture_size.x; + texture_size.x = texture_size.y; + texture_size.y = tmp; + } + + self->params.egl->glBindTexture(texture_target, texture_id); + gsr_color_conversion_swizzle_texture_source(self, texture_target, source_color); + + const vec2f pos_norm = { + ((float)destination_pos.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f, + ((float)destination_pos.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f, + }; + + const vec2f size_norm = { + ((float)source_size.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f * scale.x, + ((float)source_size.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f * scale.y, + }; + + const vec2f texture_pos_norm = { + (float)source_position.x / (texture_size.x == 0 ? 1.0f : (float)texture_size.x), + (float)source_position.y / (texture_size.y == 0 ? 1.0f : (float)texture_size.y), + }; + + const vec2f texture_size_norm = { + (float)source_size.x / (texture_size.x == 0 ? 1.0f : (float)texture_size.x), + (float)source_size.y / (texture_size.y == 0 ? 1.0f : (float)texture_size.y), + }; + + float vertices[] = { + -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y, + -1.0f + 0.0f, -1.0f + 0.0f, texture_pos_norm.x, texture_pos_norm.y, + -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y, + + -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y, + -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y, + -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y + texture_size_norm.y + }; + + if(flip & GSR_FLIP_HORIZONTAL) { + for(int i = 0; i < 6; ++i) { + const float prev_x = vertices[i*4 + 2]; + vertices[i*4 + 2] = texture_pos_norm.x + texture_size_norm.x - prev_x; + } + } + + if(flip & GSR_FLIP_VERTICAL) { + for(int i = 0; i < 6; ++i) { + const float prev_y = vertices[i*4 + 3]; + vertices[i*4 + 3] = texture_pos_norm.y + texture_size_norm.y - prev_y; + } + } + + self->params.egl->glBindVertexArray(self->vertex_array_object_id); + self->params.egl->glViewport(0, 0, dest_texture_size.x, dest_texture_size.y); + + /* TODO: this, also cleanup */ + self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id); + self->params.egl->glBufferSubData(GL_ARRAY_BUFFER, 0, 24 * sizeof(float), vertices); + + // TODO: + switch(source_color) { + case GSR_SOURCE_COLOR_RGB: + case GSR_SOURCE_COLOR_BGR: { + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example) + + int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_Y_EXTERNAL : GRAPHICS_SHADER_INDEX_Y; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + + if(self->params.num_destination_textures > 1) { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); + + shader_index = external_texture ? GRAPHICS_SHADER_INDEX_UV_EXTERNAL : GRAPHICS_SHADER_INDEX_UV; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example) + + const int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_RGB_EXTERNAL : GRAPHICS_SHADER_INDEX_RGB; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + break; + } + } + break; + } + case GSR_SOURCE_COLOR_YUYV: { + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example) + + int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL : GRAPHICS_SHADER_INDEX_YUYV_TO_Y; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + + if(self->params.num_destination_textures > 1) { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); + + shader_index = external_texture ? GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL : GRAPHICS_SHADER_INDEX_YUYV_TO_UV; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + } + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example) + + const int shader_index = external_texture ? GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL : GRAPHICS_SHADER_INDEX_YUYV_TO_RGB; + gsr_shader_use(&self->graphics_shaders[shader_index]); + self->params.egl->glUniformMatrix2fv(self->graphics_uniforms[shader_index].rotation_matrix, 1, GL_TRUE, (const float*)rotation_matrix); + self->params.egl->glUniform2f(self->graphics_uniforms[shader_index].offset, pos_norm.x, pos_norm.y); + self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6); + break; + } + } + break; + } + } + + self->params.egl->glBindVertexArray(0); + self->params.egl->glUseProgram(0); + gsr_color_conversion_swizzle_reset(self, texture_target, source_color); + self->params.egl->glBindTexture(texture_target, 0); + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture) { + assert(!external_texture || self->params.load_external_image_shader); + if(external_texture && !self->params.load_external_image_shader) { + fprintf(stderr, "gsr error: gsr_color_conversion_draw: external texture not loaded\n"); + return; + } + + vec2f scale = {0.0f, 0.0f}; + if(source_size.x > 0 && source_size.y > 0) + scale = (vec2f){ (double)destination_size.x/(double)source_size.x, (double)destination_size.y/(double)source_size.y }; + + vec2i source_position = {0, 0}; + float rotation_matrix[2][2] = {{0, 0}, {0, 0}}; + gsr_color_conversion_apply_rotation(rotation, rotation_matrix); + + const int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + self->params.egl->glBindTexture(texture_target, texture_id); + + source_position.x += source_pos.x; + source_position.y += source_pos.y; + gsr_color_conversion_draw_graphics(self, texture_id, external_texture, rotation, flip, rotation_matrix, source_position, source_size, destination_pos, texture_size, scale, source_color); + + self->params.egl->glFlush(); + // TODO: Use the minimal barrier required + self->params.egl->glMemoryBarrier(GL_ALL_BARRIER_BITS); // GL_SHADER_IMAGE_ACCESS_BARRIER_BIT + self->params.egl->glUseProgram(0); + + self->params.egl->glBindTexture(texture_target, 0); +} + +void gsr_color_conversion_clear(gsr_color_conversion *self) { + float color1[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + float color2[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + + switch(self->params.destination_color) { + case GSR_DESTINATION_COLOR_NV12: + case GSR_DESTINATION_COLOR_P010: { + color2[0] = 0.5f; + color2[1] = 0.5f; + color2[2] = 0.0f; + color2[3] = 1.0f; + break; + } + case GSR_DESTINATION_COLOR_RGB8: { + color2[0] = 0.0f; + color2[1] = 0.0f; + color2[2] = 0.0f; + color2[3] = 1.0f; + break; + } + } + + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]); + self->params.egl->glClearColor(color1[0], color1[1], color1[2], color1[3]); + self->params.egl->glClear(GL_COLOR_BUFFER_BIT); + + if(self->params.num_destination_textures > 1) { + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]); + self->params.egl->glClearColor(color2[0], color2[1], color2[2], color2[3]); + self->params.egl->glClear(GL_COLOR_BUFFER_BIT); + } + + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void gsr_color_conversion_read_destination_texture(gsr_color_conversion *self, int destination_texture_index, int x, int y, int width, int height, unsigned int color_format, unsigned int data_format, void *pixels) { + assert(destination_texture_index >= 0 && destination_texture_index < self->params.num_destination_textures); + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[destination_texture_index]); + self->params.egl->glReadPixels(x, y, width, height, color_format, data_format, pixels); + self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +gsr_rotation gsr_monitor_rotation_to_rotation(gsr_monitor_rotation monitor_rotation) { + return (gsr_rotation)monitor_rotation; +} diff --git a/src/cuda.c b/src/cuda.c new file mode 100644 index 0000000..00683cf --- /dev/null +++ b/src/cuda.c @@ -0,0 +1,119 @@ +#include "../include/cuda.h" +#include "../include/library_loader.h" +#include <string.h> +#include <stdio.h> +#include <dlfcn.h> +#include <assert.h> + +bool gsr_cuda_load(gsr_cuda *self, Display *display, bool do_overclock) { + memset(self, 0, sizeof(gsr_cuda)); + self->do_overclock = do_overclock; + + dlerror(); /* clear */ + void *lib = dlopen("libcuda.so.1", RTLD_LAZY); + if(!lib) { + lib = dlopen("libcuda.so", RTLD_LAZY); + if(!lib) { + fprintf(stderr, "gsr error: gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s\n", dlerror()); + return false; + } + } + + const dlsym_assign required_dlsym[] = { + { (void**)&self->cuInit, "cuInit" }, + { (void**)&self->cuDeviceGetCount, "cuDeviceGetCount" }, + { (void**)&self->cuDeviceGet, "cuDeviceGet" }, + { (void**)&self->cuCtxCreate_v2, "cuCtxCreate_v2" }, + { (void**)&self->cuCtxDestroy_v2, "cuCtxDestroy_v2" }, + { (void**)&self->cuCtxPushCurrent_v2, "cuCtxPushCurrent_v2" }, + { (void**)&self->cuCtxPopCurrent_v2, "cuCtxPopCurrent_v2" }, + { (void**)&self->cuGetErrorString, "cuGetErrorString" }, + { (void**)&self->cuMemcpy2D_v2, "cuMemcpy2D_v2" }, + { (void**)&self->cuMemcpy2DAsync_v2, "cuMemcpy2DAsync_v2" }, + { (void**)&self->cuStreamSynchronize, "cuStreamSynchronize" }, + + { (void**)&self->cuGraphicsGLRegisterImage, "cuGraphicsGLRegisterImage" }, + { (void**)&self->cuGraphicsEGLRegisterImage, "cuGraphicsEGLRegisterImage" }, + { (void**)&self->cuGraphicsResourceSetMapFlags, "cuGraphicsResourceSetMapFlags" }, + { (void**)&self->cuGraphicsMapResources, "cuGraphicsMapResources" }, + { (void**)&self->cuGraphicsUnmapResources, "cuGraphicsUnmapResources" }, + { (void**)&self->cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource" }, + { (void**)&self->cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray" }, + + { NULL, NULL } + }; + + CUresult res; + + if(!dlsym_load_list(lib, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1\n"); + goto fail; + } + + res = self->cuInit(0); + if(res != CUDA_SUCCESS) { + const char *err_str = "unknown"; + self->cuGetErrorString(res, &err_str); + fprintf(stderr, "gsr error: gsr_cuda_load failed: cuInit failed, error: %s (result: %d)\n", err_str, res); + goto fail; + } + + int nGpu = 0; + self->cuDeviceGetCount(&nGpu); + if(nGpu <= 0) { + fprintf(stderr, "gsr error: gsr_cuda_load failed: no cuda supported devices found\n"); + goto fail; + } + + // TODO: Use the device associated with the opengl graphics context + CUdevice cu_dev; + res = self->cuDeviceGet(&cu_dev, 0); + if(res != CUDA_SUCCESS) { + const char *err_str = "unknown"; + self->cuGetErrorString(res, &err_str); + fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)\n", err_str, res); + goto fail; + } + + res = self->cuCtxCreate_v2(&self->cu_ctx, CU_CTX_SCHED_AUTO, cu_dev); + if(res != CUDA_SUCCESS) { + const char *err_str = "unknown"; + self->cuGetErrorString(res, &err_str); + fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)\n", err_str, res); + goto fail; + } + + if(self->do_overclock && display) { + if(gsr_overclock_load(&self->overclock, display)) + gsr_overclock_start(&self->overclock); + else + fprintf(stderr, "gsr warning: gsr_cuda_load: failed to load xnvctrl, failed to overclock memory transfer rate\n"); + } else if(self->do_overclock && !display) { + fprintf(stderr, "gsr warning: gsr_cuda_load: overclocking enabled but no X server is running. Overclocking has been disabled\n"); + } + + self->library = lib; + return true; + + fail: + dlclose(lib); + memset(self, 0, sizeof(gsr_cuda)); + return false; +} + +void gsr_cuda_unload(gsr_cuda *self) { + if(self->do_overclock && self->overclock.xnvctrl.library) { + gsr_overclock_stop(&self->overclock); + gsr_overclock_unload(&self->overclock); + } + + if(self->library) { + if(self->cu_ctx) { + self->cuCtxDestroy_v2(self->cu_ctx); + self->cu_ctx = 0; + } + dlclose(self->library); + } + + memset(self, 0, sizeof(gsr_cuda)); +} diff --git a/src/cursor.c b/src/cursor.c new file mode 100644 index 0000000..19047ba --- /dev/null +++ b/src/cursor.c @@ -0,0 +1,144 @@ +#include "../include/cursor.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +#include <X11/extensions/Xfixes.h> + +// TODO: Test cursor visibility with XFixesHideCursor + +static bool gsr_cursor_set_from_x11_cursor_image(gsr_cursor *self, XFixesCursorImage *x11_cursor_image, bool *visible) { + uint8_t *cursor_data = NULL; + uint8_t *out = NULL; + *visible = false; + + if(!x11_cursor_image) + goto err; + + if(!x11_cursor_image->pixels) + goto err; + + self->hotspot.x = x11_cursor_image->xhot; + self->hotspot.y = x11_cursor_image->yhot; + self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id); + + self->size.x = x11_cursor_image->width; + self->size.y = x11_cursor_image->height; + const unsigned long *pixels = x11_cursor_image->pixels; + cursor_data = malloc(self->size.x * self->size.y * 4); + if(!cursor_data) + goto err; + out = cursor_data; + /* Un-premultiply alpha */ + for(int y = 0; y < self->size.y; ++y) { + for(int x = 0; x < self->size.x; ++x) { + uint32_t pixel = *pixels++; + uint8_t *in = (uint8_t*)&pixel; + uint8_t alpha = in[3]; + if(alpha == 0) { + alpha = 1; + } else { + *visible = true; + } + + out[0] = (float)in[2] * 255.0/(float)alpha; + out[1] = (float)in[1] * 255.0/(float)alpha; + out[2] = (float)in[0] * 255.0/(float)alpha; + out[3] = in[3]; + out += 4; + in += 4; + } + } + + // TODO: glTextureSubImage2D if same size + self->egl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, cursor_data); + free(cursor_data); + + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + self->egl->glBindTexture(GL_TEXTURE_2D, 0); + XFree(x11_cursor_image); + return true; + + err: + self->egl->glBindTexture(GL_TEXTURE_2D, 0); + if(x11_cursor_image) + XFree(x11_cursor_image); + return false; +} + +int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) { + int x_fixes_error_base = 0; + + assert(egl); + assert(display); + memset(self, 0, sizeof(*self)); + self->egl = egl; + self->display = display; + + self->x_fixes_event_base = 0; + if(!XFixesQueryExtension(self->display, &self->x_fixes_event_base, &x_fixes_error_base)) { + fprintf(stderr, "gsr error: gsr_cursor_init: your X11 server is missing the XFixes extension\n"); + gsr_cursor_deinit(self); + return -1; + } + + self->egl->glGenTextures(1, &self->texture_id); + + XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), XFixesDisplayCursorNotifyMask); + gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display), &self->visible); + self->cursor_image_set = true; + + return 0; +} + +void gsr_cursor_deinit(gsr_cursor *self) { + if(!self->display) + return; + + if(self->texture_id) { + self->egl->glDeleteTextures(1, &self->texture_id); + self->texture_id = 0; + } + + if(self->display) + XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), 0); + + self->display = NULL; + self->egl = NULL; +} + +bool gsr_cursor_on_event(gsr_cursor *self, XEvent *xev) { + if(!self->display) + return false; + + bool updated = false; + + if(xev->type == self->x_fixes_event_base + XFixesCursorNotify) { + XFixesCursorNotifyEvent *cursor_notify_event = (XFixesCursorNotifyEvent*)xev; + if(cursor_notify_event->subtype == XFixesDisplayCursorNotify && cursor_notify_event->window == DefaultRootWindow(self->display)) { + self->cursor_image_set = false; + } + } + + if(!self->cursor_image_set) { + self->cursor_image_set = true; + gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display), &self->visible); + updated = true; + } + + return updated; +} + +void gsr_cursor_tick(gsr_cursor *self, Window relative_to) { + if(!self->display) + return; + + Window dummy_window; + int dummy_i; + unsigned int dummy_u; + XQueryPointer(self->display, relative_to, &dummy_window, &dummy_window, &dummy_i, &dummy_i, &self->position.x, &self->position.y, &dummy_u); +} diff --git a/src/damage.c b/src/damage.c new file mode 100644 index 0000000..caae80a --- /dev/null +++ b/src/damage.c @@ -0,0 +1,475 @@ +#include "../include/damage.h" +#include "../include/utils.h" +#include "../include/window/window.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <X11/extensions/Xdamage.h> +#include <X11/extensions/Xrandr.h> + +typedef struct { + vec2i pos; + vec2i size; +} gsr_rectangle; + +static bool rectangles_intersect(gsr_rectangle rect1, gsr_rectangle rect2) { + return rect1.pos.x < rect2.pos.x + rect2.size.x && rect1.pos.x + rect1.size.x > rect2.pos.x && + rect1.pos.y < rect2.pos.y + rect2.size.y && rect1.pos.y + rect1.size.y > rect2.pos.y; +} + +static bool xrandr_is_supported(Display *display) { + int major_version = 0; + int minor_version = 0; + if(!XRRQueryVersion(display, &major_version, &minor_version)) + return false; + + return major_version > 1 || (major_version == 1 && minor_version >= 2); +} + +static int gsr_damage_get_tracked_monitor_index(const gsr_damage *self, const char *monitor_name) { + for(int i = 0; i < self->num_monitors_tracked; ++i) { + if(strcmp(self->monitors_tracked[i].monitor_name, monitor_name) == 0) + return i; + } + return -1; +} + +static void add_monitor_callback(const gsr_monitor *monitor, void *userdata) { + gsr_damage *self = userdata; + + const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor->name); + gsr_damage_monitor *damage_monitor = NULL; + if(damage_monitor_index != -1) { + damage_monitor = &self->monitors_tracked[damage_monitor_index]; + damage_monitor->monitor = NULL; + } + + if(self->num_monitors + 1 > GSR_DAMAGE_MAX_MONITORS) { + fprintf(stderr, "gsr error: gsr_damage_on_output_change: max monitors reached\n"); + return; + } + + char *monitor_name_copy = strdup(monitor->name); + if(!monitor_name_copy) { + fprintf(stderr, "gsr error: gsr_damage_on_output_change: strdup failed for monitor: %s\n", monitor->name); + return; + } + + self->monitors[self->num_monitors] = *monitor; + self->monitors[self->num_monitors].name = monitor_name_copy; + ++self->num_monitors; + + if(damage_monitor) + damage_monitor->monitor = &self->monitors[self->num_monitors - 1]; +} + +bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool track_cursor) { + memset(self, 0, sizeof(*self)); + self->egl = egl; + self->track_cursor = track_cursor; + self->cursor = cursor; + + if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11) { + fprintf(stderr, "gsr error: gsr_damage_init: damage tracking is not supported on wayland\n"); + return false; + } + self->display = gsr_window_get_display(egl->window); + + if(!XDamageQueryExtension(self->display, &self->damage_event, &self->damage_error)) { + fprintf(stderr, "gsr error: gsr_damage_init: XDamage is not supported by your X11 server\n"); + gsr_damage_deinit(self); + return false; + } + + if(!XRRQueryExtension(self->display, &self->randr_event, &self->randr_error)) { + fprintf(stderr, "gsr error: gsr_damage_init: XRandr is not supported by your X11 server\n"); + gsr_damage_deinit(self); + return false; + } + + if(!xrandr_is_supported(self->display)) { + fprintf(stderr, "gsr error: gsr_damage_init: your X11 randr version is too old\n"); + gsr_damage_deinit(self); + return false; + } + + XRRSelectInput(self->display, DefaultRootWindow(self->display), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask); + + self->monitor_damage = XDamageCreate(self->display, DefaultRootWindow(self->display), XDamageReportNonEmpty); + if(!self->monitor_damage) { + fprintf(stderr, "gsr error: gsr_damage_init: XDamageCreate failed\n"); + gsr_damage_deinit(self); + return false; + } + XDamageSubtract(self->display, self->monitor_damage, None, None); + + for_each_active_monitor_output_x11_not_cached(self->display, add_monitor_callback, self); + + self->damaged = true; + return true; +} + +static void gsr_damage_deinit_monitors(gsr_damage *self) { + for(int i = 0; i < self->num_monitors; ++i) { + free((char*)self->monitors[i].name); + } + self->num_monitors = 0; +} + +void gsr_damage_deinit(gsr_damage *self) { + if(self->monitor_damage) { + XDamageDestroy(self->display, self->monitor_damage); + self->monitor_damage = None; + } + + for(int i = 0; i < self->num_monitors_tracked; ++i) { + free(self->monitors_tracked[i].monitor_name); + } + self->num_monitors_tracked = 0; + + for(int i = 0; i < self->num_windows_tracked; ++i) { + XSelectInput(self->display, self->windows_tracked[i].window_id, 0); + XDamageDestroy(self->display, self->windows_tracked[i].damage); + } + self->num_windows_tracked = 0; + + self->all_monitors_tracked_refcount = 0; + gsr_damage_deinit_monitors(self); + + self->damage_event = 0; + self->damage_error = 0; + + self->randr_event = 0; + self->randr_error = 0; +} + +static int gsr_damage_get_tracked_window_index(const gsr_damage *self, int64_t window) { + for(int i = 0; i < self->num_windows_tracked; ++i) { + if(self->windows_tracked[i].window_id == window) + return i; + } + return -1; +} + +bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) { + if(self->damage_event == 0 || window == None) + return false; + + const int damage_window_index = gsr_damage_get_tracked_window_index(self, window); + if(damage_window_index != -1) { + ++self->windows_tracked[damage_window_index].refcount; + return true; + } + + if(self->num_windows_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { + fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: max window targets reached\n"); + return false; + } + + XWindowAttributes win_attr; + win_attr.x = 0; + win_attr.y = 0; + win_attr.width = 0; + win_attr.height = 0; + if(!XGetWindowAttributes(self->display, window, &win_attr)) + fprintf(stderr, "gsr warning: gsr_damage_start_tracking_window failed: failed to get window attributes: %ld\n", (long)window); + + const Damage damage = XDamageCreate(self->display, window, XDamageReportNonEmpty); + if(!damage) { + fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: XDamageCreate failed\n"); + return false; + } + XDamageSubtract(self->display, damage, None, None); + + XSelectInput(self->display, window, StructureNotifyMask | ExposureMask); + + gsr_damage_window *damage_window = &self->windows_tracked[self->num_windows_tracked]; + ++self->num_windows_tracked; + + damage_window->window_id = window; + damage_window->window_pos.x = win_attr.x; + damage_window->window_pos.y = win_attr.y; + damage_window->window_size.x = win_attr.width; + damage_window->window_size.y = win_attr.height; + damage_window->damage = damage; + damage_window->refcount = 1; + return true; +} + +void gsr_damage_stop_tracking_window(gsr_damage *self, int64_t window) { + if(window == None) + return; + + const int damage_window_index = gsr_damage_get_tracked_window_index(self, window); + if(damage_window_index == -1) + return; + + gsr_damage_window *damage_window = &self->windows_tracked[damage_window_index]; + --damage_window->refcount; + if(damage_window->refcount <= 0) { + XSelectInput(self->display, damage_window->window_id, 0); + XDamageDestroy(self->display, damage_window->damage); + self->windows_tracked[damage_window_index] = self->windows_tracked[self->num_windows_tracked - 1]; + --self->num_windows_tracked; + } +} + +static gsr_monitor* gsr_damage_get_monitor_by_id(gsr_damage *self, RRCrtc id) { + for(int i = 0; i < self->num_monitors; ++i) { + if(self->monitors[i].monitor_identifier == id) + return &self->monitors[i]; + } + return NULL; +} + +static gsr_monitor* gsr_damage_get_monitor_by_name(gsr_damage *self, const char *name) { + for(int i = 0; i < self->num_monitors; ++i) { + if(strcmp(self->monitors[i].name, name) == 0) + return &self->monitors[i]; + } + return NULL; +} + +bool gsr_damage_start_tracking_monitor(gsr_damage *self, const char *monitor_name) { + if(self->damage_event == 0) + return false; + + if(strcmp(monitor_name, "screen-direct") == 0 || strcmp(monitor_name, "screen-direct-force") == 0) + monitor_name = NULL; + + if(!monitor_name) { + ++self->all_monitors_tracked_refcount; + return true; + } + + const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor_name); + if(damage_monitor_index != -1) { + ++self->monitors_tracked[damage_monitor_index].refcount; + return true; + } + + if(self->num_monitors_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { + fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: max monitor targets reached\n"); + return false; + } + + char *monitor_name_copy = strdup(monitor_name); + if(!monitor_name_copy) { + fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: strdup failed for monitor: %s\n", monitor_name); + return false; + } + + gsr_monitor *monitor = gsr_damage_get_monitor_by_name(self, monitor_name); + if(!monitor) { + fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: failed to find monitor: %s\n", monitor_name); + free(monitor_name_copy); + return false; + } + + gsr_damage_monitor *damage_monitor = &self->monitors_tracked[self->num_monitors_tracked]; + ++self->num_monitors_tracked; + + damage_monitor->monitor_name = monitor_name_copy; + damage_monitor->monitor = monitor; + damage_monitor->refcount = 1; + return true; +} + +void gsr_damage_stop_tracking_monitor(gsr_damage *self, const char *monitor_name) { + if(strcmp(monitor_name, "screen-direct") == 0 || strcmp(monitor_name, "screen-direct-force") == 0) + monitor_name = NULL; + + if(!monitor_name) { + --self->all_monitors_tracked_refcount; + if(self->all_monitors_tracked_refcount < 0) + self->all_monitors_tracked_refcount = 0; + return; + } + + const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor_name); + if(damage_monitor_index == -1) + return; + + gsr_damage_monitor *damage_monitor = &self->monitors_tracked[damage_monitor_index]; + --damage_monitor->refcount; + if(damage_monitor->refcount <= 0) { + free(damage_monitor->monitor_name); + self->monitors_tracked[damage_monitor_index] = self->monitors_tracked[self->num_monitors_tracked - 1]; + --self->num_monitors_tracked; + } +} + +static void gsr_damage_on_crtc_change(gsr_damage *self, XEvent *xev) { + const XRRCrtcChangeNotifyEvent *rr_crtc_change_event = (XRRCrtcChangeNotifyEvent*)xev; + if(rr_crtc_change_event->crtc == 0) + return; + + if(rr_crtc_change_event->width == 0 || rr_crtc_change_event->height == 0) + return; + + gsr_monitor *monitor = gsr_damage_get_monitor_by_id(self, rr_crtc_change_event->crtc); + if(!monitor) + return; + + if(rr_crtc_change_event->x != monitor->pos.x || rr_crtc_change_event->y != monitor->pos.y || + (int)rr_crtc_change_event->width != monitor->size.x || (int)rr_crtc_change_event->height != monitor->size.y) { + monitor->pos.x = rr_crtc_change_event->x; + monitor->pos.y = rr_crtc_change_event->y; + + monitor->size.x = rr_crtc_change_event->width; + monitor->size.y = rr_crtc_change_event->height; + } +} + +static void gsr_damage_on_output_change(gsr_damage *self, XEvent *xev) { + const XRROutputChangeNotifyEvent *rr_output_change_event = (XRROutputChangeNotifyEvent*)xev; + if(!rr_output_change_event->output) + return; + + gsr_damage_deinit_monitors(self); + for_each_active_monitor_output_x11_not_cached(self->display, add_monitor_callback, self); +} + +static void gsr_damage_on_randr_event(gsr_damage *self, XEvent *xev) { + const XRRNotifyEvent *rr_event = (XRRNotifyEvent*)xev; + switch(rr_event->subtype) { + case RRNotify_CrtcChange: + gsr_damage_on_crtc_change(self, xev); + break; + case RRNotify_OutputChange: + gsr_damage_on_output_change(self, xev); + break; + } +} + +static void gsr_damage_on_damage_event(gsr_damage *self, XEvent *xev) { + const XDamageNotifyEvent *de = (XDamageNotifyEvent*)xev; + XserverRegion region = XFixesCreateRegion(self->display, NULL, 0); + /* Subtract all the damage, repairing the window */ + XDamageSubtract(self->display, de->damage, None, region); + + if(self->all_monitors_tracked_refcount > 0) + self->damaged = true; + + if(!self->damaged) { + for(int i = 0; i < self->num_windows_tracked; ++i) { + const gsr_damage_window *damage_window = &self->windows_tracked[i]; + if(damage_window->window_id == (int64_t)de->drawable) { + self->damaged = true; + break; + } + } + } + + if(!self->damaged) { + int num_rectangles = 0; + XRectangle *rectangles = XFixesFetchRegion(self->display, region, &num_rectangles); + if(rectangles) { + for(int i = 0; i < num_rectangles; ++i) { + const gsr_rectangle damage_region = { (vec2i){rectangles[i].x, rectangles[i].y}, (vec2i){rectangles[i].width, rectangles[i].height} }; + for(int j = 0; j < self->num_monitors_tracked; ++j) { + const gsr_monitor *monitor = self->monitors_tracked[j].monitor; + if(!monitor) + continue; + + const gsr_rectangle monitor_region = { monitor->pos, monitor->size }; + self->damaged = rectangles_intersect(monitor_region, damage_region); + if(self->damaged) + goto intersection_found; + } + } + + intersection_found: + XFree(rectangles); + } + } + + XFixesDestroyRegion(self->display, region); + XFlush(self->display); +} + +static void gsr_damage_on_tick_cursor(gsr_damage *self) { + if(self->cursor->position.x == self->cursor_pos.x && self->cursor->position.y == self->cursor_pos.y) + return; + + self->cursor_pos = self->cursor->position; + const gsr_rectangle cursor_region = { self->cursor->position, self->cursor->size }; + + if(self->all_monitors_tracked_refcount > 0) + self->damaged = true; + + if(!self->damaged) { + for(int i = 0; i < self->num_windows_tracked; ++i) { + const gsr_damage_window *damage_window = &self->windows_tracked[i]; + const gsr_rectangle window_region = { damage_window->window_pos, damage_window->window_size }; + if(rectangles_intersect(window_region, cursor_region)) { + self->damaged = true; + break; + } + } + } + + if(!self->damaged) { + for(int i = 0; i < self->num_monitors_tracked; ++i) { + const gsr_monitor *monitor = self->monitors_tracked[i].monitor; + if(!monitor) + continue; + + const gsr_rectangle monitor_region = { monitor->pos, monitor->size }; + if(rectangles_intersect(monitor_region, cursor_region)) { + self->damaged = true; + break; + } + } + } +} + +static void gsr_damage_on_window_configure_notify(gsr_damage *self, XEvent *xev) { + for(int i = 0; i < self->num_windows_tracked; ++i) { + gsr_damage_window *damage_window = &self->windows_tracked[i]; + if(damage_window->window_id == (int64_t)xev->xconfigure.window) { + damage_window->window_pos.x = xev->xconfigure.x; + damage_window->window_pos.y = xev->xconfigure.y; + + damage_window->window_size.x = xev->xconfigure.width; + damage_window->window_size.y = xev->xconfigure.height; + break; + } + } +} + +void gsr_damage_on_event(gsr_damage *self, XEvent *xev) { + if(self->damage_event == 0) + return; + + if(xev->type == ConfigureNotify) + gsr_damage_on_window_configure_notify(self, xev); + + if(self->randr_event) { + if(xev->type == self->randr_event + RRScreenChangeNotify) + XRRUpdateConfiguration(xev); + + if(xev->type == self->randr_event + RRNotify) + gsr_damage_on_randr_event(self, xev); + } + + if(self->damage_event && xev->type == self->damage_event + XDamageNotify) + gsr_damage_on_damage_event(self, xev); +} + +void gsr_damage_tick(gsr_damage *self) { + if(self->damage_event == 0) + return; + + if(self->track_cursor && self->cursor->visible && !self->damaged) + gsr_damage_on_tick_cursor(self); +} + +bool gsr_damage_is_damaged(gsr_damage *self) { + return self->damage_event == 0 || self->damaged; +} + +void gsr_damage_clear(gsr_damage *self) { + self->damaged = false; +} diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 0000000..f12891f --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,913 @@ +#include "../include/dbus.h" + +#include <sys/random.h> + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <assert.h> + +/* TODO: Make non-blocking when GPU Screen Recorder is turned into a library */ +/* TODO: Make sure responses matches the requests */ + +#define DESKTOP_PORTAL_SIGNAL_RULE "type='signal',interface='org.freedesktop.Portal.Request'" + +typedef enum { + DICT_TYPE_STRING, + DICT_TYPE_UINT32, + DICT_TYPE_BOOL, +} dict_value_type; + +typedef struct { + const char *key; + dict_value_type value_type; + union { + char *str; + dbus_uint32_t u32; + dbus_bool_t boolean; + }; +} dict_entry; + +static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { + /* TODO: Use other functions on other platforms than linux */ + if(getrandom(buffer, buffer_size, 0) < buffer_size) { + fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + return false; + } + + for(int i = 0; i < buffer_size; ++i) { + unsigned char c = *(unsigned char*)&buffer[i]; + buffer[i] = alphabet[c % alphabet_size]; + } + + return true; +} + +static bool generate_random_characters_standard_alphabet(char *buffer, int buffer_size) { + return generate_random_characters(buffer, buffer_size, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62); +} + +static const char* dict_value_type_to_string(dict_value_type type) { + switch(type) { + case DICT_TYPE_STRING: return "string"; + case DICT_TYPE_UINT32: return "uint32"; + case DICT_TYPE_BOOL: return "boolean"; + } + return "(unknown)"; +} + +bool gsr_dbus_init(gsr_dbus *self, const char *screencast_restore_token) { + memset(self, 0, sizeof(*self)); + dbus_error_init(&self->err); + + self->random_str[DBUS_RANDOM_STR_SIZE] = '\0'; + if(!generate_random_characters_standard_alphabet(self->random_str, DBUS_RANDOM_STR_SIZE)) { + fprintf(stderr, "gsr error: gsr_dbus_init: failed to generate random string\n"); + return false; + } + + self->con = dbus_bus_get(DBUS_BUS_SESSION, &self->err); + if(dbus_error_is_set(&self->err)) { + fprintf(stderr, "gsr error: gsr_dbus_init: dbus_bus_get failed with error: %s\n", self->err.message); + return false; + } + + if(!self->con) { + fprintf(stderr, "gsr error: gsr_dbus_init: failed to get dbus session\n"); + return false; + } + + /* TODO: Check the name */ + const int ret = dbus_bus_request_name(self->con, "com.dec05eba.gpu_screen_recorder", DBUS_NAME_FLAG_REPLACE_EXISTING, &self->err); + if(dbus_error_is_set(&self->err)) { + fprintf(stderr, "gsr error: gsr_dbus_init: dbus_bus_request_name failed with error: %s\n", self->err.message); + gsr_dbus_deinit(self); + return false; + } + + if(screencast_restore_token) { + self->screencast_restore_token = strdup(screencast_restore_token); + if(!self->screencast_restore_token) { + fprintf(stderr, "gsr error: gsr_dbus_init: failed to clone restore token\n"); + gsr_dbus_deinit(self); + return false; + } + } + + (void)ret; + // if(ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + // fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: dbus_bus_request_name failed to get primary owner\n"); + // return false; + // } + + return true; +} + +void gsr_dbus_deinit(gsr_dbus *self) { + if(self->screencast_restore_token) { + free(self->screencast_restore_token); + self->screencast_restore_token = NULL; + } + + if(self->desktop_portal_rule_added) { + dbus_bus_remove_match(self->con, DESKTOP_PORTAL_SIGNAL_RULE, NULL); + // dbus_connection_flush(self->con); + self->desktop_portal_rule_added = false; + } + + if(self->con) { + dbus_error_free(&self->err); + + dbus_bus_release_name(self->con, "com.dec05eba.gpu_screen_recorder", NULL); + + // Apparently shouldn't be used when a connection is setup by using dbus_bus_get + //dbus_connection_close(self->con); + dbus_connection_unref(self->con); + self->con = NULL; + } +} + +static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *interface, const char *property_name, uint32_t *result) { + *result = 0; + + DBusMessage *msg = dbus_message_new_method_call( + "org.freedesktop.portal.Desktop", // target for the method call + "/org/freedesktop/portal/desktop", // object to call on + "org.freedesktop.DBus.Properties", // interface to call on + "Get"); // method name + if(!msg) { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_message_new_method_call failed\n"); + return false; + } + + DBusMessageIter it; + dbus_message_iter_init_append(msg, &it); + + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &interface)) { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add interface\n"); + dbus_message_unref(msg); + return false; + } + + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &property_name)) { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add property_name\n"); + dbus_message_unref(msg); + return false; + } + + DBusPendingCall *pending = NULL; + if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_connection_send_with_reply failed\n"); + dbus_message_unref(msg); + return false; + } + dbus_connection_flush(self->con); + + //fprintf(stderr, "Request Sent\n"); + + dbus_message_unref(msg); + msg = NULL; + + dbus_pending_call_block(pending); + + msg = dbus_pending_call_steal_reply(pending); + if(!msg) { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_pending_call_steal_reply failed\n"); + dbus_pending_call_unref(pending); + dbus_message_unref(msg); + return false; + } + + dbus_pending_call_unref(pending); + pending = NULL; + + DBusMessageIter resp_args; + if(!dbus_message_iter_init(msg, &resp_args)) { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is missing arguments\n"); + dbus_message_unref(msg); + return false; + } else if(DBUS_TYPE_UINT32 == dbus_message_iter_get_arg_type(&resp_args)) { + dbus_message_iter_get_basic(&resp_args, result); + } else if(DBUS_TYPE_VARIANT == dbus_message_iter_get_arg_type(&resp_args)) { + DBusMessageIter variant_iter; + dbus_message_iter_recurse(&resp_args, &variant_iter); + + if(dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&variant_iter, result); + } else { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not a variant with an uint32, %c\n", dbus_message_iter_get_arg_type(&variant_iter)); + dbus_message_unref(msg); + return false; + } + } else { + fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not an uint32, %c\n", dbus_message_iter_get_arg_type(&resp_args)); + dbus_message_unref(msg); + return false; + // TODO: Check dbus_error_is_set? + } + + dbus_message_unref(msg); + return true; +} + +static uint32_t gsr_dbus_get_screencast_version_cached(gsr_dbus *self) { + if(self->screencast_version == 0) + gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "version", &self->screencast_version); + return self->screencast_version; +} + +static bool gsr_dbus_ensure_desktop_portal_rule_added(gsr_dbus *self) { + if(self->desktop_portal_rule_added) + return true; + + dbus_bus_add_match(self->con, DESKTOP_PORTAL_SIGNAL_RULE, &self->err); + dbus_connection_flush(self->con); + if(dbus_error_is_set(&self->err)) { + fprintf(stderr, "gsr error: gsr_dbus_ensure_desktop_portal_rule_added: failed to add dbus rule %s, error: %s\n", DESKTOP_PORTAL_SIGNAL_RULE, self->err.message); + return false; + } + self->desktop_portal_rule_added = true; + return true; +} + +static void gsr_dbus_portal_get_unique_handle_token(gsr_dbus *self, char *buffer, int size) { + snprintf(buffer, size, "gpu_screen_recorder_handle_%s_%u", self->random_str, self->handle_counter++); +} + +static void gsr_dbus_portal_get_unique_session_token(gsr_dbus *self, char *buffer, int size) { + snprintf(buffer, size, "gpu_screen_recorder_session_%s", self->random_str); +} + +static bool dbus_add_dict(DBusMessageIter *it, const dict_entry *entries, int num_entries) { + DBusMessageIter array_it; + if(!dbus_message_iter_open_container(it, DBUS_TYPE_ARRAY, "{sv}", &array_it)) + return false; + + for (int i = 0; i < num_entries; ++i) { + DBusMessageIter entry_it = DBUS_MESSAGE_ITER_INIT_CLOSED; + DBusMessageIter variant_it = DBUS_MESSAGE_ITER_INIT_CLOSED; + + if(!dbus_message_iter_open_container(&array_it, DBUS_TYPE_DICT_ENTRY, NULL, &entry_it)) + goto entry_err; + + if(!dbus_message_iter_append_basic(&entry_it, DBUS_TYPE_STRING, &entries[i].key)) + goto entry_err; + + switch (entries[i].value_type) { + case DICT_TYPE_STRING: { + if(!dbus_message_iter_open_container(&entry_it, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &variant_it)) + goto entry_err; + if(!dbus_message_iter_append_basic(&variant_it, DBUS_TYPE_STRING, &entries[i].str)) + goto entry_err; + break; + } + case DICT_TYPE_UINT32: { + if(!dbus_message_iter_open_container(&entry_it, DBUS_TYPE_VARIANT, DBUS_TYPE_UINT32_AS_STRING, &variant_it)) + goto entry_err; + if(!dbus_message_iter_append_basic(&variant_it, DBUS_TYPE_UINT32, &entries[i].u32)) + goto entry_err; + break; + } + case DICT_TYPE_BOOL: { + if(!dbus_message_iter_open_container(&entry_it, DBUS_TYPE_VARIANT, DBUS_TYPE_BOOLEAN_AS_STRING, &variant_it)) + goto entry_err; + if(!dbus_message_iter_append_basic(&variant_it, DBUS_TYPE_BOOLEAN, &entries[i].boolean)) + goto entry_err; + break; + } + } + + dbus_message_iter_close_container(&entry_it, &variant_it); + dbus_message_iter_close_container(&array_it, &entry_it); + continue; + + entry_err: + dbus_message_iter_abandon_container_if_open(&array_it, &variant_it); + dbus_message_iter_abandon_container_if_open(&array_it, &entry_it); + dbus_message_iter_abandon_container_if_open(it, &array_it); + return false; + } + + return dbus_message_iter_close_container(it, &array_it); +} + +/* If |response_msg| is NULL then we dont wait for a response signal */ +static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_name, const char *session_handle, const char *parent_window, const dict_entry *entries, int num_entries, int *resp_fd, DBusMessage **response_msg) { + if(resp_fd) + *resp_fd = -1; + + if(response_msg) + *response_msg = NULL; + + if(!gsr_dbus_ensure_desktop_portal_rule_added(self)) + return false; + + DBusMessage *msg = dbus_message_new_method_call( + "org.freedesktop.portal.Desktop", // target for the method call + "/org/freedesktop/portal/desktop", // object to call on + "org.freedesktop.portal.ScreenCast", // interface to call on + method_name); // method name + if(!msg) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_message_new_method_call failed\n"); + return false; + } + + DBusMessageIter it; + dbus_message_iter_init_append(msg, &it); + + if(session_handle) { + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &session_handle)) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add session_handle\n"); + dbus_message_unref(msg); + return false; + } + } + + if(parent_window) { + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent_window)) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add parent_window\n"); + dbus_message_unref(msg); + return false; + } + } + + if(!dbus_add_dict(&it, entries, num_entries)) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add dict\n"); + dbus_message_unref(msg); + return false; + } + + DBusPendingCall *pending = NULL; + if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_connection_send_with_reply failed\n"); + dbus_message_unref(msg); + return false; + } + dbus_connection_flush(self->con); + + //fprintf(stderr, "Request Sent\n"); + + dbus_message_unref(msg); + msg = NULL; + + dbus_pending_call_block(pending); + + msg = dbus_pending_call_steal_reply(pending); + if(!msg) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_pending_call_steal_reply failed\n"); + dbus_pending_call_unref(pending); + dbus_message_unref(msg); + return false; + } + + dbus_pending_call_unref(pending); + pending = NULL; + + DBusMessageIter resp_args; + if(!dbus_message_iter_init(msg, &resp_args)) { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is missing arguments\n"); + dbus_message_unref(msg); + return false; + } else if (DBUS_TYPE_OBJECT_PATH == dbus_message_iter_get_arg_type(&resp_args)) { + const char *res = NULL; + dbus_message_iter_get_basic(&resp_args, &res); + } else if(DBUS_TYPE_UNIX_FD == dbus_message_iter_get_arg_type(&resp_args)) { + int fd = -1; + dbus_message_iter_get_basic(&resp_args, &fd); + + if(resp_fd) + *resp_fd = fd; + } else if(DBUS_TYPE_STRING == dbus_message_iter_get_arg_type(&resp_args)) { + char *err = NULL; + dbus_message_iter_get_basic(&resp_args, &err); + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed with error: %s\n", err); + + dbus_message_unref(msg); + return false; + // TODO: Check dbus_error_is_set? + } else { + fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is not an object path or unix fd\n"); + dbus_message_unref(msg); + return false; + // TODO: Check dbus_error_is_set? + } + + dbus_message_unref(msg); + if(!response_msg) + return true; + + /* TODO: Add timeout, but take into consideration user interactive signals (such as selecting a monitor to capture for ScreenCast) */ + for (;;) { + const int timeout_milliseconds = 10; + dbus_connection_read_write(self->con, timeout_milliseconds); + *response_msg = dbus_connection_pop_message(self->con); + + if(!*response_msg) + continue; + + if(!dbus_message_is_signal(*response_msg, "org.freedesktop.portal.Request", "Response")) { + dbus_message_unref(*response_msg); + *response_msg = NULL; + continue; + } + + break; + } + + return true; +} + +static int gsr_dbus_get_response_status(DBusMessageIter *resp_args) { + if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_UINT32) { + fprintf(stderr, "gsr error: gsr_dbus_get_response_status: missing uint32 in response\n"); + return -1; + } + + dbus_uint32_t response_status = 0; + dbus_message_iter_get_basic(resp_args, &response_status); + + dbus_message_iter_next(resp_args); + return (int)response_status; +} + +static dict_entry* find_dict_entry_by_key(dict_entry *entries, int num_entries, const char *key) { + for(int i = 0; i < num_entries; ++i) { + if(strcmp(entries[i].key, key) == 0) + return &entries[i]; + } + return NULL; +} + +static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) { + if(dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: value is not a variant\n"); + return false; + } + + DBusMessageIter variant_iter; + dbus_message_iter_recurse(iter, &variant_iter); + + switch(dbus_message_iter_get_arg_type(&variant_iter)) { + case DBUS_TYPE_STRING: { + if(entry->value_type != DICT_TYPE_STRING) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a string\n", dict_value_type_to_string(entry->value_type)); + return false; + } + + const char *value = NULL; + dbus_message_iter_get_basic(&variant_iter, &value); + + if(!value) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to get entry value as value\n"); + return false; + } + + if(entry->str) { + free(entry->str); + entry->str = NULL; + } + + entry->str = strdup(value); + if(!entry->str) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to copy value\n"); + return false; + } + return true; + } + case DBUS_TYPE_UINT32: { + if(entry->value_type != DICT_TYPE_UINT32) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was an uint32\n", dict_value_type_to_string(entry->value_type)); + return false; + } + + dbus_message_iter_get_basic(&variant_iter, &entry->u32); + return true; + } + case DBUS_TYPE_BOOLEAN: { + if(entry->value_type != DICT_TYPE_BOOL) { + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a boolean\n", dict_value_type_to_string(entry->value_type)); + return false; + } + + dbus_message_iter_get_basic(&variant_iter, &entry->boolean); + return true; + } + } + + fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: got unexpected type, expected string, uint32 or boolean\n"); + return false; +} + +/* + Parses a{sv} into matching key entries in |entries|. + If the entry value is a string then it's allocated with malloc and is null-terminated + and has to be free by the caller. + The entry values should be 0 before this method is called. + The entries are free'd if this function fails. +*/ +static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, int num_entries) { + if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_ARRAY) { + fprintf(stderr, "gsr error: gsr_dbus_get_map: missing array in response\n"); + return false; + } + + DBusMessageIter subiter; + dbus_message_iter_recurse(resp_args, &subiter); + + while(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_INVALID) { + DBusMessageIter dictiter = DBUS_MESSAGE_ITER_INIT_CLOSED; + const char *key = NULL; + dict_entry *entry = NULL; + + // fprintf(stderr, " array element type: %c, %s\n", + // dbus_message_iter_get_arg_type(&subiter), + // dbus_message_iter_get_signature(&subiter)); + if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { + fprintf(stderr, "gsr error: gsr_dbus_get_map: array value is not an entry\n"); + return false; + } + + dbus_message_iter_recurse(&subiter, &dictiter); + + if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { + fprintf(stderr, "gsr error: gsr_dbus_get_map: entry key is not a string\n"); + goto error; + } + + dbus_message_iter_get_basic(&dictiter, &key); + if(!key) { + fprintf(stderr, "gsr error: gsr_dbus_get_map: failed to get entry key as value\n"); + goto error; + } + + entry = find_dict_entry_by_key(entries, num_entries, key); + if(!entry) { + dbus_message_iter_next(&subiter); + continue; + } + + if(!dbus_message_iter_next(&dictiter)) { + fprintf(stderr, "gsr error: gsr_dbus_get_map: missing entry value\n"); + goto error; + } + + if(!gsr_dbus_get_variant_value(&dictiter, entry)) + goto error; + + dbus_message_iter_next(&subiter); + } + + return true; + + error: + for(int i = 0; i < num_entries; ++i) { + if(entries[i].value_type == DICT_TYPE_STRING) { + free(entries[i].str); + entries[i].str = NULL; + } + } + return false; +} + +int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { + assert(session_handle); + *session_handle = NULL; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + char session_handle_token[64]; + gsr_dbus_portal_get_unique_session_token(self, session_handle_token, sizeof(session_handle_token)); + + dict_entry args[2]; + args[0].key = "handle_token"; + args[0].value_type = DICT_TYPE_STRING; + args[0].str = handle_token; + + args[1].key = "session_handle_token"; + args[1].value_type = DICT_TYPE_STRING; + args[1].str = session_handle_token; + + DBusMessage *response_msg = NULL; + if(!gsr_dbus_call_screencast_method(self, "CreateSession", NULL, NULL, args, 2, NULL, &response_msg)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: failed to setup ScreenCast session. Make sure you have a desktop portal running with support for the ScreenCast interface and that the desktop portal matches the Wayland compositor you are running.\n"); + return -1; + } + + // TODO: Verify signal path matches |res|, maybe check the below + // DBUS_TYPE_ARRAY value? + //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); + DBusMessageIter resp_args; + if(!dbus_message_iter_init(response_msg, &resp_args)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + dbus_message_unref(response_msg); + return -1; + } + + const int response_status = gsr_dbus_get_response_status(&resp_args); + if(response_status != 0) { + dbus_message_unref(response_msg); + return response_status; + } + + dict_entry entries[1]; + entries[0].key = "session_handle"; + entries[0].str = NULL; + entries[0].value_type = DICT_TYPE_STRING; + if(!gsr_dbus_get_map(&resp_args, entries, 1)) { + dbus_message_unref(response_msg); + return -1; + } + + if(!entries[0].str) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing \"session_handle\" in response\n"); + dbus_message_unref(response_msg); + return -1; + } + + *session_handle = entries[0].str; + //fprintf(stderr, "session handle: |%s|\n", entries[0].str); + //free(entries[0].str); + + dbus_message_unref(response_msg); + return 0; +} + +static uint32_t unset_unsupported_capture_types(uint32_t requested_capture_types, uint32_t available_capture_types) { + if(!(available_capture_types & GSR_PORTAL_CAPTURE_TYPE_MONITOR)) + requested_capture_types &= ~GSR_PORTAL_CAPTURE_TYPE_MONITOR; + if(!(available_capture_types & GSR_PORTAL_CAPTURE_TYPE_WINDOW)) + requested_capture_types &= ~GSR_PORTAL_CAPTURE_TYPE_WINDOW; + if(!(available_capture_types & GSR_PORTAL_CAPTURE_TYPE_VIRTUAL)) + requested_capture_types &= ~GSR_PORTAL_CAPTURE_TYPE_VIRTUAL; + return requested_capture_types; +} + +static uint32_t unset_unsupported_cursor_modes(uint32_t requested_cursor_modes, uint32_t available_cursor_modes) { + if(!(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_HIDDEN)) + requested_cursor_modes &= ~GSR_PORTAL_CURSOR_MODE_HIDDEN; + if(!(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_EMBEDDED)) + requested_cursor_modes &= ~GSR_PORTAL_CURSOR_MODE_EMBEDDED; + if(!(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_METADATA)) + requested_cursor_modes &= ~GSR_PORTAL_CURSOR_MODE_METADATA; + return requested_cursor_modes; +} + +int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handle, uint32_t capture_type, uint32_t cursor_mode) { + assert(session_handle); + + uint32_t available_source_types = 0; + gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableSourceTypes", &available_source_types); + if(available_source_types == 0) + fprintf(stderr, "gsr error: gsr_dbus_screencast_select_sources: no source types are available\n"); + capture_type = unset_unsupported_capture_types(capture_type, available_source_types); + + uint32_t available_cursor_modes = 0; + gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes", &available_cursor_modes); + if(available_cursor_modes == 0) + fprintf(stderr, "gsr error: gsr_dbus_screencast_select_sources: no cursors modes are available\n"); + cursor_mode = unset_unsupported_cursor_modes(cursor_mode, available_cursor_modes); + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + int num_arg_dict = 4; + dict_entry args[6]; + args[0].key = "types"; + args[0].value_type = DICT_TYPE_UINT32; + args[0].u32 = capture_type; + + args[1].key = "multiple"; + args[1].value_type = DICT_TYPE_BOOL; + args[1].boolean = false; /* TODO: Wayland ignores this and still gives the option to select multiple sources. Support that case.. */ + + args[2].key = "handle_token"; + args[2].value_type = DICT_TYPE_STRING; + args[2].str = handle_token; + + args[3].key = "cursor_mode"; + args[3].value_type = DICT_TYPE_UINT32; + args[3].u32 = cursor_mode; + + const int screencast_server_version = gsr_dbus_get_screencast_version_cached(self); + if(screencast_server_version >= 4) { + num_arg_dict = 5; + args[4].key = "persist_mode"; + args[4].value_type = DICT_TYPE_UINT32; + args[4].u32 = 2; /* persist until explicitly revoked */ + + if(self->screencast_restore_token && self->screencast_restore_token[0]) { + num_arg_dict = 6; + + args[5].key = "restore_token"; + args[5].value_type = DICT_TYPE_STRING; + args[5].str = self->screencast_restore_token; + } + } else if(self->screencast_restore_token && self->screencast_restore_token[0]) { + fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: tried to use restore token but this option is only available in screencast version >= 4, your wayland compositors screencast version is %d\n", screencast_server_version); + } + + DBusMessage *response_msg = NULL; + if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) { + if(num_arg_dict == 6) { + /* We dont know what the error exactly is but assume it may be because of invalid restore token. In that case try without restore token */ + fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: SelectSources failed, retrying without restore_token\n"); + num_arg_dict = 5; + if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) + return -1; + } else { + return -1; + } + } + + // TODO: Verify signal path matches |res|, maybe check the below + //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); + DBusMessageIter resp_args; + if(!dbus_message_iter_init(response_msg, &resp_args)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + dbus_message_unref(response_msg); + return -1; + } + + + const int response_status = gsr_dbus_get_response_status(&resp_args); + if(response_status != 0) { + dbus_message_unref(response_msg); + return response_status; + } + + dbus_message_unref(response_msg); + return 0; +} + +static dbus_uint32_t screencast_stream_get_pipewire_node(DBusMessageIter *iter) { + DBusMessageIter subiter; + dbus_message_iter_recurse(iter, &subiter); + + if(dbus_message_iter_get_arg_type(&subiter) == DBUS_TYPE_STRUCT) { + DBusMessageIter structiter; + dbus_message_iter_recurse(&subiter, &structiter); + + if(dbus_message_iter_get_arg_type(&structiter) == DBUS_TYPE_UINT32) { + dbus_uint32_t data = 0; + dbus_message_iter_get_basic(&structiter, &data); + return data; + } + } + + return 0; +} + +int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32_t *pipewire_node) { + assert(session_handle); + *pipewire_node = 0; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + dict_entry args[1]; + args[0].key = "handle_token"; + args[0].value_type = DICT_TYPE_STRING; + args[0].str = handle_token; + + DBusMessage *response_msg = NULL; + if(!gsr_dbus_call_screencast_method(self, "Start", session_handle, "", args, 1, NULL, &response_msg)) + return -1; + + // TODO: Verify signal path matches |res|, maybe check the below + //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); + DBusMessageIter resp_args; + if(!dbus_message_iter_init(response_msg, &resp_args)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing response\n"); + dbus_message_unref(response_msg); + return -1; + } + + const int response_status = gsr_dbus_get_response_status(&resp_args); + if(response_status != 0) { + dbus_message_unref(response_msg); + return response_status; + } + + if(dbus_message_iter_get_arg_type(&resp_args) != DBUS_TYPE_ARRAY) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing array in response\n"); + dbus_message_unref(response_msg); + return -1; + } + + DBusMessageIter subiter; + dbus_message_iter_recurse(&resp_args, &subiter); + + while(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_INVALID) { + DBusMessageIter dictiter = DBUS_MESSAGE_ITER_INIT_CLOSED; + const char *key = NULL; + + // fprintf(stderr, " array element type: %c, %s\n", + // dbus_message_iter_get_arg_type(&subiter), + // dbus_message_iter_get_signature(&subiter)); + if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: array value is not an entry\n"); + goto error; + } + + dbus_message_iter_recurse(&subiter, &dictiter); + + if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: entry key is not a string\n"); + goto error; + } + + dbus_message_iter_get_basic(&dictiter, &key); + if(!key) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: failed to get entry key as value\n"); + goto error; + } + + if(strcmp(key, "restore_token") == 0) { + if(!dbus_message_iter_next(&dictiter)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing restore_token value\n"); + goto error; + } + + if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a variant\n"); + goto error; + } + + DBusMessageIter variant_iter; + dbus_message_iter_recurse(&dictiter, &variant_iter); + + if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_STRING) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a string\n"); + goto error; + } + + char *restore_token_str = NULL; + dbus_message_iter_get_basic(&variant_iter, &restore_token_str); + + if(restore_token_str) { + if(self->screencast_restore_token) { + free(self->screencast_restore_token); + self->screencast_restore_token = NULL; + } + self->screencast_restore_token = strdup(restore_token_str); + //fprintf(stderr, "got restore token: %s\n", self->screencast_restore_token); + } + } else if(strcmp(key, "streams") == 0) { + if(!dbus_message_iter_next(&dictiter)) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing streams value\n"); + goto error; + } + + if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not a variant\n"); + goto error; + } + + DBusMessageIter variant_iter; + dbus_message_iter_recurse(&dictiter, &variant_iter); + + if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_ARRAY) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not an array\n"); + goto error; + } + + int num_streams = dbus_message_iter_get_element_count(&variant_iter); + //fprintf(stderr, "num streams: %d\n", num_streams); + /* Skip over all streams except the last one, since kde can return multiple streams even if only 1 is requested. The last one is the valid one */ + for(int i = 0; i < num_streams - 1; ++i) { + screencast_stream_get_pipewire_node(&variant_iter); + } + + if(num_streams > 0) { + *pipewire_node = screencast_stream_get_pipewire_node(&variant_iter); + //fprintf(stderr, "pipewire node: %u\n", *pipewire_node); + } + } + + dbus_message_iter_next(&subiter); + } + + if(*pipewire_node == 0) { + fprintf(stderr, "gsr error: gsr_dbus_screencast_start: no pipewire node returned\n"); + goto error; + } + + dbus_message_unref(response_msg); + return 0; + + error: + dbus_message_unref(response_msg); + return -1; +} + +bool gsr_dbus_screencast_open_pipewire_remote(gsr_dbus *self, const char *session_handle, int *pipewire_fd) { + assert(session_handle); + *pipewire_fd = -1; + return gsr_dbus_call_screencast_method(self, "OpenPipeWireRemote", session_handle, NULL, NULL, 0, pipewire_fd, NULL); +} + +const char* gsr_dbus_screencast_get_restore_token(gsr_dbus *self) { + return self->screencast_restore_token; +} diff --git a/src/defs.c b/src/defs.c new file mode 100644 index 0000000..319d21b --- /dev/null +++ b/src/defs.c @@ -0,0 +1,100 @@ +#include "../include/defs.h" +#include <assert.h> + +bool video_codec_is_hdr(gsr_video_codec video_codec) { + // TODO: Vulkan + switch(video_codec) { + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_AV1_HDR: + return true; + default: + return false; + } +} + +gsr_video_codec hdr_video_codec_to_sdr_video_codec(gsr_video_codec video_codec) { + // TODO: Vulkan + switch(video_codec) { + case GSR_VIDEO_CODEC_HEVC_HDR: + return GSR_VIDEO_CODEC_HEVC; + case GSR_VIDEO_CODEC_AV1_HDR: + return GSR_VIDEO_CODEC_AV1; + default: + return video_codec; + } +} + +gsr_color_depth video_codec_to_bit_depth(gsr_video_codec video_codec) { + // TODO: 10-bit Vulkan + switch(video_codec) { + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: + return GSR_COLOR_DEPTH_10_BITS; + default: + return GSR_COLOR_DEPTH_8_BITS; + } +} + +const char* video_codec_to_string(gsr_video_codec video_codec) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: return "h264"; + case GSR_VIDEO_CODEC_HEVC: return "hevc"; + case GSR_VIDEO_CODEC_HEVC_HDR: return "hevc_hdr"; + case GSR_VIDEO_CODEC_HEVC_10BIT: return "hevc_10bit"; + case GSR_VIDEO_CODEC_AV1: return "av1"; + case GSR_VIDEO_CODEC_AV1_HDR: return "av1_hdr"; + case GSR_VIDEO_CODEC_AV1_10BIT: return "av1_10bit"; + case GSR_VIDEO_CODEC_VP8: return "vp8"; + case GSR_VIDEO_CODEC_VP9: return "vp9"; + case GSR_VIDEO_CODEC_H264_VULKAN: return "h264_vulkan"; + case GSR_VIDEO_CODEC_HEVC_VULKAN: return "hevc_vulkan"; + } + return ""; +} + +// bool video_codec_is_hevc(gsr_video_codec video_codec) { +// // TODO: 10-bit vulkan +// switch(video_codec) { +// case GSR_VIDEO_CODEC_HEVC: +// case GSR_VIDEO_CODEC_HEVC_HDR: +// case GSR_VIDEO_CODEC_HEVC_10BIT: +// case GSR_VIDEO_CODEC_HEVC_VULKAN: +// return true; +// default: +// return false; +// } +// } + +bool video_codec_is_av1(gsr_video_codec video_codec) { + // TODO: Vulkan + switch(video_codec) { + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: + return true; + default: + return false; + } +} + +bool video_codec_is_vulkan(gsr_video_codec video_codec) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264_VULKAN: + case GSR_VIDEO_CODEC_HEVC_VULKAN: + return true; + default: + return false; + } +} + +const char* audio_codec_get_name(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return "aac"; + case GSR_AUDIO_CODEC_OPUS: return "opus"; + case GSR_AUDIO_CODEC_FLAC: return "flac"; + } + assert(false); + return ""; +} diff --git a/src/egl.c b/src/egl.c new file mode 100644 index 0000000..ce97178 --- /dev/null +++ b/src/egl.c @@ -0,0 +1,526 @@ +#include "../include/egl.h" +#include "../include/window/window.h" +#include "../include/library_loader.h" +#include "../include/utils.h" + +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <dlfcn.h> +#include <assert.h> +#include <unistd.h> + +// TODO: rename gsr_egl to something else since this includes both egl and glx and in the future maybe vulkan too + +#define GLX_DRAWABLE_TYPE 0x8010 +#define GLX_RENDER_TYPE 0x8011 +#define GLX_RGBA_BIT 0x00000001 +#define GLX_WINDOW_BIT 0x00000001 +#define GLX_PIXMAP_BIT 0x00000002 +#define GLX_BIND_TO_TEXTURE_RGBA_EXT 0x20D1 +#define GLX_BIND_TO_TEXTURE_TARGETS_EXT 0x20D3 +#define GLX_TEXTURE_2D_BIT_EXT 0x00000002 +#define GLX_DOUBLEBUFFER 5 +#define GLX_RED_SIZE 8 +#define GLX_GREEN_SIZE 9 +#define GLX_BLUE_SIZE 10 +#define GLX_ALPHA_SIZE 11 +#define GLX_DEPTH_SIZE 12 +#define GLX_RGBA_TYPE 0x8014 + +// TODO: Create egl context without surface (in other words, x11/wayland agnostic, doesn't require x11/wayland dependency) +static bool gsr_egl_create_window(gsr_egl *self, bool enable_debug) { + EGLConfig ecfg; + int32_t num_config = 0; + + const int32_t attr[] = { + EGL_BUFFER_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_NONE, EGL_NONE + }; + + int32_t ctxattr[] = { + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL_NONE, EGL_NONE, EGL_NONE + }; + + if(enable_debug) { + ctxattr[2] = EGL_CONTEXT_OPENGL_DEBUG; + ctxattr[3] = EGL_TRUE; + } + + self->eglBindAPI(EGL_OPENGL_ES_API); + + self->egl_display = self->eglGetDisplay((EGLNativeDisplayType)gsr_window_get_display(self->window)); + if(!self->egl_display) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n"); + goto fail; + } + + if(!self->eglInitialize(self->egl_display, NULL, NULL)) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n"); + goto fail; + } + + if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n"); + goto fail; + } + + self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr); + if(!self->egl_context) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n"); + goto fail; + } + + self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)gsr_window_get_window(self->window), NULL); + if(!self->egl_surface) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n"); + goto fail; + } + + if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make egl context current\n"); + goto fail; + } + + return true; + + fail: + gsr_egl_unload(self); + return false; +} + +static GLXFBConfig glx_fb_config_choose(gsr_egl *self, Display *display) { + // TODO: OpenGL debug context? + const int glx_visual_attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + // TODO: + //GLX_BIND_TO_TEXTURE_RGBA_EXT, 1, + //GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT, + GLX_DOUBLEBUFFER, True, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, 0, + GLX_DEPTH_SIZE, 0, + None, None + }; + + // TODO: Cleanup + int c = 0; + GLXFBConfig *fb_configs = self->glXChooseFBConfig(display, DefaultScreen(display), glx_visual_attribs, &c); + if(c == 0 || !fb_configs) + return NULL; + + return fb_configs[0]; +} + +static bool gsr_egl_switch_to_glx_context(gsr_egl *self) { + // TODO: Cleanup + assert(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11); + Display *display = gsr_window_get_display(self->window); + const Window window = (Window)gsr_window_get_window(self->window); + + if(self->egl_context) { + self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL); + self->eglDestroyContext(self->egl_display, self->egl_context); + self->egl_context = NULL; + } + + if(self->egl_surface) { + self->eglDestroySurface(self->egl_display, self->egl_surface); + self->egl_surface = NULL; + } + + if(self->egl_display) { + self->eglTerminate(self->egl_display); + self->egl_display = NULL; + } + + self->glx_fb_config = glx_fb_config_choose(self, display); + if(!self->glx_fb_config) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a suitable fb config\n"); + goto fail; + } + + // TODO: + //self->glx_context = self->glXCreateContextAttribsARB(display, self->glx_fb_config, NULL, True, context_attrib_list); + self->glx_context = self->glXCreateNewContext(display, self->glx_fb_config, GLX_RGBA_TYPE, NULL, True); + if(!self->glx_context) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create glx context\n"); + goto fail; + } + + if(!self->glXMakeContextCurrent(display, window, window, self->glx_context)) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make glx context current\n"); + goto fail; + } + + return true; + + fail: + if(self->glx_context) { + self->glXMakeContextCurrent(display, None, None, NULL); + self->glXDestroyContext(display, self->glx_context); + self->glx_context = NULL; + self->glx_fb_config = NULL; + } + return false; +} + +static bool gsr_egl_load_egl(gsr_egl *self, void *library) { + const dlsym_assign required_dlsym[] = { + { (void**)&self->eglGetError, "eglGetError" }, + { (void**)&self->eglGetDisplay, "eglGetDisplay" }, + { (void**)&self->eglInitialize, "eglInitialize" }, + { (void**)&self->eglTerminate, "eglTerminate" }, + { (void**)&self->eglChooseConfig, "eglChooseConfig" }, + { (void**)&self->eglCreateWindowSurface, "eglCreateWindowSurface" }, + { (void**)&self->eglCreateContext, "eglCreateContext" }, + { (void**)&self->eglMakeCurrent, "eglMakeCurrent" }, + { (void**)&self->eglCreateImage, "eglCreateImage" }, + { (void**)&self->eglDestroyContext, "eglDestroyContext" }, + { (void**)&self->eglDestroySurface, "eglDestroySurface" }, + { (void**)&self->eglDestroyImage, "eglDestroyImage" }, + { (void**)&self->eglSwapInterval, "eglSwapInterval" }, + { (void**)&self->eglSwapBuffers, "eglSwapBuffers" }, + { (void**)&self->eglBindAPI, "eglBindAPI" }, + { (void**)&self->eglGetProcAddress, "eglGetProcAddress" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(library, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n"); + return false; + } + + return true; +} + +static bool gsr_egl_proc_load_egl(gsr_egl *self) { + self->eglExportDMABUFImageQueryMESA = (FUNC_eglExportDMABUFImageQueryMESA)self->eglGetProcAddress("eglExportDMABUFImageQueryMESA"); + self->eglExportDMABUFImageMESA = (FUNC_eglExportDMABUFImageMESA)self->eglGetProcAddress("eglExportDMABUFImageMESA"); + self->glEGLImageTargetTexture2DOES = (FUNC_glEGLImageTargetTexture2DOES)self->eglGetProcAddress("glEGLImageTargetTexture2DOES"); + self->eglQueryDisplayAttribEXT = (FUNC_eglQueryDisplayAttribEXT)self->eglGetProcAddress("eglQueryDisplayAttribEXT"); + self->eglQueryDeviceStringEXT = (FUNC_eglQueryDeviceStringEXT)self->eglGetProcAddress("eglQueryDeviceStringEXT"); + self->eglQueryDmaBufModifiersEXT = (FUNC_eglQueryDmaBufModifiersEXT)self->eglGetProcAddress("eglQueryDmaBufModifiersEXT"); + + self->glCreateMemoryObjectsEXT = (FUNC_glCreateMemoryObjectsEXT)self->eglGetProcAddress("glCreateMemoryObjectsEXT"); + self->glImportMemoryFdEXT = (FUNC_glImportMemoryFdEXT)self->eglGetProcAddress("glImportMemoryFdEXT"); + self->glIsMemoryObjectEXT = (FUNC_glIsMemoryObjectEXT)self->eglGetProcAddress("glIsMemoryObjectEXT"); + self->glTexStorageMem2DEXT = (FUNC_glTexStorageMem2DEXT)self->eglGetProcAddress("glTexStorageMem2DEXT"); + self->glBufferStorageMemEXT = (FUNC_glBufferStorageMemEXT)self->eglGetProcAddress("glBufferStorageMemEXT"); + self->glNamedBufferStorageMemEXT = (FUNC_glNamedBufferStorageMemEXT)self->eglGetProcAddress("glNamedBufferStorageMemEXT"); + self->glMemoryObjectParameterivEXT = (FUNC_glMemoryObjectParameterivEXT)self->eglGetProcAddress("glMemoryObjectParameterivEXT"); + + if(!self->eglExportDMABUFImageQueryMESA) { + fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageQueryMESA\n"); + return false; + } + + if(!self->eglExportDMABUFImageMESA) { + fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageMESA\n"); + return false; + } + + if(!self->glEGLImageTargetTexture2DOES) { + fprintf(stderr, "gsr error: gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES\n"); + return false; + } + + return true; +} + +static bool gsr_egl_load_glx(gsr_egl *self, void *library) { + const dlsym_assign required_dlsym[] = { + { (void**)&self->glXGetProcAddress, "glXGetProcAddress" }, + { (void**)&self->glXChooseFBConfig, "glXChooseFBConfig" }, + { (void**)&self->glXMakeContextCurrent, "glXMakeContextCurrent" }, + { (void**)&self->glXCreateNewContext, "glXCreateNewContext" }, + { (void**)&self->glXDestroyContext, "glXDestroyContext" }, + { (void**)&self->glXSwapBuffers, "glXSwapBuffers" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(library, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGLX.so.0\n"); + return false; + } + + self->glXCreateContextAttribsARB = (FUNC_glXCreateContextAttribsARB)self->glXGetProcAddress((const unsigned char*)"glXCreateContextAttribsARB"); + if(!self->glXCreateContextAttribsARB) { + fprintf(stderr, "gsr error: gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB\n"); + return false; + } + + self->glXSwapIntervalEXT = (FUNC_glXSwapIntervalEXT)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalEXT"); + self->glXSwapIntervalMESA = (FUNC_glXSwapIntervalMESA)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalMESA"); + self->glXSwapIntervalSGI = (FUNC_glXSwapIntervalSGI)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalSGI"); + + return true; +} + +static bool gsr_egl_load_gl(gsr_egl *self, void *library) { + const dlsym_assign required_dlsym[] = { + { (void**)&self->glGetError, "glGetError" }, + { (void**)&self->glGetString, "glGetString" }, + { (void**)&self->glFlush, "glFlush" }, + { (void**)&self->glFinish, "glFinish" }, + { (void**)&self->glClear, "glClear" }, + { (void**)&self->glClearColor, "glClearColor" }, + { (void**)&self->glGenTextures, "glGenTextures" }, + { (void**)&self->glDeleteTextures, "glDeleteTextures" }, + { (void**)&self->glActiveTexture, "glActiveTexture" }, + { (void**)&self->glBindTexture, "glBindTexture" }, + { (void**)&self->glBindImageTexture, "glBindImageTexture" }, + { (void**)&self->glTexParameteri, "glTexParameteri" }, + { (void**)&self->glTexParameteriv, "glTexParameteriv" }, + { (void**)&self->glTexParameterfv, "glTexParameterfv" }, + { (void**)&self->glTexImage2D, "glTexImage2D" }, + { (void**)&self->glTexSubImage2D, "glTexSubImage2D" }, + { (void**)&self->glTexStorage2D, "glTexStorage2D" }, + { (void**)&self->glGetTexImage, "glGetTexImage" }, + { (void**)&self->glGenFramebuffers, "glGenFramebuffers" }, + { (void**)&self->glBindFramebuffer, "glBindFramebuffer" }, + { (void**)&self->glDeleteFramebuffers, "glDeleteFramebuffers" }, + { (void**)&self->glMemoryBarrier, "glMemoryBarrier" }, + { (void**)&self->glViewport, "glViewport" }, + { (void**)&self->glFramebufferTexture2D, "glFramebufferTexture2D" }, + { (void**)&self->glDrawBuffers, "glDrawBuffers" }, + { (void**)&self->glCheckFramebufferStatus, "glCheckFramebufferStatus" }, + { (void**)&self->glBindBuffer, "glBindBuffer" }, + { (void**)&self->glGenBuffers, "glGenBuffers" }, + { (void**)&self->glBufferData, "glBufferData" }, + { (void**)&self->glBufferSubData, "glBufferSubData" }, + { (void**)&self->glDeleteBuffers, "glDeleteBuffers" }, + { (void**)&self->glGenVertexArrays, "glGenVertexArrays" }, + { (void**)&self->glBindVertexArray, "glBindVertexArray" }, + { (void**)&self->glDeleteVertexArrays, "glDeleteVertexArrays" }, + { (void**)&self->glCreateProgram, "glCreateProgram" }, + { (void**)&self->glCreateShader, "glCreateShader" }, + { (void**)&self->glAttachShader, "glAttachShader" }, + { (void**)&self->glBindAttribLocation, "glBindAttribLocation" }, + { (void**)&self->glCompileShader, "glCompileShader" }, + { (void**)&self->glLinkProgram, "glLinkProgram" }, + { (void**)&self->glShaderSource, "glShaderSource" }, + { (void**)&self->glUseProgram, "glUseProgram" }, + { (void**)&self->glGetProgramInfoLog, "glGetProgramInfoLog" }, + { (void**)&self->glGetShaderiv, "glGetShaderiv" }, + { (void**)&self->glGetShaderInfoLog, "glGetShaderInfoLog" }, + { (void**)&self->glDeleteProgram, "glDeleteProgram" }, + { (void**)&self->glDeleteShader, "glDeleteShader" }, + { (void**)&self->glGetProgramiv, "glGetProgramiv" }, + { (void**)&self->glVertexAttribPointer, "glVertexAttribPointer" }, + { (void**)&self->glEnableVertexAttribArray, "glEnableVertexAttribArray" }, + { (void**)&self->glDrawArrays, "glDrawArrays" }, + { (void**)&self->glEnable, "glEnable" }, + { (void**)&self->glDisable, "glDisable" }, + { (void**)&self->glBlendFunc, "glBlendFunc" }, + { (void**)&self->glPixelStorei, "glPixelStorei" }, + { (void**)&self->glGetUniformLocation, "glGetUniformLocation" }, + { (void**)&self->glUniform1f, "glUniform1f" }, + { (void**)&self->glUniform2f, "glUniform2f" }, + { (void**)&self->glUniform1i, "glUniform1i" }, + { (void**)&self->glUniform2i, "glUniform2i" }, + { (void**)&self->glUniformMatrix2fv, "glUniformMatrix2fv" }, + { (void**)&self->glDebugMessageCallback, "glDebugMessageCallback" }, + { (void**)&self->glScissor, "glScissor" }, + { (void**)&self->glReadPixels, "glReadPixels" }, + { (void**)&self->glMapBufferRange, "glMapBufferRange" }, + { (void**)&self->glUnmapBuffer, "glUnmapBuffer" }, + { (void**)&self->glGetIntegerv, "glGetIntegerv" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(library, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n"); + return false; + } + + return true; +} + +#define GL_DEBUG_TYPE_ERROR 0x824C +#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B +static void debug_callback(unsigned int source, unsigned int type, unsigned int id, unsigned int severity, int length, const char* message, const void* userParam) { + (void)source; + (void)id; + (void)length; + (void)userParam; + if(severity != GL_DEBUG_SEVERITY_NOTIFICATION) + fprintf(stderr, "gsr info: gl callback: %s type = 0x%x, severity = 0x%x, message = %s\n", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); +} + +/* TODO: check for glx swap control extension string (GLX_EXT_swap_control, etc) */ +static void set_vertical_sync_enabled(gsr_egl *egl, int enabled) { + int result = 0; + + if(egl->glXSwapIntervalEXT) { + assert(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11); + Display *display = gsr_window_get_display(egl->window); + const Window window = (Window)gsr_window_get_window(egl->window); + egl->glXSwapIntervalEXT(display, window, enabled ? 1 : 0); + } else if(egl->glXSwapIntervalMESA) { + result = egl->glXSwapIntervalMESA(enabled ? 1 : 0); + } else if(egl->glXSwapIntervalSGI) { + result = egl->glXSwapIntervalSGI(enabled ? 1 : 0); + } else { + static int warned = 0; + if (!warned) { + warned = 1; + fprintf(stderr, "gsr warning: setting vertical sync not supported\n"); + } + } + + if(result != 0) + fprintf(stderr, "gsr warning: setting vertical sync failed\n"); +} + +static void gsr_egl_disable_vsync(gsr_egl *self) { + switch(self->context_type) { + case GSR_GL_CONTEXT_TYPE_EGL: { + self->eglSwapInterval(self->egl_display, 0); + break; + } + case GSR_GL_CONTEXT_TYPE_GLX: { + set_vertical_sync_enabled(self, 0); + break; + } + } +} + +bool gsr_egl_load(gsr_egl *self, gsr_window *window, bool is_monitor_capture, bool enable_debug) { + memset(self, 0, sizeof(gsr_egl)); + self->context_type = GSR_GL_CONTEXT_TYPE_EGL; + self->window = window; + + dlerror(); /* clear */ + self->egl_library = dlopen("libEGL.so.1", RTLD_LAZY); + if(!self->egl_library) { + fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror()); + goto fail; + } + + self->glx_library = dlopen("libGLX.so.0", RTLD_LAZY); + + self->gl_library = dlopen("libGL.so.1", RTLD_LAZY); + if(!self->gl_library) { + fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror()); + goto fail; + } + + if(!gsr_egl_load_egl(self, self->egl_library)) + goto fail; + + /* In some distros (alpine for example libGLX doesn't exist, but libGL can be used instead) */ + if(!gsr_egl_load_glx(self, self->glx_library ? self->glx_library : self->gl_library)) + goto fail; + + if(!gsr_egl_load_gl(self, self->gl_library)) + goto fail; + + if(!gsr_egl_proc_load_egl(self)) + goto fail; + + if(!gsr_egl_create_window(self, enable_debug)) + goto fail; + + if(!gl_get_gpu_info(self, &self->gpu_info)) + goto fail; + + if(self->eglQueryDisplayAttribEXT && self->eglQueryDeviceStringEXT) { + intptr_t device = 0; + if(self->eglQueryDisplayAttribEXT(self->egl_display, EGL_DEVICE_EXT, &device) && device) + self->dri_card_path = self->eglQueryDeviceStringEXT((void*)device, EGL_DRM_DEVICE_FILE_EXT); + } + + /* Nvfbc requires glx */ + if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11 && is_monitor_capture && self->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { + self->context_type = GSR_GL_CONTEXT_TYPE_GLX; + self->dri_card_path = NULL; + if(!gsr_egl_switch_to_glx_context(self)) + goto fail; + } + + if(enable_debug) { + self->glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + self->glDebugMessageCallback(debug_callback, NULL); + } + + self->glEnable(GL_BLEND); + self->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + self->glPixelStorei(GL_PACK_ALIGNMENT, 1); + self->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + gsr_egl_disable_vsync(self); + + if(self->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { + /* This fixes nvenc codecs unable to load on openSUSE tumbleweed because of a cuda error. Don't ask me why */ + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + if(inside_flatpak) + system("flatpak-spawn --host -- sh -c 'grep -q openSUSE /etc/os-release && nvidia-smi -f /dev/null'"); + else + system("sh -c 'grep -q openSUSE /etc/os-release && nvidia-smi -f /dev/null'"); + } + + return true; + + fail: + gsr_egl_unload(self); + return false; +} + +void gsr_egl_unload(gsr_egl *self) { + if(self->egl_context) { + self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL); + self->eglDestroyContext(self->egl_display, self->egl_context); + self->egl_context = NULL; + } + + if(self->egl_surface) { + self->eglDestroySurface(self->egl_display, self->egl_surface); + self->egl_surface = NULL; + } + + if(self->egl_display) { + self->eglTerminate(self->egl_display); + self->egl_display = NULL; + } + + if(self->glx_context) { + assert(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11); + Display *display = gsr_window_get_display(self->window); + self->glXMakeContextCurrent(display, None, None, NULL); + self->glXDestroyContext(display, self->glx_context); + self->glx_context = NULL; + self->glx_fb_config = NULL; + } + + if(self->egl_library) { + dlclose(self->egl_library); + self->egl_library = NULL; + } + + if(self->glx_library) { + dlclose(self->glx_library); + self->glx_library = NULL; + } + + if(self->gl_library) { + dlclose(self->gl_library); + self->gl_library = NULL; + } + + memset(self, 0, sizeof(gsr_egl)); +} + +void gsr_egl_swap_buffers(gsr_egl *self) { + self->glFlush(); + // TODO: Use the minimal barrier required + self->glMemoryBarrier(GL_ALL_BARRIER_BITS); // GL_SHADER_IMAGE_ACCESS_BARRIER_BIT +} diff --git a/src/encoder/encoder.c b/src/encoder/encoder.c new file mode 100644 index 0000000..0f8eda5 --- /dev/null +++ b/src/encoder/encoder.c @@ -0,0 +1,155 @@ +#include "../../include/encoder/encoder.h" +#include "../../include/utils.h" + +#include <string.h> +#include <stdio.h> + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> + +bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size_t replay_buffer_num_packets, double replay_buffer_time, const char *replay_directory) { + memset(self, 0, sizeof(*self)); + self->num_recording_destinations = 0; + self->recording_destination_id_counter = 0; + + if(pthread_mutex_init(&self->file_write_mutex, NULL) != 0) { + fprintf(stderr, "gsr error: gsr_encoder_init: failed to create mutex\n"); + return false; + } + self->mutex_created = true; + + if(replay_buffer_num_packets > 0) { + self->replay_buffer = gsr_replay_buffer_create(replay_storage, replay_directory, replay_buffer_time, replay_buffer_num_packets); + if(!self->replay_buffer) { + fprintf(stderr, "gsr error: gsr_encoder_init: failed to create replay buffer\n"); + gsr_encoder_deinit(self); + return false; + } + } + + return true; +} + +void gsr_encoder_deinit(gsr_encoder *self) { + if(self->mutex_created) { + self->mutex_created = false; + pthread_mutex_destroy(&self->file_write_mutex); + } + + if(self->replay_buffer) { + gsr_replay_buffer_destroy(self->replay_buffer); + self->replay_buffer = NULL; + } + + self->num_recording_destinations = 0; + self->recording_destination_id_counter = 0; +} + +void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_context, int64_t pts, int stream_index) { + for(;;) { + AVPacket *av_packet = av_packet_alloc(); + if(!av_packet) + break; + + av_packet->data = NULL; + av_packet->size = 0; + int res = avcodec_receive_packet(codec_context, av_packet); + if(res == 0) { // we have a packet, send the packet to the muxer + av_packet->stream_index = stream_index; + av_packet->pts = pts; + av_packet->dts = pts; + + if(self->replay_buffer) { + const double time_now = clock_get_monotonic_seconds(); + if(!gsr_replay_buffer_append(self->replay_buffer, av_packet, time_now)) + fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to add replay buffer data\n"); + } + + pthread_mutex_lock(&self->file_write_mutex); + const bool is_keyframe = av_packet->flags & AV_PKT_FLAG_KEY; + for(size_t i = 0; i < self->num_recording_destinations; ++i) { + gsr_encoder_recording_destination *recording_destination = &self->recording_destinations[i]; + if(recording_destination->codec_context != codec_context) + continue; + + if(is_keyframe) + recording_destination->has_received_keyframe = true; + else if(!recording_destination->has_received_keyframe) + continue; + + av_packet->pts = pts - recording_destination->start_pts; + av_packet->dts = pts - recording_destination->start_pts; + + av_packet_rescale_ts(av_packet, codec_context->time_base, recording_destination->stream->time_base); + // TODO: Is av_interleaved_write_frame needed?. Answer: might be needed for mkv but dont use it! it causes frames to be inconsistent, skipping frames and duplicating frames. + // TODO: av_interleaved_write_frame might be needed for cfr, or always for flv + const int ret = av_write_frame(recording_destination->format_context, av_packet); + if(ret < 0) { + char error_buffer[AV_ERROR_MAX_STRING_SIZE]; + if(av_strerror(ret, error_buffer, sizeof(error_buffer)) < 0) + snprintf(error_buffer, sizeof(error_buffer), "Unknown error"); + fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet->stream_index, error_buffer, ret); + } + } + pthread_mutex_unlock(&self->file_write_mutex); + + av_packet_free(&av_packet); + } else if (res == AVERROR(EAGAIN)) { // we have no packet + // fprintf(stderr, "No packet!\n"); + av_packet_free(&av_packet); + break; + } else if (res == AVERROR_EOF) { // this is the end of the stream + av_packet_free(&av_packet); + fprintf(stderr, "End of stream!\n"); + break; + } else { + av_packet_free(&av_packet); + fprintf(stderr, "Unexpected error: %d\n", res); + break; + } + } +} + +size_t gsr_encoder_add_recording_destination(gsr_encoder *self, AVCodecContext *codec_context, AVFormatContext *format_context, AVStream *stream, int64_t start_pts) { + if(self->num_recording_destinations >= GSR_MAX_RECORDING_DESTINATIONS) { + fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, reached the max amount of recording destinations (%d)\n", GSR_MAX_RECORDING_DESTINATIONS); + return (size_t)-1; + } + + for(size_t i = 0; i < self->num_recording_destinations; ++i) { + if(self->recording_destinations[i].stream == stream) { + fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, the stream %p already exists as an output\n", (void*)stream); + return (size_t)-1; + } + } + + pthread_mutex_lock(&self->file_write_mutex); + gsr_encoder_recording_destination *recording_destination = &self->recording_destinations[self->num_recording_destinations]; + recording_destination->id = self->recording_destination_id_counter; + recording_destination->codec_context = codec_context; + recording_destination->format_context = format_context; + recording_destination->stream = stream; + recording_destination->start_pts = start_pts; + recording_destination->has_received_keyframe = false; + + ++self->recording_destination_id_counter; + ++self->num_recording_destinations; + pthread_mutex_unlock(&self->file_write_mutex); + + return recording_destination->id; +} + +bool gsr_encoder_remove_recording_destination(gsr_encoder *self, size_t id) { + bool found = false; + pthread_mutex_lock(&self->file_write_mutex); + for(size_t i = 0; i < self->num_recording_destinations; ++i) { + if(self->recording_destinations[i].id == id) { + self->recording_destinations[i] = self->recording_destinations[self->num_recording_destinations - 1]; + --self->num_recording_destinations; + found = true; + break; + } + } + pthread_mutex_unlock(&self->file_write_mutex); + return found; +} diff --git a/src/encoder/video/nvenc.c b/src/encoder/video/nvenc.c new file mode 100644 index 0000000..0318a17 --- /dev/null +++ b/src/encoder/video/nvenc.c @@ -0,0 +1,241 @@ +#include "../../../include/encoder/video/nvenc.h" +#include "../../../include/egl.h" +#include "../../../include/cuda.h" +#include "../../../include/window/window.h" + +#include <libavcodec/avcodec.h> +#include <libavutil/hwcontext_cuda.h> + +#include <stdlib.h> + +typedef struct { + gsr_video_encoder_nvenc_params params; + + unsigned int target_textures[2]; + vec2i target_texture_size[2]; + + AVBufferRef *device_ctx; + + gsr_cuda cuda; + CUgraphicsResource cuda_graphics_resources[2]; + CUarray mapped_arrays[2]; + CUstream cuda_stream; +} gsr_video_encoder_nvenc; + +static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context) { + self->device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA); + if(!self->device_ctx) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + return false; + } + + AVHWDeviceContext *hw_device_context = (AVHWDeviceContext*)self->device_ctx->data; + AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext*)hw_device_context->hwctx; + cuda_device_context->cuda_ctx = self->cuda.cu_ctx; + if(av_hwdevice_ctx_init(self->device_ctx) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + av_buffer_unref(&self->device_ctx); + return false; + } + + AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); + if(!frame_context) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hwframe context\n"); + av_buffer_unref(&self->device_ctx); + return false; + } + + AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)frame_context->data; + hw_frame_context->width = video_codec_context->width; + hw_frame_context->height = video_codec_context->height; + hw_frame_context->sw_format = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12; + hw_frame_context->format = video_codec_context->pix_fmt; + hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; + + if (av_hwframe_ctx_init(frame_context) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to initialize hardware frame context " + "(note: ffmpeg version needs to be > 4.0)\n"); + av_buffer_unref(&self->device_ctx); + //av_buffer_unref(&frame_context); + return false; + } + + self->cuda_stream = cuda_device_context->stream; + video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context); + av_buffer_unref(&frame_context); + return true; +} + +static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cuda_graphics_resource, CUarray *mapped_array, unsigned int texture_id) { + CUresult res; + res = cuda->cuGraphicsGLRegisterImage(cuda_graphics_resource, texture_id, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_NONE); + if (res != CUDA_SUCCESS) { + const char *err_str = "unknown"; + cuda->cuGetErrorString(res, &err_str); + fprintf(stderr, "gsr error: cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture " "id: %u\n", err_str, texture_id); + return false; + } + + res = cuda->cuGraphicsResourceSetMapFlags(*cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_NONE); + res = cuda->cuGraphicsMapResources(1, cuda_graphics_resource, 0); + + res = cuda->cuGraphicsSubResourceGetMappedArray(mapped_array, *cuda_graphics_resource, 0, 0); + return true; +} + +static bool gsr_video_encoder_nvenc_setup_textures(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context, AVFrame *frame) { + const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); + if(res < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + return false; + } + + const unsigned int internal_formats_nv12[2] = { GL_R8, GL_RG8 }; + const unsigned int internal_formats_p010[2] = { GL_R16, GL_RG16 }; + const unsigned int formats[2] = { GL_RED, GL_RG }; + const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size + + for(int i = 0; i < 2; ++i) { + self->target_texture_size[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; + self->target_textures[i] = gl_create_texture(self->params.egl, self->target_texture_size[i].x, self->target_texture_size[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); + if(self->target_textures[i] == 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: failed to create opengl texture\n"); + return false; + } + + if(!cuda_register_opengl_texture(&self->cuda, &self->cuda_graphics_resources[i], &self->mapped_arrays[i], self->target_textures[i])) { + return false; + } + } + + return true; +} + +static void gsr_video_encoder_nvenc_stop(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context); + +static bool gsr_video_encoder_nvenc_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) { + gsr_video_encoder_nvenc *self = encoder->priv; + + const bool is_x11 = gsr_window_get_display_server(self->params.egl->window) == GSR_DISPLAY_SERVER_X11; + const bool overclock = is_x11 ? self->params.overclock : false; + Display *display = is_x11 ? gsr_window_get_display(self->params.egl->window) : NULL; + if(!gsr_cuda_load(&self->cuda, display, overclock)) { + fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_start: failed to load cuda\n"); + gsr_video_encoder_nvenc_stop(self, video_codec_context); + return false; + } + + video_codec_context->width = FFALIGN(video_codec_context->width, 2); + video_codec_context->height = FFALIGN(video_codec_context->height, 2); + + if(video_codec_context->width < 128) + video_codec_context->width = 128; + + if(video_codec_context->height < 128) + video_codec_context->height = 128; + + frame->width = video_codec_context->width; + frame->height = video_codec_context->height; + + if(!gsr_video_encoder_nvenc_setup_context(self, video_codec_context)) { + gsr_video_encoder_nvenc_stop(self, video_codec_context); + return false; + } + + if(!gsr_video_encoder_nvenc_setup_textures(self, video_codec_context, frame)) { + gsr_video_encoder_nvenc_stop(self, video_codec_context); + return false; + } + + return true; +} + +void gsr_video_encoder_nvenc_stop(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context) { + self->params.egl->glDeleteTextures(2, self->target_textures); + self->target_textures[0] = 0; + self->target_textures[1] = 0; + + if(video_codec_context->hw_frames_ctx) + av_buffer_unref(&video_codec_context->hw_frames_ctx); + if(self->device_ctx) + av_buffer_unref(&self->device_ctx); + + if(self->cuda.cu_ctx) { + for(int i = 0; i < 2; ++i) { + if(self->cuda_graphics_resources[i]) { + self->cuda.cuGraphicsUnmapResources(1, &self->cuda_graphics_resources[i], 0); + self->cuda.cuGraphicsUnregisterResource(self->cuda_graphics_resources[i]); + self->cuda_graphics_resources[i] = 0; + } + } + } + + gsr_cuda_unload(&self->cuda); +} + +static void gsr_video_encoder_nvenc_copy_textures_to_frame(gsr_video_encoder *encoder, AVFrame *frame, gsr_color_conversion *color_conversion) { + gsr_video_encoder_nvenc *self = encoder->priv; + const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size + for(int i = 0; i < 2; ++i) { + CUDA_MEMCPY2D memcpy_struct; + memcpy_struct.srcXInBytes = 0; + memcpy_struct.srcY = 0; + memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY; + + memcpy_struct.dstXInBytes = 0; + memcpy_struct.dstY = 0; + memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE; + + memcpy_struct.srcArray = self->mapped_arrays[i]; + memcpy_struct.srcPitch = frame->width / div[i]; + memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i]; + memcpy_struct.dstPitch = frame->linesize[i]; + memcpy_struct.WidthInBytes = frame->width * (self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? 2 : 1); + memcpy_struct.Height = frame->height / div[i]; + // TODO: Remove this copy if possible + self->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, self->cuda_stream); + } + + // TODO: needed? + self->cuda.cuStreamSynchronize(self->cuda_stream); +} + +static void gsr_video_encoder_nvenc_get_textures(gsr_video_encoder *encoder, unsigned int *textures, vec2i *texture_sizes, int *num_textures, gsr_destination_color *destination_color) { + gsr_video_encoder_nvenc *self = encoder->priv; + textures[0] = self->target_textures[0]; + textures[1] = self->target_textures[1]; + texture_sizes[0] = self->target_texture_size[0]; + texture_sizes[1] = self->target_texture_size[1]; + *num_textures = 2; + *destination_color = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? GSR_DESTINATION_COLOR_P010 : GSR_DESTINATION_COLOR_NV12; +} + +static void gsr_video_encoder_nvenc_destroy(gsr_video_encoder *encoder, AVCodecContext *video_codec_context) { + gsr_video_encoder_nvenc_stop(encoder->priv, video_codec_context); + free(encoder->priv); + free(encoder); +} + +gsr_video_encoder* gsr_video_encoder_nvenc_create(const gsr_video_encoder_nvenc_params *params) { + gsr_video_encoder *encoder = calloc(1, sizeof(gsr_video_encoder)); + if(!encoder) + return NULL; + + gsr_video_encoder_nvenc *encoder_cuda = calloc(1, sizeof(gsr_video_encoder_nvenc)); + if(!encoder_cuda) { + free(encoder); + return NULL; + } + + encoder_cuda->params = *params; + + *encoder = (gsr_video_encoder) { + .start = gsr_video_encoder_nvenc_start, + .copy_textures_to_frame = gsr_video_encoder_nvenc_copy_textures_to_frame, + .get_textures = gsr_video_encoder_nvenc_get_textures, + .destroy = gsr_video_encoder_nvenc_destroy, + .priv = encoder_cuda + }; + + return encoder; +} diff --git a/src/encoder/video/software.c b/src/encoder/video/software.c new file mode 100644 index 0000000..b473bb9 --- /dev/null +++ b/src/encoder/video/software.c @@ -0,0 +1,129 @@ +#include "../../../include/encoder/video/software.h" +#include "../../../include/egl.h" +#include "../../../include/utils.h" + +#include <libavcodec/avcodec.h> +#include <libavutil/frame.h> + +#include <stdlib.h> + +#define LINESIZE_ALIGNMENT 4 + +typedef struct { + gsr_video_encoder_software_params params; + + unsigned int target_textures[2]; + vec2i texture_sizes[2]; +} gsr_video_encoder_software; + +static bool gsr_video_encoder_software_setup_textures(gsr_video_encoder_software *self, AVCodecContext *video_codec_context, AVFrame *frame) { + int res = av_frame_get_buffer(frame, LINESIZE_ALIGNMENT); + if(res < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_get_buffer failed: %d\n", res); + return false; + } + + res = av_frame_make_writable(frame); + if(res < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_make_writable failed: %d\n", res); + return false; + } + + const unsigned int internal_formats_nv12[2] = { GL_R8, GL_RG8 }; + const unsigned int internal_formats_p010[2] = { GL_R16, GL_RG16 }; + const unsigned int formats[2] = { GL_RED, GL_RG }; + const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size + + for(int i = 0; i < 2; ++i) { + self->texture_sizes[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; + self->target_textures[i] = gl_create_texture(self->params.egl, self->texture_sizes[i].x, self->texture_sizes[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); + if(self->target_textures[i] == 0) { + fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create opengl texture\n"); + return false; + } + } + + return true; +} + +static void gsr_video_encoder_software_stop(gsr_video_encoder_software *self, AVCodecContext *video_codec_context); + +static bool gsr_video_encoder_software_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) { + gsr_video_encoder_software *self = encoder->priv; + + video_codec_context->width = FFALIGN(video_codec_context->width, LINESIZE_ALIGNMENT); + video_codec_context->height = FFALIGN(video_codec_context->height, 2); + + frame->width = video_codec_context->width; + frame->height = video_codec_context->height; + + if(!gsr_video_encoder_software_setup_textures(self, video_codec_context, frame)) { + gsr_video_encoder_software_stop(self, video_codec_context); + return false; + } + + return true; +} + +void gsr_video_encoder_software_stop(gsr_video_encoder_software *self, AVCodecContext *video_codec_context) { + (void)video_codec_context; + self->params.egl->glDeleteTextures(2, self->target_textures); + self->target_textures[0] = 0; + self->target_textures[1] = 0; +} + +static void gsr_video_encoder_software_copy_textures_to_frame(gsr_video_encoder *encoder, AVFrame *frame, gsr_color_conversion *color_conversion) { + (void)encoder; + //gsr_video_encoder_software *self = encoder->priv; + // TODO: hdr support + const unsigned int formats[2] = { GL_RED, GL_RG }; + const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size + for(int i = 0; i < 2; ++i) { + // TODO: Use glPixelStore? + gsr_color_conversion_read_destination_texture(color_conversion, i, 0, 0, frame->width / div[i], frame->height / div[i], formats[i], GL_UNSIGNED_BYTE, frame->data[i]); + } + // cap_kms->kms.base.egl->eglSwapBuffers(cap_kms->kms.base.egl->egl_display, cap_kms->kms.base.egl->egl_surface); + + //self->params.egl->glFlush(); + //self->params.egl->glFinish(); +} + +static void gsr_video_encoder_software_get_textures(gsr_video_encoder *encoder, unsigned int *textures, vec2i *texture_sizes, int *num_textures, gsr_destination_color *destination_color) { + gsr_video_encoder_software *self = encoder->priv; + textures[0] = self->target_textures[0]; + textures[1] = self->target_textures[1]; + texture_sizes[0] = self->texture_sizes[0]; + texture_sizes[1] = self->texture_sizes[1]; + *num_textures = 2; + *destination_color = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? GSR_DESTINATION_COLOR_P010 : GSR_DESTINATION_COLOR_NV12; +} + +static void gsr_video_encoder_software_destroy(gsr_video_encoder *encoder, AVCodecContext *video_codec_context) { + gsr_video_encoder_software_stop(encoder->priv, video_codec_context); + free(encoder->priv); + free(encoder); +} + +gsr_video_encoder* gsr_video_encoder_software_create(const gsr_video_encoder_software_params *params) { + gsr_video_encoder *encoder = calloc(1, sizeof(gsr_video_encoder)); + if(!encoder) + return NULL; + + gsr_video_encoder_software *encoder_software = calloc(1, sizeof(gsr_video_encoder_software)); + if(!encoder_software) { + free(encoder); + return NULL; + } + + encoder_software->params = *params; + + *encoder = (gsr_video_encoder) { + .start = gsr_video_encoder_software_start, + .copy_textures_to_frame = gsr_video_encoder_software_copy_textures_to_frame, + .get_textures = gsr_video_encoder_software_get_textures, + .destroy = gsr_video_encoder_software_destroy, + .priv = encoder_software + }; + + return encoder; +} diff --git a/src/encoder/video/vaapi.c b/src/encoder/video/vaapi.c new file mode 100644 index 0000000..b32d8d3 --- /dev/null +++ b/src/encoder/video/vaapi.c @@ -0,0 +1,347 @@ +#include "../../../include/encoder/video/vaapi.h" +#include "../../../include/utils.h" +#include "../../../include/egl.h" + +#include <libavcodec/avcodec.h> +#include <libavutil/hwcontext_vaapi.h> +#include <libavutil/intreadwrite.h> + +#include <va/va.h> +#include <va/va_drm.h> +#include <va/va_drmcommon.h> + +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> + +typedef struct { + gsr_video_encoder_vaapi_params params; + + unsigned int target_textures[2]; + vec2i texture_sizes[2]; + + AVBufferRef *device_ctx; + VADisplay va_dpy; + VADRMPRIMESurfaceDescriptor prime; +} gsr_video_encoder_vaapi; + +static bool gsr_video_encoder_vaapi_setup_context(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context) { + char render_path[128]; + if(!gsr_card_path_get_render_path(self->params.egl->card_path, render_path)) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to get /dev/dri/renderDXXX file from %s\n", self->params.egl->card_path); + return false; + } + + if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hardware device context\n"); + return false; + } + + AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); + if(!frame_context) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hwframe context\n"); + av_buffer_unref(&self->device_ctx); + return false; + } + + AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)frame_context->data; + hw_frame_context->width = video_codec_context->width; + hw_frame_context->height = video_codec_context->height; + hw_frame_context->sw_format = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12; + hw_frame_context->format = video_codec_context->pix_fmt; + hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; + + //hw_frame_context->initial_pool_size = 20; + + AVVAAPIDeviceContext *vactx = ((AVHWDeviceContext*)self->device_ctx->data)->hwctx; + self->va_dpy = vactx->display; + + if (av_hwframe_ctx_init(frame_context) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to initialize hardware frame context " + "(note: ffmpeg version needs to be > 4.0)\n"); + av_buffer_unref(&self->device_ctx); + //av_buffer_unref(&frame_context); + return false; + } + + video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context); + av_buffer_unref(&frame_context); + return true; +} + +static uint32_t fourcc(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + return (d << 24) | (c << 16) | (b << 8) | a; +} + +static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context, AVFrame *frame) { + const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); + if(res < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + return false; + } + + VASurfaceID target_surface_id = (uintptr_t)frame->data[3]; + + VAStatus va_status = vaExportSurfaceHandle(self->va_dpy, target_surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, &self->prime); + if(va_status != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: vaExportSurfaceHandle failed, error: %d\n", va_status); + return false; + } + vaSyncSurface(self->va_dpy, target_surface_id); + + const uint32_t formats_nv12[2] = { fourcc('R', '8', ' ', ' '), fourcc('G', 'R', '8', '8') }; + const uint32_t formats_p010[2] = { fourcc('R', '1', '6', ' '), fourcc('G', 'R', '3', '2') }; + + if(self->prime.fourcc == VA_FOURCC_NV12 || self->prime.fourcc == VA_FOURCC_P010) { + const uint32_t *formats = self->prime.fourcc == VA_FOURCC_NV12 ? formats_nv12 : formats_p010; + const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size + + self->params.egl->glGenTextures(2, self->target_textures); + for(int i = 0; i < 2; ++i) { + const int layer = i; + + int fds[4]; + uint32_t offsets[4]; + uint32_t pitches[4]; + uint64_t modifiers[4]; + for(uint32_t j = 0; j < self->prime.layers[layer].num_planes; ++j) { + // TODO: Close these? in _stop, using self->prime + fds[j] = self->prime.objects[self->prime.layers[layer].object_index[j]].fd; + offsets[j] = self->prime.layers[layer].offset[j]; + pitches[j] = self->prime.layers[layer].pitch[j]; + modifiers[j] = self->prime.objects[self->prime.layers[layer].object_index[j]].drm_format_modifier; + } + + intptr_t img_attr[44]; + setup_dma_buf_attrs(img_attr, formats[i], self->prime.width / div[i], self->prime.height / div[i], + fds, offsets, pitches, modifiers, self->prime.layers[layer].num_planes, true); + self->texture_sizes[i] = (vec2i){ self->prime.width / div[i], self->prime.height / div[i] }; + + while(self->params.egl->eglGetError() != EGL_SUCCESS){} + EGLImage image = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr); + if(!image) { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to create egl image from drm fd for output drm fd, error: %d\n", self->params.egl->eglGetError()); + return false; + } + + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->target_textures[i]); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + while(self->params.egl->glGetError()) {} + while(self->params.egl->eglGetError() != EGL_SUCCESS){} + self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); + if(self->params.egl->glGetError() != 0 || self->params.egl->eglGetError() != EGL_SUCCESS) { + // TODO: Get the error properly + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to bind egl image to gl texture, error: %d\n", self->params.egl->eglGetError()); + self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + return false; + } + + self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + } + + return true; + } else { + fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010\n", self->prime.fourcc); + return false; + } +} + +static void gsr_video_encoder_vaapi_stop(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context); + +static bool supports_hevc_without_padding(const char *card_path) { +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 28, 100) && VA_CHECK_VERSION(1, 21, 0) + VAStatus va_status; + VAConfigID va_config = 0; + unsigned int num_surface_attr = 0; + VASurfaceAttrib *surface_attr_list = NULL; + bool supports_surface_attrib_alignment_size = false; + int va_major = 0; + int va_minor = 0; + bool initialized = false; + + char render_path[128]; + if(!gsr_card_path_get_render_path(card_path, render_path)) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + return false; + } + + const int drm_fd = open(render_path, O_RDWR); + if(drm_fd == -1) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to open device %s\n", render_path); + return false; + } + + const VADisplay va_dpy = vaGetDisplayDRM(drm_fd); + if(!va_dpy) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get vaapi display for device %s\n", render_path); + goto done; + } + + vaSetInfoCallback(va_dpy, NULL, NULL); + + if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: vaInitialize failed\n"); + goto done; + } + initialized = true; + + va_status = vaCreateConfig(va_dpy, VAProfileHEVCMain, VAEntrypointEncSlice, NULL, 0, &va_config); + if(va_status != VA_STATUS_SUCCESS) { + va_status = vaCreateConfig(va_dpy, VAProfileHEVCMain, VAEntrypointEncSliceLP, NULL, 0, &va_config); + if(va_status != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to create hevc vaapi config, error: %s (%d)\n", vaErrorStr(va_status), va_status); + return false; + } + } + + va_status = vaQuerySurfaceAttributes(va_dpy, va_config, 0, &num_surface_attr); + if(va_status != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes size, error: %s (%d)\n", vaErrorStr(va_status), va_status); + goto done; + } + + surface_attr_list = malloc(num_surface_attr * sizeof(VASurfaceAttrib)); + if(!surface_attr_list) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to allocate memory for %u vaapi surface attributes, error: %s (%d)\n", num_surface_attr, vaErrorStr(va_status), va_status); + goto done; + } + + va_status = vaQuerySurfaceAttributes(va_dpy, va_config, surface_attr_list, &num_surface_attr); + if(va_status != VA_STATUS_SUCCESS) { + fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes data, error: %s (%d)\n", vaErrorStr(va_status), va_status); + goto done; + } + + for(unsigned int i = 0; i < num_surface_attr; ++i) { + if(surface_attr_list[i].type == VASurfaceAttribAlignmentSize) { + supports_surface_attrib_alignment_size = true; + break; + } + } + + done: + free(surface_attr_list); + if(va_config > 0) + vaDestroyConfig(va_dpy, va_config); + if(initialized) + vaTerminate(va_dpy); + if(drm_fd > 0) + close(drm_fd); + return supports_surface_attrib_alignment_size; +#else + return false; +#endif +} + +static bool gsr_video_encoder_vaapi_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) { + gsr_video_encoder_vaapi *self = encoder->priv; + + if(self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_AMD && video_codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(supports_hevc_without_padding(self->params.egl->card_path)) { + video_codec_context->width = FFALIGN(video_codec_context->width, 2); + video_codec_context->height = FFALIGN(video_codec_context->height, 2); + } else { + video_codec_context->width = FFALIGN(video_codec_context->width, 256); + video_codec_context->height = FFALIGN(video_codec_context->height, 256); + } + } else if(self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_AMD && video_codec_context->codec_id == AV_CODEC_ID_AV1) { + // TODO: Dont do this for VCN 5 and forward which should fix this hardware bug + video_codec_context->width = FFALIGN(video_codec_context->width, 64); + // AMD driver has special case handling for 1080 height to set it to 1082 instead of 1088 (1080 aligned to 16). + // TODO: Set height to 1082 in this case, but it wont work because it will be aligned to 1088. + if(video_codec_context->height == 1080) { + video_codec_context->height = 1080; + } else { + video_codec_context->height = FFALIGN(video_codec_context->height, 16); + } + } else { + video_codec_context->width = FFALIGN(video_codec_context->width, 2); + video_codec_context->height = FFALIGN(video_codec_context->height, 2); + } + + if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) { + fprintf(stderr, "gsr warning: gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264 codec instead (-k h264) to get around this issue\n"); + } + + if(video_codec_context->width < 128) + video_codec_context->width = 128; + + if(video_codec_context->height < 128) + video_codec_context->height = 128; + + frame->width = video_codec_context->width; + frame->height = video_codec_context->height; + + if(!gsr_video_encoder_vaapi_setup_context(self, video_codec_context)) { + gsr_video_encoder_vaapi_stop(self, video_codec_context); + return false; + } + + if(!gsr_video_encoder_vaapi_setup_textures(self, video_codec_context, frame)) { + gsr_video_encoder_vaapi_stop(self, video_codec_context); + return false; + } + + return true; +} + +void gsr_video_encoder_vaapi_stop(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context) { + self->params.egl->glDeleteTextures(2, self->target_textures); + self->target_textures[0] = 0; + self->target_textures[1] = 0; + + if(video_codec_context->hw_frames_ctx) + av_buffer_unref(&video_codec_context->hw_frames_ctx); + if(self->device_ctx) + av_buffer_unref(&self->device_ctx); + + for(uint32_t i = 0; i < self->prime.num_objects; ++i) { + if(self->prime.objects[i].fd > 0) { + close(self->prime.objects[i].fd); + self->prime.objects[i].fd = 0; + } + } +} + +static void gsr_video_encoder_vaapi_get_textures(gsr_video_encoder *encoder, unsigned int *textures, vec2i *texture_sizes, int *num_textures, gsr_destination_color *destination_color) { + gsr_video_encoder_vaapi *self = encoder->priv; + textures[0] = self->target_textures[0]; + textures[1] = self->target_textures[1]; + texture_sizes[0] = self->texture_sizes[0]; + texture_sizes[1] = self->texture_sizes[1]; + *num_textures = 2; + *destination_color = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? GSR_DESTINATION_COLOR_P010 : GSR_DESTINATION_COLOR_NV12; +} + +static void gsr_video_encoder_vaapi_destroy(gsr_video_encoder *encoder, AVCodecContext *video_codec_context) { + gsr_video_encoder_vaapi_stop(encoder->priv, video_codec_context); + free(encoder->priv); + free(encoder); +} + +gsr_video_encoder* gsr_video_encoder_vaapi_create(const gsr_video_encoder_vaapi_params *params) { + gsr_video_encoder *encoder = calloc(1, sizeof(gsr_video_encoder)); + if(!encoder) + return NULL; + + gsr_video_encoder_vaapi *encoder_vaapi = calloc(1, sizeof(gsr_video_encoder_vaapi)); + if(!encoder_vaapi) { + free(encoder); + return NULL; + } + + encoder_vaapi->params = *params; + + *encoder = (gsr_video_encoder) { + .start = gsr_video_encoder_vaapi_start, + .get_textures = gsr_video_encoder_vaapi_get_textures, + .destroy = gsr_video_encoder_vaapi_destroy, + .priv = encoder_vaapi + }; + + return encoder; +} diff --git a/src/encoder/video/video.c b/src/encoder/video/video.c new file mode 100644 index 0000000..80f2cf0 --- /dev/null +++ b/src/encoder/video/video.c @@ -0,0 +1,28 @@ +#include "../../../include/encoder/video/video.h" + +#include <assert.h> + +bool gsr_video_encoder_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) { + assert(!encoder->started); + bool res = encoder->start(encoder, video_codec_context, frame); + if(res) + encoder->started = true; + return res; +} + +void gsr_video_encoder_destroy(gsr_video_encoder *encoder, AVCodecContext *video_codec_context) { + assert(encoder->started); + encoder->started = false; + encoder->destroy(encoder, video_codec_context); +} + +void gsr_video_encoder_copy_textures_to_frame(gsr_video_encoder *encoder, AVFrame *frame, gsr_color_conversion *color_conversion) { + assert(encoder->started); + if(encoder->copy_textures_to_frame) + encoder->copy_textures_to_frame(encoder, frame, color_conversion); +} + +void gsr_video_encoder_get_textures(gsr_video_encoder *encoder, unsigned int *textures, vec2i *texture_sizes, int *num_textures, gsr_destination_color *destination_color) { + assert(encoder->started); + encoder->get_textures(encoder, textures, texture_sizes, num_textures, destination_color); +} diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c new file mode 100644 index 0000000..5fa3c3d --- /dev/null +++ b/src/encoder/video/vulkan.c @@ -0,0 +1,315 @@ +#include "../../../include/encoder/video/vulkan.h" +#include "../../../include/utils.h" +#include "../../../include/egl.h" + +#include <libavcodec/avcodec.h> +#define VK_NO_PROTOTYPES +//#include <libavutil/hwcontext_vulkan.h> + +//#include <vulkan/vulkan_core.h> + +#define GL_HANDLE_TYPE_OPAQUE_FD_EXT 0x9586 +#define GL_TEXTURE_TILING_EXT 0x9580 +#define GL_OPTIMAL_TILING_EXT 0x9584 +#define GL_LINEAR_TILING_EXT 0x9585 + +typedef struct { + gsr_video_encoder_vulkan_params params; + unsigned int target_textures[2]; + vec2i texture_sizes[2]; + AVBufferRef *device_ctx; +} gsr_video_encoder_vulkan; + +static bool gsr_video_encoder_vulkan_setup_context(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context) { + AVDictionary *options = NULL; + //av_dict_set(&options, "linear_images", "1", 0); + //av_dict_set(&options, "disable_multiplane", "1", 0); +#if 0 + // TODO: Use correct device + if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VULKAN, NULL, options, 0) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hardware device context\n"); + return false; + } + + AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); + if(!frame_context) { + fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hwframe context\n"); + av_buffer_unref(&self->device_ctx); + return false; + } + + AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)frame_context->data; + hw_frame_context->width = video_codec_context->width; + hw_frame_context->height = video_codec_context->height; + hw_frame_context->sw_format = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12; + hw_frame_context->format = video_codec_context->pix_fmt; + hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; + + //AVVulkanFramesContext *vk_frame_ctx = (AVVulkanFramesContext*)hw_frame_context->hwctx; + //hw_frame_context->initial_pool_size = 20; + + if (av_hwframe_ctx_init(frame_context) < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to initialize hardware frame context " + "(note: ffmpeg version needs to be > 4.0)\n"); + av_buffer_unref(&self->device_ctx); + //av_buffer_unref(&frame_context); + return false; + } + + video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context); + av_buffer_unref(&frame_context); +#endif + return true; +} +#if 0 +static AVVulkanDeviceContext* video_codec_context_get_vulkan_data(AVCodecContext *video_codec_context) { + AVBufferRef *hw_frames_ctx = video_codec_context->hw_frames_ctx; + if(!hw_frames_ctx) + return NULL; + + AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)hw_frames_ctx->data; + AVHWDeviceContext *device_context = (AVHWDeviceContext*)hw_frame_context->device_ctx; + if(device_context->type != AV_HWDEVICE_TYPE_VULKAN) + return NULL; + + return (AVVulkanDeviceContext*)device_context->hwctx; +} + +static uint32_t get_memory_type_idx(VkPhysicalDevice pdev, const VkMemoryRequirements *mem_reqs, VkMemoryPropertyFlagBits prop_flags, PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties) { + VkPhysicalDeviceMemoryProperties pdev_mem_props; + uint32_t i; + + vkGetPhysicalDeviceMemoryProperties(pdev, &pdev_mem_props); + + for (i = 0; i < pdev_mem_props.memoryTypeCount; i++) { + const VkMemoryType *type = &pdev_mem_props.memoryTypes[i]; + + if ((mem_reqs->memoryTypeBits & (1 << i)) && + (type->propertyFlags & prop_flags) == prop_flags) { + return i; + break; + } + } + return UINT32_MAX; +} +#endif +static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context, AVFrame *frame) { + const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); + if(res < 0) { + fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + return false; + } + + while(self->params.egl->glGetError()) {} +#if 0 + AVVkFrame *target_surface_id = (AVVkFrame*)frame->data[0]; + AVVulkanDeviceContext* vv = video_codec_context_get_vulkan_data(video_codec_context); + const size_t luma_size = frame->width * frame->height; + if(vv) { + PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements = (PFN_vkGetImageMemoryRequirements)vv->get_proc_addr(vv->inst, "vkGetImageMemoryRequirements"); + PFN_vkAllocateMemory vkAllocateMemory = (PFN_vkAllocateMemory)vv->get_proc_addr(vv->inst, "vkAllocateMemory"); + PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties = (PFN_vkGetPhysicalDeviceMemoryProperties)vv->get_proc_addr(vv->inst, "vkGetPhysicalDeviceMemoryProperties"); + PFN_vkGetMemoryFdKHR vkGetMemoryFdKHR = (PFN_vkGetMemoryFdKHR)vv->get_proc_addr(vv->inst, "vkGetMemoryFdKHR"); + + VkMemoryRequirements mem_reqs = {0}; + vkGetImageMemoryRequirements(vv->act_dev, target_surface_id->img[0], &mem_reqs); + + fprintf(stderr, "size: %lu, alignment: %lu, memory bits: 0x%08x\n", mem_reqs.size, mem_reqs.alignment, mem_reqs.memoryTypeBits); + VkDeviceMemory mem; + { + VkExportMemoryAllocateInfo exp_mem_info; + VkMemoryAllocateInfo mem_alloc_info; + VkMemoryDedicatedAllocateInfoKHR ded_info; + + memset(&exp_mem_info, 0, sizeof(exp_mem_info)); + exp_mem_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + exp_mem_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + memset(&ded_info, 0, sizeof(ded_info)); + ded_info.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + ded_info.image = target_surface_id->img[0]; + + exp_mem_info.pNext = &ded_info; + + memset(&mem_alloc_info, 0, sizeof(mem_alloc_info)); + mem_alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + mem_alloc_info.pNext = &exp_mem_info; + mem_alloc_info.allocationSize = target_surface_id->size[0]; + mem_alloc_info.memoryTypeIndex = get_memory_type_idx(vv->phys_dev, &mem_reqs, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vkGetPhysicalDeviceMemoryProperties); + + if (mem_alloc_info.memoryTypeIndex == UINT32_MAX) { + fprintf(stderr, "No suitable memory type index found.\n"); + return VK_NULL_HANDLE; + } + + if (vkAllocateMemory(vv->act_dev, &mem_alloc_info, 0, &mem) != + VK_SUCCESS) + return VK_NULL_HANDLE; + + fprintf(stderr, "memory: %p\n", (void*)mem); + + } + + fprintf(stderr, "target surface id: %p, %zu, %zu\n", (void*)target_surface_id->mem[0], target_surface_id->offset[0], target_surface_id->offset[1]); + fprintf(stderr, "vkGetMemoryFdKHR: %p\n", (void*)vkGetMemoryFdKHR); + + int fd = 0; + VkMemoryGetFdInfoKHR fd_info; + memset(&fd_info, 0, sizeof(fd_info)); + fd_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + fd_info.memory = target_surface_id->mem[0]; + fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + if(vkGetMemoryFdKHR(vv->act_dev, &fd_info, &fd) != VK_SUCCESS) { + fprintf(stderr, "failed!\n"); + } else { + fprintf(stderr, "fd: %d\n", fd); + } + + fprintf(stderr, "glImportMemoryFdEXT: %p, size: %zu\n", (void*)self->params.egl->glImportMemoryFdEXT, target_surface_id->size[0]); + const int tiling = target_surface_id->tiling == VK_IMAGE_TILING_LINEAR ? GL_LINEAR_TILING_EXT : GL_OPTIMAL_TILING_EXT; + + if(tiling != GL_OPTIMAL_TILING_EXT) { + fprintf(stderr, "tiling %d is not supported, only GL_OPTIMAL_TILING_EXT (%d) is supported\n", tiling, GL_OPTIMAL_TILING_EXT); + } + + + unsigned int gl_memory_obj = 0; + self->params.egl->glCreateMemoryObjectsEXT(1, &gl_memory_obj); + + //const int dedicated = GL_TRUE; + //self->params.egl->glMemoryObjectParameterivEXT(gl_memory_obj, GL_DEDICATED_MEMORY_OBJECT_EXT, &dedicated); + + self->params.egl->glImportMemoryFdEXT(gl_memory_obj, target_surface_id->size[0], GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); + if(!self->params.egl->glIsMemoryObjectEXT(gl_memory_obj)) + fprintf(stderr, "failed to create object!\n"); + + fprintf(stderr, "gl memory obj: %u, error: %d\n", gl_memory_obj, self->params.egl->glGetError()); + + // fprintf(stderr, "0 gl error: %d\n", self->params.egl->glGetError()); + // unsigned int vertex_buffer = 0; + // self->params.egl->glGenBuffers(1, &vertex_buffer); + // self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); + // self->params.egl->glBufferStorageMemEXT(GL_ARRAY_BUFFER, target_surface_id->size[0], gl_memory_obj, target_surface_id->offset[0]); + // fprintf(stderr, "1 gl error: %d\n", self->params.egl->glGetError()); + + // fprintf(stderr, "0 gl error: %d\n", self->params.egl->glGetError()); + // unsigned int buffer = 0; + // self->params.egl->glCreateBuffers(1, &buffer); + // self->params.egl->glNamedBufferStorageMemEXT(buffer, target_surface_id->size[0], gl_memory_obj, target_surface_id->offset[0]); + // fprintf(stderr, "1 gl error: %d\n", self->params.egl->glGetError()); + + self->params.egl->glGenTextures(1, &self->target_textures[0]); + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->target_textures[0]); + + fprintf(stderr, "1 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_TILING_EXT, tiling); + + fprintf(stderr, "tiling: %d\n", tiling); + + fprintf(stderr, "2 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glTexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_R8, frame->width, frame->height, gl_memory_obj, target_surface_id->offset[0]); + + fprintf(stderr, "3 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + + self->params.egl->glGenTextures(1, &self->target_textures[1]); + self->params.egl->glBindTexture(GL_TEXTURE_2D, self->target_textures[1]); + + fprintf(stderr, "1 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_TILING_EXT, tiling); + + fprintf(stderr, "tiling: %d\n", tiling); + + fprintf(stderr, "2 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glTexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RG8, frame->width/2, frame->height/2, gl_memory_obj, target_surface_id->offset[0] + luma_size); + + fprintf(stderr, "3 gl error: %d\n", self->params.egl->glGetError()); + self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); + + self->texture_sizes[0] = (vec2i){ frame->width, frame->height }; + self->texture_sizes[1] = (vec2i){ frame->width/2, frame->height/2 }; + } +#endif + return true; +} + +static void gsr_video_encoder_vulkan_stop(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context); + +static bool gsr_video_encoder_vulkan_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) { + gsr_video_encoder_vulkan *self = encoder->priv; + + video_codec_context->width = FFALIGN(video_codec_context->width, 2); + video_codec_context->height = FFALIGN(video_codec_context->height, 2); + + if(video_codec_context->width < 128) + video_codec_context->width = 128; + + if(video_codec_context->height < 128) + video_codec_context->height = 128; + + frame->width = video_codec_context->width; + frame->height = video_codec_context->height; + + if(!gsr_video_encoder_vulkan_setup_context(self, video_codec_context)) { + gsr_video_encoder_vulkan_stop(self, video_codec_context); + return false; + } + + if(!gsr_video_encoder_vulkan_setup_textures(self, video_codec_context, frame)) { + gsr_video_encoder_vulkan_stop(self, video_codec_context); + return false; + } + + return true; +} + +void gsr_video_encoder_vulkan_stop(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context) { + self->params.egl->glDeleteTextures(2, self->target_textures); + self->target_textures[0] = 0; + self->target_textures[1] = 0; + + if(video_codec_context->hw_frames_ctx) + av_buffer_unref(&video_codec_context->hw_frames_ctx); + if(self->device_ctx) + av_buffer_unref(&self->device_ctx); +} + +static void gsr_video_encoder_vulkan_get_textures(gsr_video_encoder *encoder, unsigned int *textures, vec2i *texture_sizes, int *num_textures, gsr_destination_color *destination_color) { + gsr_video_encoder_vulkan *self = encoder->priv; + textures[0] = self->target_textures[0]; + textures[1] = self->target_textures[1]; + texture_sizes[0] = self->texture_sizes[0]; + texture_sizes[1] = self->texture_sizes[1]; + *num_textures = 2; + *destination_color = self->params.color_depth == GSR_COLOR_DEPTH_10_BITS ? GSR_DESTINATION_COLOR_P010 : GSR_DESTINATION_COLOR_NV12; +} + +static void gsr_video_encoder_vulkan_destroy(gsr_video_encoder *encoder, AVCodecContext *video_codec_context) { + gsr_video_encoder_vulkan_stop(encoder->priv, video_codec_context); + free(encoder->priv); + free(encoder); +} + +gsr_video_encoder* gsr_video_encoder_vulkan_create(const gsr_video_encoder_vulkan_params *params) { + gsr_video_encoder *encoder = calloc(1, sizeof(gsr_video_encoder)); + if(!encoder) + return NULL; + + gsr_video_encoder_vulkan *encoder_vulkan = calloc(1, sizeof(gsr_video_encoder_vulkan)); + if(!encoder_vulkan) { + free(encoder); + return NULL; + } + + encoder_vulkan->params = *params; + + *encoder = (gsr_video_encoder) { + .start = gsr_video_encoder_vulkan_start, + .copy_textures_to_frame = NULL, + .get_textures = gsr_video_encoder_vulkan_get_textures, + .destroy = gsr_video_encoder_vulkan_destroy, + .priv = encoder_vulkan + }; + + return encoder; +} diff --git a/src/image_writer.c b/src/image_writer.c new file mode 100644 index 0000000..afa9744 --- /dev/null +++ b/src/image_writer.c @@ -0,0 +1,83 @@ +#include "../include/image_writer.h" +#include "../include/egl.h" +#include "../include/utils.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "../external/stb_image_write.h" + +#include <stdlib.h> +#include <stdint.h> +#include <stdio.h> +#include <assert.h> + +/* TODO: Support hdr/10-bit */ +bool gsr_image_writer_init_opengl(gsr_image_writer *self, gsr_egl *egl, int width, int height) { + memset(self, 0, sizeof(*self)); + self->egl = egl; + self->width = width; + self->height = height; + self->texture = gl_create_texture(self->egl, self->width, self->height, GL_RGBA8, GL_RGBA, GL_NEAREST); /* TODO: use GL_RGB16 instead of GL_RGB8 for hdr/10-bit */ + if(self->texture == 0) { + fprintf(stderr, "gsr error: gsr_image_writer_init: failed to create texture\n"); + return false; + } + return true; +} + +void gsr_image_writer_deinit(gsr_image_writer *self) { + if(self->texture) { + self->egl->glDeleteTextures(1, &self->texture); + self->texture = 0; + } +} + +static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const char *filepath, gsr_image_format image_format, int quality, const void *data) { + if(quality < 1) + quality = 1; + else if(quality > 100) + quality = 100; + + bool success = false; + switch(image_format) { + case GSR_IMAGE_FORMAT_JPEG: + success = stbi_write_jpg(filepath, self->width, self->height, 4, data, quality); + break; + case GSR_IMAGE_FORMAT_PNG: + success = stbi_write_png(filepath, self->width, self->height, 4, data, 0); + break; + } + + if(!success) + fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to write image data to output file %s\n", filepath); + + return success; +} + +static bool gsr_image_writer_write_opengl_texture_to_file(gsr_image_writer *self, const char *filepath, gsr_image_format image_format, int quality) { + uint8_t *frame_data = malloc(self->width * self->height * 4); + if(!frame_data) { + fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to allocate memory for image frame\n"); + return false; + } + + unsigned int fbo = 0; + self->egl->glGenFramebuffers(1, &fbo); + self->egl->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + self->egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->texture, 0); + + self->egl->glReadPixels(0, 0, self->width, self->height, GL_RGBA, GL_UNSIGNED_BYTE, frame_data); + + self->egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + self->egl->glDeleteFramebuffers(1, &fbo); + + self->egl->glFlush(); + self->egl->glFinish(); + + const bool success = gsr_image_writer_write_memory_to_file(self, filepath, image_format, quality, frame_data); + free(frame_data); + return success; +} + +bool gsr_image_writer_write_to_file(gsr_image_writer *self, const char *filepath, gsr_image_format image_format, int quality) { + return gsr_image_writer_write_opengl_texture_to_file(self, filepath, image_format, quality); +} diff --git a/src/library_loader.c b/src/library_loader.c new file mode 100644 index 0000000..0aeee9b --- /dev/null +++ b/src/library_loader.c @@ -0,0 +1,34 @@ +#include "../include/library_loader.h" + +#include <dlfcn.h> +#include <stdbool.h> +#include <stdio.h> + +void* dlsym_print_fail(void *handle, const char *name, bool required) { + dlerror(); + void *sym = dlsym(handle, name); + char *err_str = dlerror(); + + if(!sym) + fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)"); + + return sym; +} + +/* |dlsyms| should be null terminated */ +bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms) { + bool success = true; + for(int i = 0; dlsyms[i].func; ++i) { + *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, true); + if(!*dlsyms[i].func) + success = false; + } + return success; +} + +/* |dlsyms| should be null terminated */ +void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms) { + for(int i = 0; dlsyms[i].func; ++i) { + *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, false); + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..396426f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,4629 @@ +extern "C" { +#include "../include/capture/nvfbc.h" +#include "../include/capture/xcomposite.h" +#include "../include/capture/ximage.h" +#include "../include/capture/kms.h" +#include "../include/capture/v4l2.h" +#ifdef GSR_PORTAL +#include "../include/capture/portal.h" +#include "../include/dbus.h" +#endif +#ifdef GSR_APP_AUDIO +#include "../include/pipewire_audio.h" +#endif +#include "../include/encoder/encoder.h" +#include "../include/encoder/video/nvenc.h" +#include "../include/encoder/video/vaapi.h" +#include "../include/encoder/video/vulkan.h" +#include "../include/encoder/video/software.h" +#include "../include/codec_query/nvenc.h" +#include "../include/codec_query/vaapi.h" +#include "../include/codec_query/vulkan.h" +#include "../include/window/x11.h" +#include "../include/window/wayland.h" +#include "../include/egl.h" +#include "../include/utils.h" +#include "../include/damage.h" +#include "../include/color_conversion.h" +#include "../include/image_writer.h" +#include "../include/args_parser.h" +#include "../include/plugins.h" +#include "../kms/client/kms_client.h" +} + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string> +#include <thread> +#include <mutex> +#include <signal.h> +#include <sys/stat.h> +#include <unistd.h> +#include <sys/wait.h> +#include <inttypes.h> +#include <libgen.h> +#include <malloc.h> + +#include "../include/sound.hpp" + +extern "C" { +#include <libavutil/pixfmt.h> +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libavutil/opt.h> +#include <libswresample/swresample.h> +#include <libavutil/avutil.h> +#include <libavutil/time.h> +#include <libavutil/mastering_display_metadata.h> +#include <libavfilter/avfilter.h> +#include <libavfilter/buffersink.h> +#include <libavfilter/buffersrc.h> +} + +#include <future> + +#ifndef GSR_VERSION +#define GSR_VERSION "unknown" +#endif + +// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd. + +// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 + +static const int AUDIO_SAMPLE_RATE = 48000; + +static const int VIDEO_STREAM_INDEX = 0; + +static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; + +typedef struct { + const gsr_window *window; +} MonitorOutputCallbackUserdata; + +static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) { + const MonitorOutputCallbackUserdata *options = (MonitorOutputCallbackUserdata*)userdata; + vec2i monitor_position = monitor->pos; + vec2i monitor_size = monitor->size; + if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; + drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); + if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) + std::swap(monitor_size.x, monitor_size.y); + } + fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); +} + +typedef struct { + char *output_name; +} FirstOutputCallback; + +static void get_first_output_callback(const gsr_monitor *monitor, void *userdata) { + FirstOutputCallback *data = (FirstOutputCallback*)userdata; + if(!data->output_name) + data->output_name = strdup(monitor->name); +} + +typedef struct { + gsr_window *window; + vec2i position; + char *output_name; + vec2i monitor_pos; + vec2i monitor_size; +} MonitorByPositionCallback; + +static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *userdata) { + MonitorByPositionCallback *data = (MonitorByPositionCallback*)userdata; + + vec2i monitor_position = monitor->pos; + vec2i monitor_size = monitor->size; + if(gsr_window_get_display_server(data->window) == GSR_DISPLAY_SERVER_WAYLAND) { + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; + drm_monitor_get_display_server_data(data->window, monitor, &monitor_rotation, &monitor_position); + if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) + std::swap(monitor_size.x, monitor_size.y); + } + + if(!data->output_name && data->position.x >= monitor_position.x && data->position.x <= monitor_position.x + monitor_size.x + && data->position.y >= monitor_position.y && data->position.y <= monitor_position.y + monitor_size.y) + { + data->output_name = strdup(monitor->name); + data->monitor_pos = monitor_position; + data->monitor_size = monitor_size; + } +} + +static char* av_error_to_string(int err) { + if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0) + strcpy(av_error_buffer, "Unknown error"); + return av_error_buffer; +} + +static int x11_error_handler(Display*, XErrorEvent*) { + return 0; +} + +static int x11_io_error_handler(Display*) { + return 0; +} + +static AVCodecID audio_codec_get_id(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return AV_CODEC_ID_AAC; + case GSR_AUDIO_CODEC_OPUS: return AV_CODEC_ID_OPUS; + case GSR_AUDIO_CODEC_FLAC: return AV_CODEC_ID_FLAC; + } + assert(false); + return AV_CODEC_ID_AAC; +} + +static AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_context, gsr_audio_codec audio_codec, const AVCodec *codec, bool mix_audio) { + (void)audio_codec_context; + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_OPUS: { + bool supports_s16 = false; + bool supports_flt = false; + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 15, 0) + for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + #else + const enum AVSampleFormat *sample_fmts = NULL; + if(avcodec_get_supported_config(audio_codec_context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void**)&sample_fmts, NULL) >= 0) { + if(sample_fmts) { + for(size_t i = 0; sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + } else { + // What a dumb API. It returns NULL if all formats are supported + supports_s16 = true; + supports_flt = true; + } + } + #endif + + // Amix only works with float audio + if(mix_audio) + supports_s16 = false; + + if(!supports_s16 && !supports_flt) { + fprintf(stderr, "gsr warning: opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n"); + fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n"); + fprintf(stderr, " Falling back to fltp audio sample format instead.\n"); + } + + if(supports_s16) + return AV_SAMPLE_FMT_S16; + else if(supports_flt) + return AV_SAMPLE_FMT_FLT; + else + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_FLAC: { + return AV_SAMPLE_FMT_S32; + } + } + assert(false); + return AV_SAMPLE_FMT_FLTP; +} + +static int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 160000; + case GSR_AUDIO_CODEC_OPUS: return 128000; + case GSR_AUDIO_CODEC_FLAC: return 128000; + } + assert(false); + return 128000; +} + +static AudioFormat audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { + switch(audio_codec_context->sample_fmt) { + case AV_SAMPLE_FMT_FLT: return F32; + case AV_SAMPLE_FMT_FLTP: return S32; + case AV_SAMPLE_FMT_S16: return S16; + case AV_SAMPLE_FMT_S32: return S32; + default: return S16; + } +} + +static AVSampleFormat audio_format_to_sample_format(const AudioFormat audio_format) { + switch(audio_format) { + case S16: return AV_SAMPLE_FMT_S16; + case S32: return AV_SAMPLE_FMT_S32; + case F32: return AV_SAMPLE_FMT_FLT; + } + assert(false); + return AV_SAMPLE_FMT_S16; +} + +static AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate) { + (void)fps; + const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); + if (!codec) { + fprintf(stderr, "gsr error: Could not find %s audio encoder\n", audio_codec_get_name(audio_codec)); + _exit(1); + } + + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + assert(codec->type == AVMEDIA_TYPE_AUDIO); + codec_context->codec_id = codec->id; + codec_context->sample_fmt = audio_codec_get_sample_format(codec_context, audio_codec, codec, mix_audio); + codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate; + codec_context->sample_rate = AUDIO_SAMPLE_RATE; + if(audio_codec == GSR_AUDIO_CODEC_AAC) { +#if LIBAVCODEC_VERSION_MAJOR < 62 + codec_context->profile = FF_PROFILE_AAC_LOW; +#else + codec_context->profile = AV_PROFILE_AAC_LOW; +#endif + } +#if LIBAVCODEC_VERSION_MAJOR < 60 + codec_context->channel_layout = AV_CH_LAYOUT_STEREO; + codec_context->channels = 2; +#else + av_channel_layout_default(&codec_context->ch_layout, 2); +#endif + + codec_context->time_base.num = 1; + codec_context->time_base.den = codec_context->sample_rate; + codec_context->thread_count = 1; + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +static int vbr_get_quality_parameter(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 160 * qp_multiply; + case GSR_VIDEO_QUALITY_HIGH: + return 130 * qp_multiply; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return 110 * qp_multiply; + case GSR_VIDEO_QUALITY_ULTRA: + return 90 * qp_multiply; + } + } else if(codec_context->codec_id == AV_CODEC_ID_H264) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 35 * qp_multiply; + case GSR_VIDEO_QUALITY_HIGH: + return 30 * qp_multiply; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return 25 * qp_multiply; + case GSR_VIDEO_QUALITY_ULTRA: + return 22 * qp_multiply; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 35 * qp_multiply; + case GSR_VIDEO_QUALITY_HIGH: + return 30 * qp_multiply; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return 25 * qp_multiply; + case GSR_VIDEO_QUALITY_ULTRA: + return 22 * qp_multiply; + } + } else if(codec_context->codec_id == AV_CODEC_ID_VP8 || codec_context->codec_id == AV_CODEC_ID_VP9) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 35 * qp_multiply; + case GSR_VIDEO_QUALITY_HIGH: + return 30 * qp_multiply; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return 25 * qp_multiply; + case GSR_VIDEO_QUALITY_ULTRA: + return 22 * qp_multiply; + } + } + assert(false); + return 22 * qp_multiply; +} + +static AVCodecContext *create_video_codec_context(AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl &egl, const args_parser &arg_parser, int width, int height) { + const bool use_software_video_encoder = arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU; + const bool hdr = video_codec_is_hdr(arg_parser.video_codec); + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + //double fps_ratio = (double)fps / 30.0; + + assert(codec->type == AVMEDIA_TYPE_VIDEO); + codec_context->codec_id = codec->id; + codec_context->width = width; + codec_context->height = height; + // Timebase: This is the fundamental unit of time (in seconds) in terms + // of which frame timestamps are represented. For fixed-fps content, + // timebase should be 1/framerate and timestamp increments should be + // identical to 1 + codec_context->time_base.num = 1; + codec_context->time_base.den = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? arg_parser.fps : AV_TIME_BASE; + codec_context->framerate.num = arg_parser.fps; + codec_context->framerate.den = 1; + codec_context->sample_aspect_ratio.num = 0; + codec_context->sample_aspect_ratio.den = 0; + if(arg_parser.low_latency_recording) { + codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); + codec_context->flags2 |= AV_CODEC_FLAG2_FAST; + //codec_context->gop_size = std::numeric_limits<int>::max(); + //codec_context->keyint_min = std::numeric_limits<int>::max(); + codec_context->gop_size = arg_parser.fps * arg_parser.keyint; + } else { + // High values reduce file size but increases time it takes to seek + codec_context->gop_size = arg_parser.fps * arg_parser.keyint; + } + codec_context->max_b_frames = 0; + codec_context->pix_fmt = pix_fmt; + codec_context->color_range = arg_parser.color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; + if(hdr) { + codec_context->color_primaries = AVCOL_PRI_BT2020; + codec_context->color_trc = AVCOL_TRC_SMPTE2084; + codec_context->colorspace = AVCOL_SPC_BT2020_NCL; + } else { + codec_context->color_primaries = AVCOL_PRI_BT709; + codec_context->color_trc = AVCOL_TRC_BT709; + codec_context->colorspace = AVCOL_SPC_BT709; + } + //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER; + // Can't use this because it's fucking broken in ffmpeg 8 or new mesa. It produces garbage output + //if(codec->id == AV_CODEC_ID_HEVC) + // codec_context->codec_tag = MKTAG('h', 'v', 'c', '1'); // QuickTime on MacOS requires this or the video wont be playable + + if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_CBR) { + codec_context->bit_rate = arg_parser.video_bitrate; + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = 0;//codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_VBR) { + const int quality = vbr_get_quality_parameter(codec_context, arg_parser.video_quality, hdr); + switch(arg_parser.video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + } + + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else { + //codec_context->rc_buffer_size = 50000 * 1000; + } + //codec_context->profile = FF_PROFILE_H264_MAIN; + if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO) + codec_context->mb_decision = 2; + + if(!use_software_video_encoder && egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && arg_parser.bitrate_mode != GSR_BITRATE_MODE_CBR) { + // 8 bit / 10 bit = 80%, and increase it even more + const float quality_multiply = hdr ? (8.0f/10.0f * 0.7f) : 1.0f; + if(codec_context->codec_id == AV_CODEC_ID_AV1 || codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { + switch(arg_parser.video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->global_quality = 130 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->global_quality = 110 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->global_quality = 95 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->global_quality = 85 * quality_multiply; + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_VP8) { + switch(arg_parser.video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->global_quality = 35 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->global_quality = 30 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->global_quality = 25 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->global_quality = 10 * quality_multiply; + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_VP9) { + switch(arg_parser.video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->global_quality = 35 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->global_quality = 30 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->global_quality = 25 * quality_multiply; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->global_quality = 10 * quality_multiply; + break; + } + } + } + + av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0); + //av_opt_set_int(codec_context->priv_data, "cbr", true, 0); + + if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) { + // TODO: More options, better options + //codec_context->bit_rate = codec_context->width * codec_context->height; + switch(arg_parser.bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(arg_parser.video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cqp", 0); + else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "constqp", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(arg_parser.video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "vbr", 0); + else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "vbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(arg_parser.video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cbr", 0); + else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "cbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CBR", 0); + break; + } + } + //codec_context->global_quality = 4; + //codec_context->compression_level = 2; + } + + //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0); + + if(arg_parser.tune == GSR_TUNE_QUALITY) + codec_context->max_b_frames = 2; + + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +static void open_audio(AVCodecContext *audio_codec_context) { + AVDictionary *options = nullptr; + av_dict_set(&options, "strict", "experimental", 0); + + int ret; + ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options); + if(ret < 0) { + fprintf(stderr, "failed to open codec, reason: %s\n", av_error_to_string(ret)); + _exit(1); + } +} + +static AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) { + AVFrame *frame = av_frame_alloc(); + if(!frame) { + fprintf(stderr, "failed to allocate audio frame\n"); + _exit(1); + } + + frame->sample_rate = audio_codec_context->sample_rate; + frame->nb_samples = audio_codec_context->frame_size; + frame->format = audio_codec_context->sample_fmt; +#if LIBAVCODEC_VERSION_MAJOR < 60 + frame->channels = audio_codec_context->channels; + frame->channel_layout = audio_codec_context->channel_layout; +#else + av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout); +#endif + + int ret = av_frame_get_buffer(frame, 0); + if(ret < 0) { + fprintf(stderr, "failed to allocate audio data buffers, reason: %s\n", av_error_to_string(ret)); + _exit(1); + } + + return frame; +} + +static void dict_set_profile(AVCodecContext *codec_context, gsr_gpu_vendor vendor, gsr_color_depth color_depth, gsr_video_codec video_codec, AVDictionary **options) { + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 17, 100) + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set(options, "profile", "high10", 0); + //else + av_dict_set(options, "profile", "high", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(vendor == GSR_GPU_VENDOR_NVIDIA) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set(options, "profile", "main", 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set(options, "profile", "main10", 0); + else + av_dict_set(options, "profile", "main", 0); + } + #else + const bool use_nvidia_values = vendor == GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(video_codec); + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set_int(options, "profile", AV_PROFILE_H264_HIGH_10, 0); + //else + av_dict_set_int(options, "profile", use_nvidia_values ? 2 : AV_PROFILE_H264_HIGH, 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(use_nvidia_values) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set_int(options, "profile", AV_PROFILE_AV1_MAIN, 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "profile", use_nvidia_values ? 1 : AV_PROFILE_HEVC_MAIN_10, 0); + else + av_dict_set_int(options, "profile", use_nvidia_values ? 0 : AV_PROFILE_HEVC_MAIN, 0); + } + #endif +} + +static void video_software_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_H264) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 34 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } +} + +static void open_video_software(AVCodecContext *codec_context, const args_parser &arg_parser) { + const bool hdr = video_codec_is_hdr(arg_parser.video_codec); + AVDictionary *options = nullptr; + + if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) + video_software_set_qp(codec_context, arg_parser.video_quality, hdr, &options); + + av_dict_set(&options, "preset", "veryfast", 0); + av_dict_set(&options, "tune", "film", 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); + _exit(1); + } +} + +static void video_set_rc(gsr_video_codec video_codec, gsr_gpu_vendor vendor, gsr_bitrate_mode bitrate_mode, AVDictionary **options) { + switch(bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cqp", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "constqp", 0); + else + av_dict_set(options, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "vbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "vbr", 0); + else + av_dict_set(options, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "cbr", 0); + else + av_dict_set(options, "rc_mode", "CBR", 0); + break; + } + } +} + +static void video_hardware_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, gsr_gpu_vendor vendor, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + if(vendor == GSR_GPU_VENDOR_NVIDIA) { + // TODO: Test if these should be in the same range as vaapi + if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_H264) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_VP8 || codec_context->codec_id == AV_CODEC_ID_VP9) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } + } else { + if(codec_context->codec_id == AV_CODEC_ID_AV1) { + // Using global_quality option + } else if(codec_context->codec_id == AV_CODEC_ID_H264) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_VP8 || codec_context->codec_id == AV_CODEC_ID_VP9) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + av_dict_set_int(options, "qp", 35 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_HIGH: + av_dict_set_int(options, "qp", 30 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + av_dict_set_int(options, "qp", 25 * qp_multiply, 0); + break; + case GSR_VIDEO_QUALITY_ULTRA: + av_dict_set_int(options, "qp", 22 * qp_multiply, 0); + break; + } + } + } +} + +static void open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl &egl, const args_parser &arg_parser) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); + const bool hdr = video_codec_is_hdr(arg_parser.video_codec); + AVDictionary *options = nullptr; + + if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) + video_hardware_set_qp(codec_context, arg_parser.video_quality, egl.gpu_info.vendor, hdr, &options); + + video_set_rc(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.bitrate_mode, &options); + + // TODO: Enable multipass + + dict_set_profile(codec_context, egl.gpu_info.vendor, color_depth, arg_parser.video_codec, &options); + + if(video_codec_is_vulkan(arg_parser.video_codec)) { + av_dict_set_int(&options, "async_depth", 3, 0); + av_dict_set(&options, "tune", "hq", 0); + av_dict_set(&options, "usage", "record", 0); // TODO: Set to stream when streaming + av_dict_set(&options, "content", "rendered", 0); + } else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { + // TODO: These dont seem to be necessary + // av_dict_set_int(&options, "zerolatency", 1, 0); + // if(codec_context->codec_id == AV_CODEC_ID_AV1) { + // av_dict_set(&options, "tune", "ll", 0); + // } else if(codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { + // av_dict_set(&options, "preset", "llhq", 0); + // av_dict_set(&options, "tune", "ll", 0); + // } + av_dict_set(&options, "tune", "hq", 0); + + switch(arg_parser.tune) { + case GSR_TUNE_PERFORMANCE: + //av_dict_set(&options, "multipass", "qres", 0); + break; + case GSR_TUNE_QUALITY: + av_dict_set(&options, "multipass", "fullres", 0); + av_dict_set(&options, "preset", "p6", 0); + av_dict_set_int(&options, "rc-lookahead", 0, 0); + break; + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: h264 10bit? + // TODO: + // switch(pixel_format) { + // case GSR_PIXEL_FORMAT_YUV420: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH, 0); + // break; + // case GSR_PIXEL_FORMAT_YUV444: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH_444, 0); + // break; + // } + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(arg_parser.pixel_format) { + case GSR_PIXEL_FORMAT_YUV420: + av_dict_set(&options, "rgb_mode", "yuv420", 0); + break; + case GSR_PIXEL_FORMAT_YUV444: + av_dict_set(&options, "rgb_mode", "yuv444", 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0); + } + } else { + // TODO: More quality options + if(low_power) + av_dict_set_int(&options, "low_power", 1, 0); + // Improves performance but increases vram. + // TODO: Might need a different async_depth for optimal performance on different amd/intel gpus + av_dict_set_int(&options, "async_depth", 3, 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // Removed because it causes stutter in games for some people + //av_dict_set_int(&options, "quality", 5, 0); // quality preset + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + av_dict_set(&options, "tier", "main", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(hdr) + av_dict_set(&options, "sei", "hdr", 0); + } + + // TODO: vp8/vp9 10bit + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); + _exit(1); + } +} + +static const int save_replay_seconds_full = -1; + +static sig_atomic_t running = 1; +static sig_atomic_t toggle_pause = 0; +static sig_atomic_t toggle_replay_recording = 0; +static sig_atomic_t save_replay_seconds = 0; + +static void stop_handler(int) { + running = 0; +} + +static void toggle_pause_handler(int) { + toggle_pause = 1; +} + +static void toggle_replay_recording_handler(int) { + toggle_replay_recording = 1; +} + +static void save_replay_handler(int) { + save_replay_seconds = save_replay_seconds_full; +} + +static void save_replay_10_seconds_handler(int) { + save_replay_seconds = 10; +} + +static void save_replay_30_seconds_handler(int) { + save_replay_seconds = 30; +} + +static void save_replay_1_minute_handler(int) { + save_replay_seconds = 60; +} + +static void save_replay_5_minutes_handler(int) { + save_replay_seconds = 60*5; +} + +static void save_replay_10_minutes_handler(int) { + save_replay_seconds = 60*10; +} + +static void save_replay_30_minutes_handler(int) { + save_replay_seconds = 60*30; +} + +static std::string get_date_str() { + char str[128]; + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, sizeof(str)-1, "%Y-%m-%d_%H-%M-%S", t); + return str; +} + +static std::string get_date_only_str() { + char str[128]; + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, sizeof(str)-1, "%Y-%m-%d", t); + return str; +} + +static std::string get_time_only_str() { + char str[128]; + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, sizeof(str)-1, "%H-%M-%S", t); + return str; +} + +static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { + AVStream *stream = avformat_new_stream(av_format_context, nullptr); + if (!stream) { + fprintf(stderr, "gsr error: Could not allocate stream\n"); + _exit(1); + } + stream->id = av_format_context->nb_streams - 1; + stream->time_base = codec_context->time_base; + stream->avg_frame_rate = codec_context->framerate; + return stream; +} + +static void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) { + char script_file_full[PATH_MAX]; + script_file_full[0] = '\0'; + if(!realpath(script_file, script_file_full)) { + fprintf(stderr, "gsr error: script file not found: %s\n", script_file); + return; + } + + const char *args[7]; + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + + if(inside_flatpak) { + args[0] = "flatpak-spawn"; + args[1] = "--host"; + args[2] = "--"; + args[3] = script_file_full; + args[4] = video_file; + args[5] = type; + args[6] = NULL; + } else { + args[0] = script_file_full; + args[1] = video_file; + args[2] = type; + args[3] = NULL; + } + + pid_t pid = fork(); + if(pid == -1) { + perror(script_file_full); + return; + } else if(pid == 0) { // child + setsid(); + signal(SIGHUP, SIG_IGN); + + pid_t second_child = fork(); + if(second_child == 0) { // child + execvp(args[0], (char* const*)args); + perror(script_file_full); + _exit(127); + } else if(second_child != -1) { // parent + _exit(0); + } + } else { // parent + waitpid(pid, NULL, 0); + } +} + +static double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps) { + const double fps_inv = 1.0 / (double)fps; + const double base = 0.01 + 1.0/165.0; + switch(audio_codec) { + case GSR_AUDIO_CODEC_OPUS: + return std::max(0.0, base - fps_inv); + case GSR_AUDIO_CODEC_AAC: + return std::max(0.0, (base + 0.008) * 2.0 - fps_inv); + case GSR_AUDIO_CODEC_FLAC: + // TODO: Test + return std::max(0.0, base - fps_inv); + } + assert(false); + return std::max(0.0, base - fps_inv); +} + +struct AudioDeviceData { + SoundDevice sound_device; + AudioInput audio_input; + AVFilterContext *src_filter_ctx = nullptr; + AVFrame *frame = nullptr; + std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read +}; + +// TODO: Cleanup +struct AudioTrack { + std::string name; + AVCodecContext *codec_context = nullptr; + + std::vector<AudioDeviceData> audio_devices; + AVFilterGraph *graph = nullptr; + AVFilterContext *sink = nullptr; + int stream_index = 0; + int64_t pts = 0; +}; + +static bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { + size_t light_metadata_size = 0; + size_t mastering_display_metadata_size = 0; + AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size); + #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100) + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc(); + mastering_display_metadata_size = sizeof(*mastering_display_metadata); + #else + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size); + #endif + + if(!light_metadata || !mastering_display_metadata) { + if(light_metadata) + av_freep(&light_metadata); + + if(mastering_display_metadata) + av_freep(&mastering_display_metadata); + + return false; + } + + if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) { + av_freep(&light_metadata); + av_freep(&mastering_display_metadata); + return false; + } + + // TODO: More error checking + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0; + #else + const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL; + #endif + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0; + #else + const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL; + #endif + + if(!content_light_level_added) + av_freep(&light_metadata); + + if(!mastering_display_metadata_added) + av_freep(&mastering_display_metadata); + + // Return true even on failure because we dont want to retry adding hdr metadata on failure + return true; +} + +struct RecordingStartAudio { + const AudioTrack *audio_track; + AVStream *stream; +}; + +struct RecordingStartResult { + AVFormatContext *av_format_context = nullptr; + AVStream *video_stream = nullptr; + std::vector<RecordingStartAudio> audio_inputs; +}; + +typedef enum { + VVEC2I_TYPE_PIXELS, + VVEC2I_TYPE_SCALAR +} vvec2i_type; + +typedef struct { + int x, y; + vvec2i_type x_type; + vvec2i_type y_type; +} vvec2i; + +struct CaptureSource { + std::string name; + CaptureSourceType type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + gsr_capture_alignment halign = GSR_CAPTURE_ALIGN_CENTER; + gsr_capture_alignment valign = GSR_CAPTURE_ALIGN_CENTER; + gsr_capture_v4l2_pixfmt v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + uint32_t flip = GSR_FLIP_NONE; + vvec2i pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + vvec2i size = {100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + vec2i region_pos = {0, 0}; + vec2i region_size = {0, 0}; + bool region_set = false; + int64_t window_id = 0; +}; + +struct VideoSource { + gsr_capture *capture; + gsr_capture_metadata metadata; + CaptureSource *capture_source; +}; + +static RecordingStartResult start_recording_create_streams(const char *filename, const char *container_format, AVCodecContext *video_codec_context, const std::vector<AudioTrack> &audio_tracks, bool hdr, std::vector<VideoSource> &video_sources) { + AVFormatContext *av_format_context; + avformat_alloc_output_context2(&av_format_context, nullptr, container_format, filename); + + AVStream *video_stream = create_stream(av_format_context, video_codec_context); + avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); + + RecordingStartResult result; + result.audio_inputs.reserve(audio_tracks.size()); + + for(const AudioTrack &audio_track : audio_tracks) { + AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); + if(!audio_track.name.empty()) + av_dict_set(&audio_stream->metadata, "title", audio_track.name.c_str(), 0); + avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); + result.audio_inputs.push_back({&audio_track, audio_stream}); + } + + const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); + if(open_ret < 0) { + fprintf(stderr, "gsr error: start: could not open '%s': %s\n", filename, av_error_to_string(open_ret)); + return result; + } + + AVDictionary *options = nullptr; + av_dict_set(&options, "strict", "experimental", 0); + + const int header_write_ret = avformat_write_header(av_format_context, &options); + av_dict_free(&options); + if(header_write_ret < 0) { + fprintf(stderr, "gsr error: start: error occurred when writing header to output file: %s\n", av_error_to_string(header_write_ret)); + avio_close(av_format_context->pb); + avformat_free_context(av_format_context); + return result; + } + + for(VideoSource &video_source : video_sources) { + if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) + break; + } + + result.av_format_context = av_format_context; + result.video_stream = video_stream; + return result; +} + +static bool stop_recording_close_streams(AVFormatContext *av_format_context) { + bool trailer_written = true; + if(av_write_trailer(av_format_context) != 0) { + fprintf(stderr, "gsr error: end: failed to write trailer\n"); + trailer_written = false; + } + + const bool closed = avio_close(av_format_context->pb) == 0; + avformat_free_context(av_format_context); + return trailer_written && closed; +} + +static std::future<void> save_replay_thread; +static std::string save_replay_output_filepath; + +static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) { + std::string output_filepath; + if(date_folders) { + std::string output_folder = directory + '/' + get_date_only_str(); + if(create_directory_recursive(&output_folder[0]) != 0) + fprintf(stderr, "gsr error: failed to create directory: %s\n", output_folder.c_str()); + output_filepath = output_folder + "/" + filename_prefix + "_" + get_time_only_str() + "." + file_extension; + } else { + if(create_directory_recursive(&directory[0]) != 0) + fprintf(stderr, "gsr error: failed to create directory: %s\n", directory.c_str()); + output_filepath = directory + "/" + filename_prefix + "_" + get_date_str() + "." + file_extension; + } + return output_filepath; +} + +static RecordingStartAudio* get_recording_start_item_by_stream_index(RecordingStartResult &result, int stream_index) { + for(auto &audio_input : result.audio_inputs) { + if(audio_input.stream->index == stream_index) + return &audio_input; + } + return nullptr; +} + +struct AudioPtsOffset { + int64_t pts_offset = 0; + int stream_index = 0; +}; + +static void save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector<AudioTrack> &audio_tracks, gsr_replay_buffer *replay_buffer, std::string output_dir, const char *container_format, const std::string &file_extension, bool date_folders, bool hdr, std::vector<VideoSource> &video_sources, int current_save_replay_seconds) { + if(save_replay_thread.valid()) + return; + + const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(replay_buffer, current_save_replay_seconds); + const gsr_replay_buffer_iterator video_start_iterator = gsr_replay_buffer_find_keyframe(replay_buffer, search_start_iterator, video_stream_index, false); + if(video_start_iterator.packet_index == (size_t)-1) { + fprintf(stderr, "gsr error: failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded\n"); + return; + } + + const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(replay_buffer, video_start_iterator)->pts; + + std::vector<AudioPtsOffset> audio_pts_offsets; + audio_pts_offsets.reserve(audio_tracks.size()); + for(const AudioTrack &audio_track : audio_tracks) { + const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(replay_buffer, video_start_iterator, audio_track.stream_index, false); + const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(replay_buffer, audio_start_iterator)->pts; + audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index}); + } + + gsr_replay_buffer *cloned_replay_buffer = gsr_replay_buffer_clone(replay_buffer); + if(!cloned_replay_buffer) { + // TODO: Return this error to mark the replay as failed + fprintf(stderr, "gsr error: failed to save replay: failed to clone replay buffer\n"); + return; + } + + std::string output_filepath = create_new_recording_filepath_from_timestamp(output_dir, "Replay", file_extension, date_folders); + RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), container_format, video_codec_context, audio_tracks, hdr, video_sources); + if(!recording_start_result.av_format_context) + return; + + save_replay_output_filepath = std::move(output_filepath); + + save_replay_thread = std::async(std::launch::async, [video_stream_index, recording_start_result, video_start_iterator, video_pts_offset, audio_pts_offsets{std::move(audio_pts_offsets)}, video_codec_context, cloned_replay_buffer]() mutable { + gsr_replay_buffer_iterator replay_iterator = video_start_iterator; + for(;;) { + AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, replay_iterator); + uint8_t *replay_packet_data = NULL; + if(replay_packet) + replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(cloned_replay_buffer, replay_iterator); + + if(!replay_packet) { + fprintf(stderr, "gsr error: save_replay_async: no replay packet\n"); + break; + } + + if(!replay_packet->data && !replay_packet_data) { + fprintf(stderr, "gsr error: save_replay_async: no replay packet data\n"); + break; + } + + // TODO: Check if successful + AVPacket av_packet; + memset(&av_packet, 0, sizeof(av_packet)); + //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size); + av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data; + av_packet.size = replay_packet->size; + av_packet.stream_index = replay_packet->stream_index; + av_packet.pts = replay_packet->pts; + av_packet.dts = replay_packet->pts; + av_packet.flags = replay_packet->flags; + //av_packet.duration = replay_packet->duration; + + AVStream *stream = recording_start_result.video_stream; + AVCodecContext *codec_context = video_codec_context; + + if(av_packet.stream_index == video_stream_index) { + av_packet.pts -= video_pts_offset; + av_packet.dts -= video_pts_offset; + } else { + RecordingStartAudio *recording_start_audio = get_recording_start_item_by_stream_index(recording_start_result, av_packet.stream_index); + if(!recording_start_audio) { + fprintf(stderr, "gsr error: save_replay_async: failed to find audio stream by index: %d\n", av_packet.stream_index); + free(replay_packet_data); + continue; + } + + const AudioTrack *audio_track = recording_start_audio->audio_track; + stream = recording_start_audio->stream; + codec_context = audio_track->codec_context; + + const AudioPtsOffset &audio_pts_offset = audio_pts_offsets[av_packet.stream_index - 1]; + assert(audio_pts_offset.stream_index == av_packet.stream_index); + av_packet.pts -= audio_pts_offset.pts_offset; + av_packet.dts -= audio_pts_offset.pts_offset; + } + + //av_packet.stream_index = stream->index; + av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); + + const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet); + if(ret < 0) + fprintf(stderr, "gsr error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet.stream_index, av_error_to_string(ret), ret); + + free(replay_packet_data); + + //av_packet_free(&av_packet); + if(!gsr_replay_buffer_iterator_next(cloned_replay_buffer, &replay_iterator)) + break; + } + + stop_recording_close_streams(recording_start_result.av_format_context); + gsr_replay_buffer_destroy(cloned_replay_buffer); + }); +} + +static void split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> callback) { + size_t index = 0; + while(index < str.size()) { + size_t end_index = str.find(delimiter, index); + if(end_index == std::string::npos) + end_index = str.size(); + + if(!callback(&str[index], end_index - index)) + break; + + index = end_index + 1; + } +} + +static bool string_starts_with(const char *str, size_t str_size, const char *substr) { + int len = strlen(substr); + return (int)str_size >= len && memcmp(str, substr, len) == 0; +} + +static bool string_starts_with(const std::string &str, const char *substr) { + return string_starts_with(str.data(), str.size(), substr); +} + +static bool string_ends_with(const char *str, const char *substr) { + int str_len = strlen(str); + int substr_len = strlen(substr); + return str_len >= substr_len && memcmp(str + str_len - substr_len, substr, substr_len) == 0; +} + +static const AudioDevice* get_audio_device_by_name(const std::vector<AudioDevice> &audio_devices, const char *name) { + for(const auto &audio_device : audio_devices) { + if(strcmp(audio_device.name.c_str(), name) == 0) + return &audio_device; + } + return nullptr; +} + +static MergedAudioInputs parse_audio_input_arg(const char *str) { + MergedAudioInputs result; + result.track_name = str; + + split_string(str, '|', [&](const char *sub, size_t size) { + if(size == 0) + return true; + + AudioInput audio_input; + audio_input.name.assign(sub, size); + + if(string_starts_with(audio_input.name.c_str(), "app:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); + audio_input.type = AudioInputType::APPLICATION; + audio_input.inverted = false; + result.audio_inputs.push_back(std::move(audio_input)); + return true; + } else if(string_starts_with(audio_input.name.c_str(), "app-inverse:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12); + audio_input.type = AudioInputType::APPLICATION; + audio_input.inverted = true; + result.audio_inputs.push_back(std::move(audio_input)); + return true; + } else if(string_starts_with(audio_input.name.c_str(), "device:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7); + audio_input.type = AudioInputType::DEVICE; + result.audio_inputs.push_back(std::move(audio_input)); + return true; + } else { + audio_input.type = AudioInputType::DEVICE; + result.audio_inputs.push_back(std::move(audio_input)); + return true; + } + }); + + return result; +} + +static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector<AVFilterContext*>& src_filter_ctx, size_t num_sources) { + char ch_layout[64]; + int err = 0; + ch_layout[0] = '\0'; + + // C89-style variable declaration to + // avoid problems because of goto + AVFilterGraph* filter_graph = nullptr; + AVFilterContext* mix_ctx = nullptr; + + const AVFilter* mix_filter = nullptr; + const AVFilter* abuffersink = nullptr; + AVFilterContext* abuffersink_ctx = nullptr; + char args[512] = { 0 }; +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + bool normalize = false; +#endif + + filter_graph = avfilter_graph_alloc(); + if (!filter_graph) { + fprintf(stderr, "Unable to create filter graph.\n"); + err = AVERROR(ENOMEM); + goto fail; + } + + for(size_t i = 0; i < num_sources; ++i) { + const AVFilter *abuffer = avfilter_get_by_name("abuffer"); + if (!abuffer) { + fprintf(stderr, "Could not find the abuffer filter.\n"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL); + if (!abuffer_ctx) { + fprintf(stderr, "Could not allocate the abuffer instance.\n"); + err = AVERROR(ENOMEM); + goto fail; + } + + #if LIBAVCODEC_VERSION_MAJOR < 60 + av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO); + #else + av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout)); + #endif + av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN); + av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN); + av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN); + + err = avfilter_init_str(abuffer_ctx, NULL); + if (err < 0) { + fprintf(stderr, "Could not initialize the abuffer filter.\n"); + goto fail; + } + + src_filter_ctx.push_back(abuffer_ctx); + } + + mix_filter = avfilter_get_by_name("amix"); + if (!mix_filter) { + av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); +#else + snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); + fprintf(stderr, "gsr warning: your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected\n"); +#endif + + err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n"); + goto fail; + } + + abuffersink = avfilter_get_by_name("abuffersink"); + if (!abuffersink) { + fprintf(stderr, "Could not find the abuffersink filter.\n"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink"); + if (!abuffersink_ctx) { + fprintf(stderr, "Could not allocate the abuffersink instance.\n"); + err = AVERROR(ENOMEM); + goto fail; + } + + err = avfilter_init_str(abuffersink_ctx, NULL); + if (err < 0) { + fprintf(stderr, "Could not initialize the abuffersink instance.\n"); + goto fail; + } + + err = 0; + for(size_t i = 0; i < src_filter_ctx.size(); ++i) { + AVFilterContext *src_ctx = src_filter_ctx[i]; + if (err >= 0) + err = avfilter_link(src_ctx, 0, mix_ctx, i); + } + if (err >= 0) + err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n"); + goto fail; + } + + err = avfilter_graph_config(filter_graph, NULL); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n"); + goto fail; + } + + *graph = filter_graph; + *sink = abuffersink_ctx; + + return 0; + +fail: + avfilter_graph_free(&filter_graph); + src_filter_ctx.clear(); // possibly unnecessary? + return err; +} + +static gsr_video_encoder* create_video_encoder(gsr_egl *egl, const args_parser &arg_parser) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); + gsr_video_encoder *video_encoder = nullptr; + + if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + gsr_video_encoder_software_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_software_create(¶ms); + return video_encoder; + } + + if(video_codec_is_vulkan(arg_parser.video_codec)) { + gsr_video_encoder_vulkan_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vulkan_create(¶ms); + return video_encoder; + } + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: { + gsr_video_encoder_vaapi_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vaapi_create(¶ms); + break; + } + case GSR_GPU_VENDOR_NVIDIA: { + gsr_video_encoder_nvenc_params params; + params.egl = egl; + params.overclock = arg_parser.overclock; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_nvenc_create(¶ms); + break; + } + } + + return video_encoder; +} + +static bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { + memset(video_codecs, 0, sizeof(*video_codecs)); + + if(use_software_video_encoder) { + video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); + video_codecs->h264.max_resolution = {4096, 2304}; + return true; + } + + if(video_codec_is_vulkan(video_codec)) + return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, cleanup); + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); + case GSR_GPU_VENDOR_NVIDIA: + return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); + } + + return false; +} + +static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { + bool *xwayland_found = (bool*)userdata; + if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) + *xwayland_found = true; + else if(memmem(monitor->name, monitor->name_len, "X11", 3)) + *xwayland_found = true; +} + +static bool is_xwayland(Display *display) { + int opcode, event, error; + if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error)) + return true; + + bool xwayland_found = false; + for_each_active_monitor_output_x11_not_cached(display, xwayland_check_callback, &xwayland_found); + return xwayland_found; +} + +static bool is_using_prime_run() { + const char *prime_render_offload = getenv("__NV_PRIME_RENDER_OFFLOAD"); + return (prime_render_offload && strcmp(prime_render_offload, "1") == 0) || getenv("DRI_PRIME"); +} + +static void disable_prime_run() { + unsetenv("__NV_PRIME_RENDER_OFFLOAD"); + unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); + unsetenv("__GLX_VENDOR_LIBRARY_NAME"); + unsetenv("__VK_LAYER_NV_optimus"); + unsetenv("DRI_PRIME"); +} + +static gsr_window* gsr_window_create(Display *display, bool wayland) { + if(wayland) + return gsr_window_wayland_create(); + else + return gsr_window_x11_create(display); +} + +static void list_system_info(bool wayland) { + printf("display_server|%s\n", wayland ? "wayland" : "x11"); + bool supports_app_audio = false; +#ifdef GSR_APP_AUDIO + supports_app_audio = pulseaudio_server_is_pipewire(); + if(supports_app_audio) { + gsr_pipewire_audio audio; + if(gsr_pipewire_audio_init(&audio)) + gsr_pipewire_audio_deinit(&audio); + else + supports_app_audio = false; + } +#endif + printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no"); +} + +static void list_gpu_info(gsr_egl *egl) { + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + printf("vendor|amd\n"); + break; + case GSR_GPU_VENDOR_INTEL: + printf("vendor|intel\n"); + break; + case GSR_GPU_VENDOR_NVIDIA: + printf("vendor|nvidia\n"); + break; + case GSR_GPU_VENDOR_BROADCOM: + printf("vendor|broadcom\n"); + break; + } + printf("card_path|%s\n", egl->card_path); +} + +static const AVCodec* get_ffmpeg_video_codec(gsr_video_codec video_codec, gsr_gpu_vendor vendor) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi"); + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi"); + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi"); + case GSR_VIDEO_CODEC_VP8: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp8_nvenc" : "vp8_vaapi"); + case GSR_VIDEO_CODEC_VP9: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp9_nvenc" : "vp9_vaapi"); + case GSR_VIDEO_CODEC_H264_VULKAN: + return avcodec_find_encoder_by_name("h264_vulkan"); + case GSR_VIDEO_CODEC_HEVC_VULKAN: + return avcodec_find_encoder_by_name("hevc_vulkan"); + } + return nullptr; +} + +static void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264, vendor)) { + supported_video_codecs->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC, vendor)) { + supported_video_codecs->hevc.supported = false; + supported_video_codecs->hevc_hdr.supported = false; + supported_video_codecs->hevc_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1, vendor)) { + supported_video_codecs->av1.supported = false; + supported_video_codecs->av1_hdr.supported = false; + supported_video_codecs->av1_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP8, vendor)) { + supported_video_codecs->vp8.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP9, vendor)) { + supported_video_codecs->vp9.supported = false; + } + + if(supported_video_codecs_vulkan) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264_VULKAN, vendor)) { + supported_video_codecs_vulkan->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC_VULKAN, vendor)) { + supported_video_codecs_vulkan->hevc.supported = false; + supported_video_codecs_vulkan->hevc_hdr.supported = false; + supported_video_codecs_vulkan->hevc_10bit.supported = false; + } + } +} + +static void list_supported_video_codecs(gsr_egl *egl, bool wayland) { + // Dont clean it up on purpose to increase shutdown speed + gsr_supported_video_codecs supported_video_codecs; + get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264, false, false, &supported_video_codecs); + + gsr_supported_video_codecs supported_video_codecs_vulkan; + get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264_VULKAN, false, false, &supported_video_codecs_vulkan); + + set_supported_video_codecs_ffmpeg(&supported_video_codecs, &supported_video_codecs_vulkan, egl->gpu_info.vendor); + + if(supported_video_codecs.h264.supported) + puts("h264"); + if(avcodec_find_encoder_by_name("libx264")) + puts("h264_software"); + if(supported_video_codecs.hevc.supported) + puts("hevc"); + if(supported_video_codecs.hevc_hdr.supported && wayland) + puts("hevc_hdr"); + if(supported_video_codecs.hevc_10bit.supported) + puts("hevc_10bit"); + if(supported_video_codecs.av1.supported) + puts("av1"); + if(supported_video_codecs.av1_hdr.supported && wayland) + puts("av1_hdr"); + if(supported_video_codecs.av1_10bit.supported) + puts("av1_10bit"); + if(supported_video_codecs.vp8.supported) + puts("vp8"); + if(supported_video_codecs.vp9.supported) + puts("vp9"); + //if(supported_video_codecs_vulkan.h264.supported) + // puts("h264_vulkan"); + //if(supported_video_codecs_vulkan.hevc.supported) + // puts("hevc_vulkan"); // TODO: hdr, 10 bit +} + +static bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor) { + return gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND || vendor != GSR_GPU_VENDOR_NVIDIA; +} + +typedef struct { + const gsr_window *window; + int num_monitors; +} capture_options_callback; + +static void output_monitor_info(const gsr_monitor *monitor, void *userdata) { + capture_options_callback *options = (capture_options_callback*)userdata; + if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { + vec2i monitor_size = monitor->size; + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; + vec2i monitor_position = {0, 0}; + drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); + if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) + std::swap(monitor_size.x, monitor_size.y); + printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y); + } else { + printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y); + } + ++options->num_monitors; +} + +static void camera_query_callback(const char *path, gsr_capture_v4l2_supported_pixfmts supported_pixfmts, vec2i size, void *userdata) { + (void)userdata; + + char pixfmt_str[32]; + if(supported_pixfmts.yuyv && supported_pixfmts.mjpeg) + snprintf(pixfmt_str, sizeof(pixfmt_str), "yuyv,mjpeg"); + else if(supported_pixfmts.yuyv) + snprintf(pixfmt_str, sizeof(pixfmt_str), "yuyv"); + else if(supported_pixfmts.mjpeg) + snprintf(pixfmt_str, sizeof(pixfmt_str), "mjpeg"); + + printf("%s|%dx%d|%s\n", path, size.x, size.y, pixfmt_str); +} + +static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool list_monitors) { + const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND; + if(!wayland) { + puts("window"); + puts("focused"); + } + + capture_options_callback options; + options.window = window; + options.num_monitors = 0; + if(list_monitors) { + const bool is_x11 = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + for_each_active_monitor_output(window, card_path, connection_type, output_monitor_info, &options); + } + + if(options.num_monitors > 0) + puts("region"); + + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + +#ifdef GSR_PORTAL + // Desktop portal capture on x11 doesn't seem to be hardware accelerated + if(!wayland) + return; + + gsr_dbus dbus; + if(!gsr_dbus_init(&dbus, NULL)) + return; + + char *session_handle = NULL; + if(gsr_dbus_screencast_create_session(&dbus, &session_handle) == 0) + puts("portal"); + + gsr_dbus_deinit(&dbus); +#endif +} + +static void version_command(void *userdata) { + (void)userdata; + puts(GSR_VERSION); + fflush(stdout); + _exit(0); +} + +static void info_command(void *userdata) { + (void)userdata; + bool wayland = false; + Display *dpy = XOpenDisplay(nullptr); + if (!dpy) { + wayland = true; + fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); + } + + XSetErrorHandler(x11_error_handler); + XSetIOErrorHandler(x11_io_error_handler); + + if(!wayland) + wayland = is_xwayland(dpy); + + if(!wayland && is_using_prime_run()) { + // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. + // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected + // to the drm device. + fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); + disable_prime_run(); + } + + gsr_window *window = gsr_window_create(dpy, wayland); + if(!window) { + fprintf(stderr, "gsr error: failed to create window\n"); + _exit(1); + } + + gsr_egl egl; + if(!gsr_egl_load(&egl, window, false, false)) { + fprintf(stderr, "gsr error: failed to load opengl\n"); + _exit(22); + } + + bool list_monitors = true; + egl.card_path[0] = '\0'; + if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { + // TODO: Allow specifying another card, and in other places + if(!gsr_get_valid_card_path(&egl, egl.card_path, true)) { + fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected\n"); + list_monitors = false; + } + } + + av_log_set_level(AV_LOG_FATAL); + + puts("section=system_info"); + list_system_info(wayland); + if(egl.gpu_info.is_steam_deck) + puts("is_steam_deck|yes"); + else + puts("is_steam_deck|no"); + printf("gsr_version|%s\n", GSR_VERSION); + puts("section=gpu_info"); + list_gpu_info(&egl); + puts("section=video_codecs"); + list_supported_video_codecs(&egl, wayland); + puts("section=image_formats"); + puts("jpeg"); + puts("png"); + puts("section=capture_options"); + list_supported_capture_options(window, egl.card_path, list_monitors); + + fflush(stdout); + + // Not needed as this will just slow down shutdown + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + //if(dpy) + // XCloseDisplay(dpy); + + _exit(0); +} + +static void list_audio_devices_command(void *userdata) { + (void)userdata; + const AudioDevices audio_devices = get_pulseaudio_inputs(); + + if(!audio_devices.default_output.empty()) + puts("default_output|Default output"); + + if(!audio_devices.default_input.empty()) + puts("default_input|Default input"); + + for(const auto &audio_input : audio_devices.audio_inputs) { + printf("%s|%s\n", audio_input.name.c_str(), audio_input.description.c_str()); + } + + fflush(stdout); + _exit(0); +} + +static bool app_audio_query_callback(const char *app_name, void*) { + puts(app_name); + return true; +} + +static void list_application_audio_command(void *userdata) { + (void)userdata; +#ifdef GSR_APP_AUDIO + if(pulseaudio_server_is_pipewire()) { + gsr_pipewire_audio audio; + if(gsr_pipewire_audio_init(&audio)) { + gsr_pipewire_audio_for_each_app(&audio, app_audio_query_callback, NULL); + gsr_pipewire_audio_deinit(&audio); + } + } +#endif + + fflush(stdout); + _exit(0); +} + +static void list_v4l2_devices(void *userdata) { + (void)userdata; + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + + fflush(stdout); + _exit(0); +} + +// |card_path| can be NULL. If not NULL then |vendor| has to be valid +static void list_capture_options_command(const char *card_path, void *userdata) { + (void)userdata; + bool wayland = false; + Display *dpy = XOpenDisplay(nullptr); + if (!dpy) { + wayland = true; + fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); + } + + XSetErrorHandler(x11_error_handler); + XSetIOErrorHandler(x11_io_error_handler); + + if(!wayland) + wayland = is_xwayland(dpy); + + if(!wayland && is_using_prime_run()) { + // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. + // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected + // to the drm device. + fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); + disable_prime_run(); + } + + gsr_window *window = gsr_window_create(dpy, wayland); + if(!window) { + fprintf(stderr, "gsr error: failed to create window\n"); + _exit(1); + } + + if(card_path) { + list_supported_capture_options(window, card_path, true); + } else { + gsr_egl egl; + if(!gsr_egl_load(&egl, window, false, false)) { + fprintf(stderr, "gsr error: failed to load opengl\n"); + _exit(1); + } + + bool list_monitors = true; + egl.card_path[0] = '\0'; + if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { + // TODO: Allow specifying another card, and in other places + if(!gsr_get_valid_card_path(&egl, egl.card_path, true)) { + fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected\n"); + list_monitors = false; + } + } + list_supported_capture_options(window, egl.card_path, list_monitors); + } + + fflush(stdout); + + // Not needed as this will just slow down shutdown + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + //if(dpy) + // XCloseDisplay(dpy); + + _exit(0); +} + +static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* window) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); + + std::string capture_source_result = window; + if(strcmp(capture_source_result.c_str(), "screen") == 0) { + FirstOutputCallback data; + data.output_name = NULL; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); + + if(data.output_name) { + capture_source_result = data.output_name; + free(data.output_name); + } else { + fprintf(stderr, "gsr error: no usable output found\n"); + _exit(51); + } + } else if(capture_use_drm || (strcmp(capture_source_result.c_str(), "screen-direct") != 0 && strcmp(capture_source_result.c_str(), "screen-direct-force") != 0)) { + gsr_monitor gmon; + if(!get_monitor_by_name(egl, connection_type, capture_source_result.c_str(), &gmon)) { + fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", capture_source_result.c_str()); + fprintf(stderr, " \"screen\"\n"); + if(!capture_use_drm) + fprintf(stderr, " \"screen-direct\"\n"); + + MonitorOutputCallbackUserdata userdata; + userdata.window = egl->window; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); + _exit(51); + } + } + return capture_source_result; +} + +static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, vec2i *monitor_pos, vec2i *monitor_size) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + + MonitorByPositionCallback data; + data.window = egl->window; + data.position = { region_position.x + region_size.x / 2, region_position.y + region_size.y / 2 }; + data.output_name = NULL; + data.monitor_pos = {0, 0}; + data.monitor_size = {0, 0}; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_position_callback, &data); + + std::string result; + if(data.output_name) { + result = data.output_name; + free(data.output_name); + } + *monitor_pos = data.monitor_pos; + *monitor_size = data.monitor_size; + return result; +} + +static gsr_kms_client kms_client; +static bool kms_client_initialized = false; +static gsr_kms_response kms_response; + +static gsr_cursor x11_cursor; +static Display *x11_cursor_display = NULL; + +static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_egl *egl, const CaptureSource &capture_source, bool prefer_ximage) { + if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { + gsr_capture_ximage_params ximage_params; + memset(&ximage_params, 0, sizeof(ximage_params)); + ximage_params.egl = egl; + ximage_params.cursor = &x11_cursor; + ximage_params.display_to_capture = capture_source.name.c_str(); + ximage_params.record_cursor = arg_parser.record_cursor; + ximage_params.output_resolution = arg_parser.output_resolution; + ximage_params.region_size = capture_source.region_size; + ximage_params.region_position = capture_source.region_pos; + return gsr_capture_ximage_create(&ximage_params); + } + + if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { + if(!kms_client_initialized) { + kms_client_initialized = true; + const int kms_init_res = gsr_kms_client_init(&kms_client, egl->card_path); + if(kms_init_res != 0) + _exit(kms_init_res < 0 ? 1 : kms_init_res); + } + + gsr_capture_kms_params kms_params; + memset(&kms_params, 0, sizeof(kms_params)); + kms_params.egl = egl; + kms_params.x11_cursor = &x11_cursor; + kms_params.kms_response = &kms_response; + kms_params.display_to_capture = capture_source.name.c_str(); + kms_params.record_cursor = arg_parser.record_cursor; + kms_params.hdr = video_codec_is_hdr(arg_parser.video_codec); + kms_params.fps = arg_parser.fps; + kms_params.output_resolution = arg_parser.output_resolution; + kms_params.region_size = capture_source.region_size; + kms_params.region_position = capture_source.region_pos; + return gsr_capture_kms_create(&kms_params); + } else { + const char *capture_source_real = capture_source.name.c_str(); + const bool direct_capture = strcmp(capture_source.name.c_str(), "screen-direct") == 0 || strcmp(capture_source.name.c_str(), "screen-direct-force") == 0; + if(direct_capture) { + capture_source_real = "screen"; + fprintf(stderr, "gsr warning: %s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.\n", capture_source.name.c_str()); + } + + gsr_capture_nvfbc_params nvfbc_params; + memset(&nvfbc_params, 0, sizeof(nvfbc_params)); + nvfbc_params.egl = egl; + nvfbc_params.display_to_capture = capture_source_real; + nvfbc_params.fps = arg_parser.fps; + nvfbc_params.direct_capture = direct_capture; + nvfbc_params.record_cursor = arg_parser.record_cursor; + nvfbc_params.output_resolution = arg_parser.output_resolution; + nvfbc_params.region_size = capture_source.region_size; + nvfbc_params.region_position = capture_source.region_pos; + return gsr_capture_nvfbc_create(&nvfbc_params); + } +} + +static std::string region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *region_position) { + vec2i monitor_pos = {0, 0}; + vec2i monitor_size = {0, 0}; + std::string window = get_monitor_by_region_center(egl, *region_position, *region_size, &monitor_pos, &monitor_size); + if(window.empty()) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + fprintf(stderr, "gsr error: the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:\n", region_size->x, region_size->y, region_position->x, region_position->y); + + MonitorOutputCallbackUserdata userdata; + userdata.window = egl->window; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); + _exit(51); + } + + // Capture whole monitor when region size is set to 0x0 + if(region_size->x == 0 && region_size->y == 0) { + region_position->x = 0; + region_position->y = 0; + } else { + region_position->x -= monitor_pos.x; + region_position->y -= monitor_pos.y; + } + return window; +} + +static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl *egl, CaptureSource &capture_source, bool prefer_ximage) { + bool follow_focused = false; + const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; + + gsr_capture *capture = nullptr; + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + if(wayland) { + fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n"); + _exit(2); + } + + if(arg_parser.output_resolution.x <= 0 || arg_parser.output_resolution.y <= 0) { + fprintf(stderr, "gsr error: invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0\n", arg_parser.output_resolution.x, arg_parser.output_resolution.y); + args_parser_print_usage(); + _exit(1); + } + + follow_focused = true; + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { +#ifdef GSR_PORTAL + // Desktop portal capture on x11 doesn't seem to be hardware accelerated + if(!wayland) { + fprintf(stderr, "gsr error: desktop portal capture is not supported on X11\n"); + _exit(1); + } + + gsr_capture_portal_params portal_params; + memset(&portal_params, 0, sizeof(portal_params)); + portal_params.egl = egl; + portal_params.record_cursor = arg_parser.record_cursor; + portal_params.restore_portal_session = arg_parser.restore_portal_session; + portal_params.portal_session_token_filepath = arg_parser.portal_session_token_filepath; + portal_params.output_resolution = arg_parser.output_resolution; + capture = gsr_capture_portal_create(&portal_params); + if(!capture) + _exit(1); +#else + fprintf(stderr, "gsr error: option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option\n"); + _exit(2); +#endif + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) { + capture_source.name = region_get_data(egl, &capture_source.region_size, &capture_source.region_pos); + capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); + if(!capture) + _exit(1); + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { + capture_source.name = validate_monitor_get_valid(egl, capture_source.name.c_str()); + capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); + if(!capture) + _exit(1); + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { + gsr_capture_v4l2_params v4l2_params; + memset(&v4l2_params, 0, sizeof(v4l2_params)); + v4l2_params.egl = egl; + v4l2_params.output_resolution = arg_parser.output_resolution; + v4l2_params.device_path = capture_source.name.c_str(); + v4l2_params.pixfmt = capture_source.v4l2_pixfmt; + v4l2_params.fps = arg_parser.fps; + capture = gsr_capture_v4l2_create(&v4l2_params); + if(!capture) + _exit(1); + } else { + if(wayland) { + fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture\n"); + _exit(2); + } + } + + if(!capture) { + gsr_capture_xcomposite_params xcomposite_params; + memset(&xcomposite_params, 0, sizeof(xcomposite_params)); + xcomposite_params.egl = egl; + xcomposite_params.cursor = &x11_cursor; + xcomposite_params.window = capture_source.window_id; + xcomposite_params.follow_focused = follow_focused; + xcomposite_params.record_cursor = arg_parser.record_cursor; + xcomposite_params.output_resolution = arg_parser.output_resolution; + capture = gsr_capture_xcomposite_create(&xcomposite_params); + if(!capture) + _exit(1); + } + + return capture; +} + +static gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { + switch(image_format) { + case GSR_IMAGE_FORMAT_JPEG: return image_quality >= 91 ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; + case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; + } + assert(false); + return GSR_COLOR_RANGE_FULL; +} + +static int video_quality_to_image_quality_value(gsr_video_quality video_quality) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 75; + case GSR_VIDEO_QUALITY_HIGH: + return 85; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return 91; // Quality above 90 makes the jpeg image encoder (stb_image_writer) use yuv444 instead of yuv420, which greatly improves small colored text quality on dark background + case GSR_VIDEO_QUALITY_ULTRA: + return 97; + } + assert(false); + return 90; +} + +static bool any_video_sources_uses_external_image(std::vector<VideoSource> &video_sources) { + for(VideoSource &video_source : video_sources) { + if(gsr_capture_uses_external_image(video_source.capture)) + return true; + } + return false; +} + +static std::vector<VideoSource> create_video_sources(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage, std::vector<CaptureSource> &capture_sources, vec2i &video_size) { + std::vector<VideoSource> video_sources; + video_sources.reserve(capture_sources.size()); + + for(CaptureSource &capture_source : capture_sources) { + gsr_capture_metadata capture_metadata; + memset(&capture_metadata, 0, sizeof(capture_metadata)); + capture_metadata.fps = arg_parser.fps; + capture_metadata.halign = capture_source.halign; + capture_metadata.valign = capture_source.valign; + capture_metadata.flip = (gsr_flip)capture_source.flip; + video_sources.push_back(VideoSource{create_capture_impl(arg_parser, egl, capture_source, prefer_ximage), capture_metadata, &capture_source}); + } + + for(VideoSource &video_source : video_sources) { + int capture_result = gsr_capture_start(video_source.capture, &video_source.metadata); + if(capture_result != 0) { + fprintf(stderr, "gsr error: gsr_capture_start failed\n"); + _exit(capture_result); + } + } + + video_size = {0, 0}; + for(const VideoSource &video_source : video_sources) { + video_size.x = std::max(video_size.x, video_source.metadata.video_size.x); + video_size.y = std::max(video_size.y, video_source.metadata.video_size.y); + } + + for(VideoSource &video_source : video_sources) { + video_source.metadata.video_size = video_size; + } + + return video_sources; +} + +static void video_sources_update_with_real_video_size(std::vector<CaptureSource> &capture_sources, std::vector<VideoSource> &video_sources, vec2i video_size) { + assert(capture_sources.size() == video_sources.size()); + for(size_t i = 0; i < capture_sources.size(); ++i) { + CaptureSource &capture_source = capture_sources[i]; + VideoSource &video_source = video_sources[i]; + + video_source.metadata.recording_size = video_source.metadata.video_size; + // TODO: What if this updated resolution is above max resolution? + video_source.metadata.video_size = video_size; + + if(capture_source.pos.x != 0 || capture_source.pos.y != 0) { + video_source.metadata.position.x = capture_source.pos.x; + video_source.metadata.position.y = capture_source.pos.y; + + if(capture_source.pos.x_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.position.x = video_source.metadata.video_size.x * ((double)video_source.metadata.position.x / 100.0); + + if(capture_source.pos.y_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.position.y = video_source.metadata.video_size.y * ((double)video_source.metadata.position.y / 100.0); + } + + if(capture_source.size.x != 0 || capture_source.size.y != 0) { + video_source.metadata.recording_size.x = capture_source.size.x; + video_source.metadata.recording_size.y = capture_source.size.y; + + if(capture_source.size.x_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.recording_size.x = video_source.metadata.video_size.x * ((double)video_source.metadata.recording_size.x / 100.0); + + if(capture_source.size.y_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.recording_size.y = video_source.metadata.video_size.y * ((double)video_source.metadata.recording_size.y / 100.0); + } + } +} + +static void gsr_capture_kms_cleanup_kms_fds() { + for(int i = 0; i < kms_response.num_items; ++i) { + for(int j = 0; j < kms_response.items[i].num_dma_bufs; ++j) { + gsr_kms_response_dma_buf *dma_buf = &kms_response.items[i].dma_buf[j]; + if(dma_buf->fd > 0) { + close(dma_buf->fd); + dma_buf->fd = -1; + } + } + kms_response.items[i].num_dma_bufs = 0; + } + kms_response.num_items = 0; +} + +// TODO: 10-bit and hdr. +static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, std::vector<CaptureSource> &capture_sources) { + const int image_quality = video_quality_to_image_quality_value(arg_parser.video_quality); + const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); + arg_parser.fps = 60; // We want to capture an image as soon as possible + + vec2i video_size = {0, 0}; + std::vector<VideoSource> video_sources = create_video_sources(arg_parser, egl, true, capture_sources, video_size); + video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); + + gsr_image_writer image_writer; + if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { + fprintf(stderr, "gsr error: capture_image_to_file: gsr_image_write_gl_init failed\n"); + _exit(1); + } + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = color_range; + color_conversion_params.egl = egl; + color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); + + color_conversion_params.destination_textures[0] = image_writer.texture; + color_conversion_params.destination_textures_size[0] = video_size; + color_conversion_params.num_destination_textures = 1; + color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB8; + + gsr_color_conversion color_conversion; + if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { + fprintf(stderr, "gsr error: capture_image_to_file: failed to create color conversion\n"); + _exit(1); + } + + gsr_color_conversion_clear(&color_conversion); + + bool should_stop_error = false; + egl->glClear(0); + + while(running) { + while(gsr_window_process_event(window)) { + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + + for(VideoSource &video_source : video_sources) { + gsr_capture_on_event(video_source.capture, egl); + } + } + + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); + + gsr_capture_kms_cleanup_kms_fds(); + + if(kms_client_initialized) { + if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) + fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + } + + should_stop_error = false; + for(VideoSource &video_source : video_sources) { + gsr_capture_tick(video_source.capture); + if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { + running = 0; + break; + } + } + + for(VideoSource &video_source : video_sources) { + if(video_source.capture->pre_capture) + video_source.capture->pre_capture(video_source.capture, &video_source.metadata, &color_conversion); + } + + if(color_conversion.schedule_clear) { + color_conversion.schedule_clear = false; + gsr_color_conversion_clear(&color_conversion); + } + + bool all_sources_captured = true; + for(VideoSource &video_source : video_sources) { + // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. + // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. + if(gsr_capture_capture(video_source.capture, &video_source.metadata, &color_conversion) != 0) + all_sources_captured = false; + } + + gsr_capture_kms_cleanup_kms_fds(); + + if(all_sources_captured) + break; + + if(running) + usleep(30 * 1000); // 30 ms + } + + gsr_egl_swap_buffers(egl); + + if(!should_stop_error) { + if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.filename, image_format, image_quality)) { + fprintf(stderr, "gsr error: capture_image_to_file: failed to write opengl texture to image output file %s\n", arg_parser.filename); + _exit(1); + } + + if(arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "screenshot"); + } + + gsr_image_writer_deinit(&image_writer); + for(VideoSource &video_source : video_sources) { + gsr_capture_destroy(video_source.capture); + } + _exit(should_stop_error ? 3 : 0); +} + +static AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder) { + if(use_software_video_encoder) { + return AV_PIX_FMT_NV12; + } else { + if(video_codec_is_vulkan(video_codec)) + return AV_PIX_FMT_VULKAN; + else + return vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI; + } +} + +static void match_app_audio_input_to_available_apps(const std::vector<AudioInput> &requested_audio_inputs, const std::vector<std::string> &app_audio_names) { + for(const AudioInput &request_audio_input : requested_audio_inputs) { + if(request_audio_input.type != AudioInputType::APPLICATION || request_audio_input.inverted) + continue; + + bool match = false; + for(const std::string &app_name : app_audio_names) { + if(strcasecmp(app_name.c_str(), request_audio_input.name.c_str()) == 0) { + match = true; + break; + } + } + + if(!match) { + fprintf(stderr, "gsr warning: no audio application with the name \"%s\" was found, expected one of the following:\n", request_audio_input.name.c_str()); + for(const std::string &app_name : app_audio_names) { + fprintf(stderr, " * %s\n", app_name.c_str()); + } + fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n"); + } + } +} + +// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. +// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. +// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU +static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &audio_devices, const Arg *audio_input_arg) { + std::vector<MergedAudioInputs> requested_audio_inputs; + + for(int i = 0; i < audio_input_arg->num_values; ++i) { + const char *audio_input = audio_input_arg->values[i]; + if(!audio_input || audio_input[0] == '\0') + continue; + + requested_audio_inputs.push_back(parse_audio_input_arg(audio_input)); + for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) { + if(request_audio_input.type != AudioInputType::DEVICE) + continue; + + bool match = false; + + if(request_audio_input.name == "default_output") { + if(audio_devices.default_output.empty()) { + fprintf(stderr, "gsr error: -a default_output was specified but no default audio output is specified in the audio server\n"); + _exit(2); + } + match = true; + } else if(request_audio_input.name == "default_input") { + if(audio_devices.default_input.empty()) { + fprintf(stderr, "gsr error: -a default_input was specified but no default audio input is specified in the audio server\n"); + _exit(2); + } + match = true; + } else { + const bool name_is_existing_audio_device = get_audio_device_by_name(audio_devices.audio_inputs, request_audio_input.name.c_str()) != nullptr; + if(name_is_existing_audio_device) + match = true; + } + + if(!match) { + fprintf(stderr, "gsr error: Audio device '%s' is not a valid audio device, expected one of:\n", request_audio_input.name.c_str()); + if(!audio_devices.default_output.empty()) + fprintf(stderr, " default_output (Default output)\n"); + if(!audio_devices.default_input.empty()) + fprintf(stderr, " default_input (Default input)\n"); + for(const auto &audio_device_input : audio_devices.audio_inputs) { + fprintf(stderr, " %s (%s)\n", audio_device_input.name.c_str(), audio_device_input.description.c_str()); + } + _exit(50); + } + } + } + + return requested_audio_inputs; +} + +static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); +} + +static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; +} + +template <typename T> +static bool string_to_int(const char *str, size_t len, T *number) { + char number_str[32]; + snprintf(number_str, sizeof(number_str), "%.*s", (int)len, str); + + errno = 0; + *number = strtol(number_str, NULL, 0); + return errno == 0; +} + +static void capture_source_type_from_string(const char *capture_source_str, size_t size, CaptureSource &capture_source) { + char capture_source_str_n[64]; + snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); + + if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; + } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; + } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; + } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source.region_size.x, &capture_source.region_size.y, &capture_source.region_pos.x, &capture_source.region_pos.y) == 4) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; + capture_source.region_set = true; + } else if(contains_non_hex_number(capture_source_str_n)) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + } else { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + } +} + +static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { + if(len == 5 && memcmp(str, "start", 5) == 0) { + *alignment = GSR_CAPTURE_ALIGN_START; + return true; + } else if(len == 6 && memcmp(str, "center", 6) == 0) { + *alignment = GSR_CAPTURE_ALIGN_CENTER; + return true; + } else if(len == 3 && memcmp(str, "end", 3) == 0) { + *alignment = GSR_CAPTURE_ALIGN_END; + return true; + } else { + return false; + } +} + +static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { + if(len == 4 && memcmp(str, "auto", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + return true; + } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; + return true; + } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; + return true; + } else { + return false; + } +} + +static bool string_to_bool(const char *str, size_t len, bool *value) { + if(len == 4 && memcmp(str, "true", 4) == 0) { + *value = true; + return true; + } else if(len == 5 && memcmp(str, "false", 5) == 0) { + *value = false; + return true; + } else { + return false; + } +} + +static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { + bool is_first_column = true; + + split_string(capture_source_str, ';', [&](const char *sub, size_t size) { + if(size == 0) + return true; + + // First column contains the capture target + if(is_first_column) { + is_first_column = false; + return true; + } + + if(string_starts_with(sub, size, "x=")) { + capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!string_to_int(sub, size, &capture_source.pos.x)) { + fprintf(stderr, "gsr error: invalid capture target value for option x: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "y=")) { + capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!string_to_int(sub, size, &capture_source.pos.y)) { + fprintf(stderr, "gsr error: invalid capture target value for option y: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "width=")) { + capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 6; + size -= 6; + if(!string_to_int(sub, size, &capture_source.size.x)) { + fprintf(stderr, "gsr error: invalid capture target value for option width: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "height=")) { + capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 7; + size -= 7; + if(!string_to_int(sub, size, &capture_source.size.y)) { + fprintf(stderr, "gsr error: invalid capture target value for option height: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "halign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { + fprintf(stderr, "gsr error: invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "valign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { + fprintf(stderr, "gsr error: invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "pixfmt=")) { + sub += 7; + size -= 7; + if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { + fprintf(stderr, "gsr error: invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "hflip=")) { + sub += 6; + size -= 6; + bool hflip = false; + if(!string_to_bool(sub, size, &hflip)) { + fprintf(stderr, "gsr error: invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + _exit(1); + } + + if(hflip) + capture_source.flip |= GSR_FLIP_HORIZONTAL; + } else if(string_starts_with(sub, size, "vflip=")) { + sub += 6; + size -= 6; + bool vflip = false; + if(!string_to_bool(sub, size, &vflip)) { + fprintf(stderr, "gsr error: invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + _exit(1); + } + + if(vflip) + capture_source.flip |= GSR_FLIP_VERTICAL; + } else { + fprintf(stderr, "gsr error: invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip or vflip\n", (int)size, sub); + _exit(1); + } + + return true; + }); +} + +static std::vector<CaptureSource> parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { + std::vector<CaptureSource> requested_capture_sources; + + split_string(capture_source_arg, '|', [&](const char *sub, size_t size) { + if(size == 0) + return true; + + const char *substr_start = sub; + size_t capture_source_size = size; + const char *capture_source_end = (const char*)memchr(sub, ';', size); + if(capture_source_end) + capture_source_size = capture_source_end - sub; + + CaptureSource capture_source; + capture_source.region_pos = arg_parser.region_position; + capture_source.region_size = arg_parser.region_size; + + if(string_starts_with(sub, capture_source_size, "monitor:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + sub += 8; + capture_source_size -= 8; + } else if(string_starts_with(sub, capture_source_size, "window:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + sub += 7; + capture_source_size -= 7; + } else if(string_starts_with(sub, capture_source_size, "v4l2:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + sub += 5; + capture_source_size -= 5; + } else { + capture_source_type_from_string(sub, capture_source_size, capture_source); + } + + capture_source.name.assign(sub, capture_source_size); + + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { + if(!string_to_int(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { + fprintf(stderr, "gsr error: invalid window number %s\n", capture_source.name.c_str()); + args_parser_print_usage(); + _exit(1); + } + } + + /* We want good default values for v4l2 (webcam) capture, by setting webcam at bottom right, offset by -10%,-10% pixels and at size 30%,30% */ + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2 && !requested_capture_sources.empty()) { + capture_source.halign = GSR_CAPTURE_ALIGN_START; + capture_source.valign = GSR_CAPTURE_ALIGN_END; + capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + capture_source.size = {30, 30, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + } + + parse_capture_source_options(std::string(substr_start, size), capture_source); + requested_capture_sources.push_back(capture_source); + return true; + }); + + return requested_capture_sources; +} + +static bool audio_inputs_has_app_audio(const std::vector<AudioInput> &audio_inputs) { + for(const auto &audio_input : audio_inputs) { + if(audio_input.type == AudioInputType::APPLICATION) + return true; + } + return false; +} + +static bool merged_audio_inputs_has_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs) { + for(const auto &merged_audio_input : merged_audio_inputs) { + if(audio_inputs_has_app_audio(merged_audio_input.audio_inputs)) + return true; + } + return false; +} + +// Should use amix if more than 1 audio device and 0 application audio, merged +static bool audio_inputs_should_use_amix(const std::vector<AudioInput> &audio_inputs) { + int num_audio_devices = 0; + int num_app_audio = 0; + + for(const auto &audio_input : audio_inputs) { + if(audio_input.type == AudioInputType::DEVICE) + ++num_audio_devices; + else if(audio_input.type == AudioInputType::APPLICATION) + ++num_app_audio; + } + + return num_audio_devices > 1 && num_app_audio == 0; +} + +static bool merged_audio_inputs_should_use_amix(const std::vector<MergedAudioInputs> &merged_audio_inputs) { + for(const auto &merged_audio_input : merged_audio_inputs) { + if(audio_inputs_should_use_amix(merged_audio_input.audio_inputs)) + return true; + } + return false; +} + +static void validate_merged_audio_inputs_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs, const std::vector<std::string> &app_audio_names) { + for(const auto &merged_audio_input : merged_audio_inputs) { + int num_app_audio = 0; + int num_app_inverted_audio = 0; + + for(const auto &audio_input : merged_audio_input.audio_inputs) { + if(audio_input.type == AudioInputType::APPLICATION) { + if(audio_input.inverted) + ++num_app_inverted_audio; + else + ++num_app_audio; + } + } + + match_app_audio_input_to_available_apps(merged_audio_input.audio_inputs, app_audio_names); + + if(num_app_audio > 0 && num_app_inverted_audio > 0) { + fprintf(stderr, "gsr error: argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track\n"); + _exit(2); + } + } +} + +static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const std::string &file_extension, bool uses_amix) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + if(file_extension == "webm") { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from aac to opus\n"); + } + break; + } + case GSR_AUDIO_CODEC_OPUS: { + // TODO: Also check mpegts? + if(file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm") { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + fprintf(stderr, "gsr warning: opus audio codec is only supported by .mp4, .mkv and .webm files, falling back to aac instead\n"); + } + break; + } + case GSR_AUDIO_CODEC_FLAC: { + // TODO: Also check mpegts? + if(file_extension == "webm") { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from flac to opus\n"); + } else if(file_extension != "mp4" && file_extension != "mkv") { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + fprintf(stderr, "gsr warning: flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n"); + } else if(uses_amix) { + // TODO: remove this? is it true anymore? + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + fprintf(stderr, "gsr warning: flac audio codec is not supported when mixing audio sources, falling back to opus instead\n"); + } + break; + } + } + return audio_codec; +} + +static bool video_codec_only_supports_low_power_mode(const gsr_supported_video_codecs &supported_video_codecs, gsr_video_codec video_codec) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: return supported_video_codecs.h264.low_power; + case GSR_VIDEO_CODEC_HEVC: return supported_video_codecs.hevc.low_power; + case GSR_VIDEO_CODEC_HEVC_HDR: return supported_video_codecs.hevc_hdr.low_power; + case GSR_VIDEO_CODEC_HEVC_10BIT: return supported_video_codecs.hevc_10bit.low_power; + case GSR_VIDEO_CODEC_AV1: return supported_video_codecs.av1.low_power; + case GSR_VIDEO_CODEC_AV1_HDR: return supported_video_codecs.av1_hdr.low_power; + case GSR_VIDEO_CODEC_AV1_10BIT: return supported_video_codecs.av1_10bit.low_power; + case GSR_VIDEO_CODEC_VP8: return supported_video_codecs.vp8.low_power; + case GSR_VIDEO_CODEC_VP9: return supported_video_codecs.vp9.low_power; + case GSR_VIDEO_CODEC_H264_VULKAN: return supported_video_codecs.h264.low_power; + case GSR_VIDEO_CODEC_HEVC_VULKAN: return supported_video_codecs.hevc.low_power; // TODO: hdr, 10 bit + } + return false; +} + +static const AVCodec* get_av_codec_if_supported(gsr_video_codec video_codec, gsr_egl *egl, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: { + if(use_software_video_encoder) + return avcodec_find_encoder_by_name("libx264"); + else if(supported_video_codecs->h264.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC: { + if(supported_video_codecs->hevc.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: { + if(supported_video_codecs->hevc_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: { + if(supported_video_codecs->hevc_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1: { + if(supported_video_codecs->av1.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: { + if(supported_video_codecs->av1_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: { + if(supported_video_codecs->av1_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(supported_video_codecs->h264.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + // TODO: hdr, 10 bit + if(supported_video_codecs->hevc.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + } + return nullptr; +} + +static vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: { + if(use_software_video_encoder) + return {4096, 2304}; + else if(supported_video_codecs->h264.supported) + return supported_video_codecs->h264.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC: { + if(supported_video_codecs->hevc.supported) + return supported_video_codecs->hevc.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: { + if(supported_video_codecs->hevc_hdr.supported) + return supported_video_codecs->hevc_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: { + if(supported_video_codecs->hevc_10bit.supported) + return supported_video_codecs->hevc_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1: { + if(supported_video_codecs->av1.supported) + return supported_video_codecs->av1.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: { + if(supported_video_codecs->av1_hdr.supported) + return supported_video_codecs->av1_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: { + if(supported_video_codecs->av1_10bit.supported) + return supported_video_codecs->av1_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return supported_video_codecs->vp8.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return supported_video_codecs->vp9.max_resolution; + break; + } + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(supported_video_codecs->h264.supported) + return supported_video_codecs->h264.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + // TODO: hdr, 10 bit + if(supported_video_codecs->hevc.supported) + return supported_video_codecs->hevc.max_resolution; + break; + } + } + return {0, 0}; +} + +static bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution) { + if(codec_max_resolution.x == 0 || codec_max_resolution.y == 0) + return true; + return codec_max_resolution.x >= capture_resolution.x && codec_max_resolution.y >= capture_resolution.y; +} + +static void print_codec_error(gsr_video_codec video_codec) { + if(video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO) + video_codec = GSR_VIDEO_CODEC_H264; + + const char *video_codec_name = video_codec_to_string(video_codec); + fprintf(stderr, "gsr error: your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n" + " then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n" + " It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n" + " This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n" + " You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n" + " On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n" + " You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n" + " If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.\n", video_codec_name, video_codec_name, video_codec_name); +} + +static void force_cpu_encoding(args_parser *args_parser) { + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + args_parser->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; + if(args_parser->bitrate_mode == GSR_BITRATE_MODE_VBR) { + fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); + args_parser->bitrate_mode = GSR_BITRATE_MODE_QP; + } +} + +static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs) { + // TODO: software encoder for hevc, av1, vp8 and vp9 + *low_power = false; + const AVCodec *video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + + if(!video_codec_f && use_fallback_codec && args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + switch(args_parser->video_codec) { + case GSR_VIDEO_CODEC_H264: { + fprintf(stderr, "gsr error: selected video codec h264 is not supported\n"); + if(args_parser->fallback_cpu_encoding) { + fprintf(stderr, "gsr warning: gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); + force_cpu_encoding(args_parser); + } + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: { + fprintf(stderr, "gsr warning: selected video codec hevc is not supported, trying h264 instead\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: { + fprintf(stderr, "gsr warning: selected video codec av1 is not supported, trying h264 instead\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); + } + case GSR_VIDEO_CODEC_VP8: + case GSR_VIDEO_CODEC_VP9: + // TODO: Cant fallback to other codec because webm only supports vp8/vp9 + break; + case GSR_VIDEO_CODEC_H264_VULKAN: { + fprintf(stderr, "gsr warning: selected video codec h264_vulkan is not supported, trying h264 instead\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { + fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); + print_codec_error(args_parser->video_codec); + _exit(11); + } + return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); + } + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + fprintf(stderr, "gsr warning: selected video codec hevc_vulkan is not supported, trying hevc instead\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { + fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); + print_codec_error(args_parser->video_codec); + _exit(11); + } + return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); + } + } + + video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + } + + if(!video_codec_f) { + print_codec_error(args_parser->video_codec); + _exit(54); + } + + *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->video_codec); + + return video_codec_f; +} + +/* Returns -1 if none is available */ +static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { + if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { + fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); + return GSR_VIDEO_CODEC_H264; + } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { + fprintf(stderr, "gsr info: using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than capture resolution (%dx%d)\n", + supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_HEVC; + } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { + fprintf(stderr, "gsr info: using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than capture resolution (%dx%d)\n", + supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_AV1; + } else { + return (gsr_video_codec)-1; + } +} + +static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { + gsr_supported_video_codecs supported_video_codecs; + get_supported_video_codecs(egl, args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs); + // TODO: Use gsr_supported_video_codecs *supported_video_codecs_vulkan here to properly query vulkan video support + set_supported_video_codecs_ffmpeg(&supported_video_codecs, nullptr, egl->gpu_info.vendor); + + const bool video_codec_auto = args_parser->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; + if(video_codec_auto) { + if(strcmp(file_extension, "webm") == 0) { + fprintf(stderr, "gsr info: using vp8 encoder because a codec was not specified and the file extension is .webm\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_VP8; + } else if(args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + } else if(args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + args_parser->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs); + if(args_parser->video_codec == (gsr_video_codec)-1) { + if(args_parser->fallback_cpu_encoding) { + fprintf(stderr, "gsr warning: gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); + force_cpu_encoding(args_parser); + } else { + fprintf(stderr, "gsr error: no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec.\n"); + fprintf(stderr, " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding.\n"); + _exit(52); + } + } + } + } + + // TODO: Allow hevc, vp9 and av1 in (enhanced) flv (supported since ffmpeg 6.1) + if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { + if(args_parser->video_codec != GSR_VIDEO_CODEC_H264) { + args_parser->video_codec = GSR_VIDEO_CODEC_H264; + fprintf(stderr, "gsr warning: hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead.\n"); + } + } else if(strcmp(file_extension, "m3u8") == 0) { + if(video_codec_is_av1(args_parser->video_codec)) { + args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; + fprintf(stderr, "gsr warning: av1 is not compatible with hls (m3u8), falling back to hevc instead.\n"); + } + } + + const AVCodec *codec = pick_video_codec(egl, args_parser, true, low_power, &supported_video_codecs); + + const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, &supported_video_codecs); + if(!codec_supports_resolution(codec_max_resolution, video_size)) { + const char *video_codec_name = video_codec_to_string(args_parser->video_codec); + fprintf(stderr, "gsr error: The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again\n", + video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); + _exit(53); + } + + return codec; +} + +static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector<AudioInput> &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector<AVFilterContext*> &src_filter_ctx, bool use_amix) { + std::vector<AudioDeviceData> audio_track_audio_devices; + for(size_t i = 0; i < audio_inputs.size(); ++i) { + const auto &audio_input = audio_inputs[i]; + AVFilterContext *src_ctx = nullptr; + if(use_amix) + src_ctx = src_filter_ctx[i]; + + AudioDeviceData audio_device; + audio_device.audio_input = audio_input; + audio_device.src_filter_ctx = src_ctx; + + if(audio_input.name.empty()) { + audio_device.sound_device.handle = NULL; + audio_device.sound_device.frames = 0; + } else { + const std::string description = "gsr-" + audio_input.name; + if(sound_device_get_by_name(&audio_device.sound_device, description.c_str(), audio_input.name.c_str(), description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + fprintf(stderr, "gsr error: failed to get \"%s\" audio device\n", audio_input.name.c_str()); + _exit(1); + } + } + + audio_device.frame = create_audio_frame(audio_codec_context); + audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + audio_track_audio_devices.push_back(std::move(audio_device)); + } + return audio_track_audio_devices; +} + +#ifdef GSR_APP_AUDIO +static AudioDeviceData create_application_audio_audio_input(const MergedAudioInputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { + AudioDeviceData audio_device; + audio_device.frame = create_audio_frame(audio_codec_context); + audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + char random_str[8]; + if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { + fprintf(stderr, "gsr error: failed to generate random string\n"); + _exit(1); + } + + std::string combined_sink_name = "gsr-combined-"; + combined_sink_name.append(random_str, sizeof(random_str)); + combined_sink_name += ".monitor"; + + if(sound_device_get_by_name(&audio_device.sound_device, combined_sink_name.c_str(), "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + fprintf(stderr, "gsr error: failed to setup audio recording to combined sink\n"); + _exit(1); + } + + std::vector<const char*> audio_devices_sources; + for(const auto &audio_input : merged_audio_inputs.audio_inputs) { + if(audio_input.type == AudioInputType::DEVICE) + audio_devices_sources.push_back(audio_input.name.c_str()); + } + + bool app_audio_inverted = false; + std::vector<const char*> app_names; + for(const auto &audio_input : merged_audio_inputs.audio_inputs) { + if(audio_input.type == AudioInputType::APPLICATION) { + app_names.push_back(audio_input.name.c_str()); + app_audio_inverted = audio_input.inverted; + } + } + + if(!audio_devices_sources.empty()) { + if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources.data(), audio_devices_sources.size(), combined_sink_name.c_str())) { + fprintf(stderr, "gsr error: failed to add application audio link\n"); + _exit(1); + } + } + + if(app_audio_inverted) { + if(!gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { + fprintf(stderr, "gsr error: failed to add application audio link\n"); + _exit(1); + } + } else { + if(!gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { + fprintf(stderr, "gsr error: failed to add application audio link\n"); + _exit(1); + } + } + + return audio_device; +} +#endif + +static bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { + if(string_ends_with(filename, ".jpg") || string_ends_with(filename, ".jpeg")) { + *image_format = GSR_IMAGE_FORMAT_JPEG; + return true; + } else if(string_ends_with(filename, ".png")) { + *image_format = GSR_IMAGE_FORMAT_PNG; + return true; + } else { + return false; + } +} + +// TODO: replace this with start_recording_create_steams +static bool av_open_file_write_header(AVFormatContext *av_format_context, const char *filename) { + int ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); + if(ret < 0) { + fprintf(stderr, "gsr error: Could not open '%s': %s\n", filename, av_error_to_string(ret)); + return false; + } + + AVDictionary *options = nullptr; + av_dict_set(&options, "strict", "experimental", 0); + //av_dict_set_int(&av_format_context->metadata, "video_full_range_flag", 1, 0); + + ret = avformat_write_header(av_format_context, &options); + if(ret < 0) + fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret)); + + const bool success = ret >= 0; + if(!success) + avio_close(av_format_context->pb); + + av_dict_free(&options); + return success; +} + +static int audio_codec_get_frame_size(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 1024; + case GSR_AUDIO_CODEC_OPUS: return 960; + case GSR_AUDIO_CODEC_FLAC: + assert(false); + return 1024; + } + assert(false); + return 1024; +} + +static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const std::vector<MergedAudioInputs> &audio_inputs) { + if(replay_buffer_size_secs == -1) + return 0; + + int audio_fps = 0; + if(!audio_inputs.empty()) + audio_fps = AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); + + return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size()); +} + +static void set_display_server_environment_variables() { + // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) + const char *display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + setenv("DISPLAY", display, true); + } + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if(!wayland_display) { + wayland_display = "wayland-1"; + setenv("WAYLAND_DISPLAY", wayland_display, true); + } +} + +static bool is_capturing_damage_tracked_target(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type != GSR_CAPTURE_SOURCE_TYPE_V4L2) + return true; + } + return false; +} + +static bool is_capturing_type(const std::vector<CaptureSource> &capture_sources, CaptureSourceType target_type) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == target_type) + return true; + } + return false; +} + +static bool has_capture_source_with_region_set(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION && capture_source.region_set) + return true; + } + return false; +} + +static bool is_capturing_monitor_or_region(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) + return true; + } + return false; +} + +static void validate_args_with_capture_sources(args_parser &arg_parser, const std::vector<CaptureSource> &capture_sources) { + const Arg *output_resolution_arg = args_parser_get_arg(&arg_parser, "-s"); + assert(output_resolution_arg); + + const Arg *region_arg = args_parser_get_arg(&arg_parser, "-region"); + assert(region_arg); + + if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { + fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n"); + args_parser_print_usage(); + _exit(1); + } + + const bool is_capturing_region = is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION); + if(region_arg->num_values == 0) { + if(is_capturing_region && !has_capture_source_with_region_set(capture_sources)) { + fprintf(stderr, "gsr error: option -region is required when '-w region' is used\n"); + args_parser_print_usage(); + _exit(1); + } + } else { + if(is_capturing_region) { + fprintf(stderr, "gsr warning: option -region is deprecated, use -w with region directly instead, for example: -w %s\n", region_arg->values[0]); + } else { + fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n"); + args_parser_print_usage(); + _exit(1); + } + } + + if(!arg_parser.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) + fprintf(stderr, "gsr info: option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored\n"); +} + +int main(int argc, char **argv) { + setlocale(LC_ALL, "C"); // Sigh... stupid C +#ifdef __GLIBC__ + mallopt(M_MMAP_THRESHOLD, 65536); +#endif + + signal(SIGINT, stop_handler); + signal(SIGTERM, stop_handler); + signal(SIGUSR1, save_replay_handler); + signal(SIGUSR2, toggle_pause_handler); + signal(SIGRTMIN, toggle_replay_recording_handler); + signal(SIGRTMIN+1, save_replay_10_seconds_handler); + signal(SIGRTMIN+2, save_replay_30_seconds_handler); + signal(SIGRTMIN+3, save_replay_1_minute_handler); + signal(SIGRTMIN+4, save_replay_5_minutes_handler); + signal(SIGRTMIN+5, save_replay_10_minutes_handler); + signal(SIGRTMIN+6, save_replay_30_minutes_handler); + + set_display_server_environment_variables(); + + // Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue, + // where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus + // decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. + // TODO: This only seems to allow the gpu to go to lower power level states, but not higher than p2. + setenv("CUDA_DISABLE_PERF_BOOST", "1", true); + // Stop nvidia driver from buffering frames + setenv("__GL_MaxFramesAllowed", "1", true); + // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context, + // so we overwrite it + setenv("__GL_THREADED_OPTIMIZATIONS", "0", true); + // Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter. + // The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup. + // This option was added in mesa 24.1.4, released in july 17, 2024. + // TODO: Add an option to enable/disable this? + // Seems like the performance issue is not in encoding, but rendering the frame. + // Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? + setenv("AMD_DEBUG", "lowlatencyenc", true); + // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since + // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet. + // Let vaapi find the right vaapi driver instead of forcing a specific one. + unsetenv("LIBVA_DRIVER_NAME"); + // Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. + unsetenv("__GL_SYNC_TO_VBLANK"); + // Same as above, but for amd/intel + unsetenv("vblank_mode"); + + if(geteuid() == 0) { + fprintf(stderr, "gsr error: don't run gpu-screen-recorder as the root user\n"); + _exit(1); + } + + args_handlers arg_handlers; + arg_handlers.version = version_command; + arg_handlers.info = info_command; + arg_handlers.list_audio_devices = list_audio_devices_command; + arg_handlers.list_application_audio = list_application_audio_command; + arg_handlers.list_v4l2_devices = list_v4l2_devices; + arg_handlers.list_capture_options = list_capture_options_command; + + args_parser arg_parser; + if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL)) + _exit(1); + + std::vector<CaptureSource> capture_sources = parse_capture_source_arg(arg_parser.capture_source, arg_parser); + if(capture_sources.empty()) { + fprintf(stderr, "gsr error: option -w can't be empty. You need to capture video from at least one source\n"); + args_parser_print_usage(); + _exit(1); + } + validate_args_with_capture_sources(arg_parser, capture_sources); + + // TODO: Remove, this isn't true + if(arg_parser.overclock) { + int driver_major_version = 0; + int driver_minor_version = 0; + if(get_nvidia_driver_version(&driver_major_version, &driver_minor_version) && (driver_major_version > 580 || (driver_major_version == 580 && driver_minor_version >= 105))) { + fprintf(stderr, "gsr info: overclocking was set by has been forcefully disabled since your gpu supports CUDA_DISABLE_PERF_BOOST to workaround driver issue (overclocking is not needed)\n"); + arg_parser.overclock = false; + } + } + + //av_log_set_level(AV_LOG_TRACE); + + const Arg *audio_input_arg = args_parser_get_arg(&arg_parser, "-a"); + assert(audio_input_arg); + + AudioDevices audio_devices; + if(audio_input_arg->num_values > 0) + audio_devices = get_pulseaudio_inputs(); + + std::vector<MergedAudioInputs> requested_audio_inputs = parse_audio_inputs(audio_devices, audio_input_arg); + + const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs); + std::vector<std::string> app_audio_names; +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio pipewire_audio; + memset(&pipewire_audio, 0, sizeof(pipewire_audio)); + if(uses_app_audio) { + if(!pulseaudio_server_is_pipewire()) { + fprintf(stderr, "gsr error: your sound server is not PipeWire. Application audio is only available when running PipeWire audio server\n"); + _exit(2); + } + + if(!gsr_pipewire_audio_init(&pipewire_audio)) { + fprintf(stderr, "gsr error: failed to setup PipeWire audio for application audio capture\n"); + _exit(2); + } + + gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) { + std::vector<std::string> *app_audio_names = (std::vector<std::string>*)userdata; + app_audio_names->push_back(app_name); + return true; + }, &app_audio_names); + } +#else + if(uses_app_audio) { + fprintf(stderr, "gsr error: application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)\n"); + _exit(2); + } +#endif + + validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names); + + const bool is_replaying = arg_parser.replay_buffer_size_secs != -1; + + bool wayland = false; + Display *dpy = XOpenDisplay(nullptr); + if (!dpy) { + wayland = true; + fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); + } + + XSetErrorHandler(x11_error_handler); + XSetIOErrorHandler(x11_io_error_handler); + + if(!wayland) + wayland = is_xwayland(dpy); + + if(!wayland && is_using_prime_run()) { + // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. + // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected + // to the drm device. + fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); + disable_prime_run(); + } + + gsr_window *window = gsr_window_create(dpy, wayland); + if(!window) { + fprintf(stderr, "gsr error: failed to create window\n"); + _exit(1); + } + + if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { + if(is_using_prime_run()) { + fprintf(stderr, "gsr warning: use of prime-run with -w portal option is currently not supported. Disabling prime-run\n"); + disable_prime_run(); + } + + if(video_codec_is_hdr(arg_parser.video_codec)) { + fprintf(stderr, "gsr warning: portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr\n"); + arg_parser.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.video_codec); + } + } + + gsr_egl egl; + if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.gl_debug)) { + fprintf(stderr, "gsr error: failed to load opengl\n"); + _exit(1); + } + + gsr_shader_enable_debug_output(arg_parser.gl_debug); +#ifndef NDEBUG + gsr_shader_enable_debug_output(true); +#endif + + if(!args_parser_validate_with_gl_info(&arg_parser, &egl)) + _exit(1); + + egl.card_path[0] = '\0'; + if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { + // TODO: Allow specifying another card, and in other places + if(!gsr_get_valid_card_path(&egl, egl.card_path, is_capturing_monitor_or_region(capture_sources))) { + fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option\n"); + _exit(2); + } + } + + memset(&x11_cursor, 0, sizeof(x11_cursor)); + x11_cursor_display = NULL; + if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.record_cursor) { + x11_cursor_display = (Display*)gsr_window_get_display(egl.window); + gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display); + } + + // if(wayland && arg_parser.capture_source_type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { + // fprintf(stderr, "gsr warning: it's not possible to sync video to recorded monitor exactly on wayland when recording a monitor." + // " If you experience stutter in the video then record with portal capture option instead (-w portal) or use X11 instead\n"); + // } + + gsr_image_format image_format; + if(get_image_format_from_filename(arg_parser.filename, &image_format)) { + if(audio_input_arg->num_values > 0) { + fprintf(stderr, "gsr error: can't record audio (-a) when taking a screenshot\n"); + _exit(1); + } + + capture_image_to_file(arg_parser, &egl, window, image_format, capture_sources); + _exit(0); + } + + AVFormatContext *av_format_context; + // The output format is automatically guessed by the file extension + avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, arg_parser.filename); + if (!av_format_context) { + if(arg_parser.container_format) { + fprintf(stderr, "gsr error: Container format '%s' (argument -c) is not valid\n", arg_parser.container_format); + } else { + fprintf(stderr, "gsr error: Failed to deduce container format from file extension. Use the '-c' option to specify container format\n"); + args_parser_print_usage(); + _exit(1); + } + _exit(1); + } + + const AVOutputFormat *output_format = av_format_context->oformat; + + std::string file_extension = output_format->extensions ? output_format->extensions : ""; + { + size_t comma_index = file_extension.find(','); + if(comma_index != std::string::npos) + file_extension = file_extension.substr(0, comma_index); + } + + const bool force_no_audio_offset = arg_parser.is_livestream || arg_parser.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); + const double target_fps = 1.0 / (double)arg_parser.fps; + + const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); + arg_parser.audio_codec = select_audio_codec_with_fallback(arg_parser.audio_codec, file_extension, uses_amix); + + vec2i video_size = {0, 0}; + std::vector<VideoSource> video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); + + // (Some?) livestreaming services require at least one audio track to work. + // If not audio is provided then create one silent audio track. + if(arg_parser.is_livestream && requested_audio_inputs.empty()) { + fprintf(stderr, "gsr info: live streaming but no audio track was added. Adding a silent audio track\n"); + MergedAudioInputs mai; + mai.audio_inputs.push_back({""}); + requested_audio_inputs.push_back(std::move(mai)); + } + + AVStream *video_stream = nullptr; + std::vector<AudioTrack> audio_tracks; + + if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.video_codec != GSR_VIDEO_CODEC_H264) { + fprintf(stderr, "gsr error: -encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment\n"); + _exit(1); + } + + bool low_power = false; + const AVCodec *video_codec_f = select_video_codec_with_fallback(video_size, &arg_parser, file_extension.c_str(), &egl, &low_power); + + const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); + AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, egl, arg_parser, video_size.x, video_size.y); + if(!is_replaying) + video_stream = create_stream(av_format_context, video_codec_context); + + AVFrame *video_frame = av_frame_alloc(); + if(!video_frame) { + fprintf(stderr, "gsr error: Failed to allocate video frame\n"); + _exit(1); + } + video_frame->format = video_codec_context->pix_fmt; + video_frame->width = video_size.x; + video_frame->height = video_size.y; + video_frame->color_range = video_codec_context->color_range; + video_frame->color_primaries = video_codec_context->color_primaries; + video_frame->color_trc = video_codec_context->color_trc; + video_frame->colorspace = video_codec_context->colorspace; + video_frame->chroma_location = video_codec_context->chroma_sample_location; + + const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.replay_buffer_size_secs, arg_parser.fps, arg_parser.audio_codec, requested_audio_inputs); + gsr_encoder encoder; + if(!gsr_encoder_init(&encoder, arg_parser.replay_storage, estimated_replay_buffer_packets, arg_parser.replay_buffer_size_secs, arg_parser.filename)) { + fprintf(stderr, "gsr error: failed to create encoder\n"); + _exit(1); + } + + gsr_video_encoder *video_encoder = create_video_encoder(&egl, arg_parser); + if(!video_encoder) { + fprintf(stderr, "gsr error: failed to create video encoder\n"); + _exit(1); + } + + if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) { + fprintf(stderr, "gsr error: failed to start video encoder\n"); + _exit(1); + } + + video_size.x = video_codec_context->width; + video_size.y = video_codec_context->height; + video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); + + const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); + assert(plugin_arg); + + gsr_plugins plugins; + memset(&plugins, 0, sizeof(plugins)); + + if(plugin_arg->num_values > 0) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); + assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); + + const gsr_plugin_init_params plugin_init_params = { + (unsigned int)video_size.x, + (unsigned int)video_size.y, + (unsigned int)arg_parser.fps, + color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS, + egl.context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES, + }; + + if(!gsr_plugins_init(&plugins, plugin_init_params, &egl)) + _exit(1); + + for(int i = 0; i < plugin_arg->num_values; ++i) { + if(!gsr_plugins_load_plugin(&plugins, plugin_arg->values[i])) + _exit(1); + } + } + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = arg_parser.color_range; + color_conversion_params.egl = &egl; + color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); + gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); + + gsr_color_conversion color_conversion; + if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { + fprintf(stderr, "gsr error: main: failed to create color conversion\n"); + _exit(1); + } + + gsr_color_conversion_clear(&color_conversion); + + gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; + + if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + open_video_software(video_codec_context, arg_parser); + } else { + open_video_hardware(video_codec_context, low_power, egl, arg_parser); + } + + if(video_stream) { + avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); + gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0); + } + + int audio_max_frame_size = 1024; + int audio_stream_index = VIDEO_STREAM_INDEX + 1; + for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) { + const bool use_amix = audio_inputs_should_use_amix(merged_audio_inputs.audio_inputs); + AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.fps, arg_parser.audio_codec, use_amix, arg_parser.audio_bitrate); + + AVStream *audio_stream = nullptr; + if(!is_replaying) { + audio_stream = create_stream(av_format_context, audio_codec_context); + if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1) + fprintf(stderr, "gsr error: added too many audio sources\n"); + } + + if(audio_stream && !merged_audio_inputs.track_name.empty()) + av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0); + + open_audio(audio_codec_context); + if(audio_stream) + avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); + + #if LIBAVCODEC_VERSION_MAJOR < 60 + const int num_channels = audio_codec_context->channels; + #else + const int num_channels = audio_codec_context->ch_layout.nb_channels; + #endif + + //audio_frame->sample_rate = audio_codec_context->sample_rate; + + std::vector<AVFilterContext*> src_filter_ctx; + AVFilterGraph *graph = nullptr; + AVFilterContext *sink = nullptr; + if(use_amix) { + int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size()); + if(err < 0) { + fprintf(stderr, "gsr error: failed to create audio filter\n"); + _exit(1); + } + } + + // TODO: Cleanup above + + const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + + const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.audio_codec, arg_parser.fps);// * ((double)audio_codec_context->frame_size / 1024.0); + const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; + + std::vector<AudioDeviceData> audio_track_audio_devices; + if(audio_inputs_has_app_audio(merged_audio_inputs.audio_inputs)) { + assert(!use_amix); +#ifdef GSR_APP_AUDIO + audio_track_audio_devices.push_back(create_application_audio_audio_input(merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio)); +#endif + } else { + audio_track_audio_devices = create_device_audio_inputs(merged_audio_inputs.audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); + } + + AudioTrack audio_track; + audio_track.name = merged_audio_inputs.track_name; + audio_track.codec_context = audio_codec_context; + audio_track.audio_devices = std::move(audio_track_audio_devices); + audio_track.graph = graph; + audio_track.sink = sink; + audio_track.stream_index = audio_stream_index; + audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; + audio_tracks.push_back(std::move(audio_track)); + ++audio_stream_index; + + audio_max_frame_size = std::max(audio_max_frame_size, audio_codec_context->frame_size); + } + + //av_dump_format(av_format_context, 0, filename, 1); + + if(!is_replaying) { + if(!av_open_file_write_header(av_format_context, arg_parser.filename)) + _exit(1); + } + + double fps_start_time = clock_get_monotonic_seconds(); + //double frame_timer_start = fps_start_time; + int fps_counter = 0; + int damage_fps_counter = 0; + + bool paused = false; + std::atomic<double> paused_time_offset(0.0); + double paused_time_start = 0.0; + bool replay_recording = false; + RecordingStartResult replay_recording_start_result; + std::vector<size_t> replay_recording_items; + std::string replay_recording_filepath; + bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes + + std::mutex audio_filter_mutex; + + const double record_start_time = clock_get_monotonic_seconds(); + + const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; // max 4 bytes/sample, 2 channels + uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size); + if(!empty_audio) { + fprintf(stderr, "gsr error: failed to create empty audio\n"); + _exit(1); + } + memset(empty_audio, 0, audio_buffer_size); + + for(AudioTrack &audio_track : audio_tracks) { + for(AudioDeviceData &audio_device : audio_track.audio_devices) { + audio_device.thread = std::thread([&]() mutable { + const AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(audio_track.codec_context)); + // TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged + const bool needs_audio_conversion = true;//audio_track.codec_context->sample_fmt != sound_device_sample_format; + SwrContext *swr = nullptr; + if(needs_audio_conversion) { + swr = swr_alloc(); + if(!swr) { + fprintf(stderr, "Failed to create SwrContext\n"); + _exit(1); + } + #if LIBAVUTIL_VERSION_MAJOR <= 56 + av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); + #elif LIBAVUTIL_VERSION_MAJOR >= 59 + av_opt_set_chlayout(swr, "in_chlayout", &audio_track.codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_chlayout", &audio_track.codec_context->ch_layout, 0); + #else + av_opt_set_chlayout(swr, "in_channel_layout", &audio_track.codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_channel_layout", &audio_track.codec_context->ch_layout, 0); + #endif + av_opt_set_int(swr, "in_sample_rate", audio_track.codec_context->sample_rate, 0); + av_opt_set_int(swr, "out_sample_rate", audio_track.codec_context->sample_rate, 0); + av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0); + av_opt_set_sample_fmt(swr, "out_sample_fmt", audio_track.codec_context->sample_fmt, 0); + swr_init(swr); + } + + const double audio_fps = (double)audio_track.codec_context->sample_rate / (double)audio_track.codec_context->frame_size; + const int64_t timeout_ms = std::round(1000.0 / audio_fps); + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + bool first_frame = true; + int64_t num_received_frames = 0; + + while(running) { + void *sound_buffer; + int sound_buffer_size = -1; + const double time_before_read_seconds = clock_get_monotonic_seconds(); + if(audio_device.sound_device.handle) { + // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time, + // also it's 0.0 for some users??? + double latency_seconds = 0.0; + sound_buffer_size = sound_device_read_next_chunk(&audio_device.sound_device, &sound_buffer, timeout_sec * 2.0, &latency_seconds); + } + + const bool got_audio_data = sound_buffer_size >= 0; + //fprintf(stderr, "got audio data: %s\n", got_audio_data ? "yes" : "no"); + //fprintf(stderr, "time to read: %f, %s, %f\n", time_to_read_seconds, got_audio_data ? "yes" : "no", timeout_sec); + const double this_audio_frame_time = clock_get_monotonic_seconds() - paused_time_offset; + + if(paused) { + if(!audio_device.sound_device.handle) + av_usleep(timeout_ms * 1000); + + continue; + } + + int ret = av_frame_make_writable(audio_device.frame); + if (ret < 0) { + fprintf(stderr, "Failed to make audio frame writable\n"); + break; + } + + // TODO: Is this |received_audio_time| really correct? + const int64_t num_expected_frames = std::floor((this_audio_frame_time - record_start_time) / timeout_sec); + int64_t num_missing_frames = std::max((int64_t)0LL, num_expected_frames - num_received_frames); + + if(got_audio_data) + num_missing_frames = std::max((int64_t)0LL, num_missing_frames - 1); + + if(!audio_device.sound_device.handle) + num_missing_frames = std::max((int64_t)1, num_missing_frames); + + // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW. + // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY. + // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!! + // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate + // videos because bad software such as video editing software and VLC do not support variable frame rate software, + // despite nvidia shadowplay and xbox game bar producing variable frame rate videos. + // So we have to make sure we produce frames at the same relative rate as the video. + if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !audio_device.sound_device.handle) { + // TODO: + //audio_track.frame->data[0] = empty_audio; + if(first_frame || num_missing_frames >= 5) { + if(needs_audio_conversion) + swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size); + else + audio_device.frame->data[0] = empty_audio; + } + first_frame = false; + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + std::lock_guard<std::mutex> lock(audio_filter_mutex); + for(int i = 0; i < num_missing_frames; ++i) { + if(audio_track.graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { + fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); + } + } else { + ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); + } else { + fprintf(stderr, "Failed to encode audio!\n"); + } + audio_track.pts += audio_track.codec_context->frame_size; + } + + audio_device.frame->pts += audio_track.codec_context->frame_size; + num_received_frames++; + } + } + + if(!audio_device.sound_device.handle) { + av_usleep(timeout_ms * 1000); + } else if(got_audio_data) { + // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format? + if(needs_audio_conversion) + swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size); + else + audio_device.frame->data[0] = (uint8_t*)sound_buffer; + first_frame = false; + + std::lock_guard<std::mutex> lock(audio_filter_mutex); + + if(audio_track.graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { + fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); + } + } else { + ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); + } else { + fprintf(stderr, "Failed to encode audio!\n"); + } + audio_track.pts += audio_track.codec_context->frame_size; + } + + audio_device.frame->pts += audio_track.codec_context->frame_size; + num_received_frames++; + } else { + // TODO: Maybe sleep for time_to_sleep_until_next_frame/4? for better latency + const double time_after_read_seconds = clock_get_monotonic_seconds(); + const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds; + const double time_to_sleep_until_next_frame = timeout_sec - time_to_read_seconds; + if(time_to_sleep_until_next_frame > 0.0) + av_usleep(time_to_sleep_until_next_frame * 1000ULL * 1000ULL); + } + } + + if(swr) + swr_free(&swr); + }); + } + } + + std::thread amix_thread; + if(uses_amix) { + amix_thread = std::thread([&]() { + AVFrame *aframe = av_frame_alloc(); + while(running) { + { + std::lock_guard<std::mutex> lock(audio_filter_mutex); + for(AudioTrack &audio_track : audio_tracks) { + if(!audio_track.sink) + continue; + + int err = 0; + while ((err = av_buffersink_get_frame(audio_track.sink, aframe)) >= 0) { + aframe->pts = audio_track.pts; + err = avcodec_send_frame(audio_track.codec_context, aframe); + if(err >= 0){ + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&encoder, audio_track.codec_context, aframe->pts, audio_track.stream_index); + } else { + fprintf(stderr, "Failed to encode audio!\n"); + } + av_frame_unref(aframe); + audio_track.pts += audio_track.codec_context->frame_size; + } + } + } + av_usleep(5 * 1000); // 5 milliseconds + } + av_frame_free(&aframe); + }); + } + + // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. + //const double update_fps = fps + 190; + bool should_stop_error = false; + + int64_t video_pts_counter = 0; + int64_t video_prev_pts = 0; + + bool hdr_metadata_set = false; + const bool hdr = video_codec_is_hdr(arg_parser.video_codec); + + bool use_damage_tracking = false; + gsr_damage damage; + memset(&damage, 0, sizeof(damage)); + if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { + if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { + gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.record_cursor); + use_damage_tracking = true; + + for(const CaptureSource &capture_source : capture_sources) { + switch(capture_source.type) { + case GSR_CAPTURE_SOURCE_TYPE_WINDOW: + gsr_damage_start_tracking_window(&damage, capture_source.window_id); + break; + case GSR_CAPTURE_SOURCE_TYPE_MONITOR: + case GSR_CAPTURE_SOURCE_TYPE_REGION: + // TODO: When capturing a region only track damage in that region + gsr_damage_start_tracking_monitor(&damage, capture_source.name.c_str()); + break; + default: + break; + } + } + } else if(is_capturing_monitor_or_region(capture_sources)) { + fprintf(stderr, "gsr warning: \"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)\n"); + } + } + + while(running) { + while(gsr_window_process_event(window)) { + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + + gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); + for(VideoSource &video_source : video_sources) { + gsr_capture_on_event(video_source.capture, &egl); + } + } + + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); + + gsr_damage_tick(&damage); + + should_stop_error = false; + bool damaged = false; + + if(use_damage_tracking) + damaged = gsr_damage_is_damaged(&damage); + + for(VideoSource &video_source : video_sources) { + gsr_capture_tick(video_source.capture); + + if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { + running = 0; + break; + } + + if(video_source.capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + assert(video_source.capture->get_window_id); + const Window damage_target_window = video_source.capture->get_window_id(video_source.capture); + + if((int64_t)damage_target_window != video_source.capture_source->window_id) { + gsr_damage_stop_tracking_window(&damage, video_source.capture_source->window_id); + if(damage_target_window != 0) + gsr_damage_start_tracking_window(&damage, damage_target_window); + } + + video_source.capture_source->window_id = damage_target_window; + } + + if(video_source.capture->is_damaged) + damaged |= video_source.capture->is_damaged(video_source.capture); + else if(!use_damage_tracking) + damaged = true; + } + + // TODO: Readd wayland sync warning when removing this + if(arg_parser.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + damaged = true; + + if(damaged) + ++damage_fps_counter; + + ++fps_counter; + const double time_now = clock_get_monotonic_seconds(); + //const double frame_timer_elapsed = time_now - frame_timer_start; + const double elapsed = time_now - fps_start_time; + if (elapsed >= 1.0) { + if(arg_parser.verbose) { + fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); + } + fps_start_time = time_now; + fps_counter = 0; + damage_fps_counter = 0; + } + + const double this_video_frame_time = clock_get_monotonic_seconds() - paused_time_offset; + const int64_t expected_frames = std::floor((this_video_frame_time - record_start_time) / target_fps); + const int64_t num_missed_frames = expected_frames - video_pts_counter; + + if(damaged && num_missed_frames >= 1 && !paused) { + // TODO: Dont do this if no damage? + egl.glClear(0); + + gsr_damage_clear(&damage); + gsr_capture_kms_cleanup_kms_fds(); + + if(kms_client_initialized) { + if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) + fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + } + + bool capture_has_synchronous_task = false; + for(VideoSource &video_source : video_sources) { + if(video_source.capture->clear_damage) + video_source.capture->clear_damage(video_source.capture); + + if(video_source.capture->capture_has_synchronous_task) { + capture_has_synchronous_task = video_source.capture->capture_has_synchronous_task(video_source.capture); + if(capture_has_synchronous_task) { + paused_time_start = clock_get_monotonic_seconds(); + paused = true; + } + } + } + + for(VideoSource &video_source : video_sources) { + if(video_source.capture->pre_capture) + video_source.capture->pre_capture(video_source.capture, &video_source.metadata, &color_conversion); + } + + if(color_conversion.schedule_clear) { + color_conversion.schedule_clear = false; + gsr_color_conversion_clear(&color_conversion); + } + + for(VideoSource &video_source : video_sources) { + gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion); + } + + gsr_capture_kms_cleanup_kms_fds(); + + if(plugins.num_plugins > 0) { + gsr_plugins_draw(&plugins); + gsr_color_conversion_draw(&color_conversion, plugins.texture, + {0, 0}, video_size, + {0, 0}, video_size, + video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } + + if(capture_has_synchronous_task) { + paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); + paused = false; + } + + gsr_egl_swap_buffers(&egl); + gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); + + for(VideoSource &video_source : video_sources) { + if(hdr && !hdr_metadata_set && !is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) + hdr_metadata_set = true; + } + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + const int num_frames_to_encode = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + for(int i = 0; i < num_frames_to_encode; ++i) { + if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + video_frame->pts = video_pts_counter + i; + } else { + video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; + const bool same_pts = video_frame->pts == video_prev_pts; + video_prev_pts = video_frame->pts; + if(same_pts) + continue; + } + + if(force_iframe_frame) { + video_frame->pict_type = AV_PICTURE_TYPE_I; + } + + int ret = avcodec_send_frame(video_codec_context, video_frame); + if(ret == 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX); + } else { + fprintf(stderr, "gsr error: avcodec_send_frame failed, error: %s\n", av_error_to_string(ret)); + } + + if(force_iframe_frame) { + force_iframe_frame = false; + video_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + } + + video_pts_counter += num_missed_frames; + } + + if(toggle_pause == 1 && !is_replaying) { + const bool new_paused_state = !paused; + if(new_paused_state) { + paused_time_start = clock_get_monotonic_seconds(); + fprintf(stderr, "Paused\n"); + } else { + paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); + fprintf(stderr, "Unpaused\n"); + } + + toggle_pause = 0; + paused = !paused; + } + + if(toggle_replay_recording && !arg_parser.replay_recording_directory) { + toggle_replay_recording = 0; + printf("gsr error: Unable to start recording since the -ro option was not specified\n"); + fflush(stdout); + } + + if(toggle_replay_recording && arg_parser.replay_recording_directory) { + toggle_replay_recording = 0; + const bool new_replay_recording_state = !replay_recording; + if(new_replay_recording_state) { + std::lock_guard<std::mutex> lock(audio_filter_mutex); + replay_recording_items.clear(); + replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.replay_recording_directory, "Video", file_extension, arg_parser.date_folders); + replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser.container_format, video_codec_context, audio_tracks, hdr, video_sources); + if(replay_recording_start_result.av_format_context) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); + if(video_recording_destination_id != (size_t)-1) + replay_recording_items.push_back(video_recording_destination_id); + + for(const auto &audio_input : replay_recording_start_result.audio_inputs) { + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_input.audio_track->codec_context, replay_recording_start_result.av_format_context, audio_input.stream, audio_input.audio_track->pts); + if(audio_recording_destination_id != (size_t)-1) + replay_recording_items.push_back(audio_recording_destination_id); + } + + replay_recording = true; + force_iframe_frame = true; + fprintf(stderr, "Started recording\n"); + } else { + printf("gsr error: Failed to start recording\n"); + fflush(stdout); + } + } else if(replay_recording_start_result.av_format_context) { + for(size_t id : replay_recording_items) { + gsr_encoder_remove_recording_destination(&encoder, id); + } + replay_recording_items.clear(); + + if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { + fprintf(stderr, "Stopped recording\n"); + puts(replay_recording_filepath.c_str()); + fflush(stdout); + if(arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); + } else { + printf("gsr error: Failed to save recording\n"); + fflush(stdout); + } + + replay_recording_start_result = RecordingStartResult{}; + replay_recording = false; + replay_recording_filepath.clear(); + } + } + + if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + save_replay_thread.get(); + if(save_replay_output_filepath.empty()) { + printf("gsr error: Failed to save replay\n"); + fflush(stdout); + } else { + puts(save_replay_output_filepath.c_str()); + fflush(stdout); + if(arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + } + } + + if(save_replay_seconds != 0 && !save_replay_thread.valid() && is_replaying) { + int current_save_replay_seconds = save_replay_seconds; + if(current_save_replay_seconds > 0) + current_save_replay_seconds += arg_parser.keyint; + + save_replay_seconds = 0; + save_replay_output_filepath.clear(); + save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, encoder.replay_buffer, arg_parser.filename, arg_parser.container_format, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds); + + if(arg_parser.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) + gsr_replay_buffer_clear(encoder.replay_buffer); + } + + const double time_at_frame_end = clock_get_monotonic_seconds() - paused_time_offset; + const double time_elapsed_total = time_at_frame_end - record_start_time; + const int64_t frames_elapsed = std::floor(time_elapsed_total / target_fps); + const double time_at_next_frame = (frames_elapsed + 1) * target_fps; + double time_to_next_frame = time_at_next_frame - time_elapsed_total; + if(time_to_next_frame > target_fps) + time_to_next_frame = target_fps; + const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter; + + if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) + av_usleep(time_to_next_frame * 1000.0 * 1000.0); + else { + if(paused) + av_usleep(20.0 * 1000.0); // 20 milliseconds + else if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) + av_usleep(2.8 * 1000.0); // 2.8 milliseconds + } + } + + running = 0; + + if(save_replay_thread.valid()) { + save_replay_thread.get(); + if(save_replay_output_filepath.empty()) { + // TODO: Output failed to save + } else { + puts(save_replay_output_filepath.c_str()); + fflush(stdout); + if(arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + } + } + + gsr_plugins_deinit(&plugins); + + if(replay_recording_start_result.av_format_context) { + for(size_t id : replay_recording_items) { + gsr_encoder_remove_recording_destination(&encoder, id); + } + replay_recording_items.clear(); + + if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { + fprintf(stderr, "Stopped recording\n"); + puts(replay_recording_filepath.c_str()); + fflush(stdout); + if(arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); + } else { + printf("gsr error: Failed to save recording\n"); + fflush(stdout); + } + } + + for(AudioTrack &audio_track : audio_tracks) { + for(auto &audio_device : audio_track.audio_devices) { + audio_device.thread.join(); + sound_device_close(&audio_device.sound_device); + } + } + + if(amix_thread.joinable()) + amix_thread.join(); + + // TODO: Replace this with start_recording_create_steams + if(!is_replaying && av_write_trailer(av_format_context) != 0) { + fprintf(stderr, "Failed to write trailer\n"); + } + + if(!is_replaying) { + avio_close(av_format_context->pb); + avformat_free_context(av_format_context); + } + + gsr_cursor_deinit(&x11_cursor); + gsr_damage_deinit(&damage); + gsr_color_conversion_deinit(&color_conversion); + gsr_video_encoder_destroy(video_encoder, video_codec_context); + gsr_encoder_deinit(&encoder); + for(VideoSource &video_source : video_sources) { + gsr_capture_destroy(video_source.capture); + } +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio_deinit(&pipewire_audio); +#endif + if(kms_client_initialized) { + gsr_capture_kms_cleanup_kms_fds(); + gsr_kms_client_deinit(&kms_client); + } + + if(!is_replaying && arg_parser.recording_saved_script) + run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "regular"); + + if(dpy) { + // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? + //XCloseDisplay(dpy); + } + + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + + //av_frame_free(&video_frame); + free(empty_audio); + args_parser_deinit(&arg_parser); + // We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze, + // but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling + // the at_exit registered functions. + // Cuda (cuvid library in this case) seems to be waiting for a thread that never finishes execution. + // Maybe this happens because we dont clean up all ffmpeg resources? + // TODO: Investigate this. + _exit(should_stop_error ? 3 : 0); +} diff --git a/src/overclock.c b/src/overclock.c new file mode 100644 index 0000000..f9375d5 --- /dev/null +++ b/src/overclock.c @@ -0,0 +1,279 @@ +#include "../include/overclock.h" +#include <X11/Xlib.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +// HACK!!!: When a program uses cuda (including nvenc) then the nvidia driver drops to max performance level - 1 (memory transfer rate is dropped and possibly graphics clock). +// Nvidia does this because in some very extreme cases of cuda there can be memory corruption when running at max memory transfer rate. +// So to get around this we overclock memory transfer rate (maybe this should also be done for graphics clock?) to the best performance level while GPU Screen Recorder is running. + +static int min_int(int a, int b) { + return a < b ? a : b; +} + +// Fields are 0 if not set +typedef struct { + int perf; + + int nv_clock; + int nv_clock_min; + int nv_clock_max; + + int mem_clock; + int mem_clock_min; + int mem_clock_max; + + int mem_transfer_rate; + int mem_transfer_rate_min; + int mem_transfer_rate_max; +} NVCTRLPerformanceLevel; + +#define MAX_PERFORMANCE_LEVELS 12 +typedef struct { + NVCTRLPerformanceLevel performance_level[MAX_PERFORMANCE_LEVELS]; + int num_performance_levels; +} NVCTRLPerformanceLevelQuery; + +typedef void (*split_callback)(const char *str, size_t size, void *userdata); +static void split_by_delimiter(const char *str, size_t size, char delimiter, split_callback callback, void *userdata) { + const char *it = str; + while(it < str + size) { + const char *prev_it = it; + it = memchr(it, delimiter, (str + size) - it); + if(!it) + it = str + size; + + callback(prev_it, it - prev_it, userdata); + it += 1; // skip delimiter + } +} + +typedef enum { + NVCTRL_GPU_NVCLOCK, + NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE, +} NvCTRLAttributeType; + +static unsigned int attribute_type_to_attribute_param(NvCTRLAttributeType attribute_type) { + switch(attribute_type) { + case NVCTRL_GPU_NVCLOCK: + return NV_CTRL_GPU_NVCLOCK_OFFSET; + case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE: + return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET; + } + return 0; +} + +static unsigned int attribute_type_to_attribute_param_all_levels(NvCTRLAttributeType attribute_type) { + switch(attribute_type) { + case NVCTRL_GPU_NVCLOCK: + return NV_CTRL_GPU_NVCLOCK_OFFSET_ALL_PERFORMANCE_LEVELS; + case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE: + return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS; + } + return 0; +} + +// Returns 0 on error +static int xnvctrl_get_attribute_max_value(gsr_xnvctrl *xnvctrl, int num_performance_levels, NvCTRLAttributeType attribute_type) { + NVCTRLAttributeValidValuesRec valid; + if(xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), &valid)) { + return valid.u.range.max; + } + + if(num_performance_levels > 0 && xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, num_performance_levels - 1, attribute_type_to_attribute_param(attribute_type), &valid)) { + return valid.u.range.max; + } + + return 0; +} + +static bool xnvctrl_set_attribute_offset(gsr_xnvctrl *xnvctrl, int num_performance_levels, int offset, NvCTRLAttributeType attribute_type) { + bool success = false; + + // NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS works (or at least used to?) without Xorg running as root + // so we try that first. NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS also only works with GTX 1000+. + // TODO: Reverse engineer NVIDIA Xorg driver so we can set this always without root access. + if(xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), offset)) + success = true; + + for(int i = 0; i < num_performance_levels; ++i) { + success |= xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, i, attribute_type_to_attribute_param(attribute_type), offset); + } + + return success; +} + +static void strip(const char **str, int *size) { + const char *str_d = *str; + int s_d = *size; + + const char *start = str_d; + const char *end = start + s_d; + + while(str_d < end) { + char c = *str_d; + if(c != ' ' && c != '\t' && c != '\n') + break; + ++str_d; + } + + int start_offset = str_d - start; + while(s_d > start_offset) { + char c = start[s_d]; + if(c != ' ' && c != '\t' && c != '\n') + break; + --s_d; + } + + *str = str_d; + *size = s_d; +} + +static void attribute_callback(const char *str, size_t size, void *userdata) { + if(size > 255 - 1) + return; + + int size_i = size; + strip(&str, &size_i); + + char attribute[255]; + memcpy(attribute, str, size_i); + attribute[size_i] = '\0'; + + const char *sep = strchr(attribute, '='); + if(!sep) + return; + + const char *attribute_name = attribute; + size_t attribute_name_len = sep - attribute_name; + const char *attribute_value_str = sep + 1; + + int attribute_value = 0; + if(sscanf(attribute_value_str, "%d", &attribute_value) != 1) + return; + + NVCTRLPerformanceLevel *performance_level = userdata; + if(attribute_name_len == 4 && memcmp(attribute_name, "perf", 4) == 0) + performance_level->perf = attribute_value; + else if(attribute_name_len == 7 && memcmp(attribute_name, "nvclock", 7) == 0) + performance_level->nv_clock = attribute_value; + else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmin", 10) == 0) + performance_level->nv_clock_min = attribute_value; + else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmax", 10) == 0) + performance_level->nv_clock_max = attribute_value; + else if(attribute_name_len == 8 && memcmp(attribute_name, "memclock", 8) == 0) + performance_level->mem_clock = attribute_value; + else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmin", 11) == 0) + performance_level->mem_clock_min = attribute_value; + else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmax", 11) == 0) + performance_level->mem_clock_max = attribute_value; + else if(attribute_name_len == 15 && memcmp(attribute_name, "memTransferRate", 15) == 0) + performance_level->mem_transfer_rate = attribute_value; + else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemin", 18) == 0) + performance_level->mem_transfer_rate_min = attribute_value; + else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemax", 18) == 0) + performance_level->mem_transfer_rate_max = attribute_value; +} + +static void attribute_line_callback(const char *str, size_t size, void *userdata) { + NVCTRLPerformanceLevelQuery *query = userdata; + if(query->num_performance_levels >= MAX_PERFORMANCE_LEVELS) + return; + + NVCTRLPerformanceLevel *current_performance_level = &query->performance_level[query->num_performance_levels]; + memset(current_performance_level, 0, sizeof(NVCTRLPerformanceLevel)); + ++query->num_performance_levels; + split_by_delimiter(str, size, ',', attribute_callback, current_performance_level); +} + +static bool xnvctrl_get_performance_levels(gsr_xnvctrl *xnvctrl, NVCTRLPerformanceLevelQuery *query) { + bool success = false; + memset(query, 0, sizeof(NVCTRLPerformanceLevelQuery)); + + char *attributes = NULL; + if(!xnvctrl->XNVCTRLQueryTargetStringAttribute(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, NV_CTRL_STRING_PERFORMANCE_MODES, &attributes)) { + success = false; + goto done; + } + + split_by_delimiter(attributes, strlen(attributes), ';', attribute_line_callback, query); + success = true; + + done: + if(attributes) + XFree(attributes); + + return success; +} + +static int compare_mem_transfer_rate_max_asc(const void *a, const void *b) { + const NVCTRLPerformanceLevel *perf_a = a; + const NVCTRLPerformanceLevel *perf_b = b; + return perf_a->mem_transfer_rate_max - perf_b->mem_transfer_rate_max; +} + +bool gsr_overclock_load(gsr_overclock *self, Display *display) { + memset(self, 0, sizeof(gsr_overclock)); + self->num_performance_levels = 0; + + return gsr_xnvctrl_load(&self->xnvctrl, display); +} + +void gsr_overclock_unload(gsr_overclock *self) { + gsr_xnvctrl_unload(&self->xnvctrl); +} + +bool gsr_overclock_start(gsr_overclock *self) { + int basep = 0; + int errorp = 0; + if(!self->xnvctrl.XNVCTRLQueryExtension(self->xnvctrl.display, &basep, &errorp)) { + fprintf(stderr, "gsr warning: gsr_overclock_start: xnvctrl is not supported on your system, failed to overclock memory transfer rate\n"); + return false; + } + + NVCTRLPerformanceLevelQuery query; + if(!xnvctrl_get_performance_levels(&self->xnvctrl, &query) || query.num_performance_levels == 0) { + fprintf(stderr, "gsr warning: gsr_overclock_start: failed to get performance levels for overclocking\n"); + return false; + } + self->num_performance_levels = query.num_performance_levels; + + qsort(query.performance_level, query.num_performance_levels, sizeof(NVCTRLPerformanceLevel), compare_mem_transfer_rate_max_asc); + + int target_transfer_rate_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE); + if(query.num_performance_levels > 1) { + const int transfer_rate_max_diff = query.performance_level[query.num_performance_levels - 1].mem_transfer_rate_max - query.performance_level[query.num_performance_levels - 2].mem_transfer_rate_max; + target_transfer_rate_offset = min_int(target_transfer_rate_offset, transfer_rate_max_diff); + if(target_transfer_rate_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_transfer_rate_offset, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE)) { + fprintf(stderr, "gsr info: gsr_overclock_start: successfully set memory transfer rate offset to %d\n", target_transfer_rate_offset); + } else { + fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock memory transfer rate offset to %d\n", target_transfer_rate_offset); + } + } + + // TODO: Sort by nv_clock_max + + // TODO: Enable. Crashes on my system (gtx 1080) so it's disabled for now. Seems to crash even if graphics clock is increasd by 1, let alone 1200 + /* + int target_nv_clock_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_GPU_NVCLOCK); + if(query.num_performance_levels > 1) { + const int nv_clock_max_diff = query.performance_level[query.num_performance_levels - 1].nv_clock_max - query.performance_level[query.num_performance_levels - 2].nv_clock_max; + target_nv_clock_offset = min_int(target_nv_clock_offset, nv_clock_max_diff); + if(target_nv_clock_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_nv_clock_offset, NVCTRL_GPU_NVCLOCK)) { + fprintf(stderr, "gsr info: gsr_overclock_start: successfully set nv clock offset to %d\n", target_nv_clock_offset); + } else { + fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock nv clock offset to %d\n", target_nv_clock_offset); + } + } + */ + + XSync(self->xnvctrl.display, False); + return true; +} + +void gsr_overclock_stop(gsr_overclock *self) { + xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE); + //xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_GPU_NVCLOCK); + XSync(self->xnvctrl.display, False); +} diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c new file mode 100644 index 0000000..b8ef8ea --- /dev/null +++ b/src/pipewire_audio.c @@ -0,0 +1,864 @@ +#include "../include/pipewire_audio.h" + +#include <pipewire/pipewire.h> +#include <pipewire/extensions/metadata.h> +#include <pipewire/impl-module.h> + +typedef struct { + const gsr_pipewire_audio_port *output_port; + const gsr_pipewire_audio_port *input_port; +} gsr_pipewire_audio_desired_link; + +static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { + gsr_pipewire_audio *self = user_data; + //fprintf(stderr, "server name: %s\n", info->name); +} + +static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, const char *message) { + gsr_pipewire_audio *self = user_data; + //fprintf(stderr, "gsr error: pipewire: error id:%u seq:%d res:%d: %s\n", id, seq, res, message); + pw_thread_loop_signal(self->thread_loop, false); +} + +static void on_core_done_cb(void *user_data, uint32_t id, int seq) { + gsr_pipewire_audio *self = user_data; + if(id == PW_ID_CORE && self->server_version_sync == seq) + pw_thread_loop_signal(self->thread_loop, false); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info_cb, + .done = on_core_done_cb, + .error = on_core_error_cb, +}; + +static gsr_pipewire_audio_node* gsr_pipewire_audio_get_node_by_name_case_insensitive(gsr_pipewire_audio *self, const char *node_name, gsr_pipewire_audio_node_type node_type) { + for(size_t i = 0; i < self->num_stream_nodes; ++i) { + const gsr_pipewire_audio_node *node = &self->stream_nodes[i]; + if(node->type == node_type && strcasecmp(node->name, node_name) == 0) + return &self->stream_nodes[i]; + } + return NULL; +} + +static gsr_pipewire_audio_port* gsr_pipewire_audio_get_node_port_by_name(gsr_pipewire_audio *self, uint32_t node_id, const char *port_name) { + for(size_t i = 0; i < self->num_ports; ++i) { + if(self->ports[i].node_id == node_id && strcmp(self->ports[i].name, port_name) == 0) + return &self->ports[i]; + } + return NULL; +} + +static bool requested_link_matches_name_case_insensitive(const gsr_pipewire_audio_requested_link *requested_link, const char *name) { + for(int i = 0; i < requested_link->num_outputs; ++i) { + if(requested_link->outputs[i].type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD && strcasecmp(requested_link->outputs[i].name, name) == 0) + return true; + } + return false; +} + +static bool requested_link_matches_name_case_insensitive_any_type(const gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link, const char *name) { + for(int i = 0; i < requested_link->num_outputs; ++i) { + switch(requested_link->outputs[i].type) { + case GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD: { + if(strcasecmp(requested_link->outputs[i].name, name) == 0) + return true; + break; + } + case GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT: { + if(strcasecmp(self->default_output_device_name, name) == 0) + return true; + break; + } + case GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT: { + if(strcasecmp(self->default_input_device_name, name) == 0) + return true; + break; + } + } + } + return false; +} + +static bool requested_link_has_type(const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type type) { + for(int i = 0; i < requested_link->num_outputs; ++i) { + if(requested_link->outputs[i].type == type) + return true; + } + return false; +} + +static void gsr_pipewire_get_node_input_port_by_type(gsr_pipewire_audio *self, const gsr_pipewire_audio_node *input_node, gsr_pipewire_audio_link_input_type input_type, + const gsr_pipewire_audio_port **input_fl_port, const gsr_pipewire_audio_port **input_fr_port) +{ + *input_fl_port = NULL; + *input_fr_port = NULL; + + switch(input_type) { + case GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM: { + *input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "input_FL"); + *input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "input_FR"); + break; + } + case GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK: { + *input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "playback_FL"); + *input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "playback_FR"); + break; + } + } +} + +static bool string_starts_with(const char *str, const char *substr) { + const int len = strlen(str); + const int substr_len = strlen(substr); + return len >= substr_len && memcmp(str, substr, substr_len) == 0; +} + +static bool string_ends_with(const char *str, const char *substr) { + const int len = strlen(str); + const int substr_len = strlen(substr); + return len >= substr_len && memcmp(str + len - substr_len, substr, substr_len) == 0; +} + +/* Returns number of desired links */ +static size_t gsr_pipewire_get_node_output_ports(gsr_pipewire_audio *self, const gsr_pipewire_audio_node *output_node, + gsr_pipewire_audio_desired_link *desired_links, size_t desired_links_max_size, + const gsr_pipewire_audio_port *input_fl_port, const gsr_pipewire_audio_port *input_fr_port) +{ + size_t num_desired_links = 0; + for(size_t i = 0; i < self->num_ports && num_desired_links < desired_links_max_size; ++i) { + if(self->ports[i].node_id != output_node->id) + continue; + + if(string_starts_with(self->ports[i].name, "playback_")) + continue; + + if(string_ends_with(self->ports[i].name, "_MONO") || string_ends_with(self->ports[i].name, "_FC") || string_ends_with(self->ports[i].name, "_LFE")) { + if(num_desired_links + 2 >= desired_links_max_size) + break; + + desired_links[num_desired_links + 0] = (gsr_pipewire_audio_desired_link){ .output_port = &self->ports[i], .input_port = input_fl_port }; + desired_links[num_desired_links + 1] = (gsr_pipewire_audio_desired_link){ .output_port = &self->ports[i], .input_port = input_fr_port }; + num_desired_links += 2; + } else if(string_ends_with(self->ports[i].name, "_FL") || string_ends_with(self->ports[i].name, "_RL") || string_ends_with(self->ports[i].name, "_SL")) { + if(num_desired_links + 1 >= desired_links_max_size) + break; + + desired_links[num_desired_links] = (gsr_pipewire_audio_desired_link){ .output_port = &self->ports[i], .input_port = input_fl_port }; + num_desired_links += 1; + } else if(string_ends_with(self->ports[i].name, "_FR") || string_ends_with(self->ports[i].name, "_RR") || string_ends_with(self->ports[i].name, "_SR")) { + if(num_desired_links + 1 >= desired_links_max_size) + break; + + desired_links[num_desired_links] = (gsr_pipewire_audio_desired_link){ .output_port = &self->ports[i], .input_port = input_fr_port }; + num_desired_links += 1; + } + } + return num_desired_links; +} + +static void gsr_pipewire_audio_establish_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_port *output_port, const gsr_pipewire_audio_port *input_port) { + // TODO: Detect if link already exists before so we dont create these proxies when not needed. + // We could do that by saving which nodes have been linked with which nodes after linking them. + + //fprintf(stderr, "linking!\n"); + // TODO: error check and cleanup + struct pw_properties *props = pw_properties_new(NULL, NULL); + pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_port->id); + pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_port->id); + // TODO: Clean this up when removing node + struct pw_proxy *proxy = pw_core_create_object(self->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); + //self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_properties_free(props); +} + +static void gsr_pipewire_audio_create_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link) { + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + + const gsr_pipewire_audio_port *input_fl_port = NULL; + const gsr_pipewire_audio_port *input_fr_port = NULL; + gsr_pipewire_get_node_input_port_by_type(self, stream_input_node, requested_link->input_type, &input_fl_port, &input_fr_port); + if(!input_fl_port || !input_fr_port) + return; + + gsr_pipewire_audio_desired_link desired_links[64]; + for(size_t i = 0; i < self->num_stream_nodes; ++i) { + const gsr_pipewire_audio_node *output_node = &self->stream_nodes[i]; + if(output_node->type != requested_link->output_type) + continue; + + const bool requested_link_matches_app = requested_link_matches_name_case_insensitive_any_type(self, requested_link, output_node->name); + if(requested_link->inverted) { + if(requested_link_matches_app) + continue; + } else { + if(!requested_link_matches_app) + continue; + } + + const size_t num_desired_links = gsr_pipewire_get_node_output_ports(self, output_node, desired_links, 64, input_fl_port, input_fr_port); + for(size_t j = 0; j < num_desired_links; ++j) { + gsr_pipewire_audio_establish_link(self, desired_links[j].output_port, desired_links[j].input_port); + } + } +} + +static void gsr_pipewire_audio_create_links(gsr_pipewire_audio *self) { + for(size_t i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_audio_create_link(self, &self->requested_links[i]); + } +} + +static void gsr_pipewire_audio_create_link_for_default_devices(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type default_device_type) { + if(default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD) + return; + + const char *device_name = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(device_name[0] == '\0') + return; + + if(!requested_link_has_type(requested_link, default_device_type)) + return; + + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + + const gsr_pipewire_audio_port *input_fl_port = NULL; + const gsr_pipewire_audio_port *input_fr_port = NULL; + gsr_pipewire_get_node_input_port_by_type(self, stream_input_node, requested_link->input_type, &input_fl_port, &input_fr_port); + if(!input_fl_port || !input_fr_port) + return; + + const gsr_pipewire_audio_node *stream_output_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, device_name, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE); + if(!stream_output_node) + return; + + gsr_pipewire_audio_desired_link desired_links[64]; + const size_t num_desired_links = gsr_pipewire_get_node_output_ports(self, stream_output_node, desired_links, 64, input_fl_port, input_fr_port); + for(size_t i = 0; i < num_desired_links; ++i) { + gsr_pipewire_audio_establish_link(self, desired_links[i].output_port, desired_links[i].input_port); + } +} + +static void gsr_pipewire_audio_create_links_for_default_devices(gsr_pipewire_audio *self, gsr_pipewire_audio_requested_type default_device_type) { + for(size_t i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[i], default_device_type); + } +} + +static void gsr_pipewire_audio_destroy_links_by_output_to_input(gsr_pipewire_audio *self, uint32_t output_node_id, uint32_t input_node_id) { + for(size_t i = 0; i < self->num_links; ++i) { + if(self->links[i].output_node_id == output_node_id && self->links[i].input_node_id == input_node_id) + pw_registry_destroy(self->registry, self->links[i].id); + } +} + +static void gsr_pipewire_destroy_default_device_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type default_device_type) { + if(default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD) + return; + + const char *device_name = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(device_name[0] == '\0') + return; + + if(!requested_link_has_type(requested_link, default_device_type)) + return; + + /* default_output and default_input can be the same device. In that case both are the same link and we dont want to remove the link */ + const gsr_pipewire_audio_requested_type opposite_device_type = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT : GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT; + const char *opposite_device_name = opposite_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(requested_link_has_type(requested_link, opposite_device_type) && strcmp(device_name, opposite_device_name) == 0) + return; + + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + + const gsr_pipewire_audio_node *stream_output_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, device_name, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE); + if(!stream_output_node) + return; + + if(requested_link_matches_name_case_insensitive(requested_link, stream_output_node->name)) + return; + + gsr_pipewire_audio_destroy_links_by_output_to_input(self, stream_output_node->id, stream_input_node->id); + //fprintf(stderr, "destroying a link from %u to %u\n", stream_output_node->id, stream_input_node->id); +} + +static void gsr_pipewire_destroy_default_device_links(gsr_pipewire_audio *self, gsr_pipewire_audio_requested_type default_device_type) { + for(size_t i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_destroy_default_device_link(self, &self->requested_links[i], default_device_type); + } +} + +static bool json_get_value(const char *json_str, const char *key, char *value, size_t value_size) { + char key_full[32]; + const int key_full_size = snprintf(key_full, sizeof(key_full), "\"%s\":", key); + const char *start = strstr(json_str, key_full); + if(!start) + return false; + + start += key_full_size; + const char *value_start = strchr(start, '"'); + if(!value_start) + return false; + + value_start += 1; + const char *value_end = strchr(value_start, '"'); + if(!value_end) + return false; + + snprintf(value, value_size, "%.*s", (int)(value_end - value_start), value_start); + return true; +} + +static int on_metadata_property_cb(void *data, uint32_t id, const char *key, const char *type, const char *value) { + (void)type; + gsr_pipewire_audio *self = data; + + if(id == PW_ID_CORE && key && value) { + char value_decoded[128]; + if(strcmp(key, "default.audio.sink") == 0) { + if(json_get_value(value, "name", value_decoded, sizeof(value_decoded)) && strcmp(value_decoded, self->default_output_device_name) != 0) { + gsr_pipewire_destroy_default_device_links(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + snprintf(self->default_output_device_name, sizeof(self->default_output_device_name), "%s", value_decoded); + gsr_pipewire_audio_create_links_for_default_devices(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + } + } else if(strcmp(key, "default.audio.source") == 0) { + if(json_get_value(value, "name", value_decoded, sizeof(value_decoded)) && strcmp(value_decoded, self->default_input_device_name) != 0) { + gsr_pipewire_destroy_default_device_links(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); + snprintf(self->default_input_device_name, sizeof(self->default_input_device_name), "%s", value_decoded); + gsr_pipewire_audio_create_links_for_default_devices(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); + } + } + } + + return 0; +} + +static const struct pw_metadata_events metadata_events = { + PW_VERSION_METADATA_EVENTS, + .property = on_metadata_property_cb, +}; + +static void on_metadata_proxy_removed_cb(void *data) { + gsr_pipewire_audio *self = data; + if(self->metadata_proxy) { + pw_proxy_destroy(self->metadata_proxy); + self->metadata_proxy = NULL; + } +} + +static void on_metadata_proxy_destroy_cb(void *data) { + gsr_pipewire_audio *self = data; + + spa_hook_remove(&self->metadata_listener); + spa_hook_remove(&self->metadata_proxy_listener); + spa_zero(self->metadata_listener); + spa_zero(self->metadata_proxy_listener); + + self->metadata_proxy = NULL; +} + +static const struct pw_proxy_events metadata_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_metadata_proxy_removed_cb, + .destroy = on_metadata_proxy_destroy_cb, +}; + +static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint32_t id) { + if(self->metadata_proxy) { + pw_proxy_destroy(self->metadata_proxy); + self->metadata_proxy = NULL; + } + + self->metadata_proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); + if(!self->metadata_proxy) { + fprintf(stderr, "gsr error: gsr_pipewire_audio_listen_on_metadata: failed to bind to registry\n"); + return false; + } + + pw_proxy_add_object_listener(self->metadata_proxy, &self->metadata_listener, &metadata_events, self); + pw_proxy_add_listener(self->metadata_proxy, &self->metadata_proxy_listener, &metadata_proxy_events, self); + + self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + return true; +} + +static bool array_ensure_capacity(void **array, size_t size, size_t *capacity_items, size_t element_size) { + if(size + 1 >= *capacity_items) { + size_t new_capacity_items = *capacity_items * 2; + if(new_capacity_items == 0) + new_capacity_items = 32; + + void *new_data = realloc(*array, new_capacity_items * element_size); + if(!new_data) { + fprintf(stderr, "gsr error: pipewire_audio: failed to reallocate memory\n"); + return false; + } + + *array = new_data; + *capacity_items = new_capacity_items; + } + return true; +} + +static void registry_event_global(void *data, uint32_t id, uint32_t permissions, + const char *type, uint32_t version, + const struct spa_dict *props) +{ + //fprintf(stderr, "add: id: %d, type: %s\n", (int)id, type); + gsr_pipewire_audio *self = (gsr_pipewire_audio*)data; + if(!props || !type || !self->running) + return; + + //pw_properties_new_dict(props); + + if(strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + const char *node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + //fprintf(stderr, " node id: %u, node name: %s, media class: %s\n", id, node_name, media_class); + const bool is_stream_output = media_class && strcmp(media_class, "Stream/Output/Audio") == 0; + const bool is_stream_input = media_class && strcmp(media_class, "Stream/Input/Audio") == 0; + const bool is_sink = media_class && string_starts_with(media_class, "Audio/Sink"); // Matches Audio/Sink/Virtual as well + const bool is_source = media_class && string_starts_with(media_class, "Audio/Source"); // Matches Audio/Source/Virtual as well + if(node_name && (is_stream_output || is_stream_input || is_sink || is_source)) { + //const char *application_binary = spa_dict_lookup(props, PW_KEY_APP_PROCESS_BINARY); + //const char *application_name = spa_dict_lookup(props, PW_KEY_APP_NAME); + //fprintf(stderr, " node name: %s, app binary: %s, app name: %s\n", node_name, application_binary, application_name); + + if(!array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node))) + return; + + char *node_name_copy = strdup(node_name); + if(node_name_copy) { + self->stream_nodes[self->num_stream_nodes].id = id; + self->stream_nodes[self->num_stream_nodes].name = node_name_copy; + if(is_stream_output) + self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT; + else if(is_stream_input) + self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT; + else if(is_sink || is_source) + self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + ++self->num_stream_nodes; + + gsr_pipewire_audio_create_links(self); + } + } + } else if(strcmp(type, PW_TYPE_INTERFACE_Port) == 0) { + const char *port_name = spa_dict_lookup(props, PW_KEY_PORT_NAME); + + const char *port_direction = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); + gsr_pipewire_audio_port_direction direction = -1; + if(port_direction && strcmp(port_direction, "in") == 0) + direction = GSR_PIPEWIRE_AUDIO_PORT_DIRECTION_INPUT; + else if(port_direction && strcmp(port_direction, "out") == 0) + direction = GSR_PIPEWIRE_AUDIO_PORT_DIRECTION_OUTPUT; + + const char *node_id = spa_dict_lookup(props, PW_KEY_NODE_ID); + const int node_id_num = node_id ? atoi(node_id) : 0; + + if(port_name && direction >= 0 && node_id_num > 0) { + if(!array_ensure_capacity((void**)&self->ports, self->num_ports, &self->ports_capacity_items, sizeof(gsr_pipewire_audio_port))) + return; + + //fprintf(stderr, " port name: %s, node id: %d, direction: %s\n", port_name, node_id_num, port_direction); + char *port_name_copy = strdup(port_name); + if(port_name_copy) { + //fprintf(stderr, " port id: %u, node id: %u, name: %s\n", id, node_id_num, port_name_copy); + self->ports[self->num_ports].id = id; + self->ports[self->num_ports].node_id = node_id_num; + self->ports[self->num_ports].direction = direction; + self->ports[self->num_ports].name = port_name_copy; + ++self->num_ports; + + gsr_pipewire_audio_create_links(self); + } + } + } else if(strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { + const char *output_node = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_NODE); + const char *input_node = spa_dict_lookup(props, PW_KEY_LINK_INPUT_NODE); + + const uint32_t output_node_id_num = output_node ? atoi(output_node) : 0; + const uint32_t input_node_id_num = input_node ? atoi(input_node) : 0; + if(output_node_id_num > 0 && input_node_id_num > 0) { + if(!array_ensure_capacity((void**)&self->links, self->num_links, &self->links_capacity_items, sizeof(gsr_pipewire_audio_link))) + return; + + //fprintf(stderr, " new link (%u): %u -> %u\n", id, output_node_id_num, input_node_id_num); + self->links[self->num_links].id = id; + self->links[self->num_links].output_node_id = output_node_id_num; + self->links[self->num_links].input_node_id = input_node_id_num; + ++self->num_links; + } + } else if(strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) { + const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); + if(name && strcmp(name, "default") == 0) + gsr_pipewire_audio_listen_on_metadata(self, id); + } +} + +static bool gsr_pipewire_audio_remove_node_by_id(gsr_pipewire_audio *self, uint32_t node_id) { + for(size_t i = 0; i < self->num_stream_nodes; ++i) { + if(self->stream_nodes[i].id != node_id) + continue; + + free(self->stream_nodes[i].name); + self->stream_nodes[i] = self->stream_nodes[self->num_stream_nodes - 1]; + --self->num_stream_nodes; + return true; + } + return false; +} + +static bool gsr_pipewire_audio_remove_port_by_id(gsr_pipewire_audio *self, uint32_t port_id) { + for(size_t i = 0; i < self->num_ports; ++i) { + if(self->ports[i].id != port_id) + continue; + + free(self->ports[i].name); + self->ports[i] = self->ports[self->num_ports - 1]; + --self->num_ports; + return true; + } + return false; +} + +static bool gsr_pipewire_audio_remove_link_by_id(gsr_pipewire_audio *self, uint32_t link_id) { + for(size_t i = 0; i < self->num_links; ++i) { + if(self->links[i].id != link_id) + continue; + + self->links[i] = self->links[self->num_links - 1]; + --self->num_links; + return true; + } + return false; +} + +static void registry_event_global_remove(void *data, uint32_t id) { + //fprintf(stderr, "remove: %d\n", (int)id); + gsr_pipewire_audio *self = (gsr_pipewire_audio*)data; + if(gsr_pipewire_audio_remove_node_by_id(self, id)) { + //fprintf(stderr, "removed node\n"); + return; + } + + if(gsr_pipewire_audio_remove_port_by_id(self, id)) { + //fprintf(stderr, "removed port\n"); + return; + } + + if(gsr_pipewire_audio_remove_link_by_id(self, id)) { + //fprintf(stderr, "removed link\n"); + return; + } +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { + memset(self, 0, sizeof(*self)); + self->running = true; + + pw_init(NULL, NULL); + + self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); + if(!self->thread_loop) { + fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire thread\n"); + gsr_pipewire_audio_deinit(self); + return false; + } + + self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); + if(!self->context) { + fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire context\n"); + gsr_pipewire_audio_deinit(self); + return false; + } + + pw_context_load_module(self->context, "libpipewire-module-link-factory", NULL, NULL); + + if(pw_thread_loop_start(self->thread_loop) < 0) { + fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to start thread\n"); + gsr_pipewire_audio_deinit(self); + return false; + } + + pw_thread_loop_lock(self->thread_loop); + + self->core = pw_context_connect(self->context, pw_properties_new(PW_KEY_REMOTE_NAME, NULL, NULL), 0); + if(!self->core) { + pw_thread_loop_unlock(self->thread_loop); + gsr_pipewire_audio_deinit(self); + return false; + } + + // TODO: Error check + pw_core_add_listener(self->core, &self->core_listener, &core_events, self); + + self->registry = pw_core_get_registry(self->core, PW_VERSION_REGISTRY, 0); + pw_registry_add_listener(self->registry, &self->registry_listener, ®istry_events, self); + + self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_thread_loop_wait(self->thread_loop); + + pw_thread_loop_unlock(self->thread_loop); + return true; +} + +static gsr_pipewire_audio_link* gsr_pipewire_audio_get_first_link_to_node(gsr_pipewire_audio *self, uint32_t node_id) { + for(size_t i = 0; i < self->num_links; ++i) { + if(self->links[i].input_node_id == node_id) + return &self->links[i]; + } + return NULL; +} + +static void gsr_pipewire_audio_destroy_requested_links(gsr_pipewire_audio *self) { + pw_thread_loop_lock(self->thread_loop); + + self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_thread_loop_wait(self->thread_loop); + + for(size_t requested_link_index = 0; requested_link_index < self->num_requested_links; ++requested_link_index) { + const gsr_pipewire_audio_node_type requested_link_node_type = self->requested_links[requested_link_index].input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, self->requested_links[requested_link_index].input_name, requested_link_node_type); + if(!stream_input_node) + continue; + + for(;;) { + gsr_pipewire_audio_link *link = gsr_pipewire_audio_get_first_link_to_node(self, stream_input_node->id); + if(!link) + break; + + pw_registry_destroy(self->registry, link->id); + + self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_thread_loop_wait(self->thread_loop); + + usleep(10 * 1000); + } + } + + pw_thread_loop_unlock(self->thread_loop); +} + +void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) { + self->running = false; + + if(self->thread_loop) { + /* We need to manually destroy links first, otherwise the linked audio sources will be paused when closing the program */ + gsr_pipewire_audio_destroy_requested_links(self); + //pw_thread_loop_wait(self->thread_loop); + pw_thread_loop_stop(self->thread_loop); + } + + if(self->metadata_proxy) { + spa_hook_remove(&self->metadata_listener); + spa_hook_remove(&self->metadata_proxy_listener); + pw_proxy_destroy(self->metadata_proxy); + spa_zero(self->metadata_listener); + spa_zero(self->metadata_proxy_listener); + self->metadata_proxy = NULL; + } + + spa_hook_remove(&self->registry_listener); + spa_hook_remove(&self->core_listener); + + if(self->core) { + pw_core_disconnect(self->core); + self->core = NULL; + } + + if(self->context) { + pw_context_destroy(self->context); + self->context = NULL; + } + + if(self->thread_loop) { + pw_thread_loop_destroy(self->thread_loop); + self->thread_loop = NULL; + } + + if(self->stream_nodes) { + for(size_t i = 0; i < self->num_stream_nodes; ++i) { + free(self->stream_nodes[i].name); + } + self->num_stream_nodes = 0; + self->stream_nodes_capacity_items = 0; + + free(self->stream_nodes); + self->stream_nodes = NULL; + } + + if(self->ports) { + for(size_t i = 0; i < self->num_ports; ++i) { + free(self->ports[i].name); + } + self->num_ports = 0; + self->ports_capacity_items = 0; + + free(self->ports); + self->ports = NULL; + } + + if(self->links) { + self->num_links = 0; + self->links_capacity_items = 0; + + free(self->links); + self->links = NULL; + } + + if(self->requested_links) { + for(size_t i = 0; i < self->num_requested_links; ++i) { + for(int j = 0; j < self->requested_links[i].num_outputs; ++j) { + free(self->requested_links[i].outputs[j].name); + } + free(self->requested_links[i].outputs); + free(self->requested_links[i].input_name); + } + self->num_requested_links = 0; + self->requested_links_capacity_items = 0; + + free(self->requested_links); + self->requested_links = NULL; + } + +#if PW_CHECK_VERSION(0, 3, 49) + pw_deinit(); +#endif +} + +static bool string_remove_suffix(char *str, const char *suffix) { + int str_len = strlen(str); + int suffix_len = strlen(suffix); + if(str_len >= suffix_len && memcmp(str + str_len - suffix_len, suffix, suffix_len) == 0) { + str[str_len - suffix_len] = '\0'; + return true; + } else { + return false; + } +} + +static bool gsr_pipewire_audio_add_links_to_output(gsr_pipewire_audio *self, const char **output_names, int num_output_names, const char *input_name, gsr_pipewire_audio_node_type output_type, gsr_pipewire_audio_link_input_type input_type, bool inverted) { + if(!array_ensure_capacity((void**)&self->requested_links, self->num_requested_links, &self->requested_links_capacity_items, sizeof(gsr_pipewire_audio_requested_link))) + return false; + + gsr_pipewire_audio_requested_output *outputs = calloc(num_output_names, sizeof(gsr_pipewire_audio_requested_output)); + if(!outputs) + return false; + + char *input_name_copy = strdup(input_name); + if(!input_name_copy) + goto error; + + if(input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK) + string_remove_suffix(input_name_copy, ".monitor"); + + for(int i = 0; i < num_output_names; ++i) { + outputs[i].name = strdup(output_names[i]); + if(!outputs[i].name) + goto error; + + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD; + if(output_type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE) { + string_remove_suffix(outputs[i].name, ".monitor"); + + if(strcmp(outputs[i].name, "default_output") == 0) + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT; + else if(strcmp(outputs[i].name, "default_input") == 0) + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT; + else + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD; + } + } + + pw_thread_loop_lock(self->thread_loop); + self->requested_links[self->num_requested_links].outputs = outputs; + self->requested_links[self->num_requested_links].num_outputs = num_output_names; + self->requested_links[self->num_requested_links].input_name = input_name_copy; + self->requested_links[self->num_requested_links].output_type = output_type; + self->requested_links[self->num_requested_links].input_type = input_type; + self->requested_links[self->num_requested_links].inverted = inverted; + ++self->num_requested_links; + gsr_pipewire_audio_create_link(self, &self->requested_links[self->num_requested_links - 1]); + // TODO: Remove these? + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[self->num_requested_links - 1], GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[self->num_requested_links - 1], GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); + pw_thread_loop_unlock(self->thread_loop); + + return true; + + error: + free(input_name_copy); + for(int i = 0; i < num_output_names; ++i) { + free(outputs[i].name); + } + free(outputs); + return false; +} + +bool gsr_pipewire_audio_add_link_from_apps_to_stream(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *stream_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, false); +} + +bool gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *stream_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, true); +} + +bool gsr_pipewire_audio_add_link_from_apps_to_sink(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *sink_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); +} + +bool gsr_pipewire_audio_add_link_from_apps_to_sink_inverted(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *sink_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, true); +} + +bool gsr_pipewire_audio_add_link_from_sources_to_stream(gsr_pipewire_audio *self, const char **source_names, int num_source_names, const char *stream_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, source_names, num_source_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, false); +} + +bool gsr_pipewire_audio_add_link_from_sources_to_sink(gsr_pipewire_audio *self, const char **source_names, int num_source_names, const char *sink_name_input) { + return gsr_pipewire_audio_add_links_to_output(self, source_names, num_source_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); +} + +void gsr_pipewire_audio_for_each_app(gsr_pipewire_audio *self, gsr_pipewire_audio_app_query_callback callback, void *userdata) { + pw_thread_loop_lock(self->thread_loop); + for(int i = 0; i < (int)self->num_stream_nodes; ++i) { + const gsr_pipewire_audio_node *node = &self->stream_nodes[i]; + if(node->type != GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT) + continue; + + bool duplicate_app = false; + for(int j = i - 1; j >= 0; --j) { + const gsr_pipewire_audio_node *prev_node = &self->stream_nodes[j]; + if(prev_node->type != GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT) + continue; + + if(strcasecmp(node->name, prev_node->name) == 0) { + duplicate_app = true; + break; + } + } + + if(duplicate_app) + continue; + + if(!callback(node->name, userdata)) + break; + } + pw_thread_loop_unlock(self->thread_loop); +} diff --git a/src/pipewire_video.c b/src/pipewire_video.c new file mode 100644 index 0000000..2550525 --- /dev/null +++ b/src/pipewire_video.c @@ -0,0 +1,947 @@ +#include "../include/pipewire_video.h" +#include "../include/egl.h" +#include "../include/utils.h" + +#include <pipewire/pipewire.h> +#include <spa/param/video/format-utils.h> +#include <spa/debug/types.h> + +#include <drm_fourcc.h> + +#include <fcntl.h> +#include <unistd.h> + +/* This code is partially based on xr-video-player pipewire implementation which is based on obs-studio's pipewire implementation */ + +/* TODO: Make gsr_pipewire_video_init asynchronous */ +/* TODO: Support hdr when pipewire supports it */ +/* TODO: Test all of the image formats */ + +#ifndef SPA_POD_PROP_FLAG_DONT_FIXATE +#define SPA_POD_PROP_FLAG_DONT_FIXATE (1 << 4) +#endif + +#if !PW_CHECK_VERSION(0, 3, 62) +enum spa_meta_videotransform_value { + SPA_META_TRANSFORMATION_None = 0, /**< no transform */ + SPA_META_TRANSFORMATION_90, /**< 90 degree counter-clockwise */ + SPA_META_TRANSFORMATION_180, /**< 180 degree counter-clockwise */ + SPA_META_TRANSFORMATION_270, /**< 270 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped, /**< 180 degree flipped around the vertical axis. Equivalent + * to a reflexion through the vertical line splitting the + * buffer in two equal sized parts */ + SPA_META_TRANSFORMATION_Flipped90, /**< flip then rotate around 90 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped180, /**< flip then rotate around 180 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped270, /**< flip then rotate around 270 degree counter-clockwise */ +}; + +/** a transformation of the buffer */ +struct spa_meta_videotransform { + uint32_t transform; /**< orientation transformation that was applied to the buffer, + * one of enum spa_meta_videotransform_value */ +}; + +#define SPA_META_VideoTransform 8 +#endif + +#define CURSOR_META_SIZE(width, height) \ + (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + \ + width * height * 4) + +static bool parse_pw_version(gsr_pipewire_video_data_version *dst, const char *version) { + const int n_matches = sscanf(version, "%d.%d.%d", &dst->major, &dst->minor, &dst->micro); + return n_matches == 3; +} + +static bool check_pw_version(const gsr_pipewire_video_data_version *pw_version, int major, int minor, int micro) { + if (pw_version->major != major) + return pw_version->major > major; + if (pw_version->minor != minor) + return pw_version->minor > minor; + return pw_version->micro >= micro; +} + +static void update_pw_versions(gsr_pipewire_video *self, const char *version) { + fprintf(stderr, "gsr info: pipewire: server version: %s\n", version); + fprintf(stderr, "gsr info: pipewire: library version: %s\n", pw_get_library_version()); + fprintf(stderr, "gsr info: pipewire: header version: %s\n", pw_get_headers_version()); + if(!parse_pw_version(&self->server_version, version)) + fprintf(stderr, "gsr error: pipewire: failed to parse server version\n"); +} + +static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { + gsr_pipewire_video *self = user_data; + update_pw_versions(self, info->version); +} + +static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, const char *message) { + gsr_pipewire_video *self = user_data; + fprintf(stderr, "gsr error: pipewire: error id:%u seq:%d res:%d: %s\n", id, seq, res, message); + pw_thread_loop_signal(self->thread_loop, false); +} + +static void on_core_done_cb(void *user_data, uint32_t id, int seq) { + gsr_pipewire_video *self = user_data; + if (id == PW_ID_CORE && self->server_version_sync == seq) + pw_thread_loop_signal(self->thread_loop, false); +} + +static bool is_cursor_format_supported(const enum spa_video_format format) { + switch(format) { + case SPA_VIDEO_FORMAT_RGBx: return true; + case SPA_VIDEO_FORMAT_BGRx: return true; + case SPA_VIDEO_FORMAT_RGBA: return true; + case SPA_VIDEO_FORMAT_BGRA: return true; + case SPA_VIDEO_FORMAT_RGB: return true; + case SPA_VIDEO_FORMAT_BGR: return true; + case SPA_VIDEO_FORMAT_ARGB: return true; + case SPA_VIDEO_FORMAT_ABGR: return true; +#if PW_CHECK_VERSION(0, 3, 41) + case SPA_VIDEO_FORMAT_xRGB_210LE: return true; + case SPA_VIDEO_FORMAT_xBGR_210LE: return true; + case SPA_VIDEO_FORMAT_ARGB_210LE: return true; + case SPA_VIDEO_FORMAT_ABGR_210LE: return true; +#endif + default: break; + } + return false; +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info_cb, + .done = on_core_done_cb, + .error = on_core_error_cb, +}; + +static void on_process_cb(void *user_data) { + gsr_pipewire_video *self = user_data; + + /* Find the most recent buffer */ + struct pw_buffer *pw_buf = NULL; + for(;;) { + struct pw_buffer *aux = pw_stream_dequeue_buffer(self->stream); + if(!aux) + break; + if(pw_buf) + pw_stream_queue_buffer(self->stream, pw_buf); + pw_buf = aux; + } + + if(!pw_buf) { + fprintf(stderr, "gsr info: pipewire: out of buffers!\n"); + return; + } + + struct spa_buffer *buffer = pw_buf->buffer; + const bool has_buffer = buffer->datas[0].chunk->size != 0; + + pthread_mutex_lock(&self->mutex); + + bool buffer_updated = false; + if(has_buffer && buffer->datas[0].type == SPA_DATA_DmaBuf) { + for(size_t i = 0; i < self->dmabuf_num_planes; ++i) { + if(self->dmabuf_data[i].fd > 0) { + close(self->dmabuf_data[i].fd); + self->dmabuf_data[i].fd = -1; + } + } + + self->dmabuf_num_planes = buffer->n_datas; + if(self->dmabuf_num_planes > GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES) + self->dmabuf_num_planes = GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES; + + for(size_t i = 0; i < self->dmabuf_num_planes; ++i) { + self->dmabuf_data[i].fd = dup(buffer->datas[i].fd); + self->dmabuf_data[i].offset = buffer->datas[i].chunk->offset; + self->dmabuf_data[i].stride = buffer->datas[i].chunk->stride; + } + + buffer_updated = true; + } + + // TODO: Move down to read_metadata + struct spa_meta_region *region = spa_buffer_find_meta_data(buffer, SPA_META_VideoCrop, sizeof(*region)); + if(region && spa_meta_region_is_valid(region)) { + // fprintf(stderr, "gsr info: pipewire: crop Region available (%dx%d+%d+%d)\n", + // region->region.position.x, region->region.position.y, + // region->region.size.width, region->region.size.height); + self->crop.x = region->region.position.x; + self->crop.y = region->region.position.y; + self->crop.width = region->region.size.width; + self->crop.height = region->region.size.height; + self->crop.valid = true; + } else { + self->crop.valid = false; + } + + struct spa_meta_videotransform *video_transform = spa_buffer_find_meta_data(buffer, SPA_META_VideoTransform, sizeof(*video_transform)); + enum spa_meta_videotransform_value transform = SPA_META_TRANSFORMATION_None; + if(video_transform) + transform = video_transform->transform; + + self->rotation = GSR_MONITOR_ROT_0; + switch(transform) { + case SPA_META_TRANSFORMATION_90: + self->rotation = GSR_MONITOR_ROT_90; + break; + case SPA_META_TRANSFORMATION_180: + self->rotation = GSR_MONITOR_ROT_180; + break; + case SPA_META_TRANSFORMATION_270: + self->rotation = GSR_MONITOR_ROT_270; + break; + default: + // TODO: Support other rotations. Wayland compositors dont use them yet so it's ok to not support it now + break; + } + + const struct spa_meta *video_damage = spa_buffer_find_meta(buffer, SPA_META_VideoDamage); + if(video_damage) { + struct spa_meta_region *meta_region = NULL; + spa_meta_for_each(meta_region, video_damage) { + if(meta_region->region.size.width == 0 || meta_region->region.size.height == 0) + continue; + + self->damaged = true; + break; + } + } else if(buffer_updated) { + self->damaged = true; + } + + const struct spa_meta_cursor *cursor = spa_buffer_find_meta_data(buffer, SPA_META_Cursor, sizeof(*cursor)); + self->cursor.valid = cursor && spa_meta_cursor_is_valid(cursor); + + if (self->cursor.visible && self->cursor.valid) { + struct spa_meta_bitmap *bitmap = NULL; + if (cursor->bitmap_offset) + bitmap = SPA_MEMBER(cursor, cursor->bitmap_offset, struct spa_meta_bitmap); + + // TODO: Maybe check if the cursor is actually visible by checking if there are visible pixels + if (bitmap && bitmap->size.width > 0 && bitmap->size.height > 0 && is_cursor_format_supported(bitmap->format)) { + const uint8_t *bitmap_data = SPA_MEMBER(bitmap, bitmap->offset, uint8_t); + fprintf(stderr, "gsr info: pipewire: cursor bitmap update, size: %dx%d, format: %s\n", + (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format)); + + const size_t bitmap_size = bitmap->size.width * bitmap->size.height * 4; + uint8_t *new_bitmap_data = realloc(self->cursor.data, bitmap_size); + if(new_bitmap_data) { + self->cursor.data = new_bitmap_data; + /* TODO: Convert bgr and other image formats to rgb here */ + memcpy(self->cursor.data, bitmap_data, bitmap_size); + } + + self->cursor.hotspot_x = cursor->hotspot.x; + self->cursor.hotspot_y = cursor->hotspot.y; + self->cursor.width = bitmap->size.width; + self->cursor.height = bitmap->size.height; + self->damaged = true; + } + + if(cursor->position.x != self->cursor.x || cursor->position.y != self->cursor.y) + self->damaged = true; + + self->cursor.x = cursor->position.x; + self->cursor.y = cursor->position.y; + + //fprintf(stderr, "gsr info: pipewire: cursor: %d %d %d %d\n", cursor->hotspot.x, cursor->hotspot.y, cursor->position.x, cursor->position.y); + } + + pthread_mutex_unlock(&self->mutex); + pw_stream_queue_buffer(self->stream, pw_buf); +} + +static void on_param_changed_cb(void *user_data, uint32_t id, const struct spa_pod *param) { + gsr_pipewire_video *self = user_data; + + if (!param || id != SPA_PARAM_Format) + return; + + int result = spa_format_parse(param, &self->format.media_type, &self->format.media_subtype); + if (result < 0) + return; + + if (self->format.media_type != SPA_MEDIA_TYPE_video || self->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + pthread_mutex_lock(&self->mutex); + spa_format_video_raw_parse(param, &self->format.info.raw); + pthread_mutex_unlock(&self->mutex); + + uint32_t buffer_types = 0; + const bool has_modifier = spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier) != NULL; + if(has_modifier || check_pw_version(&self->server_version, 0, 3, 24)) + buffer_types |= 1 << SPA_DATA_DmaBuf; + + fprintf(stderr, "gsr info: pipewire: negotiated format:\n"); + + fprintf(stderr, "gsr info: pipewire: Format: %d (%s)\n", + self->format.info.raw.format, + spa_debug_type_find_name(spa_type_video_format, self->format.info.raw.format)); + + self->has_modifier = has_modifier; + if(self->has_modifier) { + fprintf(stderr, "gsr info: pipewire: Modifier: 0x%" PRIx64 "\n", self->format.info.raw.modifier); + } + + fprintf(stderr, "gsr info: pipewire: Size: %dx%d\n", self->format.info.raw.size.width, self->format.info.raw.size.height); + fprintf(stderr, "gsr info: pipewire: Framerate: %d/%d\n", self->format.info.raw.framerate.num, self->format.info.raw.framerate.denom); + + uint8_t params_buffer[2048]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + const struct spa_pod *params[5]; + int param_index = 0; + + params[param_index++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), + SPA_PARAM_META_size, + SPA_POD_Int(sizeof(struct spa_meta_region))); + + params[param_index++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), + SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int( + sizeof(struct spa_meta_region) * 16, + sizeof(struct spa_meta_region) * 1, + sizeof(struct spa_meta_region) * 16)); + + params[param_index++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), + SPA_PARAM_META_size, + SPA_POD_CHOICE_RANGE_Int(CURSOR_META_SIZE(64, 64), + CURSOR_META_SIZE(1, 1), + CURSOR_META_SIZE(1024, 1024))); + + params[param_index++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)); + +#if PW_CHECK_VERSION(0, 3, 62) + if (check_pw_version(&self->server_version, 0, 3, 62)) { + /* Video transformation */ + params[param_index++] = spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, + SPA_POD_Id(SPA_META_VideoTransform), + SPA_PARAM_META_size, + SPA_POD_Int(sizeof(struct spa_meta_videotransform))); + } +#endif + + pw_stream_update_params(self->stream, params, param_index); + self->negotiated = true; +} + +static void on_state_changed_cb(void *user_data, enum pw_stream_state prev_state, enum pw_stream_state new_state, const char *error) { + gsr_pipewire_video *self = user_data; + + fprintf(stderr, "gsr info: pipewire: stream %p previous state: \"%s\", new state: \"%s\" (error: %s)\n", + (void*)self->stream, pw_stream_state_as_string(prev_state), pw_stream_state_as_string(new_state), + error ? error : "none"); + + pthread_mutex_lock(&self->mutex); + if(new_state == PW_STREAM_STATE_PAUSED) { + self->paused_start_secs = clock_get_monotonic_seconds(); + self->paused = true; + } else { + self->paused = false; + } + pthread_mutex_unlock(&self->mutex); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed_cb, + .param_changed = on_param_changed_cb, + .process = on_process_cb, +}; + +static inline struct spa_pod *build_format(struct spa_pod_builder *b, + const gsr_pipewire_video_video_info *ovi, + uint32_t format, const uint64_t *modifiers, + size_t modifier_count) +{ + struct spa_pod_frame format_frame; + + spa_pod_builder_push_object(b, &format_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); + + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); + + if (modifier_count > 0) { + struct spa_pod_frame modifier_frame; + + spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); + spa_pod_builder_push_choice(b, &modifier_frame, SPA_CHOICE_Enum, 0); + + /* The first element of choice pods is the preferred value. Here + * we arbitrarily pick the first modifier as the preferred one. + */ + // TODO: + spa_pod_builder_long(b, modifiers[0]); + + for(uint32_t i = 0; i < modifier_count; i++) + spa_pod_builder_long(b, modifiers[i]); + + spa_pod_builder_pop(b, &modifier_frame); + } + + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(32, 32), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(16384, 16384)), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(ovi->fps_num, ovi->fps_den), + &SPA_FRACTION(0, 1), &SPA_FRACTION(500, 1)), + 0); + return spa_pod_builder_pop(b, &format_frame); +} + +/* https://gstreamer.freedesktop.org/documentation/additional/design/mediatype-video-raw.html?gi-language=c#formats */ +/* For some reason gstreamer formats are in opposite order to drm formats */ +static int64_t spa_video_format_to_drm_format(const enum spa_video_format format) { + switch(format) { + case SPA_VIDEO_FORMAT_RGBx: return DRM_FORMAT_XBGR8888; + case SPA_VIDEO_FORMAT_BGRx: return DRM_FORMAT_XRGB8888; + case SPA_VIDEO_FORMAT_RGBA: return DRM_FORMAT_ABGR8888; + case SPA_VIDEO_FORMAT_BGRA: return DRM_FORMAT_ARGB8888; + case SPA_VIDEO_FORMAT_RGB: return DRM_FORMAT_XBGR8888; + case SPA_VIDEO_FORMAT_BGR: return DRM_FORMAT_XRGB8888; + //case SPA_VIDEO_FORMAT_ARGB: return DRM_FORMAT_BGRA8888; + //case SPA_VIDEO_FORMAT_ABGR: return DRM_FORMAT_RGBA8888; +#if PW_CHECK_VERSION(0, 3, 41) + case SPA_VIDEO_FORMAT_xRGB_210LE: return DRM_FORMAT_XRGB2101010; + case SPA_VIDEO_FORMAT_xBGR_210LE: return DRM_FORMAT_XBGR2101010; + case SPA_VIDEO_FORMAT_ARGB_210LE: return DRM_FORMAT_ARGB2101010; + case SPA_VIDEO_FORMAT_ABGR_210LE: return DRM_FORMAT_ABGR2101010; +#endif + default: break; + } + return DRM_FORMAT_INVALID; +} + +#if PW_CHECK_VERSION(0, 3, 41) +#define GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS GSR_PIPEWIRE_VIDEO_MAX_VIDEO_FORMATS +#else +#define GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS 6 +#endif + +static const enum spa_video_format video_formats[GSR_PIPEWIRE_VIDEO_MAX_VIDEO_FORMATS] = { + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_BGR, + SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_BGRA, + //SPA_VIDEO_FORMAT_ARGB, + //SPA_VIDEO_FORMAT_ABGR, +#if PW_CHECK_VERSION(0, 3, 41) + SPA_VIDEO_FORMAT_xRGB_210LE, + SPA_VIDEO_FORMAT_xBGR_210LE, + SPA_VIDEO_FORMAT_ARGB_210LE, + SPA_VIDEO_FORMAT_ABGR_210LE, +#endif +}; + +static bool gsr_pipewire_video_build_format_params(gsr_pipewire_video *self, struct spa_pod_builder *pod_builder, struct spa_pod **params, uint32_t *num_params) { + *num_params = 0; + + if(!check_pw_version(&self->server_version, 0, 3, 33)) + return false; + + for(size_t i = 0; i < GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS; i++) { + if(self->supported_video_formats[i].modifiers_size == 0) + continue; + params[*num_params] = build_format(pod_builder, &self->video_info, self->supported_video_formats[i].format, self->modifiers + self->supported_video_formats[i].modifiers_index, self->supported_video_formats[i].modifiers_size); + ++(*num_params); + } + + return true; +} + +static void renegotiate_format(void *data, uint64_t expirations) { + (void)expirations; + gsr_pipewire_video *self = (gsr_pipewire_video*)data; + + pw_thread_loop_lock(self->thread_loop); + + struct spa_pod *params[GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS]; + uint32_t num_video_formats = 0; + uint8_t params_buffer[4096]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + if (!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { + fprintf(stderr, "gsr error: renegotiate_format: failed to build formats\n"); + pw_thread_loop_unlock(self->thread_loop); + return; + } + + pw_stream_update_params(self->stream, (const struct spa_pod**)params, num_video_formats); + pw_thread_loop_unlock(self->thread_loop); +} + +static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum spa_video_format format, uint64_t *modifiers, int32_t max_modifiers, int32_t *num_modifiers) { + *num_modifiers = 0; + + if(max_modifiers == 0) { + fprintf(stderr, "gsr error: spa_video_format_get_modifiers: no space for modifiers left\n"); + //modifiers[0] = DRM_FORMAT_MOD_LINEAR; + //modifiers[1] = DRM_FORMAT_MOD_INVALID; + //*num_modifiers = 2; + return false; + } + + if(!self->egl->eglQueryDmaBufModifiersEXT) { + fprintf(stderr, "gsr error: spa_video_format_get_modifiers: failed to initialize modifiers because eglQueryDmaBufModifiersEXT is not available\n"); + //modifiers[0] = DRM_FORMAT_MOD_LINEAR; + //modifiers[1] = DRM_FORMAT_MOD_INVALID; + //*num_modifiers = 2; + modifiers[0] = DRM_FORMAT_MOD_INVALID; + *num_modifiers = 1; + return false; + } + + const int64_t drm_format = spa_video_format_to_drm_format(format); + if(drm_format == DRM_FORMAT_INVALID) { + fprintf(stderr, "gsr error: spa_video_format_get_modifiers: unsupported format: %d\n", (int)format); + return false; + } + + if(!self->egl->eglQueryDmaBufModifiersEXT(self->egl->egl_display, drm_format, max_modifiers, modifiers, NULL, num_modifiers)) { + fprintf(stderr, "gsr error: spa_video_format_get_modifiers: eglQueryDmaBufModifiersEXT failed with drm format %d, %" PRIi64 "\n", (int)format, drm_format); + //modifiers[0] = DRM_FORMAT_MOD_LINEAR; + //modifiers[1] = DRM_FORMAT_MOD_INVALID; + //*num_modifiers = 2; + modifiers[0] = DRM_FORMAT_MOD_INVALID; + *num_modifiers = 1; + return false; + } + + // if(*num_modifiers + 2 <= max_modifiers) { + // modifiers[*num_modifiers + 0] = DRM_FORMAT_MOD_LINEAR; + // modifiers[*num_modifiers + 1] = DRM_FORMAT_MOD_INVALID; + // *num_modifiers += 2; + // } + return true; +} + +static void gsr_pipewire_video_init_modifiers(gsr_pipewire_video *self) { + for(size_t i = 0; i < GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS; i++) { + self->supported_video_formats[i].format = video_formats[i]; + int32_t num_modifiers = 0; + spa_video_format_get_modifiers(self, self->supported_video_formats[i].format, self->modifiers + self->num_modifiers, GSR_PIPEWIRE_VIDEO_MAX_MODIFIERS - self->num_modifiers, &num_modifiers); + self->supported_video_formats[i].modifiers_index = self->num_modifiers; + self->supported_video_formats[i].modifiers_size = num_modifiers; + self->num_modifiers += num_modifiers; + } +} + +static void gsr_pipewire_video_format_remove_modifier(gsr_pipewire_video *self, gsr_video_format *video_format, uint64_t modifier) { + for(size_t i = 0; i < video_format->modifiers_size; ++i) { + if(self->modifiers[video_format->modifiers_index + i] != modifier) + continue; + + for(size_t j = i + 1; j < video_format->modifiers_size; ++j) { + self->modifiers[j - 1] = self->modifiers[j]; + } + --video_format->modifiers_size; + return; + } +} + +static void gsr_pipewire_video_remove_modifier(gsr_pipewire_video *self, uint64_t modifier) { + for(size_t i = 0; i < GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS; i++) { + gsr_video_format *video_format = &self->supported_video_formats[i]; + gsr_pipewire_video_format_remove_modifier(self, video_format, modifier); + } +} + +static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { + struct spa_pod *params[GSR_PIPEWIRE_VIDEO_NUM_VIDEO_FORMATS]; + uint32_t num_video_formats = 0; + uint8_t params_buffer[4096]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + + self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); + if(!self->thread_loop) { + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire thread\n"); + goto error; + } + + self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); + if(!self->context) { + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire context\n"); + goto error; + } + + if(pw_thread_loop_start(self->thread_loop) < 0) { + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to start thread\n"); + goto error; + } + + pw_thread_loop_lock(self->thread_loop); + + // TODO: Why pass 5 to fcntl? + self->core = pw_context_connect_fd(self->context, fcntl(self->fd, F_DUPFD_CLOEXEC, 5), NULL, 0); + if(!self->core) { + pw_thread_loop_unlock(self->thread_loop); + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect to fd %d\n", self->fd); + goto error; + } + + // TODO: Error check + pw_core_add_listener(self->core, &self->core_listener, &core_events, self); + + self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, 0); + pw_thread_loop_wait(self->thread_loop); + + gsr_pipewire_video_init_modifiers(self); + + // TODO: Cleanup? + self->reneg = pw_loop_add_event(pw_thread_loop_get_loop(self->thread_loop), renegotiate_format, self); + if(!self->reneg) { + pw_thread_loop_unlock(self->thread_loop); + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: pw_loop_add_event failed\n"); + goto error; + } + + self->stream = pw_stream_new(self->core, "com.dec05eba.gpu_screen_recorder", + pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Screen", NULL)); + if(!self->stream) { + pw_thread_loop_unlock(self->thread_loop); + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create stream\n"); + goto error; + } + pw_stream_add_listener(self->stream, &self->stream_listener, &stream_events, self); + + if(!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { + pw_thread_loop_unlock(self->thread_loop); + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to build format params\n"); + goto error; + } + + if(pw_stream_connect( + self->stream, PW_DIRECTION_INPUT, self->node, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, (const struct spa_pod**)params, + num_video_formats) < 0) + { + pw_thread_loop_unlock(self->thread_loop); + fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect stream\n"); + goto error; + } + + pw_thread_loop_unlock(self->thread_loop); + return true; + + error: + if(self->thread_loop) { + //pw_thread_loop_wait(self->thread_loop); + pw_thread_loop_stop(self->thread_loop); + } + + if(self->stream) { + pw_stream_disconnect(self->stream); + pw_stream_destroy(self->stream); + self->stream = NULL; + } + + if(self->core) { + pw_core_disconnect(self->core); + self->core = NULL; + } + + if(self->context) { + pw_context_destroy(self->context); + self->context = NULL; + } + + if(self->thread_loop) { + pw_thread_loop_destroy(self->thread_loop); + self->thread_loop = NULL; + } + return false; +} + +static int pw_init_counter = 0; +bool gsr_pipewire_video_init(gsr_pipewire_video *self, int pipewire_fd, uint32_t pipewire_node, int fps, bool capture_cursor, gsr_egl *egl) { + if(pw_init_counter == 0) + pw_init(NULL, NULL); + ++pw_init_counter; + + memset(self, 0, sizeof(*self)); + self->egl = egl; + self->fd = pipewire_fd; + self->node = pipewire_node; + if(pthread_mutex_init(&self->mutex, NULL) != 0) { + fprintf(stderr, "gsr error: gsr_pipewire_video_init: failed to initialize mutex\n"); + gsr_pipewire_video_deinit(self); + return false; + } + self->mutex_initialized = true; + self->video_info.fps_num = fps; + self->video_info.fps_den = 1; + self->cursor.visible = capture_cursor; + + if(!gsr_pipewire_video_setup_stream(self)) { + gsr_pipewire_video_deinit(self); + return false; + } + + return true; +} + +void gsr_pipewire_video_deinit(gsr_pipewire_video *self) { + if(self->thread_loop) { + //pw_thread_loop_wait(self->thread_loop); + pw_thread_loop_stop(self->thread_loop); + } + + if(self->stream) { + pw_stream_disconnect(self->stream); + pw_stream_destroy(self->stream); + self->stream = NULL; + } + + if(self->core) { + pw_core_disconnect(self->core); + self->core = NULL; + } + + if(self->context) { + pw_context_destroy(self->context); + self->context = NULL; + } + + if(self->thread_loop) { + pw_thread_loop_destroy(self->thread_loop); + self->thread_loop = NULL; + } + + if(self->fd > 0) { + close(self->fd); + self->fd = -1; + } + + for(size_t i = 0; i < self->dmabuf_num_planes; ++i) { + if(self->dmabuf_data[i].fd > 0) { + close(self->dmabuf_data[i].fd); + self->dmabuf_data[i].fd = -1; + } + } + self->dmabuf_num_planes = 0; + + self->negotiated = false; + + if(self->mutex_initialized) { + pthread_mutex_destroy(&self->mutex); + self->mutex_initialized = false; + } + + if(self->cursor.data) { + free(self->cursor.data); + self->cursor.data = NULL; + } + + --pw_init_counter; + if(pw_init_counter == 0) { +#if PW_CHECK_VERSION(0, 3, 49) + pw_deinit(); +#endif + } +} + +static EGLImage gsr_pipewire_video_create_egl_image(gsr_pipewire_video *self, const int *fds, const uint32_t *offsets, const uint32_t *pitches, const uint64_t *modifiers, bool use_modifiers) { + intptr_t img_attr[44]; + setup_dma_buf_attrs(img_attr, spa_video_format_to_drm_format(self->format.info.raw.format), self->format.info.raw.size.width, self->format.info.raw.size.height, + fds, offsets, pitches, modifiers, self->dmabuf_num_planes, use_modifiers); + while(self->egl->eglGetError() != EGL_SUCCESS){} + EGLImage image = self->egl->eglCreateImage(self->egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr); + if(!image || self->egl->eglGetError() != EGL_SUCCESS) { + if(image) + self->egl->eglDestroyImage(self->egl->egl_display, image); + return NULL; + } + return image; +} + +static EGLImage gsr_pipewire_video_create_egl_image_with_fallback(gsr_pipewire_video *self) { + int fds[GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES]; + uint32_t offsets[GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES]; + uint32_t pitches[GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES]; + uint64_t modifiers[GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES]; + for(size_t i = 0; i < self->dmabuf_num_planes; ++i) { + fds[i] = self->dmabuf_data[i].fd; + offsets[i] = self->dmabuf_data[i].offset; + pitches[i] = self->dmabuf_data[i].stride; + modifiers[i] = self->format.info.raw.modifier; + } + + EGLImage image = NULL; + if(self->no_modifiers_fallback || !self->has_modifier) { + image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, false); + } else { + image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, true); + if(!image) { + if(self->format.info.raw.modifier == DRM_FORMAT_MOD_INVALID) { + fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + self->no_modifiers_fallback = true; + image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, false); + } else { + fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, renegotiating with a different modifier\n"); + self->negotiated = false; + pw_thread_loop_lock(self->thread_loop); + gsr_pipewire_video_remove_modifier(self, self->format.info.raw.modifier); + pw_loop_signal_event(pw_thread_loop_get_loop(self->thread_loop), self->reneg); + pw_thread_loop_unlock(self->thread_loop); + } + } + } + return image; +} + +static bool gsr_pipewire_video_bind_image_to_texture(gsr_pipewire_video *self, EGLImage image, unsigned int texture_id, bool external_texture) { + const int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + while(self->egl->glGetError() != 0){} + self->egl->glBindTexture(texture_target, texture_id); + self->egl->glEGLImageTargetTexture2DOES(texture_target, image); + const bool success = self->egl->glGetError() == 0; + self->egl->glBindTexture(texture_target, 0); + return success; +} + +static void gsr_pipewire_video_bind_image_to_texture_with_fallback(gsr_pipewire_video *self, gsr_texture_map texture_map, EGLImage image) { + if(self->external_texture_fallback) { + gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); + } else { + if(!gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.texture_id, false)) { + fprintf(stderr, "gsr error: gsr_pipewire_video_map_texture: failed to bind image to texture, trying with external texture\n"); + self->external_texture_fallback = true; + gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); + } + } +} + +static void gsr_pipewire_video_update_cursor_texture(gsr_pipewire_video *self, gsr_texture_map texture_map) { + if(!self->cursor.data) + return; + + self->egl->glBindTexture(GL_TEXTURE_2D, texture_map.cursor_texture_id); + // TODO: glTextureSubImage2D if same size + self->egl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->cursor.width, self->cursor.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, self->cursor.data); + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + self->egl->glBindTexture(GL_TEXTURE_2D, 0); + + free(self->cursor.data); + self->cursor.data = NULL; +} + +bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_map_texture_output *output) { + for(int i = 0; i < GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES; ++i) { + memset(&output->dmabuf_data[i], 0, sizeof(gsr_pipewire_video_dmabuf_data)); + } + output->num_dmabuf_data = 0; + output->using_external_image = self->external_texture_fallback; + output->fourcc = 0; + output->modifiers = 0; + output->rotation = GSR_MONITOR_ROT_0; + pthread_mutex_lock(&self->mutex); + + if(!self->negotiated || self->dmabuf_data[0].fd <= 0) { + pthread_mutex_unlock(&self->mutex); + return false; + } + + EGLImage image = gsr_pipewire_video_create_egl_image_with_fallback(self); + if(!image) { + pthread_mutex_unlock(&self->mutex); + return false; + } + + gsr_pipewire_video_bind_image_to_texture_with_fallback(self, texture_map, image); + output->using_external_image = self->external_texture_fallback; + self->egl->eglDestroyImage(self->egl->egl_display, image); + + gsr_pipewire_video_update_cursor_texture(self, texture_map); + + output->texture_width = self->format.info.raw.size.width; + output->texture_height = self->format.info.raw.size.height; + + output->region.x = 0; + output->region.y = 0; + + output->region.width = self->format.info.raw.size.width; + output->region.height = self->format.info.raw.size.height; + + if(self->crop.valid) { + output->region.x = self->crop.x; + output->region.y = self->crop.y; + + output->region.width = self->crop.width; + output->region.height = self->crop.height; + } + + // TODO: Test transform + cropping + if(self->rotation == GSR_MONITOR_ROT_90 || self->rotation == GSR_MONITOR_ROT_270) { + const int temp = output->region.width; + output->region.width = output->region.height; + output->region.height = temp; + } + + /* TODO: Test if cursor hotspot is correct */ + output->cursor_region.x = self->cursor.x - self->cursor.hotspot_x; + output->cursor_region.y = self->cursor.y - self->cursor.hotspot_y; + + output->cursor_region.width = self->cursor.width; + output->cursor_region.height = self->cursor.height; + + for(size_t i = 0; i < self->dmabuf_num_planes; ++i) { + output->dmabuf_data[i] = self->dmabuf_data[i]; + self->dmabuf_data[i].fd = -1; + } + output->num_dmabuf_data = self->dmabuf_num_planes; + output->fourcc = spa_video_format_to_drm_format(self->format.info.raw.format); + output->modifiers = self->format.info.raw.modifier; + output->rotation = self->rotation; + self->dmabuf_num_planes = 0; + + pthread_mutex_unlock(&self->mutex); + return true; +} + +bool gsr_pipewire_video_is_damaged(gsr_pipewire_video *self) { + if(!self->mutex_initialized) + return false; + + bool damaged = false; + pthread_mutex_lock(&self->mutex); + damaged = self->damaged; + pthread_mutex_unlock(&self->mutex); + return damaged; +} + +void gsr_pipewire_video_clear_damage(gsr_pipewire_video *self) { + if(!self->mutex_initialized) + return; + + pthread_mutex_lock(&self->mutex); + self->damaged = false; + pthread_mutex_unlock(&self->mutex); +} + +bool gsr_pipewire_video_should_restart(gsr_pipewire_video *self) { + if(!self->mutex_initialized) + return false; + + bool should_restart = false; + pthread_mutex_lock(&self->mutex); + should_restart = self->paused && clock_get_monotonic_seconds() - self->paused_start_secs >= 3.0; + pthread_mutex_unlock(&self->mutex); + return should_restart; +} diff --git a/src/plugins.c b/src/plugins.c new file mode 100644 index 0000000..061534c --- /dev/null +++ b/src/plugins.c @@ -0,0 +1,138 @@ +#include "../include/plugins.h" +#include "../include/utils.h" +#include <stdio.h> +#include <string.h> +#include <dlfcn.h> +#include <assert.h> + +static int color_depth_to_gl_internal_format(gsr_plugin_color_depth color_depth) { + switch(color_depth) { + case GSR_PLUGIN_COLOR_DEPTH_8_BITS: + return GL_RGBA8; + case GSR_PLUGIN_COLOR_DEPTH_10_BITS: + return GL_RGBA16; + } + assert(false); + return GL_RGBA8; +} + +bool gsr_plugins_init(gsr_plugins *self, gsr_plugin_init_params init_params, gsr_egl *egl) { + memset(self, 0, sizeof(*self)); + self->init_params = init_params; + self->egl = egl; + + /* TODO: GL_RGB8? */ + const unsigned int texture = gl_create_texture(egl, init_params.width, init_params.height, color_depth_to_gl_internal_format(init_params.color_depth), GL_RGBA, GL_LINEAR); + if(texture == 0) { + fprintf(stderr, "gsr error: gsr_plugins_init failed to create texture\n"); + return false; + } + self->texture = texture; + + gsr_color_conversion_params color_conversion_params = { + .egl = egl, + .destination_color = GSR_DESTINATION_COLOR_RGB8, /* TODO: Support 10-bits, use init_params.color_depth */ + .destination_textures[0] = self->texture, + .destination_textures_size[0] = (vec2i){ init_params.width, init_params.height }, + .num_destination_textures = 1, + .color_range = GSR_COLOR_RANGE_FULL, + .load_external_image_shader = false, + //.force_graphics_shader = false, + }; + color_conversion_params.destination_textures[0] = self->texture; + + if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { + fprintf(stderr, "gsr error: gsr_plugins_init failed to create color conversion\n"); + gsr_plugins_deinit(self); + return false; + } + gsr_color_conversion_clear(&self->color_conversion); + + return true; +} + +void gsr_plugins_deinit(gsr_plugins *self) { + for(int i = self->num_plugins - 1; i >= 0; --i) { + gsr_plugin *plugin = &self->plugins[i]; + plugin->gsr_plugin_deinit(plugin->data.userdata); + fprintf(stderr, "gsr info: unloaded plugin: %s\n", plugin->data.name); + } + self->num_plugins = 0; + + if(self->texture > 0) { + self->egl->glDeleteTextures(1, &self->texture); + self->texture = 0; + } + + gsr_color_conversion_deinit(&self->color_conversion); +} + +bool gsr_plugins_load_plugin(gsr_plugins *self, const char *plugin_filepath) { + if(self->num_plugins >= GSR_MAX_PLUGINS) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed, more plugins can't load more than %d plugins. Report this as an issue\n", GSR_MAX_PLUGINS); + return false; + } + + gsr_plugin plugin; + memset(&plugin, 0, sizeof(plugin)); + + plugin.lib = dlopen(plugin_filepath, RTLD_LAZY); + if(!plugin.lib) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load \"%s\", error: %s\n", plugin_filepath, dlerror()); + return false; + } + + plugin.gsr_plugin_init = dlsym(plugin.lib, "gsr_plugin_init"); + if(!plugin.gsr_plugin_init) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_init\" in plugin \"%s\"\n", plugin_filepath); + goto fail; + } + + plugin.gsr_plugin_deinit = dlsym(plugin.lib, "gsr_plugin_deinit"); + if(!plugin.gsr_plugin_deinit) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_deinit\" in plugin \"%s\"\n", plugin_filepath); + goto fail; + } + + if(!plugin.gsr_plugin_init(&self->init_params, &plugin.data)) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", gsr_plugin_init in the plugin failed\n", plugin_filepath); + goto fail; + } + + if(!plugin.data.name) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the name (gsr_plugin_init_return.name)\n", plugin_filepath); + goto fail; + } + + if(plugin.data.version == 0) { + fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the version (gsr_plugin_init_return.version)\n", plugin_filepath); + goto fail; + } + + fprintf(stderr, "gsr info: loaded plugin: %s, name: %s, version: %u\n", plugin_filepath, plugin.data.name, plugin.data.version); + self->plugins[self->num_plugins] = plugin; + ++self->num_plugins; + return true; + + fail: + dlclose(plugin.lib); + return false; +} + +void gsr_plugins_draw(gsr_plugins *self) { + const gsr_plugin_draw_params params = { + .width = self->init_params.width, + .height = self->init_params.height, + }; + + self->egl->glBindFramebuffer(GL_FRAMEBUFFER, self->color_conversion.framebuffers[0]); + self->egl->glViewport(0, 0, self->init_params.width, self->init_params.height); + + for(int i = 0; i < self->num_plugins; ++i) { + const gsr_plugin *plugin = &self->plugins[i]; + if(plugin->data.draw) + plugin->data.draw(¶ms, plugin->data.userdata); + } + + self->egl->glBindFramebuffer(GL_FRAMEBUFFER, 0); +} diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c new file mode 100644 index 0000000..92aa645 --- /dev/null +++ b/src/replay_buffer/replay_buffer.c @@ -0,0 +1,91 @@ +#include "../../include/replay_buffer/replay_buffer.h" +#include "../../include/replay_buffer/replay_buffer_ram.h" +#include "../../include/replay_buffer/replay_buffer_disk.h" + +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +gsr_replay_buffer* gsr_replay_buffer_create(gsr_replay_storage replay_storage, const char *replay_directory, double replay_buffer_time, size_t replay_buffer_num_packets) { + gsr_replay_buffer *replay_buffer = NULL; + switch(replay_storage) { + case GSR_REPLAY_STORAGE_RAM: + replay_buffer = gsr_replay_buffer_ram_create(replay_buffer_num_packets); + break; + case GSR_REPLAY_STORAGE_DISK: + replay_buffer = gsr_replay_buffer_disk_create(replay_directory, replay_buffer_time); + break; + } + + replay_buffer->mutex_initialized = false; + replay_buffer->original_replay_buffer = NULL; + if(pthread_mutex_init(&replay_buffer->mutex, NULL) != 0) { + gsr_replay_buffer_destroy(replay_buffer); + return NULL; + } + + replay_buffer->mutex_initialized = true; + return replay_buffer; +} + +void gsr_replay_buffer_destroy(gsr_replay_buffer *self) { + self->destroy(self); + if(self->mutex_initialized && !self->original_replay_buffer) { + pthread_mutex_destroy(&self->mutex); + self->mutex_initialized = false; + } + self->original_replay_buffer = NULL; + free(self); +} + +void gsr_replay_buffer_lock(gsr_replay_buffer *self) { + if(self->original_replay_buffer) { + gsr_replay_buffer_lock(self->original_replay_buffer); + return; + } + + if(self->mutex_initialized) + pthread_mutex_lock(&self->mutex); +} + +void gsr_replay_buffer_unlock(gsr_replay_buffer *self) { + if(self->original_replay_buffer) { + gsr_replay_buffer_unlock(self->original_replay_buffer); + return; + } + + if(self->mutex_initialized) + pthread_mutex_unlock(&self->mutex); +} + +bool gsr_replay_buffer_append(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp) { + return self->append(self, av_packet, timestamp); +} + +void gsr_replay_buffer_clear(gsr_replay_buffer *self) { + self->clear(self); +} + +AVPacket* gsr_replay_buffer_iterator_get_packet(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator) { + return self->iterator_get_packet(self, iterator); +} + +uint8_t* gsr_replay_buffer_iterator_get_packet_data(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator) { + return self->iterator_get_packet_data(self, iterator); +} + +gsr_replay_buffer* gsr_replay_buffer_clone(gsr_replay_buffer *self) { + return self->clone(self); +} + +gsr_replay_buffer_iterator gsr_replay_buffer_find_packet_index_by_time_passed(gsr_replay_buffer *self, int seconds) { + return self->find_packet_index_by_time_passed(self, seconds); +} + +gsr_replay_buffer_iterator gsr_replay_buffer_find_keyframe(gsr_replay_buffer *self, gsr_replay_buffer_iterator start_iterator, int stream_index, bool invert_stream_index) { + return self->find_keyframe(self, start_iterator, stream_index, invert_stream_index); +} + +bool gsr_replay_buffer_iterator_next(gsr_replay_buffer *self, gsr_replay_buffer_iterator *iterator) { + return self->iterator_next(self, iterator); +} diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c new file mode 100644 index 0000000..3fff9f3 --- /dev/null +++ b/src/replay_buffer/replay_buffer_disk.c @@ -0,0 +1,437 @@ +#include "../../include/replay_buffer/replay_buffer_disk.h" +#include "../../include/utils.h" + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> +#include <errno.h> +#include <assert.h> + +#define REPLAY_BUFFER_FILE_SIZE_BYTES 1024 * 1024 * 256 /* 256MB */ +#define FILE_PREFIX "Replay" + +static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self); + +static void gsr_av_packet_disk_init(gsr_av_packet_disk *self, const AVPacket *av_packet, size_t data_index, double timestamp) { + self->packet = *av_packet; + self->packet.data = NULL; + self->data_index = data_index; + self->timestamp = timestamp; +} + +static gsr_replay_buffer_file* gsr_replay_buffer_file_create(char *replay_directory, size_t replay_storage_counter, double timestamp, int *replay_storage_fd) { + gsr_replay_buffer_file *self = calloc(1, sizeof(gsr_replay_buffer_file)); + if(!self) { + fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create buffer file\n"); + return NULL; + } + + if(create_directory_recursive(replay_directory) != 0) { + fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay directory: %s\n", replay_directory); + free(self); + return NULL; + } + + char filename[PATH_MAX]; + snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", replay_directory, FILE_PREFIX, (int)replay_storage_counter); + *replay_storage_fd = creat(filename, 0700); + if(*replay_storage_fd <= 0) { + fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay file: %s\n", filename); + free(self); + return NULL; + } + + self->id = replay_storage_counter; + self->start_timestamp = timestamp; + self->end_timestamp = timestamp; + self->ref_counter = 1; + self->fd = -1; + + self->packets = NULL; + self->capacity_num_packets = 0; + self->num_packets = 0; + return self; +} + +static gsr_replay_buffer_file* gsr_replay_buffer_file_ref(gsr_replay_buffer_file *self) { + if(self->ref_counter >= 1) + ++self->ref_counter; + return self; +} + +static void gsr_replay_buffer_file_free(gsr_replay_buffer_file *self, const char *replay_directory) { + self->ref_counter = 0; + + if(self->fd > 0) { + close(self->fd); + self->fd = -1; + } + + char filename[PATH_MAX]; + snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", replay_directory, FILE_PREFIX, (int)self->id); + remove(filename); + + if(self->packets) { + free(self->packets); + self->packets = NULL; + } + self->num_packets = 0; + self->capacity_num_packets = 0; + + free(self); +} + +static void gsr_replay_buffer_file_unref(gsr_replay_buffer_file *self, const char *replay_directory) { + if(self->ref_counter > 0) + --self->ref_counter; + + if(self->ref_counter <= 0) + gsr_replay_buffer_file_free(self, replay_directory); +} + +static void gsr_replay_buffer_disk_clear(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + + for(size_t i = 0; i < self->num_files; ++i) { + gsr_replay_buffer_file_unref(self->files[i], self->replay_directory); + } + self->num_files = 0; + + if(self->storage_fd > 0) { + close(self->storage_fd); + self->storage_fd = 0; + } + + self->storage_num_bytes_written = 0; + gsr_replay_buffer_unlock(&self->replay_buffer); +} + +static void gsr_replay_buffer_disk_destroy(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_disk_clear(replay_buffer); + + if(self->owns_directory) { + remove(self->replay_directory); + self->owns_directory = false; + } +} + +static bool file_write_all(int fd, const uint8_t *data, size_t size, size_t *bytes_written_total) { + *bytes_written_total = 0; + while(*bytes_written_total < size) { + const ssize_t bytes_written = write(fd, data + *bytes_written_total, size - *bytes_written_total); + if(bytes_written == -1) { + if(errno == EAGAIN) + continue; + else + return false; + } + *bytes_written_total += bytes_written; + } + return true; +} + +static bool gsr_replay_buffer_disk_create_next_file(gsr_replay_buffer_disk *self, double timestamp) { + if(self->num_files + 1 >= GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES) { + fprintf(stderr, "gsr error: gsr_replay_buffer_disk_create_next_file: too many replay buffer files created! (> %d), either reduce the replay buffer time or report this as a bug\n", (int)GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES); + return false; + } + + gsr_replay_buffer_file *replay_buffer_file = gsr_replay_buffer_file_create(self->replay_directory, self->storage_counter, timestamp, &self->storage_fd); + if(!replay_buffer_file) + return false; + + self->files[self->num_files] = replay_buffer_file; + ++self->num_files; + ++self->storage_counter; + return true; +} + +static bool gsr_replay_buffer_disk_append_to_current_file(gsr_replay_buffer_disk *self, const AVPacket *av_packet, double timestamp) { + gsr_replay_buffer_file *replay_buffer_file = self->files[self->num_files - 1]; + replay_buffer_file->end_timestamp = timestamp; + + if(replay_buffer_file->num_packets + 1 >= replay_buffer_file->capacity_num_packets) { + size_t new_capacity_num_packets = replay_buffer_file->capacity_num_packets * 2; + if(new_capacity_num_packets == 0) + new_capacity_num_packets = 256; + + void *new_packets = realloc(replay_buffer_file->packets, new_capacity_num_packets * sizeof(gsr_av_packet_disk)); + if(!new_packets) { + fprintf(stderr, "gsr error: gsr_replay_buffer_disk_append_to_current_file: failed to reallocate replay buffer file packets\n"); + return false; + } + + replay_buffer_file->capacity_num_packets = new_capacity_num_packets; + replay_buffer_file->packets = new_packets; + } + + gsr_av_packet_disk *packet = &replay_buffer_file->packets[replay_buffer_file->num_packets]; + gsr_av_packet_disk_init(packet, av_packet, self->storage_num_bytes_written, timestamp); + ++replay_buffer_file->num_packets; + + size_t bytes_written = 0; + const bool file_written = file_write_all(self->storage_fd, av_packet->data, av_packet->size, &bytes_written); + self->storage_num_bytes_written += bytes_written; + if(self->storage_num_bytes_written >= REPLAY_BUFFER_FILE_SIZE_BYTES) { + self->storage_num_bytes_written = 0; + close(self->storage_fd); + self->storage_fd = 0; + } + + return file_written; +} + +static void gsr_replay_buffer_disk_remove_first_file(gsr_replay_buffer_disk *self) { + gsr_replay_buffer_file_unref(self->files[0], self->replay_directory); + for(size_t i = 1; i < self->num_files; ++i) { + self->files[i - 1] = self->files[i]; + } + --self->num_files; +} + +static bool gsr_replay_buffer_disk_append(gsr_replay_buffer *replay_buffer, const AVPacket *av_packet, double timestamp) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + bool success = false; + gsr_replay_buffer_lock(&self->replay_buffer); + + if(self->storage_fd <= 0) { + if(!gsr_replay_buffer_disk_create_next_file(self, timestamp)) + goto done; + } + + const bool data_written = gsr_replay_buffer_disk_append_to_current_file(self, av_packet, timestamp); + + if(self->num_files > 1) { + const double buffer_time_accumulated = timestamp - self->files[1]->start_timestamp; + if(buffer_time_accumulated >= self->replay_buffer_time) + gsr_replay_buffer_disk_remove_first_file(self); + } + + success = data_written; + + done: + gsr_replay_buffer_unlock(&self->replay_buffer); + return success; +} + +static AVPacket* gsr_replay_buffer_disk_iterator_get_packet(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator iterator) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + assert(iterator.file_index < self->num_files); + assert(iterator.packet_index < self->files[iterator.file_index]->num_packets); + return &self->files[iterator.file_index]->packets[iterator.packet_index].packet; +} + +static uint8_t* gsr_replay_buffer_disk_iterator_get_packet_data(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator iterator) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + assert(iterator.file_index < self->num_files); + gsr_replay_buffer_file *file = self->files[iterator.file_index]; + assert(iterator.packet_index < file->num_packets); + + if(file->fd <= 0) { + char filename[PATH_MAX]; + snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", self->replay_directory, FILE_PREFIX, (int)file->id); + file->fd = open(filename, O_RDONLY); + if(file->fd <= 0) { + fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to open file\n"); + return NULL; + } + } + + const gsr_av_packet_disk *packet = &self->files[iterator.file_index]->packets[iterator.packet_index]; + if(lseek(file->fd, packet->data_index, SEEK_SET) == -1) { + fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to seek\n"); + return NULL; + } + + uint8_t *packet_data = malloc(packet->packet.size); + if(read(file->fd, packet_data, packet->packet.size) != packet->packet.size) { + fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to read data from file\n"); + free(packet_data); + return NULL; + } + + return packet_data; +} + +static gsr_replay_buffer* gsr_replay_buffer_disk_clone(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_disk *destination = calloc(1, sizeof(gsr_replay_buffer_disk)); + if(!destination) + return NULL; + + gsr_replay_buffer_disk_set_impl_funcs(destination); + gsr_replay_buffer_lock(&self->replay_buffer); + + destination->replay_buffer.original_replay_buffer = replay_buffer; + destination->replay_buffer.mutex = self->replay_buffer.mutex; + destination->replay_buffer.mutex_initialized = self->replay_buffer.mutex_initialized; + destination->replay_buffer_time = self->replay_buffer_time; + destination->storage_counter = self->storage_counter; + destination->storage_num_bytes_written = self->storage_num_bytes_written; + destination->storage_fd = 0; // We only want to read from the clone. If there is a need to write to it in the future then TODO change this + + for(size_t i = 0; i < self->num_files; ++i) { + destination->files[i] = gsr_replay_buffer_file_ref(self->files[i]); + } + destination->num_files = self->num_files; + + snprintf(destination->replay_directory, sizeof(destination->replay_directory), "%s", self->replay_directory); + destination->owns_directory = false; + + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer*)destination; +} + +/* Binary search */ +static size_t gsr_replay_buffer_file_find_packet_index_by_time_passed(const gsr_replay_buffer_file *self, int seconds) { + const double now = clock_get_monotonic_seconds(); + if(self->num_packets == 0) { + return 0; + } + + size_t lower_bound = 0; + size_t upper_bound = self->num_packets; + size_t index = 0; + + for(;;) { + index = lower_bound + (upper_bound - lower_bound) / 2; + const gsr_av_packet_disk *packet = &self->packets[index]; + const double time_passed_since_packet = now - packet->timestamp; + if(time_passed_since_packet >= seconds) { + if(lower_bound == index) + break; + lower_bound = index; + } else { + if(upper_bound == index) + break; + upper_bound = index; + } + } + + return index; +} + +/* Binary search */ +static gsr_replay_buffer_iterator gsr_replay_buffer_disk_find_file_index_by_time_passed(gsr_replay_buffer *replay_buffer, int seconds) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + + const double now = clock_get_monotonic_seconds(); + if(self->num_files == 0) { + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer_iterator){0, 0}; + } + + size_t lower_bound = 0; + size_t upper_bound = self->num_files; + size_t file_index = 0; + + for(;;) { + file_index = lower_bound + (upper_bound - lower_bound) / 2; + const gsr_replay_buffer_file *file = self->files[file_index]; + const double time_passed_since_file_start = now - file->start_timestamp; + const double time_passed_since_file_end = now - file->end_timestamp; + if(time_passed_since_file_start >= seconds && time_passed_since_file_end <= seconds) { + break; + } else if(time_passed_since_file_start >= seconds) { + if(lower_bound == file_index) + break; + lower_bound = file_index; + } else { + if(upper_bound == file_index) + break; + upper_bound = file_index; + } + } + + const gsr_replay_buffer_file *file = self->files[file_index]; + const size_t packet_index = gsr_replay_buffer_file_find_packet_index_by_time_passed(file, seconds); + + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer_iterator){packet_index, file_index}; +} + +static gsr_replay_buffer_iterator gsr_replay_buffer_disk_find_keyframe(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator start_iterator, int stream_index, bool invert_stream_index) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_iterator keyframe_iterator = {(size_t)-1, 0}; + gsr_replay_buffer_lock(&self->replay_buffer); + size_t packet_index = start_iterator.packet_index; + for(size_t file_index = start_iterator.file_index; file_index < self->num_files; ++file_index) { + const gsr_replay_buffer_file *file = self->files[file_index]; + for(; packet_index < file->num_packets; ++packet_index) { + const gsr_av_packet_disk *packet = &file->packets[packet_index]; + if((packet->packet.flags & AV_PKT_FLAG_KEY) && (invert_stream_index ? packet->packet.stream_index != stream_index : packet->packet.stream_index == stream_index)) { + keyframe_iterator.packet_index = packet_index; + keyframe_iterator.file_index = file_index; + goto done; + } + } + packet_index = 0; + } + done: + gsr_replay_buffer_unlock(&self->replay_buffer); + return keyframe_iterator; +} + +static bool gsr_replay_buffer_disk_iterator_next(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator *iterator) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + if(iterator->file_index >= self->num_files) + return false; + + if(iterator->packet_index + 1 >= self->files[iterator->file_index]->num_packets) { + if(iterator->file_index + 1 >= self->num_files) + return false; + + if(self->files[iterator->file_index + 1]->num_packets == 0) + return false; + + ++iterator->file_index; + iterator->packet_index = 0; + return true; + } else { + ++iterator->packet_index; + return true; + } +} + +static void get_current_time(char *time_str, size_t time_str_size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(time_str, time_str_size - 1, "%Y-%m-%d_%H-%M-%S", t); +} + +static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self) { + self->replay_buffer.destroy = gsr_replay_buffer_disk_destroy; + self->replay_buffer.append = gsr_replay_buffer_disk_append; + self->replay_buffer.clear = gsr_replay_buffer_disk_clear; + self->replay_buffer.iterator_get_packet = gsr_replay_buffer_disk_iterator_get_packet; + self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_disk_iterator_get_packet_data; + self->replay_buffer.clone = gsr_replay_buffer_disk_clone; + self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_disk_find_file_index_by_time_passed; + self->replay_buffer.find_keyframe = gsr_replay_buffer_disk_find_keyframe; + self->replay_buffer.iterator_next = gsr_replay_buffer_disk_iterator_next; +} + +gsr_replay_buffer* gsr_replay_buffer_disk_create(const char *replay_directory, double replay_buffer_time) { + assert(replay_buffer_time > 0); + gsr_replay_buffer_disk *replay_buffer = calloc(1, sizeof(gsr_replay_buffer_disk)); + if(!replay_buffer) + return NULL; + + char time_str[128]; + get_current_time(time_str, sizeof(time_str)); + + replay_buffer->num_files = 0; + replay_buffer->storage_counter = 0; + replay_buffer->replay_buffer_time = replay_buffer_time; + snprintf(replay_buffer->replay_directory, sizeof(replay_buffer->replay_directory), "%s/gsr-replay-%s.gsr", replay_directory, time_str); + replay_buffer->owns_directory = true; + + gsr_replay_buffer_disk_set_impl_funcs(replay_buffer); + return (gsr_replay_buffer*)replay_buffer; +} diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c new file mode 100644 index 0000000..890588f --- /dev/null +++ b/src/replay_buffer/replay_buffer_ram.c @@ -0,0 +1,256 @@ +#include "../../include/replay_buffer/replay_buffer_ram.h" +#include "../../include/utils.h" + +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +#include <libavutil/mem.h> + +static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self); + +static gsr_av_packet_ram* gsr_av_packet_ram_create(const AVPacket *av_packet, double timestamp) { + gsr_av_packet_ram *self = malloc(sizeof(gsr_av_packet_ram)); + if(!self) + return NULL; + + self->ref_counter = 1; + self->packet = *av_packet; + self->timestamp = timestamp; + // Why are we doing this you ask? there is a ffmpeg bug that causes cpu usage to increase over time when you have + // packets that are not being free'd until later. So we copy the packet data, free the packet and then reconstruct + // the packet later on when we need it, to keep packets alive only for a short period. + self->packet.data = av_memdup(av_packet->data, av_packet->size); + if(!self->packet.data) { + free(self); + return NULL; + } + + return self; +} + +static gsr_av_packet_ram* gsr_av_packet_ram_ref(gsr_av_packet_ram *self) { + if(self->ref_counter >= 1) + ++self->ref_counter; + return self; +} + +static void gsr_av_packet_ram_free(gsr_av_packet_ram *self) { + self->ref_counter = 0; + if(self->packet.data) { + av_free(self->packet.data); + self->packet.data = NULL; + } + free(self); +} + +static void gsr_av_packet_ram_unref(gsr_av_packet_ram *self) { + if(self->ref_counter >= 1) + --self->ref_counter; + + if(self->ref_counter <= 0) + gsr_av_packet_ram_free(self); +} + +static void gsr_replay_buffer_ram_destroy(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + for(size_t i = 0; i < self->num_packets; ++i) { + if(self->packets[i]) { + gsr_av_packet_ram_unref(self->packets[i]); + self->packets[i] = NULL; + } + } + self->num_packets = 0; + gsr_replay_buffer_unlock(&self->replay_buffer); + + if(self->packets) { + free(self->packets); + self->packets = NULL; + } + + self->capacity_num_packets = 0; + self->index = 0; +} + +static bool gsr_replay_buffer_ram_append(gsr_replay_buffer *replay_buffer, const AVPacket *av_packet, double timestamp) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + gsr_av_packet_ram *packet = gsr_av_packet_ram_create(av_packet, timestamp); + if(!packet) { + gsr_replay_buffer_unlock(&self->replay_buffer); + return false; + } + + if(self->packets[self->index]) { + gsr_av_packet_ram_unref(self->packets[self->index]); + self->packets[self->index] = NULL; + } + self->packets[self->index] = packet; + + self->index = (self->index + 1) % self->capacity_num_packets; + ++self->num_packets; + if(self->num_packets > self->capacity_num_packets) + self->num_packets = self->capacity_num_packets; + + gsr_replay_buffer_unlock(&self->replay_buffer); + return true; +} + +static void gsr_replay_buffer_ram_clear(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + for(size_t i = 0; i < self->num_packets; ++i) { + if(self->packets[i]) { + gsr_av_packet_ram_unref(self->packets[i]); + self->packets[i] = NULL; + } + } + self->num_packets = 0; + self->index = 0; + gsr_replay_buffer_unlock(&self->replay_buffer); +} + +static gsr_av_packet_ram* gsr_replay_buffer_ram_get_packet_at_index(gsr_replay_buffer *replay_buffer, size_t index) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + assert(index < self->num_packets); + size_t start_index = 0; + if(self->num_packets < self->capacity_num_packets) + start_index = self->num_packets - self->index; + else + start_index = self->index; + + const size_t offset = (start_index + index) % self->capacity_num_packets; + return self->packets[offset]; +} + +static AVPacket* gsr_replay_buffer_ram_iterator_get_packet(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator iterator) { + return &gsr_replay_buffer_ram_get_packet_at_index(replay_buffer, iterator.packet_index)->packet; +} + +static uint8_t* gsr_replay_buffer_ram_iterator_get_packet_data(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator iterator) { + (void)replay_buffer; + (void)iterator; + return NULL; +} + +static gsr_replay_buffer* gsr_replay_buffer_ram_clone(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_replay_buffer_ram *destination = calloc(1, sizeof(gsr_replay_buffer_ram)); + if(!destination) + return NULL; + + gsr_replay_buffer_ram_set_impl_funcs(destination); + gsr_replay_buffer_lock(&self->replay_buffer); + + destination->replay_buffer.original_replay_buffer = replay_buffer; + destination->replay_buffer.mutex = self->replay_buffer.mutex; + destination->replay_buffer.mutex_initialized = self->replay_buffer.mutex_initialized; + destination->capacity_num_packets = self->capacity_num_packets; + destination->index = self->index; + destination->packets = calloc(destination->capacity_num_packets, sizeof(gsr_av_packet_ram*)); + if(!destination->packets) { + free(destination); + gsr_replay_buffer_unlock(&self->replay_buffer); + return NULL; + } + + destination->num_packets = self->num_packets; + for(size_t i = 0; i < destination->num_packets; ++i) { + destination->packets[i] = gsr_av_packet_ram_ref(self->packets[i]); + } + + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer*)destination; +} + +/* Binary search */ +static gsr_replay_buffer_iterator gsr_replay_buffer_ram_find_packet_index_by_time_passed(gsr_replay_buffer *replay_buffer, int seconds) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_replay_buffer_lock(&self->replay_buffer); + + const double now = clock_get_monotonic_seconds(); + if(self->num_packets == 0) { + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer_iterator){0, 0}; + } + + size_t lower_bound = 0; + size_t upper_bound = self->num_packets; + size_t index = 0; + + for(;;) { + index = lower_bound + (upper_bound - lower_bound) / 2; + const gsr_av_packet_ram *packet = gsr_replay_buffer_ram_get_packet_at_index(replay_buffer, index); + const double time_passed_since_packet = now - packet->timestamp; + if(time_passed_since_packet >= seconds) { + if(lower_bound == index) + break; + lower_bound = index; + } else { + if(upper_bound == index) + break; + upper_bound = index; + } + } + + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer_iterator){index, 0}; +} + +static gsr_replay_buffer_iterator gsr_replay_buffer_ram_find_keyframe(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator start_iterator, int stream_index, bool invert_stream_index) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + size_t keyframe_index = (size_t)-1; + gsr_replay_buffer_lock(&self->replay_buffer); + for(size_t i = start_iterator.packet_index; i < self->num_packets; ++i) { + const gsr_av_packet_ram *packet = gsr_replay_buffer_ram_get_packet_at_index(replay_buffer, i); + if((packet->packet.flags & AV_PKT_FLAG_KEY) && (invert_stream_index ? packet->packet.stream_index != stream_index : packet->packet.stream_index == stream_index)) { + keyframe_index = i; + break; + } + } + gsr_replay_buffer_unlock(&self->replay_buffer); + return (gsr_replay_buffer_iterator){keyframe_index, 0}; +} + +static bool gsr_replay_buffer_ram_iterator_next(gsr_replay_buffer *replay_buffer, gsr_replay_buffer_iterator *iterator) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + if(iterator->packet_index + 1 < self->num_packets) { + ++iterator->packet_index; + return true; + } else { + return false; + } +} + +static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self) { + self->replay_buffer.destroy = gsr_replay_buffer_ram_destroy; + self->replay_buffer.append = gsr_replay_buffer_ram_append; + self->replay_buffer.clear = gsr_replay_buffer_ram_clear; + self->replay_buffer.iterator_get_packet = gsr_replay_buffer_ram_iterator_get_packet; + self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_ram_iterator_get_packet_data; + self->replay_buffer.clone = gsr_replay_buffer_ram_clone; + self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_ram_find_packet_index_by_time_passed; + self->replay_buffer.find_keyframe = gsr_replay_buffer_ram_find_keyframe; + self->replay_buffer.iterator_next = gsr_replay_buffer_ram_iterator_next; +} + +gsr_replay_buffer* gsr_replay_buffer_ram_create(size_t replay_buffer_num_packets) { + assert(replay_buffer_num_packets > 0); + gsr_replay_buffer_ram *replay_buffer = calloc(1, sizeof(gsr_replay_buffer_ram)); + if(!replay_buffer) + return NULL; + + replay_buffer->capacity_num_packets = replay_buffer_num_packets; + replay_buffer->num_packets = 0; + replay_buffer->index = 0; + replay_buffer->packets = calloc(replay_buffer->capacity_num_packets, sizeof(gsr_av_packet_ram*)); + if(!replay_buffer->packets) { + gsr_replay_buffer_ram_destroy(&replay_buffer->replay_buffer); + free(replay_buffer); + return NULL; + } + + gsr_replay_buffer_ram_set_impl_funcs(replay_buffer); + return (gsr_replay_buffer*)replay_buffer; +} diff --git a/src/shader.c b/src/shader.c new file mode 100644 index 0000000..d952d5d --- /dev/null +++ b/src/shader.c @@ -0,0 +1,147 @@ +#include "../include/shader.h" +#include "../include/egl.h" +#include <stdio.h> +#include <assert.h> + +static bool print_compile_errors = false; + +static int min_int(int a, int b) { + return a < b ? a : b; +} + +static unsigned int load_shader(gsr_egl *egl, unsigned int type, const char *source) { + unsigned int shader_id = egl->glCreateShader(type); + if(shader_id == 0) { + fprintf(stderr, "gsr error: load_shader: failed to create shader, error: %d\n", egl->glGetError()); + return 0; + } + + egl->glShaderSource(shader_id, 1, &source, NULL); + egl->glCompileShader(shader_id); + + int compiled = 0; + egl->glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled); + if(!compiled) { + int info_length = 0; + egl->glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &info_length); + + if(info_length > 1 && print_compile_errors) { + char info_log[4096]; + egl->glGetShaderInfoLog(shader_id, min_int(4096, info_length), NULL, info_log); + fprintf(stderr, "gsr error: load_shader: failed to compile shader, error:\n%s\nshader source:\n%s\n", info_log, source); + } + + egl->glDeleteShader(shader_id); + return 0; + } + + return shader_id; +} + +static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) { + unsigned int vertex_shader_id = 0; + unsigned int fragment_shader_id = 0; + unsigned int program_id = 0; + int linked = 0; + bool success = false; + + if(vertex_shader) { + vertex_shader_id = load_shader(egl, GL_VERTEX_SHADER, vertex_shader); + if(vertex_shader_id == 0) + goto done; + } + + if(fragment_shader) { + fragment_shader_id = load_shader(egl, GL_FRAGMENT_SHADER, fragment_shader); + if(fragment_shader_id == 0) + goto done; + } + + program_id = egl->glCreateProgram(); + if(program_id == 0) { + fprintf(stderr, "gsr error: load_program: failed to create shader program, error: %d\n", egl->glGetError()); + goto done; + } + + if(vertex_shader_id) + egl->glAttachShader(program_id, vertex_shader_id); + + if(fragment_shader_id) + egl->glAttachShader(program_id, fragment_shader_id); + + egl->glLinkProgram(program_id); + + egl->glGetProgramiv(program_id, GL_LINK_STATUS, &linked); + if(!linked) { + int info_length = 0; + egl->glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &info_length); + + if(info_length > 1) { + char info_log[4096]; + egl->glGetProgramInfoLog(program_id, min_int(4096, info_length), NULL, info_log); + fprintf(stderr, "gsr error: load program: linking shader program failed, error:\n%s\n", info_log); + } + + goto done; + } + + success = true; + done: + + if(!success) { + if(program_id) + egl->glDeleteProgram(program_id); + } + if(fragment_shader_id) + egl->glDeleteShader(fragment_shader_id); + if(vertex_shader_id) + egl->glDeleteShader(vertex_shader_id); + return program_id; +} + +int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) { + assert(egl); + self->egl = egl; + self->program_id = 0; + + if(!vertex_shader && !fragment_shader) { + fprintf(stderr, "gsr error: gsr_shader_init: vertex and fragment shader can't be NULL at the same time\n"); + return -1; + } + + self->program_id = load_program(self->egl, vertex_shader, fragment_shader); + if(self->program_id == 0) + return -1; + + return 0; +} + +void gsr_shader_deinit(gsr_shader *self) { + if(!self->egl) + return; + + if(self->program_id) { + self->egl->glDeleteProgram(self->program_id); + self->program_id = 0; + } + + self->egl = NULL; +} + +int gsr_shader_bind_attribute_location(gsr_shader *self, const char *attribute, int location) { + while(self->egl->glGetError()) {} + self->egl->glBindAttribLocation(self->program_id, location, attribute); + return self->egl->glGetError(); +} + +void gsr_shader_use(gsr_shader *self) { + self->egl->glUseProgram(self->program_id); +} + +void gsr_shader_use_none(gsr_shader *self) { + self->egl->glUseProgram(0); +} + +void gsr_shader_enable_debug_output(bool enable) { + print_compile_errors = enable; +} diff --git a/src/sound.cpp b/src/sound.cpp new file mode 100644 index 0000000..af83b15 --- /dev/null +++ b/src/sound.cpp @@ -0,0 +1,647 @@ +#include "../include/sound.hpp" +extern "C" { +#include "../include/utils.h" +} + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <cmath> +#include <time.h> +#include <mutex> + +#include <pulse/pulseaudio.h> +#include <pulse/mainloop.h> +#include <pulse/xmalloc.h> +#include <pulse/error.h> + +#define RECONNECT_TRY_TIMEOUT_SECONDS 0.5 +#define DEVICE_NAME_MAX_SIZE 128 + +#define CHECK_DEAD_GOTO(p, rerror, label) \ + do { \ + if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \ + !(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \ + if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \ + ((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \ + if (rerror) \ + *(rerror) = pa_context_errno((p)->context); \ + } else \ + if (rerror) \ + *(rerror) = PA_ERR_BADSTATE; \ + goto label; \ + } \ + } while(false); + +enum class DeviceType { + STANDARD, + DEFAULT_OUTPUT, + DEFAULT_INPUT +}; + +struct pa_handle { + pa_context *context; + pa_stream *stream; + pa_mainloop *mainloop; + + const void *read_data; + size_t read_index, read_length; + + uint8_t *output_data; + size_t output_index, output_length; + + int operation_success; + double latency_seconds; + + pa_buffer_attr attr; + pa_sample_spec ss; + + std::mutex reconnect_mutex; + DeviceType device_type; + char stream_name[256]; + bool reconnect; + double reconnect_last_tried_seconds; + + char device_name[DEVICE_NAME_MAX_SIZE]; + char default_output_device_name[DEVICE_NAME_MAX_SIZE]; + char default_input_device_name[DEVICE_NAME_MAX_SIZE]; +}; + +static void pa_sound_device_free(pa_handle *p) { + assert(p); + + if (p->stream) { + pa_stream_unref(p->stream); + p->stream = NULL; + } + + if (p->context) { + pa_context_disconnect(p->context); + pa_context_unref(p->context); + p->context = NULL; + } + + if (p->mainloop) { + pa_mainloop_free(p->mainloop); + p->mainloop = NULL; + } + + if (p->output_data) { + free(p->output_data); + p->output_data = NULL; + } + + pa_xfree(p); +} + +static void subscribe_update_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { + pa_handle *handle = (pa_handle*)userdata; + std::lock_guard<std::mutex> lock(handle->reconnect_mutex); + + if(server_info->default_sink_name) { + // TODO: Size check + snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); + if(handle->device_type == DeviceType::DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) { + handle->reconnect = true; + handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); + // TODO: Size check + snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_output_device_name); + } + } + + if(server_info->default_source_name) { + // TODO: Size check + snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); + if(handle->device_type == DeviceType::DEFAULT_INPUT && strcmp(handle->device_name, handle->default_input_device_name) != 0) { + handle->reconnect = true; + handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); + // TODO: Size check + snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_input_device_name); + } + } +} + +static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { + (void)idx; + pa_handle *handle = (pa_handle*)userdata; + if((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SERVER) { + pa_operation *pa = pa_context_get_server_info(c, subscribe_update_default_devices, handle); + if(pa) + pa_operation_unref(pa); + } +} + +static void store_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { + pa_handle *handle = (pa_handle*)userdata; + if(server_info->default_sink_name) + snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); + if(server_info->default_source_name) + snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); +} + +static bool startup_get_default_devices(pa_handle *p, const char *device_name) { + pa_operation *pa = pa_context_get_server_info(p->context, store_default_devices, p); + while(pa) { + pa_operation_state state = pa_operation_get_state(pa); + if(state == PA_OPERATION_DONE) { + pa_operation_unref(pa); + break; + } else if(state == PA_OPERATION_CANCELLED) { + pa_operation_unref(pa); + return false; + } + pa_mainloop_iterate(p->mainloop, 1, NULL); + } + + if(p->default_output_device_name[0] == '\0') { + fprintf(stderr, "gsr error: failed to find default audio output device\n"); + return false; + } + + if(strcmp(device_name, "default_output") == 0) { + snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_output_device_name); + p->device_type = DeviceType::DEFAULT_OUTPUT; + } else if(strcmp(device_name, "default_input") == 0) { + snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_input_device_name); + p->device_type = DeviceType::DEFAULT_INPUT; + } else { + snprintf(p->device_name, sizeof(p->device_name), "%s", device_name); + p->device_type = DeviceType::STANDARD; + } + + return true; +} + +static pa_handle* pa_sound_device_new(const char *server, + const char *name, + const char *device_name, + const char *stream_name, + const pa_sample_spec *ss, + const pa_buffer_attr *attr, + int *rerror) { + pa_handle *p; + int error = PA_ERR_INTERNAL; + pa_operation *pa = NULL; + + p = pa_xnew0(pa_handle, 1); + p->attr = *attr; + p->ss = *ss; + snprintf(p->stream_name, sizeof(p->stream_name), "%s", stream_name); + + p->reconnect = true; + p->reconnect_last_tried_seconds = clock_get_monotonic_seconds() - (RECONNECT_TRY_TIMEOUT_SECONDS * 1000.0 * 2.0); + p->default_output_device_name[0] = '\0'; + p->default_input_device_name[0] = '\0'; + p->device_type = DeviceType::STANDARD; + + const int buffer_size = attr->fragsize; + void *buffer = malloc(buffer_size); + if(!buffer) { + fprintf(stderr, "gsr error: failed to allocate buffer for audio\n"); + *rerror = -1; + return NULL; + } + + p->output_data = (uint8_t*)buffer; + p->output_length = buffer_size; + p->output_index = 0; + + pa_proplist *proplist = pa_proplist_new(); + pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "production"); + if(strcmp(device_name, "") == 0) { + pa_proplist_sets(proplist, "node.autoconnect", "false"); + pa_proplist_sets(proplist, "node.dont-reconnect", "true"); + } + + if (!(p->mainloop = pa_mainloop_new())) + goto fail; + + if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), name, proplist))) + goto fail; + + if (pa_context_connect(p->context, server, PA_CONTEXT_NOFLAGS, NULL) < 0) { + error = pa_context_errno(p->context); + goto fail; + } + + for (;;) { + pa_context_state_t state = pa_context_get_state(p->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) { + error = pa_context_errno(p->context); + goto fail; + } + + pa_mainloop_iterate(p->mainloop, 1, NULL); + } + + if(!startup_get_default_devices(p, device_name)) + goto fail; + + pa_context_set_subscribe_callback(p->context, subscribe_cb, p); + pa = pa_context_subscribe(p->context, PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); + if(pa) + pa_operation_unref(pa); + + pa_proplist_free(proplist); + return p; + +fail: + if (rerror) + *rerror = error; + pa_sound_device_free(p); + pa_proplist_free(proplist); + return NULL; +} + +static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) { + std::lock_guard<std::mutex> lock(p->reconnect_mutex); + + if(!p->reconnect && (!p->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state(p->stream)))) { + p->reconnect = true; + p->reconnect_last_tried_seconds = now; + } + + if(p->reconnect && now - p->reconnect_last_tried_seconds >= RECONNECT_TRY_TIMEOUT_SECONDS) { + p->reconnect_last_tried_seconds = now; + // TODO: Size check + snprintf(device_name, device_name_size, "%s", p->device_name); + return true; + } + + return false; +} + +static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, size_t device_name_size, double now) { + int r; + if(!pa_sound_device_should_reconnect(p, now, device_name, device_name_size)) + return true; + + if(p->stream) { + pa_stream_disconnect(p->stream); + pa_stream_unref(p->stream); + p->stream = NULL; + } + + if(!(p->stream = pa_stream_new(p->context, p->stream_name, &p->ss, NULL))) { + //pa_context_errno(p->context); + return false; + } + + r = pa_stream_connect_record(p->stream, device_name, &p->attr, + (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE)); + + if(r < 0) { + //pa_context_errno(p->context); + return false; + } + + pa_mainloop_iterate(p->mainloop, 0, NULL); + + std::lock_guard<std::mutex> lock(p->reconnect_mutex); + p->reconnect = false; + return true; +} + +static int pa_sound_device_read(pa_handle *p, double timeout_seconds) { + assert(p); + + const double start_time = clock_get_monotonic_seconds(); + char device_name[DEVICE_NAME_MAX_SIZE]; + + bool success = false; + int r = 0; + int *rerror = &r; + pa_usec_t latency = 0; + int negative = 0; + + pa_mainloop_iterate(p->mainloop, 0, NULL); + + if(!pa_sound_device_handle_reconnect(p, device_name, sizeof(device_name), start_time) || !p->stream) + goto fail; + + if(pa_stream_get_state(p->stream) != PA_STREAM_READY) + goto fail; + + CHECK_DEAD_GOTO(p, rerror, fail); + + while (p->output_index < p->output_length) { + if(clock_get_monotonic_seconds() - start_time >= timeout_seconds) + return -1; + + if(!p->read_data) { + pa_mainloop_prepare(p->mainloop, 1 * 1000); // 1 ms + pa_mainloop_poll(p->mainloop); + pa_mainloop_dispatch(p->mainloop); + + if(pa_stream_peek(p->stream, &p->read_data, &p->read_length) < 0) + goto fail; + + if(!p->read_data && p->read_length == 0) + continue; + + if(!p->read_data && p->read_length > 0) { + // There is a hole in the stream :( drop it. Maybe we should generate silence instead? TODO + if(pa_stream_drop(p->stream) != 0) + goto fail; + continue; + } + + if(p->read_length <= 0) { + p->read_data = NULL; + if(pa_stream_drop(p->stream) != 0) + goto fail; + + CHECK_DEAD_GOTO(p, rerror, fail); + continue; + } + + pa_operation_unref(pa_stream_update_timing_info(p->stream, NULL, NULL)); + // TODO: Deal with one pa_stream_peek not being enough. In that case we need to add multiple of these together(?) + if(pa_stream_get_latency(p->stream, &latency, &negative) >= 0) { + p->latency_seconds = negative ? -(double)latency : latency; + if(p->latency_seconds < 0.0) + p->latency_seconds = 0.0; + p->latency_seconds *= 0.0000001; + } + } + + const size_t space_free_in_output_buffer = p->output_length - p->output_index; + if(space_free_in_output_buffer < p->read_length) { + memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, space_free_in_output_buffer); + p->output_index = 0; + p->read_index += space_free_in_output_buffer; + p->read_length -= space_free_in_output_buffer; + break; + } else { + memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, p->read_length); + p->output_index += p->read_length; + p->read_data = NULL; + p->read_length = 0; + p->read_index = 0; + + if(pa_stream_drop(p->stream) != 0) + goto fail; + + if(p->output_index == p->output_length) { + p->output_index = 0; + break; + } + } + } + + success = true; + + fail: + return success ? 0 : -1; +} + +static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) { + switch(audio_format) { + case S16: return PA_SAMPLE_S16LE; + case S32: return PA_SAMPLE_S32LE; + case F32: return PA_SAMPLE_FLOAT32LE; + } + assert(false); + return PA_SAMPLE_S16LE; +} + +static int audio_format_to_get_bytes_per_sample(AudioFormat audio_format) { + switch(audio_format) { + case S16: return 2; + case S32: return 4; + case F32: return 4; + } + assert(false); + return 2; +} + +int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format) { + pa_sample_spec ss; + ss.format = audio_format_to_pulse_audio_format(audio_format); + ss.rate = 48000; + ss.channels = num_channels; + + pa_buffer_attr buffer_attr; + buffer_attr.fragsize = period_frame_size * audio_format_to_get_bytes_per_sample(audio_format) * num_channels; // 2/4 bytes/sample, @num_channels channels + buffer_attr.tlength = -1; + buffer_attr.prebuf = -1; + buffer_attr.minreq = -1; + buffer_attr.maxlength = buffer_attr.fragsize; + + int error = 0; + pa_handle *handle = pa_sound_device_new(nullptr, node_name, device_name, description, &ss, &buffer_attr, &error); + if(!handle) { + fprintf(stderr, "gsr error: pa_sound_device_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), device_name); + return -1; + } + + device->handle = handle; + device->frames = period_frame_size; + return 0; +} + +void sound_device_close(SoundDevice *device) { + if(device->handle) + pa_sound_device_free((pa_handle*)device->handle); + device->handle = NULL; +} + +int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) { + pa_handle *pa = (pa_handle*)device->handle; + if(pa_sound_device_read(pa, timeout_sec) < 0) { + //fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error)); + *latency_seconds = 0.0; + return -1; + } + *buffer = pa->output_data; + *latency_seconds = pa->latency_seconds; + return device->frames; +} + +static void pa_state_cb(pa_context *c, void *userdata) { + pa_context_state state = pa_context_get_state(c); + int *pa_ready = (int*)userdata; + switch(state) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + *pa_ready = 2; + break; + case PA_CONTEXT_READY: + *pa_ready = 1; + break; + } +} + +static void pa_sourcelist_cb(pa_context*, const pa_source_info *source_info, int eol, void *userdata) { + if(eol > 0) + return; + + AudioDevices *audio_devices = (AudioDevices*)userdata; + audio_devices->audio_inputs.push_back({ source_info->name, source_info->description }); +} + +static void pa_server_info_cb(pa_context*, const pa_server_info *server_info, void *userdata) { + AudioDevices *audio_devices = (AudioDevices*)userdata; + if(server_info->default_sink_name) + audio_devices->default_output = std::string(server_info->default_sink_name) + ".monitor"; + if(server_info->default_source_name) + audio_devices->default_input = server_info->default_source_name; +} + +static void server_info_callback(pa_context*, const pa_server_info *server_info, void *userdata) { + bool *is_server_pipewire = (bool*)userdata; + if(server_info->server_name && strstr(server_info->server_name, "PipeWire")) + *is_server_pipewire = true; +} + +static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) { + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, &audio_devices); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); +} + +AudioDevices get_pulseaudio_inputs() { + AudioDevices audio_devices; + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + + // TODO: Do this in the same connection below instead of two separate connections + get_pulseaudio_default_inputs(audio_devices); + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return audio_devices; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &audio_devices); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); + return audio_devices; +} + +bool pulseaudio_server_is_pipewire() { + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + bool is_server_pipewire = false; + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return is_server_pipewire; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_server_info(ctx, server_info_callback, &is_server_pipewire); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); + return is_server_pipewire; +} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..911fcf4 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,677 @@ +#include "../include/utils.h" +#include "../include/window/window.h" +#include "../include/capture/capture.h" + +#include <time.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/random.h> +#include <errno.h> +#include <assert.h> + +#include <xf86drmMode.h> +#include <xf86drm.h> +#include <X11/Xatom.h> +#include <X11/extensions/Xrandr.h> +#include <libavcodec/avcodec.h> +#include <libavutil/hwcontext_vaapi.h> + +#define DRM_NUM_BUF_ATTRS 4 + +double clock_get_monotonic_seconds(void) { + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 0; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001; +} + +bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { + /* TODO: Use other functions on other platforms than linux */ + if(getrandom(buffer, buffer_size, 0) < buffer_size) { + fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + return false; + } + + for(int i = 0; i < buffer_size; ++i) { + unsigned char c = *(unsigned char*)&buffer[i]; + buffer[i] = alphabet[c % alphabet_size]; + } + + return true; +} + +bool generate_random_characters_standard_alphabet(char *buffer, int buffer_size) { + return generate_random_characters(buffer, buffer_size, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62); +} + +static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) { + for(int i = 0; i < sr->nmode; ++i) { + if(sr->modes[i].id == id) + return &sr->modes[i]; + } + return NULL; +} + +static gsr_monitor_rotation x11_rotation_to_gsr_rotation(int rot) { + switch(rot) { + case RR_Rotate_0: return GSR_MONITOR_ROT_0; + case RR_Rotate_90: return GSR_MONITOR_ROT_90; + case RR_Rotate_180: return GSR_MONITOR_ROT_180; + case RR_Rotate_270: return GSR_MONITOR_ROT_270; + } + return GSR_MONITOR_ROT_0; +} + +static uint32_t x11_output_get_connector_id(Display *dpy, RROutput output, Atom randr_connector_id_atom) { + Atom type = 0; + int format = 0; + unsigned long bytes_after = 0; + unsigned long nitems = 0; + unsigned char *prop = NULL; + XRRGetOutputProperty(dpy, output, randr_connector_id_atom, 0, 128, false, false, AnyPropertyType, &type, &format, &nitems, &bytes_after, &prop); + + long result = 0; + if(type == XA_INTEGER && format == 32) + result = *(long*)prop; + + free(prop); + return result; +} + +static vec2i get_monitor_size_rotated(int width, int height, gsr_monitor_rotation rotation) { + vec2i size = { .x = width, .y = height }; + if(rotation == GSR_MONITOR_ROT_90 || rotation == GSR_MONITOR_ROT_270) { + int tmp_x = size.x; + size.x = size.y; + size.y = tmp_x; + } + return size; +} + +void for_each_active_monitor_output_x11_not_cached(Display *display, active_monitor_callback callback, void *userdata) { + XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); + if(!screen_res) + return; + + const Atom randr_connector_id_atom = XInternAtom(display, "CONNECTOR_ID", False); + + char display_name[256]; + for(int i = 0; i < screen_res->noutput; ++i) { + XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); + if(out_info && out_info->crtc && out_info->connection == RR_Connected) { + XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); + if(crt_info && crt_info->mode) { + // We want to use the current mode info width/height (mode_info->width/height) instead of crtc info width/height (crt_info->width/height) because crtc info + // is scaled if the monitor is scaled (xrandr --output DP-1 --scale 1.5). Normally this is not an issue for x11 applications, + // but gpu screen recorder captures the drm framebuffer instead of x11 api. This drm framebuffer which doesn't increase in size when using xrandr scaling. + // Maybe a better option would be to get the drm crtc size instead. + const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); + if(mode_info) { + snprintf(display_name, sizeof(display_name), "%.*s", (int)out_info->nameLen, out_info->name); + const gsr_monitor_rotation rotation = x11_rotation_to_gsr_rotation(crt_info->rotation); + const vec2i monitor_size = get_monitor_size_rotated(mode_info->width, mode_info->height, rotation); + + const gsr_monitor monitor = { + .name = display_name, + .name_len = out_info->nameLen, + .pos = { .x = crt_info->x, .y = crt_info->y }, + .size = monitor_size, + .connector_id = x11_output_get_connector_id(display, screen_res->outputs[i], randr_connector_id_atom), + .rotation = rotation, + .monitor_identifier = out_info->crtc + }; + callback(&monitor, userdata); + } + } + if(crt_info) + XRRFreeCrtcInfo(crt_info); + } + if(out_info) + XRRFreeOutputInfo(out_info); + } + + XRRFreeScreenResources(screen_res); +} + +/* TODO: Support more connector types */ +int get_connector_type_by_name(const char *name) { + int len = strlen(name); + if(len >= 5 && strncmp(name, "HDMI-", 5) == 0) + return 1; + else if(len >= 3 && strncmp(name, "DP-", 3) == 0) + return 2; + else if(len >= 12 && strncmp(name, "DisplayPort-", 12) == 0) + return 3; + else if(len >= 4 && strncmp(name, "eDP-", 4) == 0) + return 4; + else if(len >= 4 && strncmp(name, "DVI-", 4) == 0) + return 5; + else + return -1; +} + +int get_connector_type_id_by_name(const char *name) { + int len = strlen(name); + int num_start = 0; + for(int i = len - 1; i >= 0; --i) { + const bool is_num = name[i] >= '0' && name[i] <= '9'; + if(!is_num) { + num_start = i + 1; + break; + } + } + + const int num_len = len - num_start; + if(num_len <= 0) + return -1; + + return atoi(name + num_start); +} + +uint32_t monitor_identifier_from_type_and_count(int monitor_type_index, int monitor_type_count) { + return ((uint32_t)monitor_type_index << 16) | ((uint32_t)monitor_type_count); +} + +static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, const char *name, uint64_t *result) { + for(int i = 0; i < props->count_props; ++i) { + drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]); + if(prop) { + if(strcmp(name, prop->name) == 0) { + *result = props->prop_values[i]; + drmModeFreeProperty(prop); + return true; + } + drmModeFreeProperty(prop); + } + } + return false; +} + +static void for_each_active_monitor_output_drm(const char *card_path, active_monitor_callback callback, void *userdata) { + int fd = open(card_path, O_RDONLY); + if(fd == -1) { + fprintf(stderr, "gsr error: for_each_active_monitor_output_drm failed, failed to open \"%s\", error: %s\n", card_path, strerror(errno)); + return; + } + + drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1); + + char display_name[256]; + drmModeResPtr resources = drmModeGetResources(fd); + if(resources) { + for(int i = 0; i < resources->count_connectors; ++i) { + drmModeConnectorPtr connector = drmModeGetConnectorCurrent(fd, resources->connectors[i]); + if(!connector) + continue; + + if(connector->connection != DRM_MODE_CONNECTED) { + drmModeFreeConnector(connector); + continue; + } + + uint64_t crtc_id = 0; + connector_get_property_by_name(fd, connector, "CRTC_ID", &crtc_id); + + drmModeCrtcPtr crtc = drmModeGetCrtc(fd, crtc_id); + const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type); + + if(connection_name && crtc_id > 0 && crtc) { + const int connector_type_index_name = get_connector_type_by_name(display_name); + const int display_name_len = snprintf(display_name, sizeof(display_name), "%s-%u", connection_name, connector->connector_type_id); + + const gsr_monitor monitor = { + .name = display_name, + .name_len = display_name_len, + .pos = { .x = crtc->x, .y = crtc->y }, + .size = { .x = (int)crtc->width, .y = (int)crtc->height }, + .connector_id = connector->connector_id, + .rotation = GSR_MONITOR_ROT_0, + .monitor_identifier = connector_type_index_name != -1 ? monitor_identifier_from_type_and_count(connector_type_index_name, connector->connector_type_id) : 0 + }; + callback(&monitor, userdata); + } + + if(crtc) + drmModeFreeCrtc(crtc); + + drmModeFreeConnector(connector); + } + drmModeFreeResources(resources); + } + + close(fd); +} + +void for_each_active_monitor_output(const gsr_window *window, const char *card_path, gsr_connection_type connection_type, active_monitor_callback callback, void *userdata) { + switch(connection_type) { + case GSR_CONNECTION_X11: + case GSR_CONNECTION_WAYLAND: + gsr_window_for_each_active_monitor_output_cached(window, callback, userdata); + break; + case GSR_CONNECTION_DRM: + for_each_active_monitor_output_drm(card_path, callback, userdata); + break; + } +} + +static void get_monitor_by_name_callback(const gsr_monitor *monitor, void *userdata) { + get_monitor_by_name_userdata *data = (get_monitor_by_name_userdata*)userdata; + if(!data->found_monitor && strcmp(data->name, monitor->name) == 0) { + data->monitor->pos = monitor->pos; + data->monitor->size = monitor->size; + data->monitor->connector_id = monitor->connector_id; + data->monitor->rotation = monitor->rotation; + data->monitor->monitor_identifier = monitor->monitor_identifier; + data->found_monitor = true; + } +} + +bool get_monitor_by_name(const gsr_egl *egl, gsr_connection_type connection_type, const char *name, gsr_monitor *monitor) { + get_monitor_by_name_userdata userdata; + userdata.name = name; + userdata.name_len = strlen(name); + userdata.monitor = monitor; + userdata.found_monitor = false; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_name_callback, &userdata); + return userdata.found_monitor; +} + +typedef struct { + const gsr_monitor *monitor; + gsr_monitor_rotation rotation; + vec2i position; + bool match_found; +} get_monitor_by_connector_id_userdata; + +static void get_monitor_by_name_wayland_callback(const gsr_monitor *monitor, void *userdata) { + get_monitor_by_connector_id_userdata *data = (get_monitor_by_connector_id_userdata*)userdata; + if(monitor->name && data->monitor->name && strcmp(monitor->name, data->monitor->name) == 0) { + data->rotation = monitor->rotation; + data->position = monitor->pos; + data->match_found = true; + } +} + +static void get_monitor_by_connector_id_callback(const gsr_monitor *monitor, void *userdata) { + get_monitor_by_connector_id_userdata *data = (get_monitor_by_connector_id_userdata*)userdata; + if(monitor->connector_id == data->monitor->connector_id || + (!monitor->connector_id && monitor->monitor_identifier == data->monitor->monitor_identifier)) + { + data->rotation = monitor->rotation; + data->position = monitor->pos; + data->match_found = true; + } +} + +bool drm_monitor_get_display_server_data(const gsr_window *window, const gsr_monitor *monitor, gsr_monitor_rotation *monitor_rotation, vec2i *monitor_position) { + *monitor_rotation = GSR_MONITOR_ROT_0; + *monitor_position = (vec2i){0, 0}; + + if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND) { + { + get_monitor_by_connector_id_userdata userdata; + userdata.monitor = monitor; + userdata.rotation = GSR_MONITOR_ROT_0; + userdata.position = (vec2i){0, 0}; + userdata.match_found = false; + gsr_window_for_each_active_monitor_output_cached(window, get_monitor_by_name_wayland_callback, &userdata); + if(userdata.match_found) { + *monitor_rotation = userdata.rotation; + *monitor_position = userdata.position; + return true; + } + } + { + get_monitor_by_connector_id_userdata userdata; + userdata.monitor = monitor; + userdata.rotation = GSR_MONITOR_ROT_0; + userdata.position = (vec2i){0, 0}; + userdata.match_found = false; + gsr_window_for_each_active_monitor_output_cached(window, get_monitor_by_connector_id_callback, &userdata); + *monitor_rotation = userdata.rotation; + *monitor_position = userdata.position; + return userdata.match_found; + } + } else { + get_monitor_by_connector_id_userdata userdata; + userdata.monitor = monitor; + userdata.rotation = GSR_MONITOR_ROT_0; + userdata.position = (vec2i){0, 0}; + userdata.match_found = false; + gsr_window_for_each_active_monitor_output_cached(window, get_monitor_by_connector_id_callback, &userdata); + *monitor_rotation = userdata.rotation; + *monitor_position = userdata.position; + return userdata.match_found; + } +} + +bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { + const char *software_renderers[] = { "llvmpipe", "SWR", "softpipe", NULL }; + bool supported = true; + const unsigned char *gl_vendor = egl->glGetString(GL_VENDOR); + const unsigned char *gl_renderer = egl->glGetString(GL_RENDERER); + + info->gpu_version = 0; + info->is_steam_deck = false; + + if(!gl_vendor) { + fprintf(stderr, "gsr error: failed to get gpu vendor\n"); + supported = false; + goto end; + } + + if(gl_renderer) { + for(int i = 0; software_renderers[i]; ++i) { + if(strstr((const char*)gl_renderer, software_renderers[i])) { + fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]); + supported = false; + goto end; + } + } + } + + if(strstr((const char*)gl_vendor, "AMD")) + info->vendor = GSR_GPU_VENDOR_AMD; + else if(strstr((const char*)gl_vendor, "Mesa") && gl_renderer && strstr((const char*)gl_renderer, "AMD")) + info->vendor = GSR_GPU_VENDOR_AMD; + else if(strstr((const char*)gl_vendor, "Intel")) + info->vendor = GSR_GPU_VENDOR_INTEL; + else if(strstr((const char*)gl_vendor, "NVIDIA")) + info->vendor = GSR_GPU_VENDOR_NVIDIA; + else if(strstr((const char*)gl_vendor, "Broadcom")) + info->vendor = GSR_GPU_VENDOR_BROADCOM; + else { + fprintf(stderr, "gsr error: unknown gpu vendor: %s\n", gl_vendor); + supported = false; + goto end; + } + + if(gl_renderer) { + if(info->vendor == GSR_GPU_VENDOR_NVIDIA) + sscanf((const char*)gl_renderer, "%*s %*s %*s %d", &info->gpu_version); + info->is_steam_deck = strstr((const char*)gl_renderer, "vangogh") != NULL; + } + + end: + return supported; +} + +bool try_card_has_valid_plane(const char *card_path) { + drmVersion *ver = NULL; + drmModePlaneResPtr planes = NULL; + bool found_screen_card = false; + + int fd = open(card_path, O_RDONLY); + if(fd == -1) + return false; + + ver = drmGetVersion(fd); + if(!ver || strstr(ver->name, "nouveau")) + goto next; + + drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + + planes = drmModeGetPlaneResources(fd); + if(!planes) + goto next; + + for(uint32_t j = 0; j < planes->count_planes; ++j) { + drmModePlanePtr plane = drmModeGetPlane(fd, planes->planes[j]); + if(!plane) + continue; + + if(plane->fb_id) + found_screen_card = true; + + drmModeFreePlane(plane); + if(found_screen_card) + break; + } + + next: + if(planes) + drmModeFreePlaneResources(planes); + if(ver) + drmFreeVersion(ver); + close(fd); + if(found_screen_card) + return true; + + return false; +} + +bool gsr_get_valid_card_path(gsr_egl *egl, char *output, bool is_monitor_capture) { + if(egl->dri_card_path) { + snprintf(output, 128, "%s", egl->dri_card_path); + return is_monitor_capture ? try_card_has_valid_plane(output) : true; + } + + for(int i = 0; i < 10; ++i) { + snprintf(output, 128, DRM_DEV_NAME, DRM_DIR_NAME, i); + if(try_card_has_valid_plane(output)) + return true; + } + return false; +} + +bool gsr_card_path_get_render_path(const char *card_path, char *render_path) { + int fd = open(card_path, O_RDONLY); + if(fd == -1) + return false; + + char *render_path_tmp = drmGetRenderDeviceNameFromFd(fd); + if(render_path_tmp) { + snprintf(render_path, 128, "%s", render_path_tmp); + free(render_path_tmp); + close(fd); + return true; + } + + close(fd); + return false; +} + +int create_directory_recursive(char *path) { + int path_len = strlen(path); + char *p = path; + char *end = path + path_len; + for(;;) { + char *slash_p = strchr(p, '/'); + + // Skips first '/', we don't want to try and create the root directory + if(slash_p == path) { + ++p; + continue; + } + + if(!slash_p) + slash_p = end; + + char prev_char = *slash_p; + *slash_p = '\0'; + int err = mkdir(path, S_IRWXU); + *slash_p = prev_char; + + if(err == -1 && errno != EEXIST) + return err; + + if(slash_p == end) + break; + else + p = slash_p + 1; + } + return 0; +} + +void setup_dma_buf_attrs(intptr_t *img_attr, uint32_t format, uint32_t width, uint32_t height, const int *fds, const uint32_t *offsets, const uint32_t *pitches, const uint64_t *modifiers, int num_planes, bool use_modifier) { + const uint32_t plane_fd_attrs[DRM_NUM_BUF_ATTRS] = { + EGL_DMA_BUF_PLANE0_FD_EXT, + EGL_DMA_BUF_PLANE1_FD_EXT, + EGL_DMA_BUF_PLANE2_FD_EXT, + EGL_DMA_BUF_PLANE3_FD_EXT + }; + + const uint32_t plane_offset_attrs[DRM_NUM_BUF_ATTRS] = { + EGL_DMA_BUF_PLANE0_OFFSET_EXT, + EGL_DMA_BUF_PLANE1_OFFSET_EXT, + EGL_DMA_BUF_PLANE2_OFFSET_EXT, + EGL_DMA_BUF_PLANE3_OFFSET_EXT + }; + + const uint32_t plane_pitch_attrs[DRM_NUM_BUF_ATTRS] = { + EGL_DMA_BUF_PLANE0_PITCH_EXT, + EGL_DMA_BUF_PLANE1_PITCH_EXT, + EGL_DMA_BUF_PLANE2_PITCH_EXT, + EGL_DMA_BUF_PLANE3_PITCH_EXT + }; + + const uint32_t plane_modifier_lo_attrs[DRM_NUM_BUF_ATTRS] = { + EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT + }; + + const uint32_t plane_modifier_hi_attrs[DRM_NUM_BUF_ATTRS] = { + EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, + EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, + EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, + EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT + }; + + size_t img_attr_index = 0; + + img_attr[img_attr_index++] = EGL_LINUX_DRM_FOURCC_EXT; + img_attr[img_attr_index++] = format; + + img_attr[img_attr_index++] = EGL_WIDTH; + img_attr[img_attr_index++] = width; + + img_attr[img_attr_index++] = EGL_HEIGHT; + img_attr[img_attr_index++] = height; + + assert(num_planes <= DRM_NUM_BUF_ATTRS); + for(int i = 0; i < num_planes; ++i) { + img_attr[img_attr_index++] = plane_fd_attrs[i]; + img_attr[img_attr_index++] = fds[i]; + + img_attr[img_attr_index++] = plane_offset_attrs[i]; + img_attr[img_attr_index++] = offsets[i]; + + img_attr[img_attr_index++] = plane_pitch_attrs[i]; + img_attr[img_attr_index++] = pitches[i]; + + if(use_modifier) { + img_attr[img_attr_index++] = plane_modifier_lo_attrs[i]; + img_attr[img_attr_index++] = modifiers[i] & 0xFFFFFFFFULL; + + img_attr[img_attr_index++] = plane_modifier_hi_attrs[i]; + img_attr[img_attr_index++] = modifiers[i] >> 32ULL; + } + } + + img_attr[img_attr_index++] = EGL_NONE; + assert(img_attr_index <= 44); +} + +vec2i scale_keep_aspect_ratio(vec2i from, vec2i to) { + if(from.x == 0 || from.y == 0) + return (vec2i){0, 0}; + + const double height_to_width_ratio = (double)from.y / (double)from.x; + from.x = to.x; + from.y = from.x * height_to_width_ratio; + + if(from.y > to.y) { + const double width_height_ratio = (double)from.x / (double)from.y; + from.y = to.y; + from.x = from.y * width_height_ratio; + } + + return from; +} + +vec2i gsr_capture_get_target_position(vec2i output_size, gsr_capture_metadata *capture_metadata) { + vec2i target_pos = {0, 0}; + + switch(capture_metadata->halign) { + case GSR_CAPTURE_ALIGN_START: + break; + case GSR_CAPTURE_ALIGN_CENTER: + target_pos.x = capture_metadata->video_size.x/2 - output_size.x/2; + break; + case GSR_CAPTURE_ALIGN_END: + target_pos.x = capture_metadata->video_size.x - output_size.x; + break; + } + + switch(capture_metadata->valign) { + case GSR_CAPTURE_ALIGN_START: + break; + case GSR_CAPTURE_ALIGN_CENTER: + target_pos.y = capture_metadata->video_size.y/2 - output_size.y/2; + break; + case GSR_CAPTURE_ALIGN_END: + target_pos.y = capture_metadata->video_size.y - output_size.y; + break; + } + + target_pos.x += capture_metadata->position.x; + target_pos.y += capture_metadata->position.y; + return target_pos; +} + + +unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal_format, unsigned int format, int filter) { + unsigned int texture_id = 0; + egl->glGenTextures(1, &texture_id); + egl->glBindTexture(GL_TEXTURE_2D, texture_id); + //egl->glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, NULL); + // Needed for hevc_10bit for nvenc (cuGraphicsGLRegisterImage) + egl->glTexStorage2D(GL_TEXTURE_2D, 1, internal_format, width, height); + + egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); + egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + + egl->glBindTexture(GL_TEXTURE_2D, 0); + return texture_id; +} + +/* TODO: Test with optimus and open kernel modules */ +bool get_nvidia_driver_version(int *major, int *minor) { + *major = 0; + *minor = 0; + + FILE *f = fopen("/proc/driver/nvidia/version", "rb"); + if(!f) { + fprintf(stderr, "gsr warning: failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)\n"); + return false; + } + + char buffer[2048]; + size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, f); + buffer[bytes_read] = '\0'; + + bool success = false; + const char *p = strstr(buffer, "Kernel Module"); + if(p) { + p += 13; + int driver_major_version = 0, driver_minor_version = 0; + if(sscanf(p, "%d.%d", &driver_major_version, &driver_minor_version) == 2) { + *major = driver_major_version; + *minor = driver_minor_version; + success = true; + } + } + + if(!success) + fprintf(stderr, "gsr warning: failed to get nvidia driver version\n"); + + fclose(f); + return success; +} diff --git a/src/window/wayland.c b/src/window/wayland.c new file mode 100644 index 0000000..037c85f --- /dev/null +++ b/src/window/wayland.c @@ -0,0 +1,391 @@ +#include "../../include/window/wayland.h" + +#include "../../include/vec2.h" +#include "../../include/defs.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <wayland-client.h> +#include <wayland-egl.h> +#include "xdg-output-unstable-v1-client-protocol.h" + +#define GSR_MAX_OUTPUTS 32 + +typedef struct gsr_window_wayland gsr_window_wayland; + +typedef struct { + uint32_t wl_name; + struct wl_output *output; + struct zxdg_output_v1 *xdg_output; + vec2i pos; + vec2i size; + int32_t transform; + char *name; +} gsr_wayland_output; + +struct gsr_window_wayland { + struct wl_display *display; + struct wl_egl_window *window; + struct wl_registry *registry; + struct wl_surface *surface; + struct wl_compositor *compositor; + gsr_wayland_output outputs[GSR_MAX_OUTPUTS]; + int num_outputs; + struct zxdg_output_manager_v1 *xdg_output_manager; +}; + +static void output_handle_geometry(void *data, struct wl_output *wl_output, + int32_t x, int32_t y, int32_t phys_width, int32_t phys_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) { + (void)wl_output; + (void)phys_width; + (void)phys_height; + (void)subpixel; + (void)make; + (void)model; + gsr_wayland_output *gsr_output = data; + gsr_output->pos.x = x; + gsr_output->pos.y = y; + gsr_output->transform = transform; +} + +static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + (void)wl_output; + (void)flags; + (void)refresh; + gsr_wayland_output *gsr_output = data; + gsr_output->size.x = width; + gsr_output->size.y = height; +} + +static void output_handle_done(void *data, struct wl_output *wl_output) { + (void)data; + (void)wl_output; +} + +static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) { + (void)data; + (void)wl_output; + (void)factor; +} + +static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) { + (void)wl_output; + gsr_wayland_output *gsr_output = data; + if(gsr_output->name) { + free(gsr_output->name); + gsr_output->name = NULL; + } + gsr_output->name = strdup(name); +} + +static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) { + (void)data; + (void)wl_output; + (void)description; +} + +static const struct wl_output_listener output_listener = { + .geometry = output_handle_geometry, + .mode = output_handle_mode, + .done = output_handle_done, + .scale = output_handle_scale, + .name = output_handle_name, + .description = output_handle_description, +}; + +static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + (void)version; + gsr_window_wayland *window_wayland = data; + if(strcmp(interface, "wl_compositor") == 0) { + if(window_wayland->compositor) + return; + + window_wayland->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1); + } else if(strcmp(interface, wl_output_interface.name) == 0) { + if(version < 4) { + fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor\n"); + return; + } + + if(window_wayland->num_outputs == GSR_MAX_OUTPUTS) { + fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %u\n", GSR_MAX_OUTPUTS, name); + return; + } + + gsr_wayland_output *gsr_output = &window_wayland->outputs[window_wayland->num_outputs]; + window_wayland->num_outputs++; + *gsr_output = (gsr_wayland_output) { + .wl_name = name, + .output = wl_registry_bind(registry, name, &wl_output_interface, 4), + .pos = { .x = 0, .y = 0 }, + .size = { .x = 0, .y = 0 }, + .transform = 0, + .name = NULL, + }; + wl_output_add_listener(gsr_output->output, &output_listener, gsr_output); + } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + if(version < 1) { + fprintf(stderr, "gsr warning: xdg output interface version is < 1, expected >= 1 to capture a monitor\n"); + return; + } + + if(window_wayland->xdg_output_manager) + return; + + window_wayland->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1); + } +} + +static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) { + (void)data; + (void)registry; + (void)name; + // TODO: Remove output +} + +static struct wl_registry_listener registry_listener = { + .global = registry_add_object, + .global_remove = registry_remove_object, +}; + +static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) { + (void)zxdg_output_v1; + gsr_wayland_output *gsr_xdg_output = data; + gsr_xdg_output->pos.x = x; + gsr_xdg_output->pos.y = y; +} + +static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { + (void)data; + (void)xdg_output; + (void)width; + (void)height; +} + +static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { + (void)data; + (void)xdg_output; +} + +static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { + (void)data; + (void)xdg_output; + (void)name; +} + +static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { + (void)data; + (void)xdg_output; + (void)description; +} + +static const struct zxdg_output_v1_listener xdg_output_listener = { + .logical_position = xdg_output_logical_position, + .logical_size = xdg_output_handle_logical_size, + .done = xdg_output_handle_done, + .name = xdg_output_handle_name, + .description = xdg_output_handle_description, +}; + +static void gsr_window_wayland_set_monitor_outputs_from_xdg_output(gsr_window_wayland *self) { + if(!self->xdg_output_manager) { + fprintf(stderr, "gsr warning: zxdg_output_manager not found. registered monitor positions might be incorrect\n"); + return; + } + + for(int i = 0; i < self->num_outputs; ++i) { + self->outputs[i].xdg_output = zxdg_output_manager_v1_get_xdg_output(self->xdg_output_manager, self->outputs[i].output); + zxdg_output_v1_add_listener(self->outputs[i].xdg_output, &xdg_output_listener, &self->outputs[i]); + } + + // Fetch xdg_output + wl_display_roundtrip(self->display); +} + +static void gsr_window_wayland_deinit(gsr_window_wayland *self) { + if(self->window) { + wl_egl_window_destroy(self->window); + self->window = NULL; + } + + if(self->surface) { + wl_surface_destroy(self->surface); + self->surface = NULL; + } + + for(int i = 0; i < self->num_outputs; ++i) { + if(self->outputs[i].output) { + wl_output_destroy(self->outputs[i].output); + self->outputs[i].output = NULL; + } + + if(self->outputs[i].name) { + free(self->outputs[i].name); + self->outputs[i].name = NULL; + } + + if(self->outputs[i].xdg_output) { + zxdg_output_v1_destroy(self->outputs[i].xdg_output); + self->outputs[i].output = NULL; + } + } + self->num_outputs = 0; + + if(self->xdg_output_manager) { + zxdg_output_manager_v1_destroy(self->xdg_output_manager); + self->xdg_output_manager = NULL; + } + + if(self->compositor) { + wl_compositor_destroy(self->compositor); + self->compositor = NULL; + } + + if(self->registry) { + wl_registry_destroy(self->registry); + self->registry = NULL; + } + + if(self->display) { + wl_display_disconnect(self->display); + self->display = NULL; + } +} + +static bool gsr_window_wayland_init(gsr_window_wayland *self) { + self->display = wl_display_connect(NULL); + if(!self->display) { + fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to connect to the Wayland server\n"); + goto fail; + } + + self->registry = wl_display_get_registry(self->display); // TODO: Error checking + wl_registry_add_listener(self->registry, ®istry_listener, self); // TODO: Error checking + + // Fetch globals + wl_display_roundtrip(self->display); + + // Fetch wl_output + wl_display_roundtrip(self->display); + + gsr_window_wayland_set_monitor_outputs_from_xdg_output(self); + + if(!self->compositor) { + fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to find compositor\n"); + goto fail; + } + + self->surface = wl_compositor_create_surface(self->compositor); + if(!self->surface) { + fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create surface\n"); + goto fail; + } + + self->window = wl_egl_window_create(self->surface, 16, 16); + if(!self->window) { + fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create window\n"); + goto fail; + } + + return true; + + fail: + gsr_window_wayland_deinit(self); + return false; +} + +static void gsr_window_wayland_destroy(gsr_window *window) { + gsr_window_wayland *self = window->priv; + gsr_window_wayland_deinit(self); + free(self); + free(window); +} + +static bool gsr_window_wayland_process_event(gsr_window *window) { + gsr_window_wayland *self = window->priv; + // TODO: pselect on wl_display_get_fd before doing dispatch + const bool events_available = wl_display_dispatch_pending(self->display) > 0; + wl_display_flush(self->display); + return events_available; +} + +static gsr_display_server gsr_wayland_get_display_server(void) { + return GSR_DISPLAY_SERVER_WAYLAND; +} + +static void* gsr_window_wayland_get_display(gsr_window *window) { + gsr_window_wayland *self = window->priv; + return self->display; +} + +static void* gsr_window_wayland_get_window(gsr_window *window) { + gsr_window_wayland *self = window->priv; + return self->window; +} + +static gsr_monitor_rotation wayland_transform_to_gsr_rotation(int32_t rot) { + switch(rot) { + case 0: return GSR_MONITOR_ROT_0; + case 1: return GSR_MONITOR_ROT_90; + case 2: return GSR_MONITOR_ROT_180; + case 3: return GSR_MONITOR_ROT_270; + } + return GSR_MONITOR_ROT_0; +} + +static void gsr_window_wayland_for_each_active_monitor_output_cached(const gsr_window *window, active_monitor_callback callback, void *userdata) { + const gsr_window_wayland *self = window->priv; + for(int i = 0; i < self->num_outputs; ++i) { + const gsr_wayland_output *output = &self->outputs[i]; + if(!output->name) + continue; + + const int connector_type_index = get_connector_type_by_name(output->name); + const int connector_type_id = get_connector_type_id_by_name(output->name); + const gsr_monitor monitor = { + .name = output->name, + .name_len = strlen(output->name), + .pos = { .x = output->pos.x, .y = output->pos.y }, + .size = { .x = output->size.x, .y = output->size.y }, + .connector_id = 0, + .rotation = wayland_transform_to_gsr_rotation(output->transform), + .monitor_identifier = (connector_type_index != -1 && connector_type_id != -1) ? monitor_identifier_from_type_and_count(connector_type_index, connector_type_id) : 0 + }; + callback(&monitor, userdata); + } +} + +gsr_window* gsr_window_wayland_create(void) { + gsr_window *window = calloc(1, sizeof(gsr_window)); + if(!window) + return window; + + gsr_window_wayland *window_wayland = calloc(1, sizeof(gsr_window_wayland)); + if(!window_wayland) { + free(window); + return NULL; + } + + if(!gsr_window_wayland_init(window_wayland)) { + free(window_wayland); + free(window); + return NULL; + } + + *window = (gsr_window) { + .destroy = gsr_window_wayland_destroy, + .process_event = gsr_window_wayland_process_event, + .get_event_data = NULL, + .get_display_server = gsr_wayland_get_display_server, + .get_display = gsr_window_wayland_get_display, + .get_window = gsr_window_wayland_get_window, + .for_each_active_monitor_output_cached = gsr_window_wayland_for_each_active_monitor_output_cached, + .priv = window_wayland + }; + + return window; +} diff --git a/src/window/window.c b/src/window/window.c new file mode 100644 index 0000000..1c6a24e --- /dev/null +++ b/src/window/window.c @@ -0,0 +1,30 @@ +#include "../../include/window/window.h" +#include <stddef.h> + +void gsr_window_destroy(gsr_window *self); + +bool gsr_window_process_event(gsr_window *self) { + return self->process_event(self); +} + +XEvent* gsr_window_get_event_data(gsr_window *self) { + if(self->get_event_data) + return self->get_event_data(self); + return NULL; +} + +gsr_display_server gsr_window_get_display_server(const gsr_window *self) { + return self->get_display_server(); +} + +void* gsr_window_get_display(gsr_window *self) { + return self->get_display(self); +} + +void* gsr_window_get_window(gsr_window *self) { + return self->get_window(self); +} + +void gsr_window_for_each_active_monitor_output_cached(const gsr_window *self, active_monitor_callback callback, void *userdata) { + self->for_each_active_monitor_output_cached(self, callback, userdata); +} diff --git a/src/window/x11.c b/src/window/x11.c new file mode 100644 index 0000000..964422d --- /dev/null +++ b/src/window/x11.c @@ -0,0 +1,162 @@ +#include "../../include/window/x11.h" + +#include "../../include/vec2.h" +#include "../../include/defs.h" +#include "../../include/utils.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <X11/Xlib.h> + +#define GSR_MAX_OUTPUTS 32 + +typedef struct { + char *name; + vec2i pos; + vec2i size; + uint32_t connector_id; + gsr_monitor_rotation rotation; + uint32_t monitor_identifier; /* crtc id */ +} gsr_x11_output; + +typedef struct { + Display *display; + Window window; + gsr_x11_output outputs[GSR_MAX_OUTPUTS]; + int num_outputs; + XEvent xev; +} gsr_window_x11; + +static void store_x11_monitor(const gsr_monitor *monitor, void *userdata) { + gsr_window_x11 *window_x11 = userdata; + if(window_x11->num_outputs == GSR_MAX_OUTPUTS) { + fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %s\n", GSR_MAX_OUTPUTS, monitor->name); + return; + } + + char *monitor_name = strdup(monitor->name); + if(!monitor_name) + return; + + const int index = window_x11->num_outputs; + window_x11->outputs[index].name = monitor_name; + window_x11->outputs[index].pos = monitor->pos; + window_x11->outputs[index].size = monitor->size; + window_x11->outputs[index].connector_id = monitor->connector_id; + window_x11->outputs[index].rotation = monitor->rotation; + window_x11->outputs[index].monitor_identifier = monitor->monitor_identifier; + ++window_x11->num_outputs; +} + +static void gsr_window_x11_deinit(gsr_window_x11 *self) { + if(self->window) { + XDestroyWindow(self->display, self->window); + self->window = None; + } + + for(int i = 0; i < self->num_outputs; ++i) { + if(self->outputs[i].name) { + free(self->outputs[i].name); + self->outputs[i].name = NULL; + } + } + self->num_outputs = 0; +} + +static bool gsr_window_x11_init(gsr_window_x11 *self) { + self->window = XCreateWindow(self->display, DefaultRootWindow(self->display), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); + if(!self->window) { + fprintf(stderr, "gsr error: gsr_window_x11_init failed: failed to create gl window\n"); + return false; + } + + self->num_outputs = 0; + for_each_active_monitor_output_x11_not_cached(self->display, store_x11_monitor, self); + return true; +} + +static void gsr_window_x11_destroy(gsr_window *window) { + gsr_window_x11 *self = window->priv; + gsr_window_x11_deinit(self); + free(self); + free(window); +} + +static bool gsr_window_x11_process_event(gsr_window *window) { + gsr_window_x11 *self = window->priv; + if(XPending(self->display)) { + XNextEvent(self->display, &self->xev); + return true; + } + return false; +} + +static XEvent* gsr_window_x11_get_event_data(gsr_window *window) { + gsr_window_x11 *self = window->priv; + return &self->xev; +} + +static gsr_display_server gsr_window_x11_get_display_server(void) { + return GSR_DISPLAY_SERVER_X11; +} + +static void* gsr_window_x11_get_display(gsr_window *window) { + gsr_window_x11 *self = window->priv; + return self->display; +} + +static void* gsr_window_x11_get_window(gsr_window *window) { + gsr_window_x11 *self = window->priv; + return (void*)self->window; +} + +static void gsr_window_x11_for_each_active_monitor_output_cached(const gsr_window *window, active_monitor_callback callback, void *userdata) { + const gsr_window_x11 *self = window->priv; + for(int i = 0; i < self->num_outputs; ++i) { + const gsr_x11_output *output = &self->outputs[i]; + const gsr_monitor monitor = { + .name = output->name, + .name_len = strlen(output->name), + .pos = output->pos, + .size = output->size, + .connector_id = output->connector_id, + .rotation = output->rotation, + .monitor_identifier = output->monitor_identifier + }; + callback(&monitor, userdata); + } +} + +gsr_window* gsr_window_x11_create(Display *display) { + gsr_window *window = calloc(1, sizeof(gsr_window)); + if(!window) + return window; + + gsr_window_x11 *window_x11 = calloc(1, sizeof(gsr_window_x11)); + if(!window_x11) { + free(window); + return NULL; + } + + window_x11->display = display; + if(!gsr_window_x11_init(window_x11)) { + free(window_x11); + free(window); + return NULL; + } + + *window = (gsr_window) { + .destroy = gsr_window_x11_destroy, + .process_event = gsr_window_x11_process_event, + .get_event_data = gsr_window_x11_get_event_data, + .get_display_server = gsr_window_x11_get_display_server, + .get_display = gsr_window_x11_get_display, + .get_window = gsr_window_x11_get_window, + .for_each_active_monitor_output_cached = gsr_window_x11_for_each_active_monitor_output_cached, + .priv = window_x11 + }; + + return window; +} diff --git a/src/window_texture.c b/src/window_texture.c new file mode 100644 index 0000000..6b093c5 --- /dev/null +++ b/src/window_texture.c @@ -0,0 +1,134 @@ +#include "../include/window_texture.h" +#include <X11/extensions/Xcomposite.h> + +static int x11_supports_composite_named_window_pixmap(Display *display) { + int extension_major; + int extension_minor; + if(!XCompositeQueryExtension(display, &extension_major, &extension_minor)) + return 0; + + int major_version; + int minor_version; + return XCompositeQueryVersion(display, &major_version, &minor_version) && (major_version > 0 || minor_version >= 2); +} + +int window_texture_init(WindowTexture *window_texture, Display *display, Window window, gsr_egl *egl) { + window_texture->display = display; + window_texture->window = window; + window_texture->pixmap = None; + window_texture->image = NULL; + window_texture->texture_id = 0; + window_texture->redirected = 0; + window_texture->egl = egl; + window_texture->window_width = 0; + window_texture->window_height = 0; + + if(!x11_supports_composite_named_window_pixmap(display)) + return 1; + + XCompositeRedirectWindow(display, window, CompositeRedirectAutomatic); + window_texture->redirected = 1; + return window_texture_on_resize(window_texture); +} + +static void window_texture_cleanup(WindowTexture *self, int delete_texture) { + if(delete_texture && self->texture_id) { + self->egl->glDeleteTextures(1, &self->texture_id); + self->texture_id = 0; + } + + if(self->image) { + self->egl->eglDestroyImage(self->egl->egl_display, self->image); + self->image = NULL; + } + + if(self->pixmap) { + XFreePixmap(self->display, self->pixmap); + self->pixmap = None; + } +} + +void window_texture_deinit(WindowTexture *self) { + if(self->redirected) { + XCompositeUnredirectWindow(self->display, self->window, CompositeRedirectAutomatic); + self->redirected = 0; + } + window_texture_cleanup(self, 1); +} + +int window_texture_on_resize(WindowTexture *self) { + window_texture_cleanup(self, 0); + + int result = 0; + Pixmap pixmap = None; + unsigned int texture_id = 0; + EGLImage image = NULL; + + const intptr_t pixmap_attrs[] = { + EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, + EGL_NONE, + }; + + Window root_window; + int window_x, window_y; + unsigned int window_border, window_depth; + XGetGeometry(self->display, self->window, &root_window, &window_x, &window_y, &self->window_width, &self->window_height, &window_border, &window_depth); + + pixmap = XCompositeNameWindowPixmap(self->display, self->window); + if(!pixmap) { + result = 2; + goto cleanup; + } + + if(self->texture_id == 0) { + self->egl->glGenTextures(1, &texture_id); + if(texture_id == 0) { + result = 3; + goto cleanup; + } + self->egl->glBindTexture(GL_TEXTURE_2D, texture_id); + } else { + self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id); + texture_id = self->texture_id; + } + + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + while(self->egl->glGetError()) {} + while(self->egl->eglGetError() != EGL_SUCCESS) {} + + image = self->egl->eglCreateImage(self->egl->egl_display, NULL, EGL_NATIVE_PIXMAP_KHR, (EGLClientBuffer)pixmap, pixmap_attrs); + if(!image) { + result = 4; + goto cleanup; + } + + self->egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); + if(self->egl->glGetError() != 0 || self->egl->eglGetError() != EGL_SUCCESS) { + result = 5; + goto cleanup; + } + + self->pixmap = pixmap; + self->texture_id = texture_id; + self->image = image; + + cleanup: + self->egl->glBindTexture(GL_TEXTURE_2D, 0); + + if(result != 0) { + if(image) + self->egl->eglDestroyImage(self->egl->egl_display, image); + if(texture_id != 0) + self->egl->glDeleteTextures(1, &texture_id); + if(pixmap) + XFreePixmap(self->display, pixmap); + } + + return result; +} + +unsigned int window_texture_get_opengl_texture_id(WindowTexture *self) { + return self->texture_id; +} diff --git a/src/xnvctrl.c b/src/xnvctrl.c new file mode 100644 index 0000000..af46493 --- /dev/null +++ b/src/xnvctrl.c @@ -0,0 +1,46 @@ +#include "../include/xnvctrl.h" +#include "../include/library_loader.h" +#include <string.h> +#include <stdio.h> +#include <dlfcn.h> + +bool gsr_xnvctrl_load(gsr_xnvctrl *self, Display *display) { + memset(self, 0, sizeof(gsr_xnvctrl)); + self->display = display; + + dlerror(); /* clear */ + void *lib = dlopen("libXNVCtrl.so.0", RTLD_LAZY); + if(!lib) { + fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: failed to load libXNVCtrl.so.0, error: %s\n", dlerror()); + return false; + } + + const dlsym_assign required_dlsym[] = { + { (void**)&self->XNVCTRLQueryExtension, "XNVCTRLQueryExtension" }, + { (void**)&self->XNVCTRLSetTargetAttributeAndGetStatus, "XNVCTRLSetTargetAttributeAndGetStatus" }, + { (void**)&self->XNVCTRLQueryValidTargetAttributeValues, "XNVCTRLQueryValidTargetAttributeValues" }, + { (void**)&self->XNVCTRLQueryTargetStringAttribute, "XNVCTRLQueryTargetStringAttribute" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(lib, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: missing required symbols in libXNVCtrl.so.0\n"); + goto fail; + } + + self->library = lib; + return true; + + fail: + dlclose(lib); + memset(self, 0, sizeof(gsr_xnvctrl)); + return false; +} + +void gsr_xnvctrl_unload(gsr_xnvctrl *self) { + if(self->library) { + dlclose(self->library); + memset(self, 0, sizeof(gsr_xnvctrl)); + } +} |
