summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorpants <niklasjo2004@gmail.com>2026-09-21 14:16:59 -0700
committerpants <niklasjo2004@gmail.com>2026-09-21 14:16:59 -0700
commita091c00c00c1d5ae5e3da7ef31b98293c76d479f (patch)
tree62c4b3bc114faac9922873ae48672c46cc2cf7d4 /src
Diffstat (limited to 'src')
-rw-r--r--src/args_parser.c702
-rw-r--r--src/capture/capture.c53
-rw-r--r--src/capture/kms.c738
-rw-r--r--src/capture/nvfbc.c457
-rw-r--r--src/capture/portal.c482
-rw-r--r--src/capture/v4l2.c684
-rw-r--r--src/capture/xcomposite.c324
-rw-r--r--src/capture/ximage.c226
-rw-r--r--src/codec_query/nvenc.c259
-rw-r--r--src/codec_query/vaapi.c236
-rw-r--r--src/codec_query/vulkan.c156
-rw-r--r--src/color_conversion.c948
-rw-r--r--src/cuda.c119
-rw-r--r--src/cursor.c144
-rw-r--r--src/damage.c475
-rw-r--r--src/dbus.c913
-rw-r--r--src/defs.c100
-rw-r--r--src/egl.c526
-rw-r--r--src/encoder/encoder.c155
-rw-r--r--src/encoder/video/nvenc.c241
-rw-r--r--src/encoder/video/software.c129
-rw-r--r--src/encoder/video/vaapi.c347
-rw-r--r--src/encoder/video/video.c28
-rw-r--r--src/encoder/video/vulkan.c315
-rw-r--r--src/image_writer.c83
-rw-r--r--src/library_loader.c34
-rw-r--r--src/main.cpp4629
-rw-r--r--src/overclock.c279
-rw-r--r--src/pipewire_audio.c864
-rw-r--r--src/pipewire_video.c947
-rw-r--r--src/plugins.c138
-rw-r--r--src/replay_buffer/replay_buffer.c91
-rw-r--r--src/replay_buffer/replay_buffer_disk.c437
-rw-r--r--src/replay_buffer/replay_buffer_ram.c256
-rw-r--r--src/shader.c147
-rw-r--r--src/sound.cpp647
-rw-r--r--src/utils.c677
-rw-r--r--src/window/wayland.c391
-rw-r--r--src/window/window.c30
-rw-r--r--src/window/x11.c162
-rw-r--r--src/window_texture.c134
-rw-r--r--src/xnvctrl.c46
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, &param, &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(&params, 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(&params, &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(&params, &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(&params);
+ 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(&params);
+ 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(&params);
+ 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(&params);
+ 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, &registry_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(&params, 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, &registry_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));
+ }
+}