summaryrefslogtreecommitdiff
path: root/src/capture
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/capture
Diffstat (limited to 'src/capture')
-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
7 files changed, 2964 insertions, 0 deletions
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;
+}