aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/capture/capture.c400
-rw-r--r--src/capture/kms.c369
-rw-r--r--src/capture/kms_cuda.c181
-rw-r--r--src/capture/kms_vaapi.c135
-rw-r--r--src/capture/nvfbc.c535
-rw-r--r--src/capture/xcomposite.c299
-rw-r--r--src/capture/xcomposite_cuda.c155
-rw-r--r--src/capture/xcomposite_vaapi.c109
-rw-r--r--src/color_conversion.c469
-rw-r--r--src/cuda.c117
-rw-r--r--src/cursor.c127
-rw-r--r--src/egl.c651
-rw-r--r--src/library_loader.c34
-rw-r--r--src/main.cpp3217
-rw-r--r--src/overclock.c281
-rw-r--r--src/shader.c143
-rw-r--r--src/sound.cpp455
-rw-r--r--src/utils.c450
-rw-r--r--src/window_texture.c123
-rw-r--r--src/xnvctrl.c46
20 files changed, 7182 insertions, 1114 deletions
diff --git a/src/capture/capture.c b/src/capture/capture.c
new file mode 100644
index 0000000..cec0b0d
--- /dev/null
+++ b/src/capture/capture.c
@@ -0,0 +1,400 @@
+#include "../../include/capture/capture.h"
+#include "../../include/egl.h"
+#include "../../include/cuda.h"
+#include "../../include/utils.h"
+#include <stdio.h>
+#include <stdint.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+#include <libavutil/frame.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavcodec/avcodec.h>
+
+#define FOURCC_NV12 842094158
+#define FOURCC_P010 808530000
+
+int gsr_capture_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ if(cap->started)
+ return -1;
+
+ int res = cap->start(cap, video_codec_context, frame);
+ if(res == 0)
+ cap->started = true;
+
+ return res;
+}
+
+void gsr_capture_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsp_capture_tick failed: the gsr capture has not been started\n");
+ return;
+ }
+
+ if(cap->tick)
+ cap->tick(cap, video_codec_context);
+}
+
+bool gsr_capture_should_stop(gsr_capture *cap, bool *err) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_should_stop failed: the gsr capture has not been started\n");
+ return false;
+ }
+
+ if(!cap->should_stop)
+ return false;
+
+ return cap->should_stop(cap, err);
+}
+
+int gsr_capture_capture(gsr_capture *cap, AVFrame *frame) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_capture failed: the gsr capture has not been started\n");
+ return -1;
+ }
+ return cap->capture(cap, frame);
+}
+
+void gsr_capture_end(gsr_capture *cap, AVFrame *frame) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_end failed: the gsr capture has not been started\n");
+ return;
+ }
+
+ if(!cap->capture_end)
+ return;
+
+ cap->capture_end(cap, frame);
+}
+
+void gsr_capture_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ cap->destroy(cap, video_codec_context);
+}
+
+static uint32_t fourcc(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
+ return (d << 24) | (c << 16) | (b << 8) | a;
+}
+
+bool gsr_capture_base_setup_vaapi_textures(gsr_capture_base *self, AVFrame *frame, VADisplay va_dpy, VADRMPRIMESurfaceDescriptor *prime, gsr_color_range color_range) {
+ const int res = av_hwframe_get_buffer(self->video_codec_context->hw_frames_ctx, frame, 0);
+ if(res < 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: av_hwframe_get_buffer failed: %d\n", res);
+ return false;
+ }
+
+ VASurfaceID target_surface_id = (uintptr_t)frame->data[3];
+
+ VAStatus va_status = vaExportSurfaceHandle(va_dpy, target_surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, prime);
+ if(va_status != VA_STATUS_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: vaExportSurfaceHandle failed, error: %d\n", va_status);
+ return false;
+ }
+ vaSyncSurface(va_dpy, target_surface_id);
+
+ self->egl->glGenTextures(1, &self->input_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->input_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->egl->glGenTextures(1, &self->cursor_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->cursor_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ const uint32_t formats_nv12[2] = { fourcc('R', '8', ' ', ' '), fourcc('G', 'R', '8', '8') };
+ const uint32_t formats_p010[2] = { fourcc('R', '1', '6', ' '), fourcc('G', 'R', '3', '2') };
+
+ if(prime->fourcc == FOURCC_NV12 || prime->fourcc == FOURCC_P010) {
+ const uint32_t *formats = prime->fourcc == FOURCC_NV12 ? formats_nv12 : formats_p010;
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+
+ self->egl->glGenTextures(2, self->target_textures);
+ for(int i = 0; i < 2; ++i) {
+ const int layer = i;
+ const int plane = 0;
+
+ //const uint64_t modifier = prime->objects[prime->layers[layer].object_index[plane]].drm_format_modifier;
+
+ const intptr_t img_attr[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, formats[i],
+ EGL_WIDTH, prime->width / div[i],
+ EGL_HEIGHT, prime->height / div[i],
+ EGL_DMA_BUF_PLANE0_FD_EXT, prime->objects[prime->layers[layer].object_index[plane]].fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, prime->layers[layer].offset[plane],
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, prime->layers[layer].pitch[plane],
+ // TODO:
+ //EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, modifier & 0xFFFFFFFFULL,
+ //EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, modifier >> 32ULL,
+ EGL_NONE
+ };
+
+ while(self->egl->eglGetError() != EGL_SUCCESS){}
+ EGLImage image = self->egl->eglCreateImage(self->egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr);
+ if(!image) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to create egl image from drm fd for output drm fd, error: %d\n", self->egl->eglGetError());
+ return false;
+ }
+
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->target_textures[i]);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ while(self->egl->glGetError()) {}
+ while(self->egl->eglGetError() != EGL_SUCCESS){}
+ self->egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ if(self->egl->glGetError() != 0 || self->egl->eglGetError() != EGL_SUCCESS) {
+ // TODO: Get the error properly
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to bind egl image to gl texture, error: %d\n", self->egl->eglGetError());
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ return false;
+ }
+
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ }
+
+ gsr_color_conversion_params color_conversion_params = {0};
+ color_conversion_params.color_range = color_range;
+ color_conversion_params.egl = self->egl;
+ color_conversion_params.source_color = GSR_SOURCE_COLOR_RGB;
+ if(prime->fourcc == FOURCC_NV12)
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_NV12;
+ else
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_P010;
+
+ color_conversion_params.destination_textures[0] = self->target_textures[0];
+ color_conversion_params.destination_textures[1] = self->target_textures[1];
+ color_conversion_params.num_destination_textures = 2;
+
+ if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to create color conversion\n");
+ return false;
+ }
+
+ return true;
+ } else {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010\n", prime->fourcc);
+ return false;
+ }
+}
+
+static unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal_format, unsigned int format) {
+ unsigned int texture_id = 0;
+ egl->glGenTextures(1, &texture_id);
+ egl->glBindTexture(GL_TEXTURE_2D, texture_id);
+ egl->glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, NULL);
+
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+ egl->glBindTexture(GL_TEXTURE_2D, 0);
+ return texture_id;
+}
+
+static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cuda_graphics_resource, CUarray *mapped_array, unsigned int texture_id) {
+ CUresult res;
+ res = cuda->cuGraphicsGLRegisterImage(cuda_graphics_resource, texture_id, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_NONE);
+ if (res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ cuda->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture " "id: %u\n", err_str, texture_id);
+ return false;
+ }
+
+ res = cuda->cuGraphicsResourceSetMapFlags(*cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_NONE);
+ res = cuda->cuGraphicsMapResources(1, cuda_graphics_resource, 0);
+
+ res = cuda->cuGraphicsSubResourceGetMappedArray(mapped_array, *cuda_graphics_resource, 0, 0);
+ return true;
+}
+
+bool gsr_capture_base_setup_cuda_textures(gsr_capture_base *self, AVFrame *frame, gsr_cuda_context *cuda_context, gsr_color_range color_range, gsr_source_color source_color, bool hdr) {
+ // TODO:
+ const int res = av_hwframe_get_buffer(self->video_codec_context->hw_frames_ctx, frame, 0);
+ if(res < 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: av_hwframe_get_buffer failed: %d\n", res);
+ return false;
+ }
+
+ self->egl->glGenTextures(1, &self->input_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->input_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->egl->glGenTextures(1, &self->cursor_texture);
+ self->egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->cursor_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ const unsigned int internal_formats_nv12[2] = { GL_R8, GL_RG8 };
+ const unsigned int internal_formats_p010[2] = { GL_R16, GL_RG16 };
+ const unsigned int formats[2] = { GL_RED, GL_RG };
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+
+ for(int i = 0; i < 2; ++i) {
+ self->target_textures[i] = gl_create_texture(self->egl, self->video_codec_context->width / div[i], self->video_codec_context->height / div[i], !hdr ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i]);
+ if(self->target_textures[i] == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create opengl texture\n");
+ return false;
+ }
+
+ if(!cuda_register_opengl_texture(cuda_context->cuda, &cuda_context->cuda_graphics_resources[i], &cuda_context->mapped_arrays[i], self->target_textures[i])) {
+ return false;
+ }
+ }
+
+ gsr_color_conversion_params color_conversion_params = {0};
+ color_conversion_params.color_range = color_range;
+ color_conversion_params.egl = self->egl;
+ color_conversion_params.source_color = source_color;
+ if(!hdr)
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_NV12;
+ else
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_P010;
+
+ color_conversion_params.destination_textures[0] = self->target_textures[0];
+ color_conversion_params.destination_textures[1] = self->target_textures[1];
+ color_conversion_params.num_destination_textures = 2;
+ color_conversion_params.load_external_image_shader = true;
+
+ if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create color conversion\n");
+ return false;
+ }
+
+ return true;
+}
+
+void gsr_capture_base_stop(gsr_capture_base *self) {
+ gsr_color_conversion_deinit(&self->color_conversion);
+
+ if(self->egl->egl_context) {
+ if(self->input_texture) {
+ self->egl->glDeleteTextures(1, &self->input_texture);
+ self->input_texture = 0;
+ }
+
+ if(self->cursor_texture) {
+ self->egl->glDeleteTextures(1, &self->cursor_texture);
+ self->cursor_texture = 0;
+ }
+
+ self->egl->glDeleteTextures(2, self->target_textures);
+ self->target_textures[0] = 0;
+ self->target_textures[1] = 0;
+ }
+
+ if(self->video_codec_context->hw_device_ctx)
+ av_buffer_unref(&self->video_codec_context->hw_device_ctx);
+ if(self->video_codec_context->hw_frames_ctx)
+ av_buffer_unref(&self->video_codec_context->hw_frames_ctx);
+}
+
+bool drm_create_codec_context(const char *card_path, AVCodecContext *video_codec_context, int width, int height, bool hdr, VADisplay *va_dpy) {
+ char render_path[128];
+ if(!gsr_card_path_get_render_path(card_path, render_path)) {
+ fprintf(stderr, "gsr error: failed to get /dev/dri/renderDXXX file from %s\n", card_path);
+ return false;
+ }
+
+ AVBufferRef *device_ctx;
+ if(av_hwdevice_ctx_create(&device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) {
+ fprintf(stderr, "Error: Failed to create hardware device context\n");
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "Error: Failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context =
+ (AVHWFramesContext *)frame_context->data;
+ hw_frame_context->width = width;
+ hw_frame_context->height = height;
+ hw_frame_context->sw_format = hdr ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ //hw_frame_context->initial_pool_size = 20;
+
+ AVVAAPIDeviceContext *vactx =((AVHWDeviceContext*)device_ctx->data)->hwctx;
+ *va_dpy = vactx->display;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "Error: Failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
+
+bool cuda_create_codec_context(CUcontext cu_ctx, AVCodecContext *video_codec_context, int width, int height, bool hdr, CUstream *cuda_stream) {
+ AVBufferRef *device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
+ if(!device_ctx) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hardware device context\n");
+ return false;
+ }
+
+ AVHWDeviceContext *hw_device_context = (AVHWDeviceContext*)device_ctx->data;
+ AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext*)hw_device_context->hwctx;
+ cuda_device_context->cuda_ctx = cu_ctx;
+ if(av_hwdevice_ctx_init(device_ctx) < 0) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hardware device context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)frame_context->data;
+ hw_frame_context->width = width;
+ hw_frame_context->height = height;
+ hw_frame_context->sw_format = hdr ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ *cuda_stream = cuda_device_context->stream;
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
diff --git a/src/capture/kms.c b/src/capture/kms.c
new file mode 100644
index 0000000..16b20b7
--- /dev/null
+++ b/src/capture/kms.c
@@ -0,0 +1,369 @@
+#include "../../include/capture/kms.h"
+#include "../../include/capture/capture.h"
+#include "../../include/utils.h"
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/mastering_display_metadata.h>
+
+#define HDMI_STATIC_METADATA_TYPE1 0
+#define HDMI_EOTF_SMPTE_ST2084 2
+
+/* 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 int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+int gsr_capture_kms_start(gsr_capture_kms *self, const char *display_to_capture, gsr_egl *egl, AVCodecContext *video_codec_context, AVFrame *frame) {
+ memset(self, 0, sizeof(*self));
+ self->base.video_codec_context = video_codec_context;
+ self->base.egl = egl;
+
+ gsr_monitor monitor;
+ self->monitor_id.num_connector_ids = 0;
+
+ int kms_init_res = gsr_kms_client_init(&self->kms_client, egl->card_path);
+ if(kms_init_res != 0)
+ return kms_init_res;
+
+ MonitorCallbackUserdata monitor_callback_userdata = {
+ &self->monitor_id,
+ display_to_capture, strlen(display_to_capture),
+ 0,
+ };
+ for_each_active_monitor_output(egl, GSR_CONNECTION_DRM, monitor_callback, &monitor_callback_userdata);
+
+ if(!get_monitor_by_name(egl, GSR_CONNECTION_DRM, display_to_capture, &monitor)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_start: failed to find monitor by name \"%s\"\n", display_to_capture);
+ return -1;
+ }
+
+ monitor.name = display_to_capture;
+ self->monitor_rotation = drm_monitor_get_display_server_rotation(egl, &monitor);
+
+ self->capture_pos = monitor.pos;
+ if(self->monitor_rotation == GSR_MONITOR_ROT_90 || self->monitor_rotation == GSR_MONITOR_ROT_270) {
+ self->capture_size.x = monitor.size.y;
+ self->capture_size.y = monitor.size.x;
+ } else {
+ self->capture_size = monitor.size;
+ }
+
+ /* Disable vsync */
+ egl->eglSwapInterval(egl->egl_display, 0);
+
+ self->base.video_codec_context->width = max_int(2, even_number_ceil(self->capture_size.x));
+ self->base.video_codec_context->height = max_int(2, even_number_ceil(self->capture_size.y));
+
+ frame->width = self->base.video_codec_context->width;
+ frame->height = self->base.video_codec_context->height;
+ return 0;
+}
+
+void gsr_capture_kms_stop(gsr_capture_kms *self) {
+ gsr_capture_kms_cleanup_kms_fds(self);
+ gsr_kms_client_deinit(&self->kms_client);
+ gsr_capture_base_stop(&self->base);
+}
+
+static float monitor_rotation_to_radians(gsr_monitor_rotation rot) {
+ switch(rot) {
+ case GSR_MONITOR_ROT_0: return 0.0f;
+ case GSR_MONITOR_ROT_90: return M_PI_2;
+ case GSR_MONITOR_ROT_180: return M_PI;
+ case GSR_MONITOR_ROT_270: return M_PI + M_PI_2;
+ }
+ return 0.0f;
+}
+
+/* Prefer non combined planes */
+static gsr_kms_response_fd* find_drm_by_connector_id(gsr_kms_response *kms_response, uint32_t connector_id) {
+ int index_combined = -1;
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].connector_id == connector_id && !kms_response->fds[i].is_cursor) {
+ if(kms_response->fds[i].is_combined_plane)
+ index_combined = i;
+ else
+ return &kms_response->fds[i];
+ }
+ }
+
+ if(index_combined != -1)
+ return &kms_response->fds[index_combined];
+ else
+ return NULL;
+}
+
+static gsr_kms_response_fd* find_first_combined_drm(gsr_kms_response *kms_response) {
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].is_combined_plane && !kms_response->fds[i].is_cursor)
+ return &kms_response->fds[i];
+ }
+ return NULL;
+}
+
+static gsr_kms_response_fd* find_largest_drm(gsr_kms_response *kms_response) {
+ if(kms_response->num_fds == 0)
+ return NULL;
+
+ int64_t largest_size = 0;
+ gsr_kms_response_fd *largest_drm = &kms_response->fds[0];
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ const int64_t size = (int64_t)kms_response->fds[i].width * (int64_t)kms_response->fds[i].height;
+ if(size > largest_size && !kms_response->fds[i].is_cursor) {
+ largest_size = size;
+ largest_drm = &kms_response->fds[i];
+ }
+ }
+ return largest_drm;
+}
+
+static gsr_kms_response_fd* find_cursor_drm(gsr_kms_response *kms_response) {
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].is_cursor)
+ return &kms_response->fds[i];
+ }
+ return NULL;
+}
+
+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;
+}
+
+static void gsr_kms_set_hdr_metadata(gsr_capture_kms *self, AVFrame *frame, gsr_kms_response_fd *drm_fd) {
+ if(!self->mastering_display_metadata)
+ self->mastering_display_metadata = av_mastering_display_metadata_create_side_data(frame);
+
+ if(!self->light_metadata)
+ self->light_metadata = av_content_light_metadata_create_side_data(frame);
+
+ if(self->mastering_display_metadata) {
+ for(int i = 0; i < 3; ++i) {
+ self->mastering_display_metadata->display_primaries[i][0] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.display_primaries[i].x, 50000);
+ self->mastering_display_metadata->display_primaries[i][1] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.display_primaries[i].y, 50000);
+ }
+
+ self->mastering_display_metadata->white_point[0] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.white_point.x, 50000);
+ self->mastering_display_metadata->white_point[1] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.white_point.y, 50000);
+
+ self->mastering_display_metadata->min_luminance = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance, 10000);
+ self->mastering_display_metadata->max_luminance = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance, 1);
+
+ self->mastering_display_metadata->has_primaries = self->mastering_display_metadata->display_primaries[0][0].num > 0;
+ self->mastering_display_metadata->has_luminance = self->mastering_display_metadata->max_luminance.num > 0;
+ }
+
+ if(self->light_metadata) {
+ self->light_metadata->MaxCLL = drm_fd->hdr_metadata.hdmi_metadata_type1.max_cll;
+ self->light_metadata->MaxFALL = drm_fd->hdr_metadata.hdmi_metadata_type1.max_fall;
+ }
+}
+
+static vec2i swap_vec2i(vec2i value) {
+ int tmp = value.x;
+ value.x = value.y;
+ value.y = tmp;
+ return value;
+}
+
+bool gsr_capture_kms_capture(gsr_capture_kms *self, AVFrame *frame, bool hdr, bool screen_plane_use_modifiers, bool cursor_texture_is_external, bool record_cursor) {
+ //egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ self->base.egl->glClear(0);
+
+ gsr_capture_kms_cleanup_kms_fds(self);
+
+ gsr_kms_response_fd *drm_fd = NULL;
+ gsr_kms_response_fd *cursor_drm_fd = NULL;
+ bool capture_is_combined_plane = false;
+
+ if(gsr_kms_client_get_kms(&self->kms_client, &self->kms_response) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to get kms, error: %d (%s)\n", self->kms_response.result, self->kms_response.err_msg);
+ return false;
+ }
+
+ if(self->kms_response.num_fds == 0) {
+ static bool error_shown = false;
+ if(!error_shown) {
+ error_shown = true;
+ fprintf(stderr, "gsr error: no drm found, capture will fail\n");
+ }
+ return false;
+ }
+
+ for(int i = 0; i < self->monitor_id.num_connector_ids; ++i) {
+ drm_fd = find_drm_by_connector_id(&self->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) {
+ drm_fd = find_first_combined_drm(&self->kms_response);
+ if(!drm_fd)
+ drm_fd = find_largest_drm(&self->kms_response);
+ capture_is_combined_plane = true;
+ }
+
+ cursor_drm_fd = find_cursor_drm(&self->kms_response);
+
+ if(!drm_fd)
+ return false;
+
+ if(!capture_is_combined_plane && cursor_drm_fd && cursor_drm_fd->connector_id != drm_fd->connector_id)
+ cursor_drm_fd = NULL;
+
+ if(drm_fd->has_hdr_metadata && hdr && hdr_metadata_is_supported_format(&drm_fd->hdr_metadata))
+ gsr_kms_set_hdr_metadata(self, frame, 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
+ intptr_t img_attr[18] = {
+ EGL_LINUX_DRM_FOURCC_EXT, drm_fd->pixel_format,
+ EGL_WIDTH, drm_fd->width,
+ EGL_HEIGHT, drm_fd->height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, drm_fd->fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, drm_fd->offset,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, drm_fd->pitch,
+ };
+
+ if(screen_plane_use_modifiers) {
+ img_attr[12] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
+ img_attr[13] = drm_fd->modifier & 0xFFFFFFFFULL;
+
+ img_attr[14] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
+ img_attr[15] = drm_fd->modifier >> 32ULL;
+
+ img_attr[16] = EGL_NONE;
+ img_attr[17] = EGL_NONE;
+ } else {
+ img_attr[12] = EGL_NONE;
+ img_attr[13] = EGL_NONE;
+ }
+
+ EGLImage image = self->base.egl->eglCreateImage(self->base.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr);
+ self->base.egl->glBindTexture(GL_TEXTURE_2D, self->base.input_texture);
+ self->base.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ self->base.egl->eglDestroyImage(self->base.egl->egl_display, image);
+ self->base.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ vec2i capture_pos = self->capture_pos;
+ if(!capture_is_combined_plane)
+ capture_pos = (vec2i){drm_fd->x, drm_fd->y};
+
+ const float texture_rotation = monitor_rotation_to_radians(self->monitor_rotation);
+
+ gsr_color_conversion_draw(&self->base.color_conversion, self->base.input_texture,
+ (vec2i){0, 0}, self->capture_size,
+ capture_pos, self->capture_size,
+ texture_rotation, false);
+
+ if(record_cursor && cursor_drm_fd) {
+ const vec2i cursor_size = {cursor_drm_fd->width, cursor_drm_fd->height};
+ vec2i cursor_pos = {cursor_drm_fd->x, cursor_drm_fd->y};
+ switch(self->monitor_rotation) {
+ case GSR_MONITOR_ROT_0:
+ break;
+ case GSR_MONITOR_ROT_90:
+ cursor_pos = swap_vec2i(cursor_pos);
+ cursor_pos.x = self->capture_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 = self->capture_size.x - cursor_pos.x;
+ cursor_pos.y = self->capture_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 = self->capture_size.y - cursor_pos.y;
+ // TODO: Remove this horrible hack
+ cursor_pos.y -= cursor_size.y;
+ break;
+ }
+
+ const intptr_t img_attr_cursor[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, cursor_drm_fd->pixel_format,
+ EGL_WIDTH, cursor_drm_fd->width,
+ EGL_HEIGHT, cursor_drm_fd->height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, cursor_drm_fd->fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, cursor_drm_fd->offset,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, cursor_drm_fd->pitch,
+ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, cursor_drm_fd->modifier & 0xFFFFFFFFULL,
+ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, cursor_drm_fd->modifier >> 32ULL,
+ EGL_NONE
+ };
+
+ EGLImage cursor_image = self->base.egl->eglCreateImage(self->base.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr_cursor);
+ const int target = cursor_texture_is_external ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D;
+ self->base.egl->glBindTexture(target, self->base.cursor_texture);
+ self->base.egl->glEGLImageTargetTexture2DOES(target, cursor_image);
+ self->base.egl->eglDestroyImage(self->base.egl->egl_display, cursor_image);
+ self->base.egl->glBindTexture(target, 0);
+
+ gsr_color_conversion_draw(&self->base.color_conversion, self->base.cursor_texture,
+ cursor_pos, cursor_size,
+ (vec2i){0, 0}, cursor_size,
+ texture_rotation, cursor_texture_is_external);
+ }
+
+ self->base.egl->eglSwapBuffers(self->base.egl->egl_display, self->base.egl->egl_surface);
+ //self->base.egl->glFlush();
+ //self->base.egl->glFinish();
+
+ return true;
+}
+
+void gsr_capture_kms_cleanup_kms_fds(gsr_capture_kms *self) {
+ for(int i = 0; i < self->kms_response.num_fds; ++i) {
+ if(self->kms_response.fds[i].fd > 0)
+ close(self->kms_response.fds[i].fd);
+ self->kms_response.fds[i].fd = 0;
+ }
+ self->kms_response.num_fds = 0;
+}
diff --git a/src/capture/kms_cuda.c b/src/capture/kms_cuda.c
new file mode 100644
index 0000000..a9f1f8e
--- /dev/null
+++ b/src/capture/kms_cuda.c
@@ -0,0 +1,181 @@
+#include "../../include/capture/kms_cuda.h"
+#include "../../include/capture/kms.h"
+#include "../../include/cuda.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_kms kms;
+
+ gsr_capture_kms_cuda_params params;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream;
+} gsr_capture_kms_cuda;
+
+static void gsr_capture_kms_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_kms_cuda_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+
+ const int res = gsr_capture_kms_start(&cap_kms->kms, cap_kms->params.display_to_capture, cap_kms->params.egl, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return res;
+ }
+
+ // TODO: overclocking is not supported on wayland...
+ if(!gsr_cuda_load(&cap_kms->cuda, NULL, false)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_start: failed to load cuda\n");
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!cuda_create_codec_context(cap_kms->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, cap_kms->params.hdr, &cap_kms->cuda_stream)) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_kms->cuda,
+ .cuda_graphics_resources = cap_kms->cuda_graphics_resources,
+ .mapped_arrays = cap_kms->mapped_arrays
+ };
+
+ if(!gsr_capture_base_setup_cuda_textures(&cap_kms->kms.base, frame, &cuda_context, cap_kms->params.color_range, GSR_SOURCE_COLOR_RGB, cap_kms->params.hdr)) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static bool gsr_capture_kms_cuda_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ if(cap_kms->kms.should_stop) {
+ if(err)
+ *err = cap_kms->kms.stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+static void gsr_capture_kms_unload_cuda_graphics(gsr_capture_kms_cuda *cap_kms) {
+ if(cap_kms->cuda.cu_ctx) {
+ for(int i = 0; i < 2; ++i) {
+ if(cap_kms->cuda_graphics_resources[i]) {
+ cap_kms->cuda.cuGraphicsUnmapResources(1, &cap_kms->cuda_graphics_resources[i], 0);
+ cap_kms->cuda.cuGraphicsUnregisterResource(cap_kms->cuda_graphics_resources[i]);
+ cap_kms->cuda_graphics_resources[i] = 0;
+ }
+ }
+ }
+}
+
+static int gsr_capture_kms_cuda_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+
+ gsr_capture_kms_capture(&cap_kms->kms, frame, cap_kms->params.hdr, true, true, cap_kms->params.record_cursor);
+
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_kms->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width * (cap_kms->params.hdr ? 2 : 1);
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_kms->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_kms->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_kms->cuda.cuStreamSynchronize(cap_kms->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_kms_cuda_capture_end(gsr_capture *cap, AVFrame *frame) {
+ (void)frame;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ gsr_capture_kms_cleanup_kms_fds(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ gsr_capture_kms_unload_cuda_graphics(cap_kms);
+ gsr_cuda_unload(&cap_kms->cuda);
+ gsr_capture_kms_stop(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_cuda_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ if(cap->priv) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ free((void*)cap_kms->params.display_to_capture);
+ cap_kms->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_kms_cuda_create(const gsr_capture_kms_cuda_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_kms_cuda *cap_kms = calloc(1, sizeof(gsr_capture_kms_cuda));
+ 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_cuda_start,
+ .tick = NULL,
+ .should_stop = gsr_capture_kms_cuda_should_stop,
+ .capture = gsr_capture_kms_cuda_capture,
+ .capture_end = gsr_capture_kms_cuda_capture_end,
+ .destroy = gsr_capture_kms_cuda_destroy,
+ .priv = cap_kms
+ };
+
+ return cap;
+}
diff --git a/src/capture/kms_vaapi.c b/src/capture/kms_vaapi.c
new file mode 100644
index 0000000..a7e8182
--- /dev/null
+++ b/src/capture/kms_vaapi.c
@@ -0,0 +1,135 @@
+#include "../../include/capture/kms_vaapi.h"
+#include "../../include/capture/kms.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <libavcodec/avcodec.h>
+#include <va/va_drmcommon.h>
+
+typedef struct {
+ gsr_capture_kms kms;
+
+ gsr_capture_kms_vaapi_params params;
+
+ VADisplay va_dpy;
+ VADRMPRIMESurfaceDescriptor prime;
+} gsr_capture_kms_vaapi;
+
+static void gsr_capture_kms_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_kms_vaapi_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+
+ int res = gsr_capture_kms_start(&cap_kms->kms, cap_kms->params.display_to_capture, cap_kms->params.egl, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!drm_create_codec_context(cap_kms->params.egl->card_path, video_codec_context, video_codec_context->width, video_codec_context->height, cap_kms->params.hdr, &cap_kms->va_dpy)) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!gsr_capture_base_setup_vaapi_textures(&cap_kms->kms.base, frame, cap_kms->va_dpy, &cap_kms->prime, cap_kms->params.color_range)) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static bool gsr_capture_kms_vaapi_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ if(cap_kms->kms.should_stop) {
+ if(err)
+ *err = cap_kms->kms.stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+static int gsr_capture_kms_vaapi_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ gsr_capture_kms_capture(&cap_kms->kms, frame, cap_kms->params.hdr, false, false, cap_kms->params.record_cursor);
+ return 0;
+}
+
+static void gsr_capture_kms_vaapi_capture_end(gsr_capture *cap, AVFrame *frame) {
+ (void)frame;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ gsr_capture_kms_cleanup_kms_fds(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+
+ for(uint32_t i = 0; i < cap_kms->prime.num_objects; ++i) {
+ if(cap_kms->prime.objects[i].fd > 0) {
+ close(cap_kms->prime.objects[i].fd);
+ cap_kms->prime.objects[i].fd = 0;
+ }
+ }
+
+ gsr_capture_kms_stop(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_vaapi_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ if(cap->priv) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ free((void*)cap_kms->params.display_to_capture);
+ cap_kms->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_kms_vaapi_create(const gsr_capture_kms_vaapi_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_vaapi_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_kms_vaapi *cap_kms = calloc(1, sizeof(gsr_capture_kms_vaapi));
+ if(!cap_kms) {
+ free(cap);
+ return NULL;
+ }
+
+ const char *display_to_capture = strdup(params->display_to_capture);
+ if(!display_to_capture) {
+ /* TODO XCloseDisplay */
+ 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_vaapi_start,
+ .tick = NULL,
+ .should_stop = gsr_capture_kms_vaapi_should_stop,
+ .capture = gsr_capture_kms_vaapi_capture,
+ .capture_end = gsr_capture_kms_vaapi_capture_end,
+ .destroy = gsr_capture_kms_vaapi_destroy,
+ .priv = cap_kms
+ };
+
+ return cap;
+}
diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c
new file mode 100644
index 0000000..9eabb18
--- /dev/null
+++ b/src/capture/nvfbc.c
@@ -0,0 +1,535 @@
+#include "../../include/capture/nvfbc.h"
+#include "../../external/NvFBC.h"
+#include "../../include/cuda.h"
+#include "../../include/egl.h"
+#include "../../include/utils.h"
+#include <dlfcn.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <X11/Xlib.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavutil/frame.h>
+#include <libavutil/version.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_base base;
+ 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;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream; // TODO: asdasdsa
+ NVFBC_TOGL_SETUP_PARAMS setup_params;
+
+ bool direct_capture;
+ bool supports_direct_cursor;
+ bool capture_region;
+ uint32_t x, y, 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;
+
+#if defined(_WIN64) || defined(__LP64__)
+typedef unsigned long long CUdeviceptr_v2;
+#else
+typedef unsigned int CUdeviceptr_v2;
+#endif
+typedef CUdeviceptr_v2 CUdeviceptr;
+
+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;
+}
+
+/* TODO: Test with optimus and open kernel modules */
+static bool get_driver_version(int *major, int *minor) {
+ *major = 0;
+ *minor = 0;
+
+ FILE *f = fopen("/proc/driver/nvidia/version", "rb");
+ if(!f) {
+ fprintf(stderr, "gsr warning: failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)\n");
+ return false;
+ }
+
+ char buffer[2048];
+ size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, f);
+ buffer[bytes_read] = '\0';
+
+ bool success = false;
+ const char *p = strstr(buffer, "Kernel Module");
+ if(p) {
+ p += 13;
+ int driver_major_version = 0, driver_minor_version = 0;
+ if(sscanf(p, "%d.%d", &driver_major_version, &driver_minor_version) == 2) {
+ *major = driver_major_version;
+ *minor = driver_minor_version;
+ success = true;
+ }
+ }
+
+ if(!success)
+ fprintf(stderr, "gsr warning: failed to get nvidia driver version\n");
+
+ fclose(f);
+ return success;
+}
+
+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 *cap_nvfbc = 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**)&cap_nvfbc->nv_fbc_create_instance, dlsym(lib, "NvFBCCreateInstance"));
+ if(!cap_nvfbc->nv_fbc_create_instance) {
+ fprintf(stderr, "gsr error: unable to resolve symbol 'NvFBCCreateInstance'\n");
+ dlclose(lib);
+ return false;
+ }
+
+ memset(&cap_nvfbc->nv_fbc_function_list, 0, sizeof(cap_nvfbc->nv_fbc_function_list));
+ cap_nvfbc->nv_fbc_function_list.dwVersion = NVFBC_VERSION;
+ NVFBCSTATUS status = cap_nvfbc->nv_fbc_create_instance(&cap_nvfbc->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;
+ }
+
+ cap_nvfbc->library = lib;
+ return true;
+}
+
+/* TODO: check for glx swap control extension string (GLX_EXT_swap_control, etc) */
+static void set_vertical_sync_enabled(gsr_egl *egl, int enabled) {
+ int result = 0;
+
+ if(egl->glXSwapIntervalEXT) {
+ egl->glXSwapIntervalEXT(egl->x11.dpy, egl->x11.window, enabled ? 1 : 0);
+ } else if(egl->glXSwapIntervalMESA) {
+ result = egl->glXSwapIntervalMESA(enabled ? 1 : 0);
+ } else if(egl->glXSwapIntervalSGI) {
+ result = egl->glXSwapIntervalSGI(enabled ? 1 : 0);
+ } else {
+ static int warned = 0;
+ if (!warned) {
+ warned = 1;
+ fprintf(stderr, "gsr warning: setting vertical sync not supported\n");
+ }
+ }
+
+ if(result != 0)
+ fprintf(stderr, "gsr warning: setting vertical sync failed\n");
+}
+
+static void gsr_capture_nvfbc_destroy_session(gsr_capture_nvfbc *cap_nvfbc) {
+ if(cap_nvfbc->fbc_handle_created && cap_nvfbc->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;
+ cap_nvfbc->nv_fbc_function_list.nvFBCDestroyCaptureSession(cap_nvfbc->nv_fbc_handle, &destroy_capture_params);
+ cap_nvfbc->capture_session_created = false;
+ }
+}
+
+static void gsr_capture_nvfbc_destroy_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ if(cap_nvfbc->fbc_handle_created) {
+ NVFBC_DESTROY_HANDLE_PARAMS destroy_params;
+ memset(&destroy_params, 0, sizeof(destroy_params));
+ destroy_params.dwVersion = NVFBC_DESTROY_HANDLE_PARAMS_VER;
+ cap_nvfbc->nv_fbc_function_list.nvFBCDestroyHandle(cap_nvfbc->nv_fbc_handle, &destroy_params);
+ cap_nvfbc->fbc_handle_created = false;
+ cap_nvfbc->nv_fbc_handle = 0;
+ }
+}
+
+static void gsr_capture_nvfbc_destroy_session_and_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ gsr_capture_nvfbc_destroy_session(cap_nvfbc);
+ gsr_capture_nvfbc_destroy_handle(cap_nvfbc);
+}
+
+static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ 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 = cap_nvfbc->params.egl->glx_context;
+ create_params.glxFBConfig = cap_nvfbc->params.egl->glx_fb_config;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCCreateHandle(&cap_nvfbc->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 = cap_nvfbc->nv_fbc_function_list.nvFBCCreateHandle(&cap_nvfbc->nv_fbc_handle, &create_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ goto error_cleanup;
+ }
+ }
+ cap_nvfbc->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 = cap_nvfbc->nv_fbc_function_list.nvFBCGetStatus(cap_nvfbc->nv_fbc_handle, &status_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->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;
+ }
+
+ cap_nvfbc->tracking_width = XWidthOfScreen(DefaultScreenOfDisplay(cap_nvfbc->params.egl->x11.dpy));
+ cap_nvfbc->tracking_height = XHeightOfScreen(DefaultScreenOfDisplay(cap_nvfbc->params.egl->x11.dpy));
+ cap_nvfbc->tracking_type = strcmp(cap_nvfbc->params.display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT;
+ if(cap_nvfbc->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;
+ }
+
+ cap_nvfbc->output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, cap_nvfbc->params.display_to_capture, &cap_nvfbc->tracking_width, &cap_nvfbc->tracking_height);
+ if(cap_nvfbc->output_id == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: display '%s' not found\n", cap_nvfbc->params.display_to_capture);
+ goto error_cleanup;
+ }
+ }
+
+ return 0;
+
+ error_cleanup:
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ return -1;
+}
+
+static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *cap_nvfbc) {
+ 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 = (!cap_nvfbc->direct_capture || cap_nvfbc->supports_direct_cursor) ? NVFBC_TRUE : NVFBC_FALSE;
+ if(!cap_nvfbc->params.record_cursor)
+ create_capture_params.bWithCursor = false;
+ if(cap_nvfbc->capture_region)
+ create_capture_params.captureBox = (NVFBC_BOX){ cap_nvfbc->x, cap_nvfbc->y, cap_nvfbc->width, cap_nvfbc->height };
+ create_capture_params.eTrackingType = cap_nvfbc->tracking_type;
+ create_capture_params.dwSamplingRateMs = (uint32_t)ceilf(1000.0f / (float)cap_nvfbc->params.fps);
+ create_capture_params.bAllowDirectCapture = cap_nvfbc->direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
+ create_capture_params.bPushModel = cap_nvfbc->direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
+ create_capture_params.bDisableAutoModesetRecovery = true;
+ if(cap_nvfbc->tracking_type == NVFBC_TRACKING_OUTPUT)
+ create_capture_params.dwOutputId = cap_nvfbc->output_id;
+
+ NVFBCSTATUS status = cap_nvfbc->nv_fbc_function_list.nvFBCCreateCaptureSession(cap_nvfbc->nv_fbc_handle, &create_capture_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ return -1;
+ }
+ cap_nvfbc->capture_session_created = true;
+
+ memset(&cap_nvfbc->setup_params, 0, sizeof(cap_nvfbc->setup_params));
+ cap_nvfbc->setup_params.dwVersion = NVFBC_TOGL_SETUP_PARAMS_VER;
+ cap_nvfbc->setup_params.eBufferFormat = NVFBC_BUFFER_FORMAT_BGRA;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCToGLSetUp(cap_nvfbc->nv_fbc_handle, &cap_nvfbc->setup_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ gsr_capture_nvfbc_destroy_session(cap_nvfbc);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int gsr_capture_nvfbc_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+
+ cap_nvfbc->base.video_codec_context = video_codec_context;
+ cap_nvfbc->base.egl = cap_nvfbc->params.egl;
+
+ if(!gsr_cuda_load(&cap_nvfbc->cuda, cap_nvfbc->params.egl->x11.dpy, cap_nvfbc->params.overclock))
+ return -1;
+
+ if(!gsr_capture_nvfbc_load_library(cap)) {
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ return -1;
+ }
+
+ cap_nvfbc->x = max_int(cap_nvfbc->params.pos.x, 0);
+ cap_nvfbc->y = max_int(cap_nvfbc->params.pos.y, 0);
+ cap_nvfbc->width = max_int(cap_nvfbc->params.size.x, 0);
+ cap_nvfbc->height = max_int(cap_nvfbc->params.size.y, 0);
+
+ cap_nvfbc->capture_region = (cap_nvfbc->x > 0 || cap_nvfbc->y > 0 || cap_nvfbc->width > 0 || cap_nvfbc->height > 0);
+
+ cap_nvfbc->supports_direct_cursor = false;
+ bool direct_capture = cap_nvfbc->params.direct_capture;
+ int driver_major_version = 0;
+ int driver_minor_version = 0;
+ if(direct_capture && get_driver_version(&driver_major_version, &driver_minor_version)) {
+ fprintf(stderr, "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)) {
+ direct_capture = false;
+ fprintf(stderr, "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))
+ supports_direct_cursor = true;
+ else
+ fprintf(stderr, "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(cap_nvfbc) != 0) {
+ goto error_cleanup;
+ }
+
+ if(gsr_capture_nvfbc_setup_session(cap_nvfbc) != 0) {
+ goto error_cleanup;
+ }
+
+ if(cap_nvfbc->capture_region) {
+ video_codec_context->width = cap_nvfbc->width & ~1;
+ video_codec_context->height = cap_nvfbc->height & ~1;
+ } else {
+ video_codec_context->width = cap_nvfbc->tracking_width & ~1;
+ video_codec_context->height = cap_nvfbc->tracking_height & ~1;
+ }
+
+ frame->width = video_codec_context->width;
+ frame->height = video_codec_context->height;
+
+ if(!cuda_create_codec_context(cap_nvfbc->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_nvfbc->cuda_stream))
+ goto error_cleanup;
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_nvfbc->cuda,
+ .cuda_graphics_resources = cap_nvfbc->cuda_graphics_resources,
+ .mapped_arrays = cap_nvfbc->mapped_arrays
+ };
+
+ // TODO: Remove this, it creates shit we dont need
+ if(!gsr_capture_base_setup_cuda_textures(&cap_nvfbc->base, frame, &cuda_context, cap_nvfbc->params.color_range, GSR_SOURCE_COLOR_BGR, cap_nvfbc->params.hdr)) {
+ goto error_cleanup;
+ }
+ /* Disable vsync */
+ set_vertical_sync_enabled(cap_nvfbc->params.egl, 0);
+
+ return 0;
+
+ error_cleanup:
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ gsr_capture_base_stop(&cap_nvfbc->base);
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ return -1;
+}
+
+static int gsr_capture_nvfbc_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+
+ const double nvfbc_recreate_retry_time_seconds = 1.0;
+ if(cap_nvfbc->nvfbc_needs_recreate) {
+ const double now = clock_get_monotonic_seconds();
+ if(now - cap_nvfbc->nvfbc_dead_start >= nvfbc_recreate_retry_time_seconds) {
+ cap_nvfbc->nvfbc_dead_start = now;
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+
+ if(gsr_capture_nvfbc_setup_handle(cap_nvfbc) != 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(cap_nvfbc) != 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;
+ }
+
+ cap_nvfbc->nvfbc_needs_recreate = false;
+ } else {
+ return 0;
+ }
+ }
+
+ 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 = cap_nvfbc->nv_fbc_function_list.nvFBCToGLGrabFrame(cap_nvfbc->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", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds);
+ cap_nvfbc->nvfbc_needs_recreate = true;
+ cap_nvfbc->nvfbc_dead_start = clock_get_monotonic_seconds();
+ return 0;
+ }
+
+ //cap_nvfbc->params.egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ cap_nvfbc->params.egl->glClear(0);
+
+ gsr_color_conversion_draw(&cap_nvfbc->base.color_conversion, cap_nvfbc->setup_params.dwTextures[grab_params.dwTextureIndex],
+ (vec2i){0, 0}, (vec2i){frame->width, frame->height},
+ (vec2i){0, 0}, (vec2i){frame->width, frame->height},
+ 0.0f, false);
+
+ cap_nvfbc->params.egl->glXSwapBuffers(cap_nvfbc->params.egl->x11.dpy, cap_nvfbc->params.egl->x11.window);
+
+ // TODO: HDR is broken
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_nvfbc->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width * (cap_nvfbc->params.hdr ? 2 : 1);
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_nvfbc->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_nvfbc->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_nvfbc->cuda.cuStreamSynchronize(cap_nvfbc->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_nvfbc_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ if(cap_nvfbc) {
+ gsr_capture_base_stop(&cap_nvfbc->base);
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ dlclose(cap_nvfbc->library);
+ free((void*)cap_nvfbc->params.display_to_capture);
+ cap_nvfbc->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ 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,
+ .capture_end = NULL,
+ .destroy = gsr_capture_nvfbc_destroy,
+ .priv = cap_nvfbc
+ };
+
+ return cap;
+}
diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c
new file mode 100644
index 0000000..29b42d5
--- /dev/null
+++ b/src/capture/xcomposite.c
@@ -0,0 +1,299 @@
+#include "../../include/capture/xcomposite.h"
+#include "../../include/window_texture.h"
+#include "../../include/utils.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <X11/Xlib.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/frame.h>
+#include <libavcodec/avcodec.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+
+static int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+void gsr_capture_xcomposite_init(gsr_capture_xcomposite *self, const gsr_capture_xcomposite_params *params) {
+ memset(self, 0, sizeof(*self));
+ self->params = *params;
+}
+
+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;
+}
+
+int gsr_capture_xcomposite_start(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context, AVFrame *frame) {
+ self->base.video_codec_context = video_codec_context;
+ self->base.egl = self->params.egl;
+
+ if(self->params.follow_focused) {
+ self->net_active_window_atom = XInternAtom(self->params.egl->x11.dpy, "_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->params.egl->x11.dpy, 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->params.egl->x11.dpy, 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_size.x = max_int(attr.width, 0);
+ self->window_size.y = max_int(attr.height, 0);
+
+ if(self->params.follow_focused)
+ XSelectInput(self->params.egl->x11.dpy, DefaultRootWindow(self->params.egl->x11.dpy), 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->params.egl->x11.dpy, self->window, StructureNotifyMask | ExposureMask);
+
+ if(!self->params.egl->eglExportDMABUFImageQueryMESA) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: could not find eglExportDMABUFImageQueryMESA\n");
+ return -1;
+ }
+
+ if(!self->params.egl->eglExportDMABUFImageMESA) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: could not find eglExportDMABUFImageMESA\n");
+ return -1;
+ }
+
+ /* Disable vsync */
+ self->params.egl->eglSwapInterval(self->params.egl->egl_display, 0);
+ if(window_texture_init(&self->window_texture, self->params.egl->x11.dpy, 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", self->window);
+ return -1;
+ }
+
+ if(gsr_cursor_init(&self->cursor, self->params.egl, self->params.egl->x11.dpy) != 0) {
+ gsr_capture_xcomposite_stop(self);
+ return -1;
+ }
+
+ self->texture_size.x = 0;
+ self->texture_size.y = 0;
+
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, window_texture_get_opengl_texture_id(&self->window_texture));
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &self->texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &self->texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->texture_size.x = max_int(2, even_number_ceil(self->texture_size.x));
+ self->texture_size.y = max_int(2, even_number_ceil(self->texture_size.y));
+
+ video_codec_context->width = self->texture_size.x;
+ video_codec_context->height = self->texture_size.y;
+
+ if(self->params.region_size.x > 0 && self->params.region_size.y > 0) {
+ video_codec_context->width = max_int(2, even_number_ceil(self->params.region_size.x));
+ video_codec_context->height = max_int(2, even_number_ceil(self->params.region_size.y));
+ }
+
+ frame->width = video_codec_context->width;
+ frame->height = video_codec_context->height;
+
+ self->window_resize_timer = clock_get_monotonic_seconds();
+ self->clear_next_frame = true;
+ return 0;
+}
+
+void gsr_capture_xcomposite_stop(gsr_capture_xcomposite *self) {
+ window_texture_deinit(&self->window_texture);
+ gsr_cursor_deinit(&self->cursor);
+ gsr_capture_base_stop(&self->base);
+}
+
+void gsr_capture_xcomposite_tick(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context) {
+ //self->params.egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ self->params.egl->glClear(0);
+
+ bool init_new_window = false;
+ while(XPending(self->params.egl->x11.dpy)) {
+ XNextEvent(self->params.egl->x11.dpy, &self->xev);
+
+ switch(self->xev.type) {
+ case DestroyNotify: {
+ /* Window died (when not following focused window), so we stop recording */
+ if(!self->params.follow_focused && self->xev.xdestroywindow.window == self->window) {
+ self->should_stop = true;
+ self->stop_is_error = false;
+ }
+ break;
+ }
+ case Expose: {
+ /* Requires window texture recreate */
+ if(self->xev.xexpose.count == 0 && self->xev.xexpose.window == self->window) {
+ self->window_resize_timer = clock_get_monotonic_seconds();
+ self->window_resized = true;
+ }
+ break;
+ }
+ case ConfigureNotify: {
+ /* Window resized */
+ if(self->xev.xconfigure.window == self->window && (self->xev.xconfigure.width != self->window_size.x || self->xev.xconfigure.height != self->window_size.y)) {
+ self->window_size.x = max_int(self->xev.xconfigure.width, 0);
+ self->window_size.y = max_int(self->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 && self->xev.xproperty.atom == self->net_active_window_atom) {
+ init_new_window = true;
+ }
+ break;
+ }
+ }
+
+ gsr_cursor_update(&self->cursor, &self->xev);
+ }
+
+ if(self->params.follow_focused && !self->follow_focused_initialized) {
+ init_new_window = true;
+ }
+
+ if(init_new_window) {
+ Window focused_window = get_focused_window(self->params.egl->x11.dpy, self->net_active_window_atom);
+ if(focused_window != self->window || !self->follow_focused_initialized) {
+ self->follow_focused_initialized = true;
+ XSelectInput(self->params.egl->x11.dpy, self->window, 0);
+ self->window = focused_window;
+ XSelectInput(self->params.egl->x11.dpy, self->window, StructureNotifyMask | ExposureMask);
+
+ XWindowAttributes attr;
+ attr.width = 0;
+ attr.height = 0;
+ if(!XGetWindowAttributes(self->params.egl->x11.dpy, self->window, &attr))
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window);
+
+ self->window_size.x = max_int(attr.width, 0);
+ self->window_size.y = max_int(attr.height, 0);
+ self->window_resized = true;
+
+ window_texture_deinit(&self->window_texture);
+ window_texture_init(&self->window_texture, self->params.egl->x11.dpy, self->window, self->params.egl); // TODO: Do not do the below window_texture_on_resize after this
+ }
+ }
+
+ 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 = 0;
+ self->texture_size.y = 0;
+
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, window_texture_get_opengl_texture_id(&self->window_texture));
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &self->texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &self->texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->texture_size.x = min_int(video_codec_context->width, max_int(2, even_number_ceil(self->texture_size.x)));
+ self->texture_size.y = min_int(video_codec_context->height, max_int(2, even_number_ceil(self->texture_size.y)));
+
+ gsr_color_conversion_clear(&self->base.color_conversion);
+ }
+}
+
+bool gsr_capture_xcomposite_should_stop(gsr_capture_xcomposite *self, bool *err) {
+ if(self->should_stop) {
+ if(err)
+ *err = self->stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+int gsr_capture_xcomposite_capture(gsr_capture_xcomposite *self, AVFrame *frame) {
+ (void)frame;
+
+ const int target_x = max_int(0, frame->width / 2 - self->texture_size.x / 2);
+ const int target_y = max_int(0, frame->height / 2 - self->texture_size.y / 2);
+
+ // TODO: Can we do this a better way than to call it every capture?
+ if(self->params.record_cursor)
+ gsr_cursor_tick(&self->cursor, self->window);
+
+ const vec2i cursor_pos = {
+ target_x + self->cursor.position.x - self->cursor.hotspot.x,
+ target_y + self->cursor.position.y - self->cursor.hotspot.y
+ };
+
+ const bool cursor_completely_inside_window =
+ cursor_pos.x >= target_x &&
+ cursor_pos.x + self->cursor.size.x <= target_x + self->texture_size.x &&
+ cursor_pos.y >= target_y &&
+ cursor_pos.y + self->cursor.size.y <= target_y + self->texture_size.y;
+
+ const bool cursor_inside_window =
+ cursor_pos.x + self->cursor.size.x >= target_x &&
+ cursor_pos.x <= target_x + self->texture_size.x &&
+ cursor_pos.y + self->cursor.size.y >= target_y &&
+ cursor_pos.y <= target_y + self->texture_size.y;
+
+ if(self->clear_next_frame) {
+ self->clear_next_frame = false;
+ gsr_color_conversion_clear(&self->base.color_conversion);
+ }
+
+ /*
+ We dont draw the cursor if it's outside the window but if it's partially inside the window then the cursor area that is outside the window
+ will not get overdrawn the next frame causing a cursor trail to be visible since we dont clear the background.
+ To fix this we detect if the cursor is partially inside the window and clear the background only in that case.
+ */
+ if(!cursor_completely_inside_window && cursor_inside_window && self->params.record_cursor)
+ self->clear_next_frame = true;
+
+ gsr_color_conversion_draw(&self->base.color_conversion, window_texture_get_opengl_texture_id(&self->window_texture),
+ (vec2i){target_x, target_y}, self->texture_size,
+ (vec2i){0, 0}, self->texture_size,
+ 0.0f, false);
+
+ if(cursor_inside_window && self->params.record_cursor) {
+ gsr_color_conversion_draw(&self->base.color_conversion, self->cursor.texture_id,
+ cursor_pos, self->cursor.size,
+ (vec2i){0, 0}, self->cursor.size,
+ 0.0f, false);
+ }
+
+ self->params.egl->eglSwapBuffers(self->params.egl->egl_display, self->params.egl->egl_surface);
+ //self->params.egl->glFlush();
+ //self->params.egl->glFinish();
+
+ return 0;
+}
diff --git a/src/capture/xcomposite_cuda.c b/src/capture/xcomposite_cuda.c
new file mode 100644
index 0000000..6e13d2a
--- /dev/null
+++ b/src/capture/xcomposite_cuda.c
@@ -0,0 +1,155 @@
+#include "../../include/capture/xcomposite_cuda.h"
+#include "../../include/cuda.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <libavutil/frame.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_xcomposite xcomposite;
+ bool overclock;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream;
+} gsr_capture_xcomposite_cuda;
+
+static void gsr_capture_xcomposite_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_xcomposite_cuda_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+
+ const int res = gsr_capture_xcomposite_start(&cap_xcomp->xcomposite, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!gsr_cuda_load(&cap_xcomp->cuda, cap_xcomp->xcomposite.params.egl->x11.dpy, cap_xcomp->overclock)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_start: failed to load cuda\n");
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!cuda_create_codec_context(cap_xcomp->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_xcomp->cuda_stream)) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_xcomp->cuda,
+ .cuda_graphics_resources = cap_xcomp->cuda_graphics_resources,
+ .mapped_arrays = cap_xcomp->mapped_arrays
+ };
+
+ if(!gsr_capture_base_setup_cuda_textures(&cap_xcomp->xcomposite.base, frame, &cuda_context, cap_xcomp->xcomposite.params.color_range, GSR_SOURCE_COLOR_RGB, false)) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_unload_cuda_graphics(gsr_capture_xcomposite_cuda *cap_xcomp) {
+ if(cap_xcomp->cuda.cu_ctx) {
+ for(int i = 0; i < 2; ++i) {
+ if(cap_xcomp->cuda_graphics_resources[i]) {
+ cap_xcomp->cuda.cuGraphicsUnmapResources(1, &cap_xcomp->cuda_graphics_resources[i], 0);
+ cap_xcomp->cuda.cuGraphicsUnregisterResource(cap_xcomp->cuda_graphics_resources[i]);
+ cap_xcomp->cuda_graphics_resources[i] = 0;
+ }
+ }
+ }
+}
+
+static void gsr_capture_xcomposite_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_stop(&cap_xcomp->xcomposite);
+ gsr_capture_xcomposite_unload_cuda_graphics(cap_xcomp);
+ gsr_cuda_unload(&cap_xcomp->cuda);
+}
+
+static void gsr_capture_xcomposite_cuda_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_tick(&cap_xcomp->xcomposite, video_codec_context);
+}
+
+static bool gsr_capture_xcomposite_cuda_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_should_stop(&cap_xcomp->xcomposite, err);
+}
+
+static int gsr_capture_xcomposite_cuda_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+
+ gsr_capture_xcomposite_capture(&cap_xcomp->xcomposite, frame);
+
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_xcomp->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width;
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_xcomp->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_xcomp->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_xcomp->cuda.cuStreamSynchronize(cap_xcomp->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_cuda_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ if(cap->priv) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_xcomposite_cuda_create(const gsr_capture_xcomposite_cuda_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_cuda_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_xcomposite_cuda *cap_xcomp = calloc(1, sizeof(gsr_capture_xcomposite_cuda));
+ if(!cap_xcomp) {
+ free(cap);
+ return NULL;
+ }
+
+ gsr_capture_xcomposite_init(&cap_xcomp->xcomposite, &params->base);
+ cap_xcomp->overclock = params->overclock;
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_xcomposite_cuda_start,
+ .tick = gsr_capture_xcomposite_cuda_tick,
+ .should_stop = gsr_capture_xcomposite_cuda_should_stop,
+ .capture = gsr_capture_xcomposite_cuda_capture,
+ .capture_end = NULL,
+ .destroy = gsr_capture_xcomposite_cuda_destroy,
+ .priv = cap_xcomp
+ };
+
+ return cap;
+}
diff --git a/src/capture/xcomposite_vaapi.c b/src/capture/xcomposite_vaapi.c
new file mode 100644
index 0000000..8c9c56c
--- /dev/null
+++ b/src/capture/xcomposite_vaapi.c
@@ -0,0 +1,109 @@
+#include "../../include/capture/xcomposite_vaapi.h"
+#include "../../include/capture/xcomposite.h"
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_xcomposite xcomposite;
+
+ VADisplay va_dpy;
+ VADRMPRIMESurfaceDescriptor prime;
+} gsr_capture_xcomposite_vaapi;
+
+static void gsr_capture_xcomposite_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_xcomposite_vaapi_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+
+ const int res = gsr_capture_xcomposite_start(&cap_xcomp->xcomposite, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!drm_create_codec_context(cap_xcomp->xcomposite.params.egl->card_path, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_xcomp->va_dpy)) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!gsr_capture_base_setup_vaapi_textures(&cap_xcomp->xcomposite.base, frame, cap_xcomp->va_dpy, &cap_xcomp->prime, cap_xcomp->xcomposite.params.color_range)) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_vaapi_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_tick(&cap_xcomp->xcomposite, video_codec_context);
+}
+
+static bool gsr_capture_xcomposite_vaapi_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_should_stop(&cap_xcomp->xcomposite, err);
+}
+
+static int gsr_capture_xcomposite_vaapi_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_capture(&cap_xcomp->xcomposite, frame);
+}
+
+static void gsr_capture_xcomposite_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+
+ for(uint32_t i = 0; i < cap_xcomp->prime.num_objects; ++i) {
+ if(cap_xcomp->prime.objects[i].fd > 0) {
+ close(cap_xcomp->prime.objects[i].fd);
+ cap_xcomp->prime.objects[i].fd = 0;
+ }
+ }
+
+ gsr_capture_xcomposite_stop(&cap_xcomp->xcomposite);
+}
+
+static void gsr_capture_xcomposite_vaapi_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ if(cap->priv) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_xcomposite_vaapi_create(const gsr_capture_xcomposite_vaapi_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_vaapi_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_xcomposite_vaapi *cap_xcomp = calloc(1, sizeof(gsr_capture_xcomposite_vaapi));
+ if(!cap_xcomp) {
+ free(cap);
+ return NULL;
+ }
+
+ gsr_capture_xcomposite_init(&cap_xcomp->xcomposite, &params->base);
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_xcomposite_vaapi_start,
+ .tick = gsr_capture_xcomposite_vaapi_tick,
+ .should_stop = gsr_capture_xcomposite_vaapi_should_stop,
+ .capture = gsr_capture_xcomposite_vaapi_capture,
+ .capture_end = NULL,
+ .destroy = gsr_capture_xcomposite_vaapi_destroy,
+ .priv = cap_xcomp
+ };
+
+ return cap;
+}
diff --git a/src/color_conversion.c b/src/color_conversion.c
new file mode 100644
index 0000000..cd0397e
--- /dev/null
+++ b/src/color_conversion.c
@@ -0,0 +1,469 @@
+#include "../include/color_conversion.h"
+#include "../include/egl.h"
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+/* TODO: highp instead of mediump? */
+
+#define MAX_SHADERS 4
+#define MAX_FRAMEBUFFERS 2
+
+static float abs_f(float v) {
+ return v >= 0.0f ? v : -v;
+}
+
+#define ROTATE_Z "mat4 rotate_z(in float angle) {\n" \
+ " return mat4(cos(angle), -sin(angle), 0.0, 0.0,\n" \
+ " sin(angle), cos(angle), 0.0, 0.0,\n" \
+ " 0.0, 0.0, 1.0, 0.0,\n" \
+ " 0.0, 0.0, 0.0, 1.0);\n" \
+ "}\n"
+
+/* https://en.wikipedia.org/wiki/YCbCr, see study/color_space_transform_matrix.png */
+
+/* ITU-R BT2020, full */
+/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-2-201510-I!!PDF-E.pdf */
+#define RGB_TO_P010_FULL "const mat4 RGBtoYUV = mat4(0.262700, -0.139630, 0.500000, 0.000000,\n" \
+ " 0.678000, -0.360370, -0.459786, 0.000000,\n" \
+ " 0.059300, 0.500000, -0.040214, 0.000000,\n" \
+ " 0.000000, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT2020, limited (full multiplied by (235-16)/255, adding 16/255 to luma) */
+#define RGB_TO_P010_LIMITED "const mat4 RGBtoYUV = mat4(0.225613, -0.119918, 0.429412, 0.000000,\n" \
+ " 0.582282, -0.309494, -0.394875, 0.000000,\n" \
+ " 0.050928, 0.429412, -0.034537, 0.000000,\n" \
+ " 0.062745, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT709, full, custom values: 0.2110 0.7110 0.0710 */
+/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf */
+#define RGB_TO_NV12_FULL "const mat4 RGBtoYUV = mat4(0.211000, -0.113563, 0.500000, 0.000000,\n" \
+ " 0.711000, -0.382670, -0.450570, 0.000000,\n" \
+ " 0.071000, 0.500000, -0.044994, 0.000000,\n" \
+ " 0.000000, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT709, limited, custom values: 0.2100 0.7100 0.0700 (full multiplied by (235-16)/255, adding 16/255 to luma) */
+#define RGB_TO_NV12_LIMITED "const mat4 RGBtoYUV = mat4(0.180353, -0.096964, 0.429412, 0.000000,\n" \
+ " 0.609765, -0.327830, -0.385927, 0.000000,\n" \
+ " 0.060118, 0.429412, -0.038049, 0.000000,\n" \
+ " 0.062745, 0.500000, 0.500000, 1.000000);"
+
+static const char* color_format_range_get_transform_matrix(gsr_destination_color color_format, gsr_color_range color_range) {
+ switch(color_format) {
+ case GSR_DESTINATION_COLOR_NV12: {
+ switch(color_range) {
+ case GSR_COLOR_RANGE_LIMITED:
+ return RGB_TO_NV12_LIMITED;
+ case GSR_COLOR_RANGE_FULL:
+ return RGB_TO_NV12_FULL;
+ }
+ break;
+ }
+ case GSR_DESTINATION_COLOR_P010: {
+ switch(color_range) {
+ case GSR_COLOR_RANGE_LIMITED:
+ return RGB_TO_P010_LIMITED;
+ case GSR_COLOR_RANGE_FULL:
+ return RGB_TO_P010_FULL;
+ }
+ break;
+ }
+ default:
+ return NULL;
+ }
+ return NULL;
+}
+
+static int load_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+ const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
+
+ char vertex_shader[2048];
+ snprintf(vertex_shader, sizeof(vertex_shader),
+ "#version 300 es \n"
+ "in vec2 pos; \n"
+ "in vec2 texcoords; \n"
+ "out vec2 texcoords_out; \n"
+ "uniform vec2 offset; \n"
+ "uniform float rotation; \n"
+ ROTATE_Z
+ "void main() \n"
+ "{ \n"
+ " texcoords_out = (vec4(texcoords.x - 0.5, texcoords.y - 0.5, 0.0, 0.0) * rotate_z(rotation)).xy + vec2(0.5, 0.5); \n"
+ " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n"
+ "} \n");
+
+ char fragment_shader[2048];
+ if(external_texture) {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "#extension GL_OES_EGL_image_external : enable \n"
+ "#extension GL_OES_EGL_image_external_essl3 : require \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform samplerExternalOES tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ } else {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform sampler2D tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ }
+
+ if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
+ return -1;
+
+ gsr_shader_bind_attribute_location(shader, "pos", 0);
+ gsr_shader_bind_attribute_location(shader, "texcoords", 1);
+ uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
+ uniforms->rotation = egl->glGetUniformLocation(shader->program_id, "rotation");
+ return 0;
+}
+
+static unsigned int load_shader_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+ const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
+
+ char vertex_shader[2048];
+ snprintf(vertex_shader, sizeof(vertex_shader),
+ "#version 300 es \n"
+ "in vec2 pos; \n"
+ "in vec2 texcoords; \n"
+ "out vec2 texcoords_out; \n"
+ "uniform vec2 offset; \n"
+ "uniform float rotation; \n"
+ ROTATE_Z
+ "void main() \n"
+ "{ \n"
+ " texcoords_out = (vec4(texcoords.x - 0.5, texcoords.y - 0.5, 0.0, 0.0) * rotate_z(rotation)).xy + vec2(0.5, 0.5); \n"
+ " gl_Position = (vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0)) * vec4(0.5, 0.5, 1.0, 1.0) - vec4(0.5, 0.5, 0.0, 0.0); \n"
+ "} \n");
+
+ char fragment_shader[2048];
+ if(external_texture) {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "#extension GL_OES_EGL_image_external : enable \n"
+ "#extension GL_OES_EGL_image_external_essl3 : require \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform samplerExternalOES tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ } else {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform sampler2D tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ }
+
+ if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
+ return -1;
+
+ gsr_shader_bind_attribute_location(shader, "pos", 0);
+ gsr_shader_bind_attribute_location(shader, "texcoords", 1);
+ uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
+ uniforms->rotation = egl->glGetUniformLocation(shader->program_id, "rotation");
+ return 0;
+}
+
+static int load_framebuffers(gsr_color_conversion *self) {
+ /* TODO: Only generate the necessary amount of framebuffers (self->params.num_destination_textures) */
+ const unsigned int draw_buffer = GL_COLOR_ATTACHMENT0;
+ self->params.egl->glGenFramebuffers(MAX_FRAMEBUFFERS, self->framebuffers);
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[0], 0);
+ self->params.egl->glDrawBuffers(1, &draw_buffer);
+ if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for Y\n");
+ goto err;
+ }
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[1], 0);
+ self->params.egl->glDrawBuffers(1, &draw_buffer);
+ if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for UV\n");
+ goto err;
+ }
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return 0;
+
+ err:
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return -1;
+}
+
+static int create_vertices(gsr_color_conversion *self) {
+ self->params.egl->glGenVertexArrays(1, &self->vertex_array_object_id);
+ self->params.egl->glBindVertexArray(self->vertex_array_object_id);
+
+ self->params.egl->glGenBuffers(1, &self->vertex_buffer_object_id);
+ self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id);
+ self->params.egl->glBufferData(GL_ARRAY_BUFFER, 24 * sizeof(float), NULL, GL_STREAM_DRAW);
+
+ self->params.egl->glEnableVertexAttribArray(0);
+ self->params.egl->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+
+ self->params.egl->glEnableVertexAttribArray(1);
+ self->params.egl->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+
+ self->params.egl->glBindVertexArray(0);
+ return 0;
+}
+
+int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params) {
+ assert(params);
+ assert(params->egl);
+ memset(self, 0, sizeof(*self));
+ self->params.egl = params->egl;
+ self->params = *params;
+
+ switch(params->destination_color) {
+ case GSR_DESTINATION_COLOR_NV12:
+ case GSR_DESTINATION_COLOR_P010: {
+ if(self->params.num_destination_textures != 2) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)\n", self->params.num_destination_textures);
+ return -1;
+ }
+
+ if(load_shader_y(&self->shaders[0], self->params.egl, &self->uniforms[0], params->destination_color, params->color_range, false) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y shader\n");
+ goto err;
+ }
+
+ if(load_shader_uv(&self->shaders[1], self->params.egl, &self->uniforms[1], params->destination_color, params->color_range, false) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV shader\n");
+ goto err;
+ }
+
+ if(self->params.load_external_image_shader) {
+ if(load_shader_y(&self->shaders[2], self->params.egl, &self->uniforms[2], params->destination_color, params->color_range, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y shader\n");
+ goto err;
+ }
+
+ if(load_shader_uv(&self->shaders[3], self->params.egl, &self->uniforms[3], params->destination_color, params->color_range, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV shader\n");
+ goto err;
+ }
+ }
+ break;
+ }
+ }
+
+ if(load_framebuffers(self) != 0)
+ goto err;
+
+ if(create_vertices(self) != 0)
+ goto err;
+
+ return 0;
+
+ err:
+ gsr_color_conversion_deinit(self);
+ return -1;
+}
+
+void gsr_color_conversion_deinit(gsr_color_conversion *self) {
+ if(!self->params.egl)
+ return;
+
+ if(self->vertex_buffer_object_id) {
+ self->params.egl->glDeleteBuffers(1, &self->vertex_buffer_object_id);
+ self->vertex_buffer_object_id = 0;
+ }
+
+ if(self->vertex_array_object_id) {
+ self->params.egl->glDeleteVertexArrays(1, &self->vertex_array_object_id);
+ self->vertex_array_object_id = 0;
+ }
+
+ self->params.egl->glDeleteFramebuffers(MAX_FRAMEBUFFERS, self->framebuffers);
+ for(int i = 0; i < MAX_FRAMEBUFFERS; ++i) {
+ self->framebuffers[i] = 0;
+ }
+
+ for(int i = 0; i < MAX_SHADERS; ++i) {
+ gsr_shader_deinit(&self->shaders[i]);
+ }
+
+ self->params.egl = NULL;
+}
+
+static void gsr_color_conversion_swizzle_texture_source(gsr_color_conversion *self) {
+ if(self->params.source_color == GSR_SOURCE_COLOR_BGR) {
+ const int swizzle_mask[] = { GL_BLUE, GL_GREEN, GL_RED, 1 };
+ self->params.egl->glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask);
+ }
+}
+
+static void gsr_color_conversion_swizzle_reset(gsr_color_conversion *self) {
+ if(self->params.source_color == GSR_SOURCE_COLOR_BGR) {
+ const int swizzle_mask[] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA };
+ self->params.egl->glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask);
+ }
+}
+
+/* |source_pos| is in pixel coordinates and |source_size| */
+void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i source_pos, vec2i source_size, vec2i texture_pos, vec2i texture_size, float rotation, bool external_texture) {
+ // TODO: Remove this crap
+ rotation = M_PI*2.0f - rotation;
+
+ /* TODO: Do not call this every frame? */
+ vec2i dest_texture_size = {0, 0};
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->params.destination_textures[0]);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &dest_texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &dest_texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ const int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D;
+
+ self->params.egl->glBindTexture(texture_target, texture_id);
+
+ vec2i source_texture_size = {0, 0};
+ if(external_texture) {
+ source_texture_size = source_size;
+ } else {
+ /* TODO: Do not call this every frame? */
+ self->params.egl->glGetTexLevelParameteriv(texture_target, 0, GL_TEXTURE_WIDTH, &source_texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(texture_target, 0, GL_TEXTURE_HEIGHT, &source_texture_size.y);
+ }
+
+ // TODO: Remove this crap
+ if(abs_f(M_PI * 0.5f - rotation) <= 0.001f || abs_f(M_PI * 1.5f - rotation) <= 0.001f) {
+ float tmp = source_texture_size.x;
+ source_texture_size.x = source_texture_size.y;
+ source_texture_size.y = tmp;
+ }
+
+ const vec2f pos_norm = {
+ ((float)source_pos.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f,
+ ((float)source_pos.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f,
+ };
+
+ const vec2f size_norm = {
+ ((float)source_size.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f,
+ ((float)source_size.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f,
+ };
+
+ const vec2f texture_pos_norm = {
+ (float)texture_pos.x / (source_texture_size.x == 0 ? 1.0f : (float)source_texture_size.x),
+ (float)texture_pos.y / (source_texture_size.y == 0 ? 1.0f : (float)source_texture_size.y),
+ };
+
+ const vec2f texture_size_norm = {
+ (float)texture_size.x / (source_texture_size.x == 0 ? 1.0f : (float)source_texture_size.x),
+ (float)texture_size.y / (source_texture_size.y == 0 ? 1.0f : (float)source_texture_size.y),
+ };
+
+ const float vertices[] = {
+ -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y,
+ -1.0f + 0.0f, -1.0f + 0.0f, texture_pos_norm.x, texture_pos_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y,
+
+ -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y + texture_size_norm.y
+ };
+
+ gsr_color_conversion_swizzle_texture_source(self);
+
+ self->params.egl->glBindVertexArray(self->vertex_array_object_id);
+ self->params.egl->glViewport(0, 0, dest_texture_size.x, dest_texture_size.y);
+
+ /* TODO: this, also cleanup */
+ //self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id);
+ self->params.egl->glBufferSubData(GL_ARRAY_BUFFER, 0, 24 * sizeof(float), vertices);
+
+ {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example)
+
+ const int shader_index = external_texture ? 2 : 0;
+ gsr_shader_use(&self->shaders[shader_index]);
+ self->params.egl->glUniform1f(self->uniforms[shader_index].rotation, rotation);
+ self->params.egl->glUniform2f(self->uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
+ }
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+
+ const int shader_index = external_texture ? 3 : 1;
+ gsr_shader_use(&self->shaders[shader_index]);
+ self->params.egl->glUniform1f(self->uniforms[shader_index].rotation, rotation);
+ self->params.egl->glUniform2f(self->uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
+ }
+
+ self->params.egl->glBindVertexArray(0);
+ gsr_shader_use_none(&self->shaders[0]);
+ self->params.egl->glBindTexture(texture_target, 0);
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ gsr_color_conversion_swizzle_reset(self);
+}
+
+void gsr_color_conversion_clear(gsr_color_conversion *self) {
+ float color1[4] = {0.0f, 0.0f, 0.0f, 1.0f};
+ float color2[4] = {0.0f, 0.0f, 0.0f, 1.0f};
+
+ switch(self->params.destination_color) {
+ case GSR_DESTINATION_COLOR_NV12:
+ case GSR_DESTINATION_COLOR_P010: {
+ color2[0] = 0.5f;
+ color2[1] = 0.5f;
+ color2[2] = 0.0f;
+ color2[3] = 1.0f;
+ break;
+ }
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ self->params.egl->glClearColor(color1[0], color1[1], color1[2], color1[3]);
+ self->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ self->params.egl->glClearColor(color2[0], color2[1], color2[2], color2[3]);
+ self->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
diff --git a/src/cuda.c b/src/cuda.c
new file mode 100644
index 0000000..6d685b5
--- /dev/null
+++ b/src/cuda.c
@@ -0,0 +1,117 @@
+#include "../include/cuda.h"
+#include "../include/library_loader.h"
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <assert.h>
+
+bool gsr_cuda_load(gsr_cuda *self, Display *display, bool do_overclock) {
+ memset(self, 0, sizeof(gsr_cuda));
+ self->do_overclock = do_overclock;
+
+ dlerror(); /* clear */
+ void *lib = dlopen("libcuda.so.1", RTLD_LAZY);
+ if(!lib) {
+ lib = dlopen("libcuda.so", RTLD_LAZY);
+ if(!lib) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s\n", dlerror());
+ return false;
+ }
+ }
+
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->cuInit, "cuInit" },
+ { (void**)&self->cuDeviceGetCount, "cuDeviceGetCount" },
+ { (void**)&self->cuDeviceGet, "cuDeviceGet" },
+ { (void**)&self->cuCtxCreate_v2, "cuCtxCreate_v2" },
+ { (void**)&self->cuCtxDestroy_v2, "cuCtxDestroy_v2" },
+ { (void**)&self->cuCtxPushCurrent_v2, "cuCtxPushCurrent_v2" },
+ { (void**)&self->cuCtxPopCurrent_v2, "cuCtxPopCurrent_v2" },
+ { (void**)&self->cuGetErrorString, "cuGetErrorString" },
+ { (void**)&self->cuMemcpy2D_v2, "cuMemcpy2D_v2" },
+ { (void**)&self->cuMemcpy2DAsync_v2, "cuMemcpy2DAsync_v2" },
+ { (void**)&self->cuStreamSynchronize, "cuStreamSynchronize" },
+
+ { (void**)&self->cuGraphicsGLRegisterImage, "cuGraphicsGLRegisterImage" },
+ { (void**)&self->cuGraphicsEGLRegisterImage, "cuGraphicsEGLRegisterImage" },
+ { (void**)&self->cuGraphicsResourceSetMapFlags, "cuGraphicsResourceSetMapFlags" },
+ { (void**)&self->cuGraphicsMapResources, "cuGraphicsMapResources" },
+ { (void**)&self->cuGraphicsUnmapResources, "cuGraphicsUnmapResources" },
+ { (void**)&self->cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource" },
+ { (void**)&self->cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray" },
+
+ { NULL, NULL }
+ };
+
+ CUresult res;
+
+ if(!dlsym_load_list(lib, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1\n");
+ goto fail;
+ }
+
+ res = self->cuInit(0);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: cuInit failed, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ int nGpu = 0;
+ self->cuDeviceGetCount(&nGpu);
+ if(nGpu <= 0) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: no cuda supported devices found\n");
+ goto fail;
+ }
+
+ CUdevice cu_dev;
+ res = self->cuDeviceGet(&cu_dev, 0);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ res = self->cuCtxCreate_v2(&self->cu_ctx, CU_CTX_SCHED_AUTO, cu_dev);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ if(self->do_overclock) {
+ assert(display);
+ if(gsr_overclock_load(&self->overclock, display))
+ gsr_overclock_start(&self->overclock);
+ else
+ fprintf(stderr, "gsr warning: gsr_cuda_load: failed to load xnvctrl, failed to overclock memory transfer rate\n");
+ }
+
+ self->library = lib;
+ return true;
+
+ fail:
+ dlclose(lib);
+ memset(self, 0, sizeof(gsr_cuda));
+ return false;
+}
+
+void gsr_cuda_unload(gsr_cuda *self) {
+ if(self->do_overclock && self->overclock.xnvctrl.library) {
+ gsr_overclock_stop(&self->overclock);
+ gsr_overclock_unload(&self->overclock);
+ }
+
+ if(self->library) {
+ if(self->cu_ctx) {
+ self->cuCtxDestroy_v2(self->cu_ctx);
+ self->cu_ctx = 0;
+ }
+ dlclose(self->library);
+ }
+
+ memset(self, 0, sizeof(gsr_cuda));
+}
diff --git a/src/cursor.c b/src/cursor.c
new file mode 100644
index 0000000..737c33b
--- /dev/null
+++ b/src/cursor.c
@@ -0,0 +1,127 @@
+#include "../include/cursor.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include <X11/extensions/Xfixes.h>
+
+static bool gsr_cursor_set_from_x11_cursor_image(gsr_cursor *self, XFixesCursorImage *x11_cursor_image) {
+ uint8_t *cursor_data = NULL;
+ uint8_t *out = NULL;
+
+ if(!x11_cursor_image)
+ goto err;
+
+ if(!x11_cursor_image->pixels)
+ goto err;
+
+ self->hotspot.x = x11_cursor_image->xhot;
+ self->hotspot.y = x11_cursor_image->yhot;
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+
+ self->size.x = x11_cursor_image->width;
+ self->size.y = x11_cursor_image->height;
+ const unsigned long *pixels = x11_cursor_image->pixels;
+ cursor_data = malloc(self->size.x * self->size.y * 4);
+ if(!cursor_data)
+ goto err;
+ out = cursor_data;
+ /* Un-premultiply alpha */
+ for(int y = 0; y < self->size.y; ++y) {
+ for(int x = 0; x < self->size.x; ++x) {
+ uint32_t pixel = *pixels++;
+ uint8_t *in = (uint8_t*)&pixel;
+ uint8_t alpha = in[3];
+ if(alpha == 0)
+ alpha = 1;
+
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = *in++;
+ }
+ }
+
+ self->egl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, cursor_data);
+ free(cursor_data);
+
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ XFree(x11_cursor_image);
+ return true;
+
+ err:
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ if(x11_cursor_image)
+ XFree(x11_cursor_image);
+ return false;
+}
+
+int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) {
+ int x_fixes_error_base = 0;
+
+ assert(egl);
+ assert(display);
+ memset(self, 0, sizeof(*self));
+ self->egl = egl;
+ self->display = display;
+
+ self->x_fixes_event_base = 0;
+ if(!XFixesQueryExtension(self->display, &self->x_fixes_event_base, &x_fixes_error_base)) {
+ fprintf(stderr, "gsr error: gsr_cursor_init: your X11 server is missing the XFixes extension\n");
+ gsr_cursor_deinit(self);
+ return -1;
+ }
+
+ self->egl->glGenTextures(1, &self->texture_id);
+
+ XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), XFixesDisplayCursorNotifyMask);
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ self->cursor_image_set = true;
+
+ return 0;
+}
+
+void gsr_cursor_deinit(gsr_cursor *self) {
+ if(!self->egl)
+ return;
+
+ if(self->texture_id) {
+ self->egl->glDeleteTextures(1, &self->texture_id);
+ self->texture_id = 0;
+ }
+
+ XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), 0);
+
+ self->display = NULL;
+ self->egl = NULL;
+}
+
+void gsr_cursor_update(gsr_cursor *self, XEvent *xev) {
+ if(xev->type == self->x_fixes_event_base + XFixesCursorNotify) {
+ XFixesCursorNotifyEvent *cursor_notify_event = (XFixesCursorNotifyEvent*)xev;
+ if(cursor_notify_event->subtype == XFixesDisplayCursorNotify && cursor_notify_event->window == DefaultRootWindow(self->display)) {
+ self->cursor_image_set = true;
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ }
+ }
+
+ if(!self->cursor_image_set) {
+ self->cursor_image_set = true;
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ }
+}
+
+void gsr_cursor_tick(gsr_cursor *self, Window relative_to) {
+ /* TODO: Use XInput2 instead. However that doesn't work when the pointer is grabbed. Maybe check for focused window change and XSelectInput PointerMask */
+ Window dummy_window;
+ int dummy_i;
+ unsigned int dummy_u;
+ XQueryPointer(self->display, relative_to, &dummy_window, &dummy_window, &dummy_i, &dummy_i, &self->position.x, &self->position.y, &dummy_u);
+}
diff --git a/src/egl.c b/src/egl.c
new file mode 100644
index 0000000..2ad4f85
--- /dev/null
+++ b/src/egl.c
@@ -0,0 +1,651 @@
+#include "../include/egl.h"
+#include "../include/library_loader.h"
+#include "../include/utils.h"
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <dlfcn.h>
+#include <assert.h>
+
+#include <wayland-client.h>
+#include <wayland-egl.h>
+#include <unistd.h>
+#include <sys/capability.h>
+
+// TODO: rename gsr_egl to something else since this includes both egl and eglx and in the future maybe vulkan too
+
+// TODO: Move this shit to a separate wayland file, and have a separate file for x11.
+
+static void output_handle_geometry(void *data, struct wl_output *wl_output,
+ int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
+ int32_t subpixel, const char *make, const char *model,
+ int32_t transform) {
+ (void)wl_output;
+ (void)phys_width;
+ (void)phys_height;
+ (void)subpixel;
+ (void)make;
+ (void)model;
+ gsr_wayland_output *gsr_output = data;
+ gsr_output->pos.x = x;
+ gsr_output->pos.y = y;
+ gsr_output->transform = transform;
+}
+
+static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
+ (void)wl_output;
+ (void)flags;
+ (void)refresh;
+ gsr_wayland_output *gsr_output = data;
+ gsr_output->size.x = width;
+ gsr_output->size.y = height;
+}
+
+static void output_handle_done(void *data, struct wl_output *wl_output) {
+ (void)data;
+ (void)wl_output;
+}
+
+static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
+ (void)data;
+ (void)wl_output;
+ (void)factor;
+}
+
+static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
+ (void)wl_output;
+ gsr_wayland_output *gsr_output = data;
+ if(gsr_output->name) {
+ free(gsr_output->name);
+ gsr_output->name = NULL;
+ }
+ gsr_output->name = strdup(name);
+}
+
+static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
+ (void)data;
+ (void)wl_output;
+ (void)description;
+}
+
+static const struct wl_output_listener output_listener = {
+ .geometry = output_handle_geometry,
+ .mode = output_handle_mode,
+ .done = output_handle_done,
+ .scale = output_handle_scale,
+ .name = output_handle_name,
+ .description = output_handle_description,
+};
+
+static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
+ (void)version;
+ gsr_egl *egl = data;
+ if (strcmp(interface, "wl_compositor") == 0) {
+ if(egl->wayland.compositor) {
+ wl_compositor_destroy(egl->wayland.compositor);
+ egl->wayland.compositor = NULL;
+ }
+ egl->wayland.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1);
+ } else if(strcmp(interface, wl_output_interface.name) == 0) {
+ if(version < 4) {
+ fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor. Using KMS capture instead\n");
+ return;
+ }
+
+ if(egl->wayland.num_outputs == GSR_MAX_OUTPUTS) {
+ fprintf(stderr, "gsr warning: reached maximum outputs (32), ignoring output %u\n", name);
+ return;
+ }
+
+ gsr_wayland_output *gsr_output = &egl->wayland.outputs[egl->wayland.num_outputs];
+ egl->wayland.num_outputs++;
+ *gsr_output = (gsr_wayland_output) {
+ .wl_name = name,
+ .output = wl_registry_bind(registry, name, &wl_output_interface, 4),
+ .pos = { .x = 0, .y = 0 },
+ .size = { .x = 0, .y = 0 },
+ .transform = 0,
+ .name = NULL,
+ };
+ wl_output_add_listener(gsr_output->output, &output_listener, gsr_output);
+ }
+}
+
+static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
+ (void)data;
+ (void)registry;
+ (void)name;
+}
+
+static struct wl_registry_listener registry_listener = {
+ .global = registry_add_object,
+ .global_remove = registry_remove_object,
+};
+
+static void reset_cap_nice(void) {
+ cap_t caps = cap_get_proc();
+ if(!caps)
+ return;
+
+ const cap_value_t cap_to_remove = CAP_SYS_NICE;
+ cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap_to_remove, CAP_CLEAR);
+ cap_set_flag(caps, CAP_PERMITTED, 1, &cap_to_remove, CAP_CLEAR);
+ cap_set_proc(caps);
+ cap_free(caps);
+}
+
+#define GLX_DRAWABLE_TYPE 0x8010
+#define GLX_RENDER_TYPE 0x8011
+#define GLX_RGBA_BIT 0x00000001
+#define GLX_WINDOW_BIT 0x00000001
+#define GLX_PIXMAP_BIT 0x00000002
+#define GLX_BIND_TO_TEXTURE_RGBA_EXT 0x20D1
+#define GLX_BIND_TO_TEXTURE_TARGETS_EXT 0x20D3
+#define GLX_TEXTURE_2D_BIT_EXT 0x00000002
+#define GLX_DOUBLEBUFFER 5
+#define GLX_RED_SIZE 8
+#define GLX_GREEN_SIZE 9
+#define GLX_BLUE_SIZE 10
+#define GLX_ALPHA_SIZE 11
+#define GLX_DEPTH_SIZE 12
+#define GLX_RGBA_TYPE 0x8014
+
+#define GLX_CONTEXT_PRIORITY_LEVEL_EXT 0x3100
+#define GLX_CONTEXT_PRIORITY_HIGH_EXT 0x3101
+#define GLX_CONTEXT_PRIORITY_MEDIUM_EXT 0x3102
+#define GLX_CONTEXT_PRIORITY_LOW_EXT 0x3103
+
+static GLXFBConfig glx_fb_config_choose(gsr_egl *self) {
+ const int glx_visual_attribs[] = {
+ GLX_RENDER_TYPE, GLX_RGBA_BIT,
+ GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
+ // TODO:
+ //GLX_BIND_TO_TEXTURE_RGBA_EXT, 1,
+ //GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT,
+ GLX_DOUBLEBUFFER, True,
+ GLX_RED_SIZE, 8,
+ GLX_GREEN_SIZE, 8,
+ GLX_BLUE_SIZE, 8,
+ GLX_ALPHA_SIZE, 0,
+ GLX_DEPTH_SIZE, 0,
+ None, None
+ };
+
+ // TODO: Cleanup
+ int c = 0;
+ GLXFBConfig *fb_configs = self->glXChooseFBConfig(self->x11.dpy, DefaultScreen(self->x11.dpy), glx_visual_attribs, &c);
+ if(c == 0 || !fb_configs)
+ return NULL;
+
+ return fb_configs[0];
+}
+
+// TODO: Create egl context without surface (in other words, x11/wayland agnostic, doesn't require x11/wayland dependency)
+static bool gsr_egl_create_window(gsr_egl *self, bool wayland) {
+ EGLConfig ecfg;
+ int32_t num_config = 0;
+
+ const int32_t attr[] = {
+ EGL_BUFFER_SIZE, 24,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
+ EGL_NONE, EGL_NONE
+ };
+
+ const int32_t ctxattr[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, /* requires cap_sys_nice, ignored otherwise */
+ EGL_NONE, EGL_NONE
+ };
+
+ if(wayland) {
+ self->wayland.dpy = wl_display_connect(NULL);
+ if(!self->wayland.dpy) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: wl_display_connect failed\n");
+ goto fail;
+ }
+
+ self->wayland.registry = wl_display_get_registry(self->wayland.dpy); // TODO: Error checking
+ wl_registry_add_listener(self->wayland.registry, &registry_listener, self); // TODO: Error checking
+
+ // Fetch globals
+ wl_display_roundtrip(self->wayland.dpy);
+
+ // Fetch wl_output
+ wl_display_roundtrip(self->wayland.dpy);
+
+ if(!self->wayland.compositor) {
+ fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to find compositor\n");
+ goto fail;
+ }
+ } else {
+ self->x11.window = XCreateWindow(self->x11.dpy, DefaultRootWindow(self->x11.dpy), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL);
+
+ if(!self->x11.window) {
+ fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to create gl window\n");
+ goto fail;
+ }
+ }
+
+ self->eglBindAPI(EGL_OPENGL_API);
+
+ self->egl_display = self->eglGetDisplay(self->wayland.dpy ? (EGLNativeDisplayType)self->wayland.dpy : (EGLNativeDisplayType)self->x11.dpy);
+ if(!self->egl_display) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n");
+ goto fail;
+ }
+
+ if(!self->eglInitialize(self->egl_display, NULL, NULL)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n");
+ goto fail;
+ }
+
+ if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n");
+ goto fail;
+ }
+
+ self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr);
+ if(!self->egl_context) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n");
+ goto fail;
+ }
+
+ if(wayland) {
+ self->wayland.surface = wl_compositor_create_surface(self->wayland.compositor);
+ self->wayland.window = wl_egl_window_create(self->wayland.surface, 16, 16);
+ self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->wayland.window, NULL);
+ } else {
+ self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->x11.window, NULL);
+ }
+
+ if(!self->egl_surface) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n");
+ goto fail;
+ }
+
+ if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make egl context current\n");
+ goto fail;
+ }
+
+ reset_cap_nice();
+ return true;
+
+ fail:
+ reset_cap_nice();
+ gsr_egl_unload(self);
+ return false;
+}
+
+static bool gsr_egl_switch_to_glx_context(gsr_egl *self) {
+ // TODO: Cleanup
+
+ if(self->egl_context) {
+ self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL);
+ self->eglDestroyContext(self->egl_display, self->egl_context);
+ self->egl_context = NULL;
+ }
+
+ if(self->egl_surface) {
+ self->eglDestroySurface(self->egl_display, self->egl_surface);
+ self->egl_surface = NULL;
+ }
+
+ if(self->egl_display) {
+ self->eglTerminate(self->egl_display);
+ self->egl_display = NULL;
+ }
+
+ self->glx_fb_config = glx_fb_config_choose(self);
+ if(!self->glx_fb_config) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a suitable fb config\n");
+ goto fail;
+ }
+
+ // TODO:
+ //self->glx_context = self->glXCreateContextAttribsARB(self->x11.dpy, self->glx_fb_config, NULL, True, context_attrib_list);
+ self->glx_context = self->glXCreateNewContext(self->x11.dpy, self->glx_fb_config, GLX_RGBA_TYPE, NULL, True);
+ if(!self->glx_context) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create glx context\n");
+ goto fail;
+ }
+
+ if(!self->glXMakeContextCurrent(self->x11.dpy, self->x11.window, self->x11.window, self->glx_context)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make glx context current\n");
+ goto fail;
+ }
+
+ return true;
+
+ fail:
+ if(self->glx_context) {
+ self->glXMakeContextCurrent(self->x11.dpy, None, None, NULL);
+ self->glXDestroyContext(self->x11.dpy, self->glx_context);
+ self->glx_context = NULL;
+ self->glx_fb_config = NULL;
+ }
+ return false;
+}
+
+static bool gsr_egl_load_egl(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->eglGetError, "eglGetError" },
+ { (void**)&self->eglGetDisplay, "eglGetDisplay" },
+ { (void**)&self->eglInitialize, "eglInitialize" },
+ { (void**)&self->eglTerminate, "eglTerminate" },
+ { (void**)&self->eglChooseConfig, "eglChooseConfig" },
+ { (void**)&self->eglCreateWindowSurface, "eglCreateWindowSurface" },
+ { (void**)&self->eglCreateContext, "eglCreateContext" },
+ { (void**)&self->eglMakeCurrent, "eglMakeCurrent" },
+ { (void**)&self->eglCreateImage, "eglCreateImage" },
+ { (void**)&self->eglDestroyContext, "eglDestroyContext" },
+ { (void**)&self->eglDestroySurface, "eglDestroySurface" },
+ { (void**)&self->eglDestroyImage, "eglDestroyImage" },
+ { (void**)&self->eglSwapInterval, "eglSwapInterval" },
+ { (void**)&self->eglSwapBuffers, "eglSwapBuffers" },
+ { (void**)&self->eglBindAPI, "eglBindAPI" },
+ { (void**)&self->eglGetProcAddress, "eglGetProcAddress" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool gsr_egl_proc_load_egl(gsr_egl *self) {
+ self->eglExportDMABUFImageQueryMESA = (FUNC_eglExportDMABUFImageQueryMESA)self->eglGetProcAddress("eglExportDMABUFImageQueryMESA");
+ self->eglExportDMABUFImageMESA = (FUNC_eglExportDMABUFImageMESA)self->eglGetProcAddress("eglExportDMABUFImageMESA");
+ self->glEGLImageTargetTexture2DOES = (FUNC_glEGLImageTargetTexture2DOES)self->eglGetProcAddress("glEGLImageTargetTexture2DOES");
+ self->eglQueryDisplayAttribEXT = (FUNC_eglQueryDisplayAttribEXT)self->eglGetProcAddress("eglQueryDisplayAttribEXT");
+ self->eglQueryDeviceStringEXT = (FUNC_eglQueryDeviceStringEXT)self->eglGetProcAddress("eglQueryDeviceStringEXT");
+
+ if(!self->glEGLImageTargetTexture2DOES) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool gsr_egl_load_glx(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->glXGetProcAddress, "glXGetProcAddress" },
+ { (void**)&self->glXChooseFBConfig, "glXChooseFBConfig" },
+ { (void**)&self->glXMakeContextCurrent, "glXMakeContextCurrent" },
+ { (void**)&self->glXCreateNewContext, "glXCreateNewContext" },
+ { (void**)&self->glXDestroyContext, "glXDestroyContext" },
+ { (void**)&self->glXSwapBuffers, "glXSwapBuffers" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGLX.so.0\n");
+ return false;
+ }
+
+ self->glXCreateContextAttribsARB = (FUNC_glXCreateContextAttribsARB)self->glXGetProcAddress((const unsigned char*)"glXCreateContextAttribsARB");
+ if(!self->glXCreateContextAttribsARB) {
+ fprintf(stderr, "gsr error: gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB\n");
+ return false;
+ }
+
+ self->glXSwapIntervalEXT = (FUNC_glXSwapIntervalEXT)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalEXT");
+ self->glXSwapIntervalMESA = (FUNC_glXSwapIntervalMESA)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalMESA");
+ self->glXSwapIntervalSGI = (FUNC_glXSwapIntervalSGI)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalSGI");
+
+ return true;
+}
+
+static bool gsr_egl_load_gl(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->glGetError, "glGetError" },
+ { (void**)&self->glGetString, "glGetString" },
+ { (void**)&self->glFlush, "glFlush" },
+ { (void**)&self->glFinish, "glFinish" },
+ { (void**)&self->glClear, "glClear" },
+ { (void**)&self->glClearColor, "glClearColor" },
+ { (void**)&self->glGenTextures, "glGenTextures" },
+ { (void**)&self->glDeleteTextures, "glDeleteTextures" },
+ { (void**)&self->glBindTexture, "glBindTexture" },
+ { (void**)&self->glTexParameteri, "glTexParameteri" },
+ { (void**)&self->glTexParameteriv, "glTexParameteriv" },
+ { (void**)&self->glGetTexLevelParameteriv, "glGetTexLevelParameteriv" },
+ { (void**)&self->glTexImage2D, "glTexImage2D" },
+ { (void**)&self->glCopyImageSubData, "glCopyImageSubData" },
+ { (void**)&self->glClearTexImage, "glClearTexImage" },
+ { (void**)&self->glGenFramebuffers, "glGenFramebuffers" },
+ { (void**)&self->glBindFramebuffer, "glBindFramebuffer" },
+ { (void**)&self->glDeleteFramebuffers, "glDeleteFramebuffers" },
+ { (void**)&self->glViewport, "glViewport" },
+ { (void**)&self->glFramebufferTexture2D, "glFramebufferTexture2D" },
+ { (void**)&self->glDrawBuffers, "glDrawBuffers" },
+ { (void**)&self->glCheckFramebufferStatus, "glCheckFramebufferStatus" },
+ { (void**)&self->glBindBuffer, "glBindBuffer" },
+ { (void**)&self->glGenBuffers, "glGenBuffers" },
+ { (void**)&self->glBufferData, "glBufferData" },
+ { (void**)&self->glBufferSubData, "glBufferSubData" },
+ { (void**)&self->glDeleteBuffers, "glDeleteBuffers" },
+ { (void**)&self->glGenVertexArrays, "glGenVertexArrays" },
+ { (void**)&self->glBindVertexArray, "glBindVertexArray" },
+ { (void**)&self->glDeleteVertexArrays, "glDeleteVertexArrays" },
+ { (void**)&self->glCreateProgram, "glCreateProgram" },
+ { (void**)&self->glCreateShader, "glCreateShader" },
+ { (void**)&self->glAttachShader, "glAttachShader" },
+ { (void**)&self->glBindAttribLocation, "glBindAttribLocation" },
+ { (void**)&self->glCompileShader, "glCompileShader" },
+ { (void**)&self->glLinkProgram, "glLinkProgram" },
+ { (void**)&self->glShaderSource, "glShaderSource" },
+ { (void**)&self->glUseProgram, "glUseProgram" },
+ { (void**)&self->glGetProgramInfoLog, "glGetProgramInfoLog" },
+ { (void**)&self->glGetShaderiv, "glGetShaderiv" },
+ { (void**)&self->glGetShaderInfoLog, "glGetShaderInfoLog" },
+ { (void**)&self->glDeleteProgram, "glDeleteProgram" },
+ { (void**)&self->glDeleteShader, "glDeleteShader" },
+ { (void**)&self->glGetProgramiv, "glGetProgramiv" },
+ { (void**)&self->glVertexAttribPointer, "glVertexAttribPointer" },
+ { (void**)&self->glEnableVertexAttribArray, "glEnableVertexAttribArray" },
+ { (void**)&self->glDrawArrays, "glDrawArrays" },
+ { (void**)&self->glEnable, "glEnable" },
+ { (void**)&self->glBlendFunc, "glBlendFunc" },
+ { (void**)&self->glGetUniformLocation, "glGetUniformLocation" },
+ { (void**)&self->glUniform1f, "glUniform1f" },
+ { (void**)&self->glUniform2f, "glUniform2f" },
+ { (void**)&self->glDebugMessageCallback, "glDebugMessageCallback" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n");
+ return false;
+ }
+
+ return true;
+}
+
+// #define GL_DEBUG_TYPE_ERROR 0x824C
+// static void debug_callback( unsigned int source,
+// unsigned int type,
+// unsigned int id,
+// unsigned int severity,
+// int length,
+// const char* message,
+// const void* userParam )
+// {
+// (void)source;
+// (void)id;
+// (void)length;
+// (void)userParam;
+// fprintf( stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+// ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+// type, severity, message );
+// }
+
+bool gsr_egl_load(gsr_egl *self, Display *dpy, bool wayland, bool is_monitor_capture) {
+ (void)is_monitor_capture;
+ memset(self, 0, sizeof(gsr_egl));
+ self->x11.dpy = dpy;
+ self->context_type = GSR_GL_CONTEXT_TYPE_EGL;
+
+ dlerror(); /* clear */
+ self->egl_library = dlopen("libEGL.so.1", RTLD_LAZY);
+ if(!self->egl_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ self->glx_library = dlopen("libGLX.so.0", RTLD_LAZY);
+ if(!self->glx_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGLX.so.0, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ self->gl_library = dlopen("libGL.so.1", RTLD_LAZY);
+ if(!self->egl_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ if(!gsr_egl_load_egl(self, self->egl_library))
+ goto fail;
+
+ if(!gsr_egl_load_glx(self, self->glx_library))
+ goto fail;
+
+ if(!gsr_egl_load_gl(self, self->gl_library))
+ goto fail;
+
+ if(!gsr_egl_proc_load_egl(self))
+ goto fail;
+
+ if(!gsr_egl_create_window(self, wayland))
+ goto fail;
+
+ if(!gl_get_gpu_info(self, &self->gpu_info))
+ goto fail;
+
+ if(self->eglQueryDisplayAttribEXT && self->eglQueryDeviceStringEXT) {
+ intptr_t device = 0;
+ if(self->eglQueryDisplayAttribEXT(self->egl_display, EGL_DEVICE_EXT, &device) && device)
+ self->dri_card_path = self->eglQueryDeviceStringEXT((void*)device, EGL_DRM_DEVICE_FILE_EXT);
+ }
+
+ /* Nvfbc requires glx */
+ if(!wayland && is_monitor_capture && self->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) {
+ self->context_type = GSR_GL_CONTEXT_TYPE_GLX;
+ self->dri_card_path = NULL;
+ if(!gsr_egl_switch_to_glx_context(self))
+ goto fail;
+ }
+
+ self->glEnable(GL_BLEND);
+ self->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ //self->glEnable(GL_DEBUG_OUTPUT);
+ //self->glDebugMessageCallback(debug_callback, NULL);
+
+ return true;
+
+ fail:
+ gsr_egl_unload(self);
+ return false;
+}
+
+void gsr_egl_unload(gsr_egl *self) {
+ if(self->egl_context) {
+ self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL);
+ self->eglDestroyContext(self->egl_display, self->egl_context);
+ self->egl_context = NULL;
+ }
+
+ if(self->egl_surface) {
+ self->eglDestroySurface(self->egl_display, self->egl_surface);
+ self->egl_surface = NULL;
+ }
+
+ if(self->egl_display) {
+ self->eglTerminate(self->egl_display);
+ self->egl_display = NULL;
+ }
+
+ if(self->glx_context) {
+ self->glXMakeContextCurrent(self->x11.dpy, None, None, NULL);
+ self->glXDestroyContext(self->x11.dpy, self->glx_context);
+ self->glx_context = NULL;
+ self->glx_fb_config = NULL;
+ }
+
+ if(self->x11.window) {
+ XDestroyWindow(self->x11.dpy, self->x11.window);
+ self->x11.window = None;
+ }
+
+ if(self->wayland.window) {
+ wl_egl_window_destroy(self->wayland.window);
+ self->wayland.window = NULL;
+ }
+
+ if(self->wayland.surface) {
+ wl_surface_destroy(self->wayland.surface);
+ self->wayland.surface = NULL;
+ }
+
+ for(int i = 0; i < self->wayland.num_outputs; ++i) {
+ if(self->wayland.outputs[i].output) {
+ wl_output_destroy(self->wayland.outputs[i].output);
+ self->wayland.outputs[i].output = NULL;
+ }
+
+ if(self->wayland.outputs[i].name) {
+ free(self->wayland.outputs[i].name);
+ self->wayland.outputs[i].name = NULL;
+ }
+ }
+ self->wayland.num_outputs = 0;
+
+ if(self->wayland.compositor) {
+ wl_compositor_destroy(self->wayland.compositor);
+ self->wayland.compositor = NULL;
+ }
+
+ if(self->wayland.registry) {
+ wl_registry_destroy(self->wayland.registry);
+ self->wayland.registry = NULL;
+ }
+
+ if(self->wayland.dpy) {
+ wl_display_disconnect(self->wayland.dpy);
+ self->wayland.dpy = NULL;
+ }
+
+ if(self->egl_library) {
+ dlclose(self->egl_library);
+ self->egl_library = NULL;
+ }
+
+ if(self->glx_library) {
+ dlclose(self->glx_library);
+ self->glx_library = NULL;
+ }
+
+ if(self->gl_library) {
+ dlclose(self->gl_library);
+ self->gl_library = NULL;
+ }
+
+ memset(self, 0, sizeof(gsr_egl));
+}
+
+void gsr_egl_update(gsr_egl *self) {
+ if(!self->wayland.dpy)
+ return;
+
+ // TODO: pselect on wl_display_get_fd before doing dispatch
+ wl_display_dispatch(self->wayland.dpy);
+}
diff --git a/src/library_loader.c b/src/library_loader.c
new file mode 100644
index 0000000..0aeee9b
--- /dev/null
+++ b/src/library_loader.c
@@ -0,0 +1,34 @@
+#include "../include/library_loader.h"
+
+#include <dlfcn.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+void* dlsym_print_fail(void *handle, const char *name, bool required) {
+ dlerror();
+ void *sym = dlsym(handle, name);
+ char *err_str = dlerror();
+
+ if(!sym)
+ fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)");
+
+ return sym;
+}
+
+/* |dlsyms| should be null terminated */
+bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms) {
+ bool success = true;
+ for(int i = 0; dlsyms[i].func; ++i) {
+ *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, true);
+ if(!*dlsyms[i].func)
+ success = false;
+ }
+ return success;
+}
+
+/* |dlsyms| should be null terminated */
+void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms) {
+ for(int i = 0; dlsyms[i].func; ++i) {
+ *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, false);
+ }
+}
diff --git a/src/main.cpp b/src/main.cpp
index 2b29d3b..d00a9be 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,419 +1,332 @@
-/*
- Copyright (C) 2020 dec05eba
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
+extern "C" {
+#include "../include/capture/nvfbc.h"
+#include "../include/capture/xcomposite_cuda.h"
+#include "../include/capture/xcomposite_vaapi.h"
+#include "../include/capture/kms_vaapi.h"
+#include "../include/capture/kms_cuda.h"
+#include "../include/egl.h"
+#include "../include/utils.h"
+#include "../include/color_conversion.h"
+}
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
+#include <unordered_map>
#include <thread>
#include <mutex>
#include <map>
#include <signal.h>
#include <sys/stat.h>
-
#include <unistd.h>
-#include <fcntl.h>
+#include <sys/wait.h>
+#include <libgen.h>
#include "../include/sound.hpp"
-#define GLX_GLXEXT_PROTOTYPES
-#include <GL/glew.h>
-#include <GL/glx.h>
-#include <GL/glxext.h>
-#include <GLFW/glfw3.h>
-
-#include <X11/extensions/Xcomposite.h>
-
extern "C" {
#include <libavutil/pixfmt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
-#include <libavutil/hwcontext.h>
-#include <libavutil/hwcontext_cuda.h>
#include <libavutil/opt.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
#include <libavutil/time.h>
-}
-#include <cudaGL.h>
-
-extern "C" {
-#include <libavutil/hwcontext.h>
+#include <libavfilter/avfilter.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
}
-#include "../include/NvFBCLibrary.hpp"
-
#include <deque>
#include <future>
-//#include <CL/cl.h>
+// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd.
+
+// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0
static const int VIDEO_STREAM_INDEX = 0;
-static const int AUDIO_STREAM_INDEX = 1;
static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE];
+static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) {
+ (void)userdata;
+ fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y, monitor->pos.x, monitor->pos.y);
+}
+
+typedef struct {
+ const char *output_name;
+} FirstOutputCallback;
+
+static void get_first_output(const gsr_monitor *monitor, void *userdata) {
+ FirstOutputCallback *first_output = (FirstOutputCallback*)userdata;
+ if(!first_output->output_name)
+ first_output->output_name = strndup(monitor->name, monitor->name_len + 1);
+}
+
static char* av_error_to_string(int err) {
if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0)
strcpy(av_error_buffer, "Unknown error");
return av_error_buffer;
}
-struct ScopedGLXFBConfig {
- ~ScopedGLXFBConfig() {
- if (configs)
- XFree(configs);
- }
-
- GLXFBConfig *configs = nullptr;
-};
-
-struct WindowPixmap {
- WindowPixmap()
- : pixmap(None), glx_pixmap(None), texture_id(0), target_texture_id(0),
- texture_width(0), texture_height(0) {}
-
- Pixmap pixmap;
- GLXPixmap glx_pixmap;
- GLuint texture_id;
- GLuint target_texture_id;
-
- GLint texture_width;
- GLint texture_height;
-};
-
enum class VideoQuality {
MEDIUM,
HIGH,
+ VERY_HIGH,
ULTRA
};
-static double clock_get_monotonic_seconds() {
- struct timespec ts;
- ts.tv_sec = 0;
- ts.tv_nsec = 0;
- clock_gettime(CLOCK_MONOTONIC, &ts);
- return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
-}
+enum class VideoCodec {
+ H264,
+ HEVC,
+ HEVC_HDR,
+ AV1,
+ AV1_HDR
+};
-static bool x11_supports_composite_named_window_pixmap(Display *dpy) {
- int extension_major;
- int extension_minor;
- if (!XCompositeQueryExtension(dpy, &extension_major, &extension_minor))
- return false;
+enum class AudioCodec {
+ AAC,
+ OPUS,
+ FLAC
+};
- int major_version;
- int minor_version;
- return XCompositeQueryVersion(dpy, &major_version, &minor_version) &&
- (major_version > 0 || minor_version >= 2);
-}
+enum class PixelFormat {
+ YUV420,
+ YUV444
+};
-static int x11_error_handler(Display *dpy, XErrorEvent *ev) {
-#if 0
- char type_str[128];
- XGetErrorText(dpy, ev->type, type_str, sizeof(type_str));
-
- char major_opcode_str[128];
- XGetErrorText(dpy, ev->type, major_opcode_str, sizeof(major_opcode_str));
-
- char minor_opcode_str[128];
- XGetErrorText(dpy, ev->type, minor_opcode_str, sizeof(minor_opcode_str));
-
- fprintf(stderr,
- "X Error of failed request: %s\n"
- "Major opcode of failed request: %d (%s)\n"
- "Minor opcode of failed request: %d (%s)\n"
- "Serial number of failed request: %d\n",
- type_str,
- ev->request_code, major_opcode_str,
- ev->minor_code, minor_opcode_str);
-#endif
+enum class FramerateMode {
+ CONSTANT,
+ VARIABLE
+};
+
+static int x11_error_handler(Display*, XErrorEvent*) {
return 0;
}
-static int x11_io_error_handler(Display *dpy) {
+static int x11_io_error_handler(Display*) {
return 0;
}
-static void cleanup_window_pixmap(Display *dpy, WindowPixmap &pixmap) {
- if (pixmap.target_texture_id) {
- glDeleteTextures(1, &pixmap.target_texture_id);
- pixmap.target_texture_id = 0;
- }
-
- if (pixmap.texture_id) {
- glDeleteTextures(1, &pixmap.texture_id);
- pixmap.texture_id = 0;
- pixmap.texture_width = 0;
- pixmap.texture_height = 0;
- }
-
- if (pixmap.glx_pixmap) {
- glXDestroyPixmap(dpy, pixmap.glx_pixmap);
- glXReleaseTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT);
- pixmap.glx_pixmap = None;
- }
-
- if (pixmap.pixmap) {
- XFreePixmap(dpy, pixmap.pixmap);
- pixmap.pixmap = None;
+static bool video_codec_is_hdr(VideoCodec video_codec) {
+ switch(video_codec) {
+ case VideoCodec::HEVC_HDR:
+ case VideoCodec::AV1_HDR:
+ return true;
+ default:
+ return false;
}
}
-static bool recreate_window_pixmap(Display *dpy, Window window_id,
- WindowPixmap &pixmap) {
- cleanup_window_pixmap(dpy, pixmap);
-
- XWindowAttributes attr;
- if (!XGetWindowAttributes(dpy, window_id, &attr)) {
- fprintf(stderr, "Failed to get window attributes\n");
- return false;
- }
+struct PacketData {
+ PacketData() {}
+ PacketData(const PacketData&) = delete;
+ PacketData& operator=(const PacketData&) = delete;
- const int pixmap_config[] = {
- GLX_BIND_TO_TEXTURE_RGB_EXT, True,
- GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT | GLX_WINDOW_BIT,
- GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT,
- GLX_BUFFER_SIZE, 24,
- GLX_RED_SIZE, 8,
- GLX_GREEN_SIZE, 8,
- GLX_BLUE_SIZE, 8,
- GLX_ALPHA_SIZE, 0,
- // GLX_Y_INVERTED_EXT, (int)GLX_DONT_CARE,
- None};
-
- const int pixmap_attribs[] = {GLX_TEXTURE_TARGET_EXT,
- GLX_TEXTURE_2D_EXT,
- GLX_TEXTURE_FORMAT_EXT,
- GLX_TEXTURE_FORMAT_RGB_EXT,
- None};
-
- int c;
- GLXFBConfig *configs = glXChooseFBConfig(dpy, 0, pixmap_config, &c);
- if (!configs) {
- fprintf(stderr, "Failed too choose fb config\n");
- return false;
- }
- ScopedGLXFBConfig scoped_configs;
- scoped_configs.configs = configs;
-
- bool found = false;
- GLXFBConfig config;
- for (int i = 0; i < c; i++) {
- config = configs[i];
- XVisualInfo *visual = glXGetVisualFromFBConfig(dpy, config);
- if (!visual)
- continue;
-
- if (attr.depth != visual->depth) {
- XFree(visual);
- continue;
- }
- XFree(visual);
- found = true;
- break;
+ ~PacketData() {
+ av_free(data.data);
}
- if(!found) {
- fprintf(stderr, "No matching fb config found\n");
- return false;
- }
-
- Pixmap new_window_pixmap = XCompositeNameWindowPixmap(dpy, window_id);
- if (!new_window_pixmap) {
- fprintf(stderr, "Failed to get pixmap for window %ld\n", window_id);
- return false;
- }
-
- GLXPixmap glx_pixmap =
- glXCreatePixmap(dpy, config, new_window_pixmap, pixmap_attribs);
- if (!glx_pixmap) {
- fprintf(stderr, "Failed to create glx pixmap\n");
- XFreePixmap(dpy, new_window_pixmap);
- return false;
- }
-
- pixmap.pixmap = new_window_pixmap;
- pixmap.glx_pixmap = glx_pixmap;
-
- //glEnable(GL_TEXTURE_2D);
- glGenTextures(1, &pixmap.texture_id);
- glBindTexture(GL_TEXTURE_2D, pixmap.texture_id);
-
- // glEnable(GL_BLEND);
- // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
- glXBindTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT, NULL);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
- GL_NEAREST); // GL_LINEAR );
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
- GL_NEAREST); // GL_LINEAR);//GL_LINEAR_MIPMAP_LINEAR );
- //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
- glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH,
- &pixmap.texture_width);
- glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT,
- &pixmap.texture_height);
-
- if(pixmap.texture_width == 0 || pixmap.texture_height == 0) {
- pixmap.texture_width = attr.width;
- pixmap.texture_height = attr.height;
- fprintf(stderr, "Warning: failed to get texture size. You are probably running an unsupported compositor and recording the selected window doesn't work at the moment. This could also happen if you are trying to record a window with client-side decorations (GNOME issue). A black window will be displayed instead. A workaround is to record the whole monitor (which use NvFBC).\n");
- }
-
- fprintf(stderr, "texture width: %d, height: %d\n", pixmap.texture_width,
- pixmap.texture_height);
-
- // Generating this second texture is needed because
- // cuGraphicsGLRegisterImage cant be used with the texture that is mapped
- // directly to the pixmap.
- // TODO: Investigate if it's somehow possible to use the pixmap texture
- // directly, this should improve performance since only less image copy is
- // then needed every frame.
- glGenTextures(1, &pixmap.target_texture_id);
- glBindTexture(GL_TEXTURE_2D, pixmap.target_texture_id);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, pixmap.texture_width,
- pixmap.texture_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
- int err2 = glGetError();
- //fprintf(stderr, "error: %d\n", err2);
- // glXBindTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT, NULL);
- // glGenerateTextureMipmapEXT(glxpixmap, GL_TEXTURE_2D);
-
- // glGenerateMipmap(GL_TEXTURE_2D);
-
- // glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
- // glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
- GL_NEAREST); // GL_LINEAR );
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
- GL_NEAREST); // GL_LINEAR);//GL_LINEAR_MIPMAP_LINEAR );
- //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
- glBindTexture(GL_TEXTURE_2D, 0);
-
- return pixmap.texture_id != 0 && pixmap.target_texture_id != 0;
-}
+ AVPacket data;
+};
// |stream| is only required for non-replay mode
-static void receive_frames(AVCodecContext *av_codec_context, int stream_index, AVStream *stream, AVFrame *frame,
+static void receive_frames(AVCodecContext *av_codec_context, int stream_index, AVStream *stream, int64_t pts,
AVFormatContext *av_format_context,
double replay_start_time,
- std::deque<AVPacket> &frame_data_queue,
+ std::deque<std::shared_ptr<PacketData>> &frame_data_queue,
int replay_buffer_size_secs,
bool &frames_erased,
- std::mutex &write_output_mutex) {
- AVPacket av_packet;
- memset(&av_packet, 0, sizeof(av_packet));
+ std::mutex &write_output_mutex,
+ double paused_time_offset) {
for (;;) {
- av_packet.data = NULL;
- av_packet.size = 0;
- int res = avcodec_receive_packet(av_codec_context, &av_packet);
+ AVPacket *av_packet = av_packet_alloc();
+ if(!av_packet)
+ break;
+
+ av_packet->data = NULL;
+ av_packet->size = 0;
+ int res = avcodec_receive_packet(av_codec_context, av_packet);
if (res == 0) { // we have a packet, send the packet to the muxer
- av_packet.stream_index = stream_index;
- av_packet.pts = av_packet.dts = frame->pts;
+ av_packet->stream_index = stream_index;
+ av_packet->pts = pts;
+ av_packet->dts = pts;
- std::lock_guard<std::mutex> lock(write_output_mutex);
+ std::lock_guard<std::mutex> lock(write_output_mutex);
if(replay_buffer_size_secs != -1) {
- double time_now = glfwGetTime();
+ // TODO: Preallocate all frames data and use those instead.
+ // Why are we doing this you ask? there is a new ffmpeg bug that causes cpu usage to increase over time when you have
+ // packets that are not being free'd until later. So we copy the packet data, free the packet and then reconstruct
+ // the packet later on when we need it, to keep packets alive only for a short period.
+ auto new_packet = std::make_shared<PacketData>();
+ new_packet->data = *av_packet;
+ new_packet->data.data = (uint8_t*)av_malloc(av_packet->size);
+ memcpy(new_packet->data.data, av_packet->data, av_packet->size);
+
+ double time_now = clock_get_monotonic_seconds() - paused_time_offset;
double replay_time_elapsed = time_now - replay_start_time;
- AVPacket new_pack;
- av_packet_move_ref(&new_pack, &av_packet);
- frame_data_queue.push_back(std::move(new_pack));
+ frame_data_queue.push_back(std::move(new_packet));
if(replay_time_elapsed >= replay_buffer_size_secs) {
- av_packet_unref(&frame_data_queue.front());
frame_data_queue.pop_front();
frames_erased = true;
}
} else {
- av_packet_rescale_ts(&av_packet, av_codec_context->time_base, stream->time_base);
- av_packet.stream_index = stream->index;
- int ret = av_interleaved_write_frame(av_format_context, &av_packet);
+ av_packet_rescale_ts(av_packet, av_codec_context->time_base, stream->time_base);
+ av_packet->stream_index = stream->index;
+ // TODO: Is av_interleaved_write_frame needed?
+ int ret = av_write_frame(av_format_context, av_packet);
if(ret < 0) {
- fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet.stream_index, av_error_to_string(ret), ret);
+ fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet->stream_index, av_error_to_string(ret), ret);
}
}
- av_packet_unref(&av_packet);
+ av_packet_free(&av_packet);
} else if (res == AVERROR(EAGAIN)) { // we have no packet
// fprintf(stderr, "No packet!\n");
+ av_packet_free(&av_packet);
break;
} else if (res == AVERROR_EOF) { // this is the end of the stream
+ av_packet_free(&av_packet);
fprintf(stderr, "End of stream!\n");
break;
} else {
+ av_packet_free(&av_packet);
fprintf(stderr, "Unexpected error: %d\n", res);
break;
}
}
- //av_packet_unref(&av_packet);
}
-static AVCodecContext* create_audio_codec_context(AVFormatContext *av_format_context, int fps) {
- const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
+static const char* audio_codec_get_name(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return "aac";
+ case AudioCodec::OPUS: return "opus";
+ case AudioCodec::FLAC: return "flac";
+ }
+ assert(false);
+ return "";
+}
+
+static AVCodecID audio_codec_get_id(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return AV_CODEC_ID_AAC;
+ case AudioCodec::OPUS: return AV_CODEC_ID_OPUS;
+ case AudioCodec::FLAC: return AV_CODEC_ID_FLAC;
+ }
+ assert(false);
+ return AV_CODEC_ID_AAC;
+}
+
+static AVSampleFormat audio_codec_get_sample_format(AudioCodec audio_codec, const AVCodec *codec, bool mix_audio) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: {
+ return AV_SAMPLE_FMT_FLTP;
+ }
+ case AudioCodec::OPUS: {
+ bool supports_s16 = false;
+ bool supports_flt = false;
+
+ for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != -1; ++i) {
+ if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) {
+ supports_s16 = true;
+ } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) {
+ supports_flt = true;
+ }
+ }
+
+ // Amix only works with float audio
+ if(mix_audio)
+ supports_s16 = false;
+
+ if(!supports_s16 && !supports_flt) {
+ fprintf(stderr, "Warning: opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n");
+ fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n");
+ fprintf(stderr, " Falling back to fltp audio sample format instead.\n");
+ }
+
+ if(supports_s16)
+ return AV_SAMPLE_FMT_S16;
+ else if(supports_flt)
+ return AV_SAMPLE_FMT_FLT;
+ else
+ return AV_SAMPLE_FMT_FLTP;
+ }
+ case AudioCodec::FLAC: {
+ return AV_SAMPLE_FMT_S32;
+ }
+ }
+ assert(false);
+ return AV_SAMPLE_FMT_FLTP;
+}
+
+static int64_t audio_codec_get_get_bitrate(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return 160000;
+ case AudioCodec::OPUS: return 128000;
+ case AudioCodec::FLAC: return 128000;
+ }
+ assert(false);
+ return 128000;
+}
+
+static AudioFormat audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) {
+ switch(audio_codec_context->sample_fmt) {
+ case AV_SAMPLE_FMT_FLT: return F32;
+ case AV_SAMPLE_FMT_FLTP: return S32;
+ case AV_SAMPLE_FMT_S16: return S16;
+ case AV_SAMPLE_FMT_S32: return S32;
+ default: return S16;
+ }
+}
+
+static AVSampleFormat audio_format_to_sample_format(const AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return AV_SAMPLE_FMT_S16;
+ case S32: return AV_SAMPLE_FMT_S32;
+ case F32: return AV_SAMPLE_FMT_FLT;
+ }
+ assert(false);
+ return AV_SAMPLE_FMT_S16;
+}
+
+static AVCodecContext* create_audio_codec_context(int fps, AudioCodec audio_codec, bool mix_audio, int audio_bitrate) {
+ (void)fps;
+ const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec));
if (!codec) {
- fprintf(
- stderr,
- "Error: Could not find aac encoder\n");
- exit(1);
+ fprintf(stderr, "Error: Could not find %s audio encoder\n", audio_codec_get_name(audio_codec));
+ _exit(1);
}
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
assert(codec->type == AVMEDIA_TYPE_AUDIO);
- /*
- codec_context->sample_fmt = (*codec)->sample_fmts
- ? (*codec)->sample_fmts[0]
- : AV_SAMPLE_FMT_FLTP;
- */
- codec_context->codec_id = AV_CODEC_ID_AAC;
- codec_context->sample_fmt = AV_SAMPLE_FMT_FLTP;
- //codec_context->bit_rate = 64000;
+ codec_context->codec_id = codec->id;
+ codec_context->sample_fmt = audio_codec_get_sample_format(audio_codec, codec, mix_audio);
+ codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate;
codec_context->sample_rate = 48000;
+ if(audio_codec == AudioCodec::AAC)
+ codec_context->profile = FF_PROFILE_AAC_LOW;
+#if LIBAVCODEC_VERSION_MAJOR < 60
codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
codec_context->channels = 2;
+#else
+ av_channel_layout_default(&codec_context->ch_layout, 2);
+#endif
codec_context->time_base.num = 1;
codec_context->time_base.den = AV_TIME_BASE;
- codec_context->framerate.num = fps;
- codec_context->framerate.den = 1;
-
- // Some formats want stream headers to be seperate
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ codec_context->thread_count = 1;
+ codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
return codec_context;
}
-static AVCodecContext *create_video_codec_context(AVFormatContext *av_format_context,
+static AVCodecContext *create_video_codec_context(AVPixelFormat pix_fmt,
VideoQuality video_quality,
- int record_width, int record_height,
- int fps, bool use_hevc) {
- const AVCodec *codec = avcodec_find_encoder_by_name(use_hevc ? "hevc_nvenc" : "h264_nvenc");
- if (!codec) {
- codec = avcodec_find_encoder_by_name(use_hevc ? "nvenc_hevc" : "nvenc_h264");
- }
- if (!codec) {
- fprintf(
- stderr,
- "Error: Could not find %s encoder\n", use_hevc ? "hevc" : "h264");
- exit(1);
- }
+ int fps, const AVCodec *codec, bool is_livestream, gsr_gpu_vendor vendor, FramerateMode framerate_mode,
+ bool hdr, gsr_color_range color_range) {
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
@@ -421,218 +334,588 @@ static AVCodecContext *create_video_codec_context(AVFormatContext *av_format_con
assert(codec->type == AVMEDIA_TYPE_VIDEO);
codec_context->codec_id = codec->id;
- codec_context->width = record_width & ~1;
- codec_context->height = record_height & ~1;
- codec_context->bit_rate = 12500000 + (codec_context->width * codec_context->height) / 2;
// Timebase: This is the fundamental unit of time (in seconds) in terms
// of which frame timestamps are represented. For fixed-fps content,
// timebase should be 1/framerate and timestamp increments should be
// identical to 1
codec_context->time_base.num = 1;
- codec_context->time_base.den = AV_TIME_BASE;
+ codec_context->time_base.den = framerate_mode == FramerateMode::CONSTANT ? fps : AV_TIME_BASE;
codec_context->framerate.num = fps;
codec_context->framerate.den = 1;
codec_context->sample_aspect_ratio.num = 0;
codec_context->sample_aspect_ratio.den = 0;
- codec_context->gop_size = fps * 2;
+ // High values reduce file size but increases time it takes to seek
+ if(is_livestream) {
+ codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY);
+ codec_context->flags2 |= AV_CODEC_FLAG2_FAST;
+ //codec_context->gop_size = std::numeric_limits<int>::max();
+ //codec_context->keyint_min = std::numeric_limits<int>::max();
+ codec_context->gop_size = fps * 2;
+ } else {
+ codec_context->gop_size = fps * 2;
+ }
codec_context->max_b_frames = 0;
- codec_context->pix_fmt = AV_PIX_FMT_CUDA;
- codec_context->color_range = AVCOL_RANGE_JPEG;
+ codec_context->pix_fmt = pix_fmt;
+ codec_context->color_range = color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG;
+ if(hdr) {
+ codec_context->color_primaries = AVCOL_PRI_BT2020;
+ codec_context->color_trc = AVCOL_TRC_SMPTE2084;
+ codec_context->colorspace = AVCOL_SPC_BT2020_NCL;
+ } else {
+ codec_context->color_primaries = AVCOL_PRI_BT709;
+ codec_context->color_trc = AVCOL_TRC_BT709;
+ codec_context->colorspace = AVCOL_SPC_BT709;
+ }
+ //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER;
+ if(codec->id == AV_CODEC_ID_HEVC)
+ codec_context->codec_tag = MKTAG('h', 'v', 'c', '1');
switch(video_quality) {
case VideoQuality::MEDIUM:
- codec_context->bit_rate = 10000000 + (codec_context->width * codec_context->height) / 2;
- if(use_hevc) {
- codec_context->qmin = 20;
- codec_context->qmax = 35;
- } else {
- codec_context->qmin = 5;
- codec_context->qmax = 20;
- }
- //av_opt_set(codec_context->priv_data, "preset", "slow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p4", 0);
+ //codec_context->qmin = 35;
+ //codec_context->qmax = 35;
+ codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75;
break;
case VideoQuality::HIGH:
- if(use_hevc) {
- codec_context->qmin = 17;
- codec_context->qmax = 30;
- } else {
- codec_context->qmin = 5;
- codec_context->qmax = 15;
- }
- //av_opt_set(codec_context->priv_data, "preset", "slow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p5", 0);
+ //codec_context->qmin = 34;
+ //codec_context->qmax = 34;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
+ break;
+ case VideoQuality::VERY_HIGH:
+ //codec_context->qmin = 28;
+ //codec_context->qmax = 28;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
break;
case VideoQuality::ULTRA:
- codec_context->bit_rate = 15000000 + (codec_context->width * codec_context->height) / 2;
- if(use_hevc) {
- codec_context->qmin = 16;
- codec_context->qmax = 25;
- } else {
- codec_context->qmin = 3;
- codec_context->qmax = 13;
- }
- //av_opt_set(codec_context->priv_data, "preset", "veryslow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p7", 0);
+ //codec_context->qmin = 22;
+ //codec_context->qmax = 22;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
break;
}
+ //codec_context->profile = FF_PROFILE_H264_MAIN;
if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO)
codec_context->mb_decision = 2;
// stream->time_base = codec_context->time_base;
// codec_context->ticks_per_frame = 30;
//av_opt_set(codec_context->priv_data, "tune", "hq", 0);
- //av_opt_set(codec_context->priv_data, "rc", "vbr", 0);
+ // TODO: Do this for better file size? also allows setting qmin, qmax per frame? which can then be used to dynamically set bitrate to reduce quality
+ // if live streaming is slow or if the users harddrive is cant handle writing megabytes of data per second.
+ #if 0
+ char qmin_str[32];
+ snprintf(qmin_str, sizeof(qmin_str), "%d", codec_context->qmin);
+
+ char qmax_str[32];
+ snprintf(qmax_str, sizeof(qmax_str), "%d", codec_context->qmax);
+
+ av_opt_set(codec_context->priv_data, "cq", qmax_str, 0);
+ av_opt_set(codec_context->priv_data, "rc", "vbr", 0);
+ av_opt_set(codec_context->priv_data, "qmin", qmin_str, 0);
+ av_opt_set(codec_context->priv_data, "qmax", qmax_str, 0);
+ codec_context->bit_rate = 0;
+ #endif
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ codec_context->global_quality = 180;
+ break;
+ case VideoQuality::HIGH:
+ codec_context->global_quality = 140;
+ break;
+ case VideoQuality::VERY_HIGH:
+ codec_context->global_quality = 120;
+ break;
+ case VideoQuality::ULTRA:
+ codec_context->global_quality = 100;
+ break;
+ }
+ }
- // Some formats want stream headers to be seperate
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0);
+ //av_opt_set_int(codec_context->priv_data, "cbr", true, 0);
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: More options, better options
+ //codec_context->bit_rate = codec_context->width * codec_context->height;
+ av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0);
+ //codec_context->global_quality = 4;
+ //codec_context->compression_level = 2;
+ }
+
+ //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0);
+
+ //codec_context->rc_max_rate = codec_context->bit_rate;
+ //codec_context->rc_min_rate = codec_context->bit_rate;
+ //codec_context->rc_buffer_size = codec_context->bit_rate / 10;
+ // TODO: Do this when not using cqp
+ //codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate * 1000;
+
+ codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
return codec_context;
}
-static AVFrame* open_audio(AVCodecContext *audio_codec_context) {
+static bool vaapi_create_codec_context(AVCodecContext *video_codec_context, const char *card_path) {
+ char render_path[128];
+ if(!gsr_card_path_get_render_path(card_path, render_path)) {
+ fprintf(stderr, "gsr error: failed to get /dev/dri/renderDXXX file from %s\n", card_path);
+ return false;
+ }
+
+ AVBufferRef *device_ctx;
+ if(av_hwdevice_ctx_create(&device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) {
+ fprintf(stderr, "Error: Failed to create hardware device context\n");
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "Error: Failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context =
+ (AVHWFramesContext *)frame_context->data;
+ hw_frame_context->width = video_codec_context->width;
+ hw_frame_context->height = video_codec_context->height;
+ hw_frame_context->sw_format = AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ //hw_frame_context->initial_pool_size = 1;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "Error: Failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
+
+static bool check_if_codec_valid_for_hardware(const AVCodec *codec, gsr_gpu_vendor vendor, const char *card_path) {
+ // Do not use AV_PIX_FMT_CUDA because we dont want to do full check with hardware context
+ AVCodecContext *codec_context = create_video_codec_context(vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_YUV420P : AV_PIX_FMT_VAAPI, VideoQuality::VERY_HIGH, 60, codec, false, vendor, FramerateMode::CONSTANT, false, GSR_COLOR_RANGE_LIMITED);
+ if(!codec_context)
+ return false;
+
+ codec_context->width = 512;
+ codec_context->height = 512;
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ if(!vaapi_create_codec_context(codec_context, card_path)) {
+ avcodec_free_context(&codec_context);
+ return false;
+ }
+ }
+
+ bool success = false;
+ success = avcodec_open2(codec_context, codec_context->codec, NULL) == 0;
+ if(codec_context->hw_device_ctx)
+ av_buffer_unref(&codec_context->hw_device_ctx);
+ if(codec_context->hw_frames_ctx)
+ av_buffer_unref(&codec_context->hw_frames_ctx);
+ avcodec_free_context(&codec_context);
+ return success;
+}
+
+static const AVCodec* find_h264_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_h264" : "vaapi_h264");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static const AVCodec* find_h265_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_hevc" : "vaapi_hevc");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static const AVCodec* find_av1_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ // Workaround bug with av1 nvidia in older ffmpeg versions that causes the whole application to crash
+ // when avcodec_open2 is opened with av1_nvenc
+ if(vendor == GSR_GPU_VENDOR_NVIDIA && LIBAVCODEC_BUILD < AV_VERSION_INT(60, 30, 100)) {
+ return nullptr;
+ }
+
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_av1" : "vaapi_av1");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static void open_audio(AVCodecContext *audio_codec_context) {
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+
int ret;
- ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, nullptr);
+ ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options);
if(ret < 0) {
fprintf(stderr, "failed to open codec, reason: %s\n", av_error_to_string(ret));
- exit(1);
+ _exit(1);
}
+}
+static AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) {
AVFrame *frame = av_frame_alloc();
if(!frame) {
fprintf(stderr, "failed to allocate audio frame\n");
- exit(1);
+ _exit(1);
}
+ frame->sample_rate = audio_codec_context->sample_rate;
frame->nb_samples = audio_codec_context->frame_size;
frame->format = audio_codec_context->sample_fmt;
- frame->channels = audio_codec_context->channels;
+#if LIBAVCODEC_VERSION_MAJOR < 60
+ frame->channels = audio_codec_context->channels;
frame->channel_layout = audio_codec_context->channel_layout;
+#else
+ av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout);
+#endif
- ret = av_frame_get_buffer(frame, 0);
+ int ret = av_frame_get_buffer(frame, 0);
if(ret < 0) {
fprintf(stderr, "failed to allocate audio data buffers, reason: %s\n", av_error_to_string(ret));
- exit(1);
+ _exit(1);
}
return frame;
}
-static void open_video(AVCodecContext *codec_context,
- WindowPixmap &window_pixmap, AVBufferRef **device_ctx,
- CUgraphicsResource *cuda_graphics_resource, CUcontext cuda_context) {
- int ret;
+static void open_video(AVCodecContext *codec_context, VideoQuality video_quality, bool very_old_gpu, gsr_gpu_vendor vendor, PixelFormat pixel_format, bool hdr) {
+ AVDictionary *options = nullptr;
+ if(vendor == GSR_GPU_VENDOR_NVIDIA) {
+ // Disable setting preset since some nvidia gpus cant handle it nicely and greatly reduce encoding performance (from more than 60 fps to less than 45 fps) (such as Nvidia RTX A2000)
+ #if 0
+ bool supports_p4 = false;
+ bool supports_p5 = false;
+
+ const AVOption *opt = nullptr;
+ while((opt = av_opt_next(codec_context->priv_data, opt))) {
+ if(opt->type == AV_OPT_TYPE_CONST) {
+ if(strcmp(opt->name, "p4") == 0)
+ supports_p4 = true;
+ else if(strcmp(opt->name, "p5") == 0)
+ supports_p5 = true;
+ }
+ }
+ #endif
- *device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
- if(!*device_ctx) {
- fprintf(stderr, "Error: Failed to create hardware device context\n");
- exit(1);
- }
+ if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ } else if(very_old_gpu || codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 27, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 21, 0);
+ break;
+ }
+ } else {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ }
- AVHWDeviceContext *hw_device_context = (AVHWDeviceContext *)(*device_ctx)->data;
- AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext *)hw_device_context->hwctx;
- cuda_device_context->cuda_ctx = cuda_context;
- if(av_hwdevice_ctx_init(*device_ctx) < 0) {
- fprintf(stderr, "Error: Failed to create hardware device context\n");
- exit(1);
- }
+ #if 0
+ if(!supports_p4 && !supports_p5)
+ fprintf(stderr, "Info: your ffmpeg version is outdated. It's recommended that you use the flatpak version of gpu-screen-recorder version instead, which you can find at https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder\n");
+
+ //if(is_livestream) {
+ // av_dict_set_int(&options, "zerolatency", 1, 0);
+ // //av_dict_set(&options, "preset", "llhq", 0);
+ //}
+
+ // I want to use a good preset for the gpu but all gpus prefer different
+ // presets. Nvidia and ffmpeg used to support "hq" preset that chose the best preset for the gpu
+ // with pretty good performance but you now have to choose p1-p7, which are gpu agnostic and on
+ // older gpus p5-p7 slow the gpu down to a crawl...
+ // "hq" is now just an alias for p7 in ffmpeg :(
+ // TODO: Temporary disable because of stuttering?
+
+ // TODO: Preset is set to p5 for now but it should ideally be p6 or p7.
+ // This change is needed because for certain sizes of a window (or monitor?) such as 971x780 causes encoding to freeze
+ // when using h264 codec. This is a new(?) nvidia driver bug.
+ if(very_old_gpu)
+ av_dict_set(&options, "preset", supports_p4 ? "p4" : "medium", 0);
+ else
+ av_dict_set(&options, "preset", supports_p5 ? "p5" : "slow", 0);
+ #endif
+
+ av_dict_set(&options, "tune", "hq", 0);
+ av_dict_set(&options, "rc", "constqp", 0);
+
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(pixel_format) {
+ case PixelFormat::YUV420:
+ av_dict_set(&options, "profile", "high", 0);
+ break;
+ case PixelFormat::YUV444:
+ av_dict_set(&options, "profile", "high444p", 0);
+ break;
+ }
+ } else if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ switch(pixel_format) {
+ case PixelFormat::YUV420:
+ av_dict_set(&options, "rgb_mode", "yuv420", 0);
+ break;
+ case PixelFormat::YUV444:
+ av_dict_set(&options, "rgb_mode", "yuv444", 0);
+ break;
+ }
+ } else {
+ //av_dict_set(&options, "profile", "main10", 0);
+ //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0);
+ if(hdr) {
+ av_dict_set(&options, "profile", "main10", 0);
+ } else {
+ av_dict_set(&options, "profile", "main", 0);
+ }
+ }
+ } else {
+ if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ // Using global_quality option
+ } else if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 34, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 30, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 26, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 22, 0);
+ break;
+ }
+ } else {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ }
- AVBufferRef *frame_context = av_hwframe_ctx_alloc(*device_ctx);
- if (!frame_context) {
- fprintf(stderr, "Error: Failed to create hwframe context\n");
- exit(1);
- }
+ // TODO: More quality options
+ av_dict_set(&options, "rc_mode", "CQP", 0);
+ //av_dict_set_int(&options, "low_power", 1, 0);
- AVHWFramesContext *hw_frame_context =
- (AVHWFramesContext *)frame_context->data;
- hw_frame_context->width = codec_context->width;
- hw_frame_context->height = codec_context->height;
- hw_frame_context->sw_format = AV_PIX_FMT_0RGB32;
- hw_frame_context->format = codec_context->pix_fmt;
- hw_frame_context->device_ref = *device_ctx;
- hw_frame_context->device_ctx = (AVHWDeviceContext *)(*device_ctx)->data;
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ av_dict_set(&options, "profile", "high", 0);
+ //av_dict_set_int(&options, "quality", 5, 0); // quality preset
+ } else if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ av_dict_set(&options, "profile", "main", 0); // TODO: use professional instead?
+ av_dict_set(&options, "tier", "main", 0);
+ } else {
+ if(hdr) {
+ av_dict_set(&options, "profile", "main10", 0);
+ av_dict_set(&options, "sei", "hdr", 0);
+ } else {
+ av_dict_set(&options, "profile", "main", 0);
+ }
+ }
+ }
- if (av_hwframe_ctx_init(frame_context) < 0) {
- fprintf(stderr, "Error: Failed to initialize hardware frame context "
- "(note: ffmpeg version needs to be > 4.0\n");
- exit(1);
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use?
}
- codec_context->hw_device_ctx = *device_ctx;
- codec_context->hw_frames_ctx = frame_context;
+ av_dict_set(&options, "strict", "experimental", 0);
- ret = avcodec_open2(codec_context, codec_context->codec, nullptr);
+ int ret = avcodec_open2(codec_context, codec_context->codec, &options);
if (ret < 0) {
- fprintf(stderr, "Error: Could not open video codec: %s\n",
- "blabla"); // av_err2str(ret));
- exit(1);
- }
-
- if(window_pixmap.target_texture_id != 0) {
- CUresult res;
- CUcontext old_ctx;
- res = cuCtxPopCurrent(&old_ctx);
- res = cuCtxPushCurrent(cuda_context);
- res = cuGraphicsGLRegisterImage(
- cuda_graphics_resource, window_pixmap.target_texture_id, GL_TEXTURE_2D,
- CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
- // cuGraphicsUnregisterResource(*cuda_graphics_resource);
- if (res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr,
- "Error: cuGraphicsGLRegisterImage failed, error %s, texture "
- "id: %u\n",
- err_str, window_pixmap.target_texture_id);
- exit(1);
- }
- res = cuCtxPopCurrent(&old_ctx);
+ fprintf(stderr, "Error: Could not open video codec: %s\n", av_error_to_string(ret));
+ _exit(1);
}
}
-static void close_video(AVStream *video_stream, AVFrame *frame) {
- // avcodec_close(video_stream->codec);
- // av_frame_free(&frame);
+static void usage_header() {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder";
+ fprintf(stderr, "usage: %s -w <window_id|monitor|focused> [-c <container_format>] [-s WxH] -f <fps> [-a <audio_input>] [-q <quality>] [-r <replay_buffer_size_sec>] [-k h264|hevc|hevc_hdr|av1|av1_hdr] [-ac aac|opus|flac] [-ab <bitrate>] [-oc yes|no] [-fm cfr|vfr] [-cr limited|full] [-v yes|no] [-h|--help] [-o <output_file>] [-mf yes|no] [-sc <script_path>] [-cursor yes|no]\n", program_name);
}
-static void usage() {
- fprintf(stderr, "usage: gpu-screen-recorder -w <window_id> -c <container_format> -f <fps> [-a <audio_input>] [-q <quality>] [-r <replay_buffer_size_sec>] [-o <output_file>]\n");
+static void usage_full() {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder";
+ usage_header();
+ fprintf(stderr, "\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " -w Window to record or a display, \"screen\" or \"screen-direct\". The display is the display name in xrandr and if \"screen\" or \"screen-direct\" is selected then all displays are recorded and they are recorded in h265 (aka hevc)."
- "\"screen-direct\" skips one texture copy for fullscreen applications so it may lead to better performance and it works with VRR monitors when recording fullscreen application but may break some applications, such as mpv in fullscreen mode. Recording a display requires a gpu with NvFBC support.\n");
- fprintf(stderr, " -s The size (area) to record at in the format WxH, for example 1920x1080. Usually you want to set this to the size of the window. Optional, by default the size of the window, monitor or screen is used (which is passed to -w).\n");
- fprintf(stderr, " -c Container format for output file, for example mp4, or flv.\n");
- fprintf(stderr, " -f Framerate to record at. Clamped to [1,250].\n");
- fprintf(stderr, " -a Audio device to record from (pulse audio device). Optional, disabled by default.\n");
- fprintf(stderr, " -q Video quality. Should either be 'medium', 'high' or 'ultra'. Optional, set to 'medium' be default.\n");
- fprintf(stderr, " -r Replay buffer size in seconds. If this is set, then only the last seconds as set by this option will be stored"
- " and the video will only be saved when the gpu-screen-recorder is closed. This feature is similar to Nvidia's instant replay feature."
- " This option has be between 5 and 1200. Note that the replay buffer size will not always be precise, because of keyframes. Optional, disabled by default.\n");
- fprintf(stderr, " -o The output file path. If omitted then the encoded data is sent to stdout. Required in replay mode (when using -r). In replay mode this has to be an existing directory instead of a file.\n");
+ fprintf(stderr, " -w Window id to record, a display (monitor name), \"screen\", \"screen-direct-force\" or \"focused\".\n");
+ fprintf(stderr, " If this is \"screen\" or \"screen-direct-force\" then all monitors are recorded.\n");
+ fprintf(stderr, " \"screen-direct-force\" is not recommended unless you use a VRR (G-SYNC) monitor on Nvidia X11 and you are aware that using this option can cause games to freeze/crash or other issues because of Nvidia driver issues.\n");
+ fprintf(stderr, " \"screen-direct-force\" option is only available on Nvidia X11. VRR works without this option on other systems.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -c Container format for output file, for example mp4, or flv. Only required if no output file is specified or if recording in replay buffer mode.\n");
+ fprintf(stderr, " If an output file is specified and -c is not used then the container format is determined from the output filename extension.\n");
+ fprintf(stderr, " Only containers that support h264, hevc or av1 are supported, which means that only mp4, mkv, flv (and some others) are supported.\n");
+ fprintf(stderr, " WebM is not supported yet (most hardware doesn't support WebM video encoding).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -s The size (area) to record at in the format WxH, for example 1920x1080. This option is only supported (and required) when -w is \"focused\".\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -f Framerate to record at.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -a Audio device to record from (pulse audio device). Can be specified multiple times. Each time this is specified a new audio track is added for the specified audio device.\n");
+ fprintf(stderr, " A name can be given to the audio input device by prefixing the audio input with <name>/, for example \"dummy/alsa_output.pci-0000_00_1b.0.analog-stereo.monitor\".\n");
+ fprintf(stderr, " Multiple audio devices can be merged into one audio track by using \"|\" as a separator into one -a argument, for example: -a \"alsa_output1|alsa_output2\".\n");
+ fprintf(stderr, " Optional, no audio track is added by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -q Video quality. Should be either 'medium', 'high', 'very_high' or 'ultra'. 'high' is the recommended option when live streaming or when you have a slower harddrive.\n");
+ fprintf(stderr, " Optional, set to 'very_high' be default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -r Replay buffer size in seconds. If this is set, then only the last seconds as set by this option will be stored\n");
+ fprintf(stderr, " and the video will only be saved when the gpu-screen-recorder is closed. This feature is similar to Nvidia's instant replay feature.\n");
+ fprintf(stderr, " This option has be between 5 and 1200. Note that the replay buffer size will not always be precise, because of keyframes. Optional, disabled by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -k Video codec to use. Should be either 'auto', 'h264', 'hevc', 'av1', 'hevc_hdr' or 'av1_hdr'. Defaults to 'auto' which defaults to 'hevc' on AMD/Nvidia and 'h264' on intel.\n");
+ fprintf(stderr, " Forcefully set to 'h264' if the file container type is 'flv'.\n");
+ fprintf(stderr, " Forcefully set to 'hevc' on AMD/intel if video codec is 'h264' and if the file container type is 'mkv'.\n");
+ fprintf(stderr, " 'hevc_hdr' and 'av1_hdr' option is not available on X11.\n");
+ fprintf(stderr, " Note: hdr metadata is not included in the video when recording with 'hevc_hdr'/'av1_hdr' because of bugs in AMD, Intel and NVIDIA drivers (amazin', they are all bugged).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -ac Audio codec to use. Should be either 'aac', 'opus' or 'flac'. Defaults to 'opus' for .mp4/.mkv files, otherwise defaults to 'aac'.\n");
+ fprintf(stderr, " 'opus' and 'flac' is only supported by .mp4/.mkv files. 'opus' is recommended for best performance and smallest audio size.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -ab Audio bitrate to use. Optional, by default the bitrate is 128000 for opus and flac and 160000 for aac.\n");
+ fprintf(stderr, " If this is set to 0 then it's the same as if it's absent, in which case the bitrate is determined automatically depending on the audio codec.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -oc Overclock memory transfer rate to the maximum performance level. This only applies to NVIDIA on X11 and exists to overcome a bug in NVIDIA driver where performance level\n");
+ fprintf(stderr, " is dropped when you record a game. Only needed if you are recording a game that is bottlenecked by GPU. The same issue exists on Wayland but overclocking is not possible on Wayland.\n");
+ fprintf(stderr, " Works only if your have \"Coolbits\" set to \"12\" in NVIDIA X settings, see README for more information. Note! use at your own risk! Optional, disabled by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -fm Framerate mode. Should be either 'cfr' or 'vfr'. Defaults to 'vfr'.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -cr Color range. Should be either 'limited' (aka mpeg) or 'full' (aka jpeg). Defaults to 'limited'.\n");
+ fprintf(stderr, " Limited color range means that colors are in range 16-235 while full color range means that colors are in range 0-255 (when not recording with hdr).\n");
+ fprintf(stderr, " Note that some buggy video players (such as vlc) are unable to correctly display videos in full color range.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -v Prints per second, fps updates. Optional, set to 'yes' by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -h, --help\n");
+ fprintf(stderr, " Show this help.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -mf Organise replays in folders based on the current date.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -sc Run a script on the saved video file (non-blocking). The first argument to the script is the filepath to the saved video file and the second argument is the recording type (either \"regular\" or \"replay\").\n");
+ fprintf(stderr, " Not applicable for live streams.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -cursor\n");
+ fprintf(stderr, " Record cursor. Defaults to 'yes'.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " --list-supported-video-codecs\n");
+ fprintf(stderr, " List supported video codecs and exits. Prints h264, hevc, hevc_hdr, av1 and av1_hdr (if supported).\n");
+ fprintf(stderr, "\n");
+ //fprintf(stderr, " -pixfmt The pixel format to use for the output video. yuv420 is the most common format and is best supported, but the color is compressed, so colors can look washed out and certain colors of text can look bad. Use yuv444 for no color compression, but the video may not work everywhere and it may not work with hardware video decoding. Optional, defaults to yuv420\n");
+ fprintf(stderr, " -o The output file path. If omitted then the encoded data is sent to stdout. Required in replay mode (when using -r).\n");
+ fprintf(stderr, " In replay mode this has to be a directory instead of a file.\n");
+ fprintf(stderr, " The directory to the file is created (recursively) if it doesn't already exist.\n");
+ fprintf(stderr, "\n");
fprintf(stderr, "NOTES:\n");
- fprintf(stderr, " Send signal SIGINT (Ctrl+C) to gpu-screen-recorder to stop and save the recording (when not using replay mode).\n");
- fprintf(stderr, " Send signal SIGUSR1 (killall -SIGUSR1 gpu-screen-recorder) to gpu-screen-recorder to save a replay.\n");
- exit(1);
+ fprintf(stderr, " Send signal SIGINT to gpu-screen-recorder (Ctrl+C, or killall -SIGINT gpu-screen-recorder) to stop and save the recording. When in replay mode this stops recording without saving.\n");
+ fprintf(stderr, " Send signal SIGUSR1 to gpu-screen-recorder (killall -SIGUSR1 gpu-screen-recorder) to save a replay (when in replay mode).\n");
+ fprintf(stderr, " Send signal SIGUSR2 to gpu-screen-recorder (killall -SIGUSR2 gpu-screen-recorder) to pause/unpause recording. Only applicable and useful when recording (not streaming nor replay).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "EXAMPLES:\n");
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor\" -o \"$HOME/Videos/video.mp4\"\n", program_name);
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor|$(pactl get-default-source)\" -o \"$HOME/Videos/video.mp4\"\n", program_name);
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor\" -c mkv -r 60 -o \"$HOME/Videos\"\n", program_name);
+ //fprintf(stderr, " gpu-screen-recorder -w screen -f 60 -q ultra -pixfmt yuv444 -o video.mp4\n");
+ _exit(1);
+}
+
+static void usage() {
+ usage_header();
+ _exit(1);
}
-static sig_atomic_t started = 0;
static sig_atomic_t running = 1;
static sig_atomic_t save_replay = 0;
-static const char *pid_file = "/tmp/gpu-screen-recorder";
+static sig_atomic_t toggle_pause = 0;
-static void term_handler(int) {
- if(started)
- unlink(pid_file);
- exit(0);
-}
-
-static void int_handler(int) {
+static void stop_handler(int) {
running = 0;
}
@@ -640,30 +923,35 @@ static void save_replay_handler(int) {
save_replay = 1;
}
-struct Arg {
- const char *value;
- bool optional;
-};
+static void toggle_pause_handler(int) {
+ toggle_pause = 1;
+}
static bool is_hex_num(char c) {
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
}
static bool contains_non_hex_number(const char *str) {
+ bool hex_start = false;
size_t len = strlen(str);
if(len >= 2 && memcmp(str, "0x", 2) == 0) {
str += 2;
len -= 2;
+ hex_start = true;
}
+ bool is_hex = false;
for(size_t i = 0; i < len; ++i) {
char c = str[i];
if(c == '\0')
return false;
if(!is_hex_num(c))
return true;
+ if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
+ is_hex = true;
}
- return false;
+
+ return is_hex && !hex_start;
}
static std::string get_date_str() {
@@ -674,11 +962,27 @@ static std::string get_date_str() {
return str;
}
+static std::string get_date_only_str() {
+ char str[128];
+ time_t now = time(NULL);
+ struct tm *t = localtime(&now);
+ strftime(str, sizeof(str)-1, "%Y-%m-%d", t);
+ return str;
+}
+
+static std::string get_time_only_str() {
+ char str[128];
+ time_t now = time(NULL);
+ struct tm *t = localtime(&now);
+ strftime(str, sizeof(str)-1, "%H-%M-%S", t);
+ return str;
+}
+
static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) {
AVStream *stream = avformat_new_stream(av_format_context, nullptr);
if (!stream) {
fprintf(stderr, "Error: Could not allocate stream\n");
- exit(1);
+ _exit(1);
}
stream->id = av_format_context->nb_streams - 1;
stream->time_base = codec_context->time_base;
@@ -686,51 +990,174 @@ static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContex
return stream;
}
+static void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) {
+ char script_file_full[PATH_MAX];
+ script_file_full[0] = '\0';
+ if(!realpath(script_file, script_file_full)) {
+ fprintf(stderr, "Error: script file not found: %s\n", script_file);
+ return;
+ }
+
+ const char *args[6];
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+
+ if(inside_flatpak) {
+ args[0] = "flatpak-spawn";
+ args[1] = "--host";
+ args[2] = script_file_full;
+ args[3] = video_file;
+ args[4] = type;
+ args[5] = NULL;
+ } else {
+ args[0] = script_file_full;
+ args[1] = video_file;
+ args[2] = type;
+ args[3] = NULL;
+ }
+
+ pid_t pid = fork();
+ if(pid == -1) {
+ perror(script_file_full);
+ return;
+ } else if(pid == 0) { // child
+ setsid();
+ signal(SIGHUP, SIG_IGN);
+
+ pid_t second_child = fork();
+ if(second_child == 0) { // child
+ execvp(args[0], (char* const*)args);
+ perror(script_file_full);
+ _exit(127);
+ } else if(second_child != -1) { // parent
+ _exit(0);
+ }
+ } else { // parent
+ waitpid(pid, NULL, 0);
+ }
+}
+
+struct AudioDevice {
+ SoundDevice sound_device;
+ AudioInput audio_input;
+ AVFilterContext *src_filter_ctx = nullptr;
+ AVFrame *frame = nullptr;
+ std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read
+};
+
+// TODO: Cleanup
+struct AudioTrack {
+ AVCodecContext *codec_context = nullptr;
+ AVStream *stream = nullptr;
+
+ std::vector<AudioDevice> audio_devices;
+ AVFilterGraph *graph = nullptr;
+ AVFilterContext *sink = nullptr;
+ int stream_index = 0;
+ int64_t pts = 0;
+};
+
static std::future<void> save_replay_thread;
-static std::vector<AVPacket> save_replay_packets;
+static std::vector<std::shared_ptr<PacketData>> save_replay_packets;
static std::string save_replay_output_filepath;
-static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContext *audio_codec_context, int video_stream_index, int audio_stream_index, const std::deque<AVPacket> &frame_data_queue, bool frames_erased, std::string output_dir, std::string container_format) {
+static int create_directory_recursive(char *path) {
+ int path_len = strlen(path);
+ char *p = path;
+ char *end = path + path_len;
+ for(;;) {
+ char *slash_p = strchr(p, '/');
+
+ // Skips first '/', we don't want to try and create the root directory
+ if(slash_p == path) {
+ ++p;
+ continue;
+ }
+
+ if(!slash_p)
+ slash_p = end;
+
+ char prev_char = *slash_p;
+ *slash_p = '\0';
+ int err = mkdir(path, S_IRWXU);
+ *slash_p = prev_char;
+
+ if(err == -1 && errno != EEXIST)
+ return err;
+
+ if(slash_p == end)
+ break;
+ else
+ p = slash_p + 1;
+ }
+ return 0;
+}
+
+static void save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, std::vector<AudioTrack> &audio_tracks, std::deque<std::shared_ptr<PacketData>> &frame_data_queue, bool frames_erased, std::string output_dir, const char *container_format, const std::string &file_extension, std::mutex &write_output_mutex, bool make_folders) {
if(save_replay_thread.valid())
return;
size_t start_index = (size_t)-1;
- for(size_t i = 0; i < frame_data_queue.size(); ++i) {
- const AVPacket &av_packet = frame_data_queue[i];
- if((av_packet.flags & AV_PKT_FLAG_KEY) && av_packet.stream_index == video_stream_index) {
- start_index = i;
- break;
+ int64_t video_pts_offset = 0;
+ int64_t audio_pts_offset = 0;
+
+ {
+ std::lock_guard<std::mutex> lock(write_output_mutex);
+ start_index = (size_t)-1;
+ for(size_t i = 0; i < frame_data_queue.size(); ++i) {
+ const AVPacket &av_packet = frame_data_queue[i]->data;
+ if((av_packet.flags & AV_PKT_FLAG_KEY) && av_packet.stream_index == video_stream_index) {
+ start_index = i;
+ break;
+ }
}
- }
- if(start_index == (size_t)-1)
- return;
+ if(start_index == (size_t)-1)
+ return;
- int64_t pts_offset = 0;
- if(frames_erased)
- pts_offset = frame_data_queue[start_index].pts;
+ if(frames_erased) {
+ video_pts_offset = frame_data_queue[start_index]->data.pts;
+
+ // Find the next audio packet to use as audio pts offset
+ for(size_t i = start_index; i < frame_data_queue.size(); ++i) {
+ const AVPacket &av_packet = frame_data_queue[i]->data;
+ if(av_packet.stream_index != video_stream_index) {
+ audio_pts_offset = av_packet.pts;
+ break;
+ }
+ }
+ } else {
+ start_index = 0;
+ }
- save_replay_packets.resize(frame_data_queue.size());
- for(size_t i = 0; i < frame_data_queue.size(); ++i) {
- av_packet_ref(&save_replay_packets[i], &frame_data_queue[i]);
+ save_replay_packets.resize(frame_data_queue.size());
+ for(size_t i = 0; i < frame_data_queue.size(); ++i) {
+ save_replay_packets[i] = frame_data_queue[i];
+ }
}
- save_replay_output_filepath = output_dir + "/Replay_" + get_date_str() + "." + container_format;
- save_replay_thread = std::async(std::launch::async, [video_stream_index, container_format, start_index, pts_offset, video_codec_context, audio_codec_context]() mutable {
- AVFormatContext *av_format_context;
- // The output format is automatically guessed from the file extension
- avformat_alloc_output_context2(&av_format_context, nullptr, container_format.c_str(), nullptr);
+ if (make_folders) {
+ std::string output_folder = output_dir + '/' + get_date_only_str();
+ create_directory_recursive(&output_folder[0]);
+ save_replay_output_filepath = output_folder + "/Replay_" + get_time_only_str() + "." + file_extension;
+ } else {
+ create_directory_recursive(&output_dir[0]);
+ save_replay_output_filepath = output_dir + "/Replay_" + get_date_str() + "." + file_extension;
+ }
- av_format_context->flags |= AVFMT_FLAG_GENPTS;
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ save_replay_thread = std::async(std::launch::async, [video_stream_index, container_format, start_index, video_pts_offset, audio_pts_offset, video_codec_context, &audio_tracks]() mutable {
+ AVFormatContext *av_format_context;
+ avformat_alloc_output_context2(&av_format_context, nullptr, container_format, nullptr);
AVStream *video_stream = create_stream(av_format_context, video_codec_context);
- AVStream *audio_stream = audio_codec_context ? create_stream(av_format_context, audio_codec_context) : nullptr;
-
avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
- if(audio_stream)
- avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
+
+ std::unordered_map<int, AudioTrack*> stream_index_to_audio_track_map;
+ for(AudioTrack &audio_track : audio_tracks) {
+ stream_index_to_audio_track_map[audio_track.stream_index] = &audio_track;
+ AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context);
+ avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context);
+ audio_track.stream = audio_stream;
+ }
int ret = avio_open(&av_format_context->pb, save_replay_output_filepath.c_str(), AVIO_FLAG_WRITE);
if (ret < 0) {
@@ -738,26 +1165,50 @@ static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContex
return;
}
- ret = avformat_write_header(av_format_context, nullptr);
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+
+ ret = avformat_write_header(av_format_context, &options);
if (ret < 0) {
fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret));
return;
}
for(size_t i = start_index; i < save_replay_packets.size(); ++i) {
- AVPacket &av_packet = save_replay_packets[i];
+ // TODO: Check if successful
+ AVPacket av_packet;
+ memset(&av_packet, 0, sizeof(av_packet));
+ //av_packet_from_data(av_packet, save_replay_packets[i]->data.data, save_replay_packets[i]->data.size);
+ av_packet.data = save_replay_packets[i]->data.data;
+ av_packet.size = save_replay_packets[i]->data.size;
+ av_packet.stream_index = save_replay_packets[i]->data.stream_index;
+ av_packet.pts = save_replay_packets[i]->data.pts;
+ av_packet.dts = save_replay_packets[i]->data.pts;
+ av_packet.flags = save_replay_packets[i]->data.flags;
+
+ AVStream *stream = video_stream;
+ AVCodecContext *codec_context = video_codec_context;
+
+ if(av_packet.stream_index == video_stream_index) {
+ av_packet.pts -= video_pts_offset;
+ av_packet.dts -= video_pts_offset;
+ } else {
+ AudioTrack *audio_track = stream_index_to_audio_track_map[av_packet.stream_index];
+ stream = audio_track->stream;
+ codec_context = audio_track->codec_context;
- AVStream *stream = av_packet.stream_index == video_stream_index ? video_stream : audio_stream;
- AVCodecContext *codec_context = av_packet.stream_index == video_stream_index ? video_codec_context : audio_codec_context;
+ av_packet.pts -= audio_pts_offset;
+ av_packet.dts -= audio_pts_offset;
+ }
av_packet.stream_index = stream->index;
- av_packet.pts -= pts_offset;
- av_packet.dts -= pts_offset;
av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base);
- int ret = av_interleaved_write_frame(av_format_context, &av_packet);
+ ret = av_write_frame(av_format_context, &av_packet);
if(ret < 0)
fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", stream->index, av_error_to_string(ret), ret);
+
+ //av_packet_free(&av_packet);
}
if (av_write_trailer(av_format_context) != 0)
@@ -765,381 +1216,1131 @@ static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContex
avio_close(av_format_context->pb);
avformat_free_context(av_format_context);
+ av_dict_free(&options);
+
+ for(AudioTrack &audio_track : audio_tracks) {
+ audio_track.stream = nullptr;
+ }
});
}
-static bool is_process_running_program(pid_t pid, const char *program_name) {
- char filepath[256];
- snprintf(filepath, sizeof(filepath), "/proc/%ld/exe", (long)pid);
+static void split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> callback) {
+ size_t index = 0;
+ while(index < str.size()) {
+ size_t end_index = str.find(delimiter, index);
+ if(end_index == std::string::npos)
+ end_index = str.size();
+
+ if(!callback(&str[index], end_index - index))
+ break;
+
+ index = end_index + 1;
+ }
+}
+
+static std::vector<AudioInput> parse_audio_input_arg(const char *str) {
+ std::vector<AudioInput> audio_inputs;
+ split_string(str, '|', [&audio_inputs](const char *sub, size_t size) {
+ AudioInput audio_input;
+ audio_input.name.assign(sub, size);
+ const size_t index = audio_input.name.find('/');
+ if(index != std::string::npos) {
+ audio_input.description = audio_input.name.substr(0, index);
+ audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + index + 1);
+ }
+ audio_inputs.push_back(std::move(audio_input));
+ return true;
+ });
+ return audio_inputs;
+}
- char resolved_path[PATH_MAX];
- const ssize_t resolved_path_len = readlink(filepath, resolved_path, sizeof(resolved_path) - 1);
- if(resolved_path_len == -1)
+// TODO: Does this match all livestreaming cases?
+static bool is_livestream_path(const char *str) {
+ const int len = strlen(str);
+ if((len >= 7 && memcmp(str, "http://", 7) == 0) || (len >= 8 && memcmp(str, "https://", 8) == 0))
+ return true;
+ else if((len >= 7 && memcmp(str, "rtmp://", 7) == 0) || (len >= 8 && memcmp(str, "rtmps://", 8) == 0))
+ return true;
+ else
return false;
+}
+
+// TODO: Proper cleanup
+static int init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, std::vector<AVFilterContext*> &src_filter_ctx, size_t num_sources) {
+ char ch_layout[64];
+ int err = 0;
+ ch_layout[0] = '\0';
+
+ AVFilterGraph *filter_graph = avfilter_graph_alloc();
+ if (!filter_graph) {
+ fprintf(stderr, "Unable to create filter graph.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ for(size_t i = 0; i < num_sources; ++i) {
+ const AVFilter *abuffer = avfilter_get_by_name("abuffer");
+ if (!abuffer) {
+ fprintf(stderr, "Could not find the abuffer filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
- resolved_path[resolved_path_len] = '\0';
+ AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL);
+ if (!abuffer_ctx) {
+ fprintf(stderr, "Could not allocate the abuffer instance.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ #if LIBAVCODEC_VERSION_MAJOR < 60
+ av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO);
+ #else
+ av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout));
+ #endif
+ av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN);
+
+ err = avfilter_init_str(abuffer_ctx, NULL);
+ if (err < 0) {
+ fprintf(stderr, "Could not initialize the abuffer filter.\n");
+ return err;
+ }
+
+ src_filter_ctx.push_back(abuffer_ctx);
+ }
+
+ const AVFilter *mix_filter = avfilter_get_by_name("amix");
+ if (!mix_filter) {
+ av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
+
+ char args[512];
+ snprintf(args, sizeof(args), "inputs=%d", (int)num_sources);
+
+ AVFilterContext *mix_ctx;
+ err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n");
+ return err;
+ }
+
+ const AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
+ if (!abuffersink) {
+ fprintf(stderr, "Could not find the abuffersink filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
+
+ AVFilterContext *abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink");
+ if (!abuffersink_ctx) {
+ fprintf(stderr, "Could not allocate the abuffersink instance.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ err = avfilter_init_str(abuffersink_ctx, NULL);
+ if (err < 0) {
+ fprintf(stderr, "Could not initialize the abuffersink instance.\n");
+ return err;
+ }
+
+ err = 0;
+ for(size_t i = 0; i < src_filter_ctx.size(); ++i) {
+ AVFilterContext *src_ctx = src_filter_ctx[i];
+ if (err >= 0)
+ err = avfilter_link(src_ctx, 0, mix_ctx, i);
+ }
+ if (err >= 0)
+ err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n");
+ return err;
+ }
+
+ err = avfilter_graph_config(filter_graph, NULL);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n");
+ return err;
+ }
+
+ *graph = filter_graph;
+ *sink = abuffersink_ctx;
+
+ return 0;
+}
- const int program_name_len = strlen(program_name);
- return resolved_path_len >= program_name_len && memcmp(resolved_path + resolved_path_len - program_name_len, program_name, program_name_len) == 0;
+static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) {
+ bool *xwayland_found = (bool*)userdata;
+ if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0)
+ *xwayland_found = true;
+ else if(memmem(monitor->name, monitor->name_len, "X11", 3))
+ *xwayland_found = true;
}
-static void handle_existing_pid_file() {
- char buffer[256];
- int fd = open(pid_file, O_RDONLY);
- if(fd == -1)
- return;
+static bool is_xwayland(Display *display) {
+ int opcode, event, error;
+ if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error))
+ return true;
+
+ bool xwayland_found = false;
+ for_each_active_monitor_output_x11(display, xwayland_check_callback, &xwayland_found);
+ return xwayland_found;
+}
- ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
- if(bytes_read < 0) {
- perror("failed to read gpu-screen-recorder pid file");
- exit(1);
+static void list_supported_video_codecs() {
+ bool wayland = false;
+ Display *dpy = XOpenDisplay(nullptr);
+ if (!dpy) {
+ wayland = true;
+ fprintf(stderr, "Warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
}
- buffer[bytes_read] = '\0';
- close(fd);
- long pid = 0;
- if(sscanf(buffer, "%ld %120s", &pid, buffer) == 2) {
- if(is_process_running_program(pid, "gpu-screen-recorder")) {
- fprintf(stderr, "Error: gpu-screen-recorder is already running\n");
- exit(1);
+ XSetErrorHandler(x11_error_handler);
+ XSetIOErrorHandler(x11_io_error_handler);
+
+ if(!wayland)
+ wayland = is_xwayland(dpy);
+
+ gsr_egl egl;
+ if(!gsr_egl_load(&egl, dpy, wayland, false)) {
+ fprintf(stderr, "gsr error: failed to load opengl\n");
+ _exit(1);
+ }
+
+ char card_path[128];
+ card_path[0] = '\0';
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: Allow specifying another card, and in other places
+ if(!gsr_get_valid_card_path(&egl, card_path)) {
+ fprintf(stderr, "Error: no /dev/dri/cardX device found. If you are running GPU Screen Recorder with prime-run then try running without it\n");
+ _exit(2);
}
- } else {
- fprintf(stderr, "Warning: gpu-screen-recorder pid file is in incorrect format, it's possible that its corrupt. Replacing file and continuing...\n");
}
- unlink(pid_file);
+
+ av_log_set_level(AV_LOG_FATAL);
+
+ // TODO: Output hdr
+ if(find_h264_encoder(egl.gpu_info.vendor, card_path))
+ puts("h264");
+ if(find_h265_encoder(egl.gpu_info.vendor, card_path))
+ puts("hevc");
+ if(find_av1_encoder(egl.gpu_info.vendor, card_path))
+ puts("av1");
+
+ fflush(stdout);
+
+ gsr_egl_unload(&egl);
+ if(dpy)
+ XCloseDisplay(dpy);
}
-static void handle_new_pid_file(const char *mode) {
- int fd = open(pid_file, O_WRONLY|O_CREAT|O_TRUNC, 0777);
- if(fd == -1) {
- perror("failed to create gpu-screen-recorder pid file");
- exit(1);
+static gsr_capture* create_capture_impl(const char *window_str, const char *screen_region, bool wayland, gsr_gpu_info gpu_inf, gsr_egl &egl, int fps, bool overclock, VideoCodec video_codec, gsr_color_range color_range, bool record_cursor) {
+ vec2i region_size = { 0, 0 };
+ Window src_window_id = None;
+ bool follow_focused = false;
+
+ gsr_capture *capture = nullptr;
+ if(strcmp(window_str, "focused") == 0) {
+ if(wayland) {
+ fprintf(stderr, "Error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n");
+ _exit(2);
+ }
+
+ if(!screen_region) {
+ fprintf(stderr, "Error: option -s is required when using -w focused\n");
+ usage();
+ }
+
+ if(sscanf(screen_region, "%dx%d", &region_size.x, &region_size.y) != 2) {
+ fprintf(stderr, "Error: invalid value for option -s '%s', expected a value in format WxH\n", screen_region);
+ usage();
+ }
+
+ if(region_size.x <= 0 || region_size.y <= 0) {
+ fprintf(stderr, "Error: invalud value for option -s '%s', expected width and height to be greater than 0\n", screen_region);
+ usage();
+ }
+
+ follow_focused = true;
+ } else if(contains_non_hex_number(window_str)) {
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ if(strcmp(window_str, "screen") == 0) {
+ FirstOutputCallback first_output;
+ first_output.output_name = NULL;
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_DRM, get_first_output, &first_output);
+
+ if(first_output.output_name) {
+ window_str = first_output.output_name;
+ } else {
+ fprintf(stderr, "Error: no available output found\n");
+ }
+ }
+
+ gsr_monitor gmon;
+ if(!get_monitor_by_name(&egl, GSR_CONNECTION_DRM, window_str, &gmon)) {
+ fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", window_str);
+ fprintf(stderr, " \"screen\"\n");
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_DRM, monitor_output_callback_print, NULL);
+ _exit(1);
+ }
+ } else {
+ if(strcmp(window_str, "screen") != 0 && strcmp(window_str, "screen-direct") != 0 && strcmp(window_str, "screen-direct-force") != 0) {
+ gsr_monitor gmon;
+ if(!get_monitor_by_name(&egl, GSR_CONNECTION_X11, window_str, &gmon)) {
+ const int screens_width = XWidthOfScreen(DefaultScreenOfDisplay(egl.x11.dpy));
+ const int screens_height = XWidthOfScreen(DefaultScreenOfDisplay(egl.x11.dpy));
+ fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", window_str);
+ fprintf(stderr, " \"screen\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ fprintf(stderr, " \"screen-direct\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ fprintf(stderr, " \"screen-direct-force\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_X11, monitor_output_callback_print, NULL);
+ _exit(1);
+ }
+ }
+ }
+
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) {
+ if(wayland) {
+ gsr_capture_kms_cuda_params kms_params;
+ kms_params.egl = &egl;
+ kms_params.display_to_capture = window_str;
+ kms_params.gpu_inf = gpu_inf;
+ kms_params.hdr = video_codec_is_hdr(video_codec);
+ kms_params.color_range = color_range;
+ kms_params.record_cursor = record_cursor;
+ capture = gsr_capture_kms_cuda_create(&kms_params);
+ if(!capture)
+ _exit(1);
+ } else {
+ const char *capture_target = window_str;
+ bool direct_capture = strcmp(window_str, "screen-direct") == 0;
+ if(direct_capture) {
+ capture_target = "screen";
+ // TODO: Temporary disable direct capture because push model causes stuttering when it's direct capturing. This might be a nvfbc bug. This does not happen when using a compositor.
+ direct_capture = false;
+ fprintf(stderr, "Warning: screen-direct has temporary been disabled as it causes stuttering. This is likely a NvFBC bug. Falling back to \"screen\".\n");
+ }
+
+ if(strcmp(window_str, "screen-direct-force") == 0) {
+ direct_capture = true;
+ capture_target = "screen";
+ }
+
+ gsr_capture_nvfbc_params nvfbc_params;
+ nvfbc_params.egl = &egl;
+ nvfbc_params.display_to_capture = capture_target;
+ nvfbc_params.fps = fps;
+ nvfbc_params.pos = { 0, 0 };
+ nvfbc_params.size = { 0, 0 };
+ nvfbc_params.direct_capture = direct_capture;
+ nvfbc_params.overclock = overclock;
+ nvfbc_params.hdr = video_codec_is_hdr(video_codec);
+ nvfbc_params.color_range = color_range;
+ nvfbc_params.record_cursor = record_cursor;
+ capture = gsr_capture_nvfbc_create(&nvfbc_params);
+ if(!capture)
+ _exit(1);
+ }
+ } else {
+ gsr_capture_kms_vaapi_params kms_params;
+ kms_params.egl = &egl;
+ kms_params.display_to_capture = window_str;
+ kms_params.gpu_inf = gpu_inf;
+ kms_params.hdr = video_codec_is_hdr(video_codec);
+ kms_params.color_range = color_range;
+ kms_params.record_cursor = record_cursor;
+ capture = gsr_capture_kms_vaapi_create(&kms_params);
+ if(!capture)
+ _exit(1);
+ }
+ } else {
+ if(wayland) {
+ fprintf(stderr, "Error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n");
+ _exit(2);
+ }
+
+ errno = 0;
+ src_window_id = strtol(window_str, nullptr, 0);
+ if(src_window_id == None || errno == EINVAL) {
+ fprintf(stderr, "Invalid window number %s\n", window_str);
+ usage();
+ }
}
- char buffer[256];
- const int buffer_size = snprintf(buffer, sizeof(buffer), "%ld %s", (long)getpid(), mode);
- if(write(fd, buffer, buffer_size) == -1) {
- perror("failed to write gpu-screen-recorder pid file");
- exit(1);
+ if(!capture) {
+ switch(egl.gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ case GSR_GPU_VENDOR_INTEL: {
+ gsr_capture_xcomposite_vaapi_params xcomposite_params;
+ xcomposite_params.base.egl = &egl;
+ xcomposite_params.base.window = src_window_id;
+ xcomposite_params.base.follow_focused = follow_focused;
+ xcomposite_params.base.region_size = region_size;
+ xcomposite_params.base.color_range = color_range;
+ xcomposite_params.base.record_cursor = record_cursor;
+ capture = gsr_capture_xcomposite_vaapi_create(&xcomposite_params);
+ if(!capture)
+ _exit(1);
+ break;
+ }
+ case GSR_GPU_VENDOR_NVIDIA: {
+ gsr_capture_xcomposite_cuda_params xcomposite_params;
+ xcomposite_params.base.egl = &egl;
+ xcomposite_params.base.window = src_window_id;
+ xcomposite_params.base.follow_focused = follow_focused;
+ xcomposite_params.base.region_size = region_size;
+ xcomposite_params.base.color_range = color_range;
+ xcomposite_params.base.record_cursor = record_cursor;
+ xcomposite_params.overclock = overclock;
+ capture = gsr_capture_xcomposite_cuda_create(&xcomposite_params);
+ if(!capture)
+ _exit(1);
+ break;
+ }
+ }
}
- close(fd);
+
+ return capture;
}
+struct Arg {
+ std::vector<const char*> values;
+ bool optional = false;
+ bool list = false;
+
+ const char* value() const {
+ if(values.empty())
+ return nullptr;
+ return values.front();
+ }
+};
+
int main(int argc, char **argv) {
- signal(SIGTERM, term_handler);
- signal(SIGINT, int_handler);
+ signal(SIGINT, stop_handler);
signal(SIGUSR1, save_replay_handler);
+ signal(SIGUSR2, toggle_pause_handler);
+
+ // Stop nvidia driver from buffering frames
+ setenv("__GL_MaxFramesAllowed", "1", true);
+ // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
+ // so we overwrite it
+ setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
+ // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
+ // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
+ // Let vaapi find the match vaapi driver instead of forcing a specific one.
+ unsetenv("LIBVA_DRIVER_NAME");
+
+ if(argc <= 1)
+ usage_full();
+
+ if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0))
+ usage_full();
+
+ if(argc == 2 && strcmp(argv[1], "--list-supported-video-codecs") == 0) {
+ list_supported_video_codecs();
+ _exit(0);
+ }
- handle_existing_pid_file();
+ //av_log_set_level(AV_LOG_TRACE);
std::map<std::string, Arg> args = {
- { "-w", Arg { nullptr, false } },
- //{ "-s", Arg { nullptr, true } },
- { "-c", Arg { nullptr, false } },
- { "-f", Arg { nullptr, false } },
- { "-s", Arg { nullptr, true } },
- { "-a", Arg { nullptr, true } },
- { "-q", Arg { nullptr, true } },
- { "-o", Arg { nullptr, true } },
- { "-r", Arg { nullptr, true } }
+ { "-w", Arg { {}, false, false } },
+ { "-c", Arg { {}, true, false } },
+ { "-f", Arg { {}, false, false } },
+ { "-s", Arg { {}, true, false } },
+ { "-a", Arg { {}, true, true } },
+ { "-q", Arg { {}, true, false } },
+ { "-o", Arg { {}, true, false } },
+ { "-r", Arg { {}, true, false } },
+ { "-k", Arg { {}, true, false } },
+ { "-ac", Arg { {}, true, false } },
+ { "-ab", Arg { {}, true, false } },
+ { "-oc", Arg { {}, true, false } },
+ { "-fm", Arg { {}, true, false } },
+ { "-pixfmt", Arg { {}, true, false } },
+ { "-v", Arg { {}, true, false } },
+ { "-mf", Arg { {}, true, false } },
+ { "-sc", Arg { {}, true, false } },
+ { "-cr", Arg { {}, true, false } },
+ { "-cursor", Arg { {}, true, false } },
};
- for(int i = 1; i < argc - 1; i += 2) {
+ for(int i = 1; i < argc; i += 2) {
auto it = args.find(argv[i]);
if(it == args.end()) {
fprintf(stderr, "Invalid argument '%s'\n", argv[i]);
usage();
}
- it->second.value = argv[i + 1];
+
+ if(!it->second.values.empty() && !it->second.list) {
+ fprintf(stderr, "Expected argument '%s' to only be specified once\n", argv[i]);
+ usage();
+ }
+
+ if(i + 1 >= argc) {
+ fprintf(stderr, "Missing value for argument '%s'\n", argv[i]);
+ usage();
+ }
+
+ it->second.values.push_back(argv[i + 1]);
}
for(auto &it : args) {
- if(!it.second.optional && !it.second.value) {
+ if(!it.second.optional && !it.second.value()) {
fprintf(stderr, "Missing argument '%s'\n", it.first.c_str());
usage();
}
}
- Arg &audio_input_arg = args["-a"];
+ VideoCodec video_codec = VideoCodec::HEVC;
+ const char *video_codec_to_use = args["-k"].value();
+ if(!video_codec_to_use)
+ video_codec_to_use = "auto";
+
+ if(strcmp(video_codec_to_use, "h264") == 0) {
+ video_codec = VideoCodec::H264;
+ } else if(strcmp(video_codec_to_use, "h265") == 0 || strcmp(video_codec_to_use, "hevc") == 0) {
+ video_codec = VideoCodec::HEVC;
+ } else if(strcmp(video_codec_to_use, "hevc_hdr") == 0) {
+ video_codec = VideoCodec::HEVC_HDR;
+ } else if(strcmp(video_codec_to_use, "av1") == 0) {
+ video_codec = VideoCodec::AV1;
+ } else if(strcmp(video_codec_to_use, "av1_hdr") == 0) {
+ video_codec = VideoCodec::AV1_HDR;
+ } else if(strcmp(video_codec_to_use, "auto") != 0) {
+ fprintf(stderr, "Error: -k should either be either 'auto', 'h264', 'hevc', 'hevc_hdr', 'av1' or 'av1_hdr', got: '%s'\n", video_codec_to_use);
+ usage();
+ }
+
+ AudioCodec audio_codec = AudioCodec::AAC;
+ const char *audio_codec_to_use = args["-ac"].value();
+ if(!audio_codec_to_use)
+ audio_codec_to_use = "aac";
+
+ if(strcmp(audio_codec_to_use, "aac") == 0) {
+ audio_codec = AudioCodec::AAC;
+ } else if(strcmp(audio_codec_to_use, "opus") == 0) {
+ audio_codec = AudioCodec::OPUS;
+ } else if(strcmp(audio_codec_to_use, "flac") == 0) {
+ audio_codec = AudioCodec::FLAC;
+ } else {
+ fprintf(stderr, "Error: -ac should either be either 'aac', 'opus' or 'flac', got: '%s'\n", audio_codec_to_use);
+ usage();
+ }
+
+ if(audio_codec == AudioCodec::OPUS || audio_codec == AudioCodec::FLAC) {
+ fprintf(stderr, "Warning: opus and flac audio codecs are temporary disabled, using aac audio codec instead\n");
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ }
+
+ int audio_bitrate = 0;
+ const char *audio_bitrate_str = args["-ab"].value();
+ if(audio_bitrate_str) {
+ if(sscanf(audio_bitrate_str, "%d", &audio_bitrate) != 1) {
+ fprintf(stderr, "Error: -ab argument \"%s\" is not an integer\n", audio_bitrate_str);
+ usage();
+ }
+ }
+
+ bool overclock = false;
+ const char *overclock_str = args["-oc"].value();
+ if(!overclock_str)
+ overclock_str = "no";
+
+ if(strcmp(overclock_str, "yes") == 0) {
+ overclock = true;
+ } else if(strcmp(overclock_str, "no") == 0) {
+ overclock = false;
+ } else {
+ fprintf(stderr, "Error: -oc should either be either 'yes' or 'no', got: '%s'\n", overclock_str);
+ usage();
+ }
+
+ bool verbose = true;
+ const char *verbose_str = args["-v"].value();
+ if(!verbose_str)
+ verbose_str = "yes";
+
+ if(strcmp(verbose_str, "yes") == 0) {
+ verbose = true;
+ } else if(strcmp(verbose_str, "no") == 0) {
+ verbose = false;
+ } else {
+ fprintf(stderr, "Error: -v should either be either 'yes' or 'no', got: '%s'\n", verbose_str);
+ usage();
+ }
+
+ bool record_cursor = true;
+ const char *record_cursor_str = args["-cursor"].value();
+ if(!record_cursor_str)
+ record_cursor_str = "yes";
+
+ if(strcmp(record_cursor_str, "yes") == 0) {
+ record_cursor = true;
+ } else if(strcmp(record_cursor_str, "no") == 0) {
+ record_cursor = false;
+ } else {
+ fprintf(stderr, "Error: -cursor should either be either 'yes' or 'no', got: '%s'\n", record_cursor_str);
+ usage();
+ }
+
+ bool make_folders = false;
+ const char *make_folders_str = args["-mf"].value();
+ if(!make_folders_str)
+ make_folders_str = "no";
+
+ if(strcmp(make_folders_str, "yes") == 0) {
+ make_folders = true;
+ } else if(strcmp(make_folders_str, "no") == 0) {
+ make_folders = false;
+ } else {
+ fprintf(stderr, "Error: -mf should either be either 'yes' or 'no', got: '%s'\n", make_folders_str);
+ usage();
+ }
+
+ const char *recording_saved_script = args["-sc"].value();
+ if(recording_saved_script) {
+ struct stat buf;
+ if(stat(recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) {
+ fprintf(stderr, "Error: Script \"%s\" either doesn't exist or it's not a file\n", recording_saved_script);
+ usage();
+ }
+
+ if(!(buf.st_mode & S_IXUSR)) {
+ fprintf(stderr, "Error: Script \"%s\" is not executable\n", recording_saved_script);
+ usage();
+ }
+ }
+
+ PixelFormat pixel_format = PixelFormat::YUV420;
+ const char *pixfmt = args["-pixfmt"].value();
+ if(!pixfmt)
+ pixfmt = "yuv420";
- uint32_t region_x = 0;
- uint32_t region_y = 0;
- uint32_t region_width = 0;
- uint32_t region_height = 0;
+ if(strcmp(pixfmt, "yuv420") == 0) {
+ pixel_format = PixelFormat::YUV420;
+ } else if(strcmp(pixfmt, "yuv444") == 0) {
+ pixel_format = PixelFormat::YUV444;
+ } else {
+ fprintf(stderr, "Error: -pixfmt should either be either 'yuv420', or 'yuv444', got: '%s'\n", pixfmt);
+ usage();
+ }
+
+ const Arg &audio_input_arg = args["-a"];
+ std::vector<AudioInput> audio_inputs;
+ if(!audio_input_arg.values.empty())
+ audio_inputs = get_pulseaudio_inputs();
+ std::vector<MergedAudioInputs> requested_audio_inputs;
+ bool uses_amix = false;
+
+ // Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
+ // Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED.
+ // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU
+ for(const char *audio_input : audio_input_arg.values) {
+ if(!audio_input || audio_input[0] == '\0')
+ continue;
- /*
- TODO: Fix this. Doesn't work for some reason
- const char *screen_region = args["-s"].value;
- if(screen_region) {
- if(sscanf(screen_region, "%ux%u+%u+%u", &region_x, &region_y, &region_width, &region_height) != 4) {
- fprintf(stderr, "Invalid value for -s '%s', expected a value in format WxH+X+Y\n", screen_region);
- return 1;
+ requested_audio_inputs.push_back({parse_audio_input_arg(audio_input)});
+ if(requested_audio_inputs.back().audio_inputs.size() > 1)
+ uses_amix = true;
+
+ for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) {
+ bool match = false;
+ for(const auto &existing_audio_input : audio_inputs) {
+ if(strcmp(request_audio_input.name.c_str(), existing_audio_input.name.c_str()) == 0) {
+ if(request_audio_input.description.empty())
+ request_audio_input.description = "gsr-" + existing_audio_input.description;
+
+ match = true;
+ break;
+ }
+ }
+
+ if(!match) {
+ fprintf(stderr, "Error: Audio input device '%s' is not a valid audio device, expected one of:\n", request_audio_input.name.c_str());
+ for(const auto &existing_audio_input : audio_inputs) {
+ fprintf(stderr, " %s (%s)\n", existing_audio_input.name.c_str(), existing_audio_input.description.c_str());
+ }
+ _exit(2);
+ }
}
}
- */
- const char *container_format = args["-c"].value;
- int fps = atoi(args["-f"].value);
+ const char *container_format = args["-c"].value();
+ if(container_format && strcmp(container_format, "mkv") == 0)
+ container_format = "matroska";
+
+ int fps = atoi(args["-f"].value());
if(fps == 0) {
- fprintf(stderr, "Invalid fps argument: %s\n", args["-f"].value);
- return 1;
+ fprintf(stderr, "Invalid fps argument: %s\n", args["-f"].value());
+ _exit(1);
}
- if(fps > 250)
- fps = 250;
+ if(fps < 1)
+ fps = 1;
- const char *quality_str = args["-q"].value;
+ const char *quality_str = args["-q"].value();
if(!quality_str)
- quality_str = "medium";
+ quality_str = "very_high";
VideoQuality quality;
if(strcmp(quality_str, "medium") == 0) {
quality = VideoQuality::MEDIUM;
} else if(strcmp(quality_str, "high") == 0) {
quality = VideoQuality::HIGH;
+ } else if(strcmp(quality_str, "very_high") == 0) {
+ quality = VideoQuality::VERY_HIGH;
} else if(strcmp(quality_str, "ultra") == 0) {
quality = VideoQuality::ULTRA;
} else {
- fprintf(stderr, "Error: -q should either be either 'medium', 'high' or 'ultra', got: '%s'\n", quality_str);
+ fprintf(stderr, "Error: -q should either be either 'medium', 'high', 'very_high' or 'ultra', got: '%s'\n", quality_str);
usage();
}
int replay_buffer_size_secs = -1;
- const char *replay_buffer_size_secs_str = args["-r"].value;
+ const char *replay_buffer_size_secs_str = args["-r"].value();
if(replay_buffer_size_secs_str) {
replay_buffer_size_secs = atoi(replay_buffer_size_secs_str);
if(replay_buffer_size_secs < 5 || replay_buffer_size_secs > 1200) {
fprintf(stderr, "Error: option -r has to be between 5 and 1200, was: %s\n", replay_buffer_size_secs_str);
- return 1;
+ _exit(1);
}
- replay_buffer_size_secs += 5; // Add a few seconds to account of lost packets because of non-keyframe packets skipped
+ replay_buffer_size_secs += 3; // Add a few seconds to account of lost packets because of non-keyframe packets skipped
}
- CUresult res;
+ const char *window_str = strdup(args["-w"].value());
- res = cuInit(0);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: cuInit failed, error %s (result: %d)\n", err_str, res);
- return 1;
+ bool wayland = false;
+ Display *dpy = XOpenDisplay(nullptr);
+ if (!dpy) {
+ wayland = true;
+ fprintf(stderr, "Warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
}
- int nGpu = 0;
- cuDeviceGetCount(&nGpu);
- if (nGpu <= 0) {
- fprintf(stderr, "Error: no cuda supported devices found\n");
- return 1;
+ XSetErrorHandler(x11_error_handler);
+ XSetIOErrorHandler(x11_io_error_handler);
+
+ if(!wayland)
+ wayland = is_xwayland(dpy);
+
+ if(video_codec_is_hdr(video_codec) && !wayland) {
+ fprintf(stderr, "Error: hdr video codec option %s is not available on X11\n", video_codec_to_use);
+ _exit(1);
}
- CUdevice cu_dev;
- res = cuDeviceGet(&cu_dev, 0);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: unable to get CUDA device, error: %s (result: %d)\n", err_str, res);
- return 1;
+ const bool is_monitor_capture = strcmp(window_str, "focused") != 0 && contains_non_hex_number(window_str);
+ gsr_egl egl;
+ if(!gsr_egl_load(&egl, dpy, wayland, is_monitor_capture)) {
+ fprintf(stderr, "gsr error: failed to load opengl\n");
+ _exit(1);
}
- CUcontext cu_ctx;
- res = cuCtxCreate_v2(&cu_ctx, CU_CTX_SCHED_AUTO, cu_dev);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: unable to create CUDA context, error: %s (result: %d)\n", err_str, res);
- return 1;
+ bool very_old_gpu = false;
+
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl.gpu_info.gpu_version != 0 && egl.gpu_info.gpu_version < 900) {
+ fprintf(stderr, "Info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n");
+ very_old_gpu = true;
}
- uint32_t window_width = 0;
- uint32_t window_height = 0;
+ if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && overclock) {
+ fprintf(stderr, "Info: overclock option has no effect on amd/intel, ignoring option\n");
+ }
- NvFBCLibrary nv_fbc_library;
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && overclock && wayland) {
+ fprintf(stderr, "Info: overclocking is not possible on nvidia on wayland, ignoring option\n");
+ }
- const char *window_str = args["-w"].value;
- Window src_window_id = None;
- if(contains_non_hex_number(window_str)) {
- if(!nv_fbc_library.load())
- return 1;
+ egl.card_path[0] = '\0';
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: Allow specifying another card, and in other places
+ if(!gsr_get_valid_card_path(&egl, egl.card_path)) {
+ fprintf(stderr, "Error: no /dev/dri/cardX device found. If you are running GPU Screen Recorder with prime-run then try running without it\n");
+ _exit(2);
+ }
+ }
+
+ // TODO: Fix constant framerate not working properly on amd/intel because capture framerate gets locked to the same framerate as
+ // game framerate, which doesn't work well when you need to encode multiple duplicate frames (AMD/Intel is slow at encoding!).
+ // It also appears to skip audio frames on nvidia wayland? why? that should be fine, but it causes video stuttering because of audio/video sync.
+ FramerateMode framerate_mode;
+ const char *framerate_mode_str = args["-fm"].value();
+ if(!framerate_mode_str)
+ framerate_mode_str = "vfr";
+
+ if(strcmp(framerate_mode_str, "cfr") == 0) {
+ framerate_mode = FramerateMode::CONSTANT;
+ } else if(strcmp(framerate_mode_str, "vfr") == 0) {
+ framerate_mode = FramerateMode::VARIABLE;
+ } else {
+ fprintf(stderr, "Error: -fm should either be either 'cfr' or 'vfr', got: '%s'\n", framerate_mode_str);
+ usage();
+ }
- const char *capture_target = window_str;
- const bool direct_capture = strcmp(window_str, "screen-direct") == 0;
- if(direct_capture)
- capture_target = "screen";
+ gsr_color_range color_range;
+ const char *color_range_str = args["-cr"].value();
+ if(!color_range_str)
+ color_range_str = "limited";
- if(!nv_fbc_library.create(capture_target, fps, &window_width, &window_height, region_x, region_y, region_width, region_height, direct_capture))
- return 1;
+ if(strcmp(color_range_str, "limited") == 0) {
+ color_range = GSR_COLOR_RANGE_LIMITED;
+ } else if(strcmp(color_range_str, "full") == 0) {
+ color_range = GSR_COLOR_RANGE_FULL;
} else {
- errno = 0;
- src_window_id = strtol(window_str, nullptr, 0);
- if(src_window_id == None || errno == EINVAL) {
- fprintf(stderr, "Invalid window number %s\n", window_str);
- usage();
- }
+ fprintf(stderr, "Error: -cr should either be either 'limited' or 'full', got: '%s'\n", color_range_str);
+ usage();
}
- int record_width = window_width;
- int record_height = window_height;
- const char *record_area = args["-s"].value;
- if(record_area) {
- if(sscanf(record_area, "%dx%d", &record_width, &record_height) != 2) {
- fprintf(stderr, "Invalid value for -s '%s', expected a value in format WxH\n", record_area);
- return 1;
- }
+ const char *screen_region = args["-s"].value();
+
+ if(screen_region && strcmp(window_str, "focused") != 0) {
+ fprintf(stderr, "Error: option -s is only available when using -w focused\n");
+ usage();
}
- const char *filename = args["-o"].value;
+ bool is_livestream = false;
+ const char *filename = args["-o"].value();
if(filename) {
- if(replay_buffer_size_secs != -1) {
- struct stat buf;
- if(stat(filename, &buf) == -1 || !S_ISDIR(buf.st_mode)) {
- fprintf(stderr, "%s does not exist or is not a directory\n", filename);
- usage();
+ is_livestream = is_livestream_path(filename);
+ if(is_livestream) {
+ if(replay_buffer_size_secs != -1) {
+ fprintf(stderr, "Error: replay mode is not applicable to live streaming\n");
+ _exit(1);
+ }
+ } else {
+ if(replay_buffer_size_secs == -1) {
+ char directory_buf[PATH_MAX];
+ strcpy(directory_buf, filename);
+ char *directory = dirname(directory_buf);
+ if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) {
+ if(create_directory_recursive(directory) != 0) {
+ fprintf(stderr, "Error: failed to create directory for output file: %s\n", filename);
+ _exit(1);
+ }
+ }
+ } else {
+ if(!container_format) {
+ fprintf(stderr, "Error: option -c is required when using option -r\n");
+ usage();
+ }
+
+ struct stat buf;
+ if(stat(filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) {
+ fprintf(stderr, "Error: File \"%s\" exists but it's not a directory\n", filename);
+ usage();
+ }
}
}
} else {
if(replay_buffer_size_secs == -1) {
filename = "/dev/stdout";
} else {
- fprintf(stderr, "Option -o is required when using option -r\n");
+ fprintf(stderr, "Error: Option -o is required when using option -r\n");
usage();
}
- }
- const double target_fps = 1.0 / (double)fps;
-
- WindowPixmap window_pixmap;
- Display *dpy = nullptr;
- GLFWwindow *window = nullptr;
- if(src_window_id) {
- dpy = XOpenDisplay(nullptr);
- if (!dpy) {
- fprintf(stderr, "Error: Failed to open display\n");
- return 1;
+ if(!container_format) {
+ fprintf(stderr, "Error: option -c is required when not using option -o\n");
+ usage();
}
+ }
- bool has_name_pixmap = x11_supports_composite_named_window_pixmap(dpy);
- if (!has_name_pixmap) {
- fprintf(stderr, "Error: XCompositeNameWindowPixmap is not supported by "
- "your X11 server\n");
- return 1;
- }
+ AVFormatContext *av_format_context;
+ // The output format is automatically guessed by the file extension
+ avformat_alloc_output_context2(&av_format_context, nullptr, container_format, filename);
+ if (!av_format_context) {
+ if(container_format)
+ fprintf(stderr, "Error: Container format '%s' (argument -c) is not valid\n", container_format);
+ else
+ fprintf(stderr, "Error: Failed to deduce container format from file extension\n");
+ _exit(1);
+ }
- XWindowAttributes attr;
- if (!XGetWindowAttributes(dpy, src_window_id, &attr)) {
- fprintf(stderr, "Error: Invalid window id: %lu\n", src_window_id);
- return 1;
- }
+ const AVOutputFormat *output_format = av_format_context->oformat;
- window_width = attr.width;
- window_height = attr.height;
+ std::string file_extension = output_format->extensions;
+ {
+ size_t comma_index = file_extension.find(',');
+ if(comma_index != std::string::npos)
+ file_extension = file_extension.substr(0, comma_index);
+ }
- XCompositeRedirectWindow(dpy, src_window_id, CompositeRedirectAutomatic);
+ if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && file_extension == "mkv" && strcmp(video_codec_to_use, "h264") == 0) {
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ fprintf(stderr, "Warning: video codec was forcefully set to hevc because mkv container is used and mesa (AMD and Intel driver) does not support h264 in mkv files\n");
+ }
- // glXMakeContextCurrent(Display *dpy, GLXDrawable draw, GLXDrawable read,
- // GLXContext ctx)
- if (!glfwInit()) {
- fprintf(stderr, "Error: Failed to initialize glfw\n");
- return 1;
+ switch(audio_codec) {
+ case AudioCodec::AAC: {
+ break;
}
-
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
- glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
- glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
- glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
- glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
-
- window = glfwCreateWindow(1, 1, "gpu-screen-recorder", nullptr, nullptr);
- if (!window) {
- fprintf(stderr, "Error: Failed to create glfw window\n");
- glfwTerminate();
- return 1;
+ case AudioCodec::OPUS: {
+ // TODO: Also check mpegts?
+ if(file_extension != "mp4" && file_extension != "mkv") {
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ fprintf(stderr, "Warning: opus audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n");
+ }
+ break;
+ }
+ case AudioCodec::FLAC: {
+ // TODO: Also check mpegts?
+ if(file_extension != "mp4" && file_extension != "mkv") {
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ fprintf(stderr, "Warning: flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n");
+ } else if(uses_amix) {
+ audio_codec_to_use = "opus";
+ audio_codec = AudioCodec::OPUS;
+ fprintf(stderr, "Warning: flac audio codec is not supported when mixing audio sources, falling back to opus instead\n");
+ }
+ break;
}
+ }
- glfwMakeContextCurrent(window);
- glfwSwapInterval(0);
+ const double target_fps = 1.0 / (double)fps;
- //#if defined(DEBUG)
- XSetErrorHandler(x11_error_handler);
- XSetIOErrorHandler(x11_io_error_handler);
- //#endif
+ const bool video_codec_auto = strcmp(video_codec_to_use, "auto") == 0;
+ if(video_codec_auto) {
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_INTEL) {
+ const AVCodec *h264_codec = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ if(!h264_codec) {
+ fprintf(stderr, "Info: using hevc encoder because a codec was not specified and your gpu does not support h264\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ } else {
+ fprintf(stderr, "Info: using h264 encoder because a codec was not specified\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ }
+ } else {
+ const AVCodec *h265_codec = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
- glewExperimental = GL_TRUE;
- GLenum nGlewError = glewInit();
- if (nGlewError != GLEW_OK) {
- fprintf(stderr, "%s - Error initializing GLEW! %s\n", __FUNCTION__,
- glewGetErrorString(nGlewError));
- return 1;
- }
- glGetError(); // to clear the error caused deep in GLEW
+ if(h265_codec && fps > 60) {
+ fprintf(stderr, "Warning: recording at higher fps than 60 with hevc might result in recording at a very low fps. If this happens, switch to h264 or av1\n");
+ }
- if (!recreate_window_pixmap(dpy, src_window_id, window_pixmap)) {
- fprintf(stderr, "Error: Failed to create glx pixmap for window: %lu\n",
- src_window_id);
- return 1;
+ // hevc generally allows recording at a higher resolution than h264 on nvidia cards. On a gtx 1080 4k is the max resolution for h264 but for hevc it's 8k.
+ // Another important info is that when recording at a higher fps than.. 60? hevc has very bad performance. For example when recording at 144 fps the fps drops to 1
+ // while with h264 the fps doesn't drop.
+ if(!h265_codec) {
+ fprintf(stderr, "Info: using h264 encoder because a codec was not specified and your gpu does not support hevc\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ } else {
+ fprintf(stderr, "Info: using hevc encoder because a codec was not specified\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ }
}
+ }
- if(!record_area) {
- record_width = window_pixmap.texture_width;
- record_height = window_pixmap.texture_height;
- fprintf(stderr, "Record size: %dx%d\n", record_width, record_height);
+ // TODO: Allow hevc, vp9 and av1 in (enhanced) flv (supported since ffmpeg 6.1)
+ const bool is_flv = strcmp(file_extension.c_str(), "flv") == 0;
+ if(video_codec != VideoCodec::H264 && is_flv) {
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ fprintf(stderr, "Warning: hevc/av1 is not compatible with flv, falling back to h264 instead.\n");
+ }
+
+ const AVCodec *video_codec_f = nullptr;
+ switch(video_codec) {
+ case VideoCodec::H264:
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR:
+ video_codec_f = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR:
+ video_codec_f = find_av1_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+
+ if(!video_codec_auto && !video_codec_f && !is_flv) {
+ switch(video_codec) {
+ case VideoCodec::H264: {
+ fprintf(stderr, "Warning: selected video codec h264 is not supported, trying hevc instead\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ video_codec_f = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR: {
+ fprintf(stderr, "Warning: selected video codec hevc is not supported, trying h264 instead\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR: {
+ fprintf(stderr, "Warning: selected video codec av1 is not supported, trying h264 instead\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
}
- } else {
- window_pixmap.texture_id = 0;
- window_pixmap.target_texture_id = 0;
- window_pixmap.texture_width = window_width;
- window_pixmap.texture_height = window_height;
+ }
- if (!glfwInit()) {
- fprintf(stderr, "Error: Failed to initialize glfw\n");
- return 1;
+ if(!video_codec_f) {
+ const char *video_codec_name = "";
+ switch(video_codec) {
+ case VideoCodec::H264: {
+ video_codec_name = "h264";
+ break;
+ }
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR: {
+ video_codec_name = "hevc";
+ break;
+ }
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR: {
+ video_codec_name = "av1";
+ break;
+ }
}
+
+ fprintf(stderr, "Error: your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n"
+ " then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver or libva-mesa-driver).\n"
+ " It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n"
+ " This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n"
+ " You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC profile.\n"
+ " On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n"
+ " You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n"
+ " Make sure you have mesa-extra freedesktop runtime installed when using the flatpak (this should be the default), which can be installed with this command:\n"
+ " flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra", video_codec_name, video_codec_name, video_codec_name);
+ _exit(2);
}
- // Video start
- AVFormatContext *av_format_context;
- // The output format is automatically guessed by the file extension
- avformat_alloc_output_context2(&av_format_context, nullptr, container_format,
- nullptr);
- if (!av_format_context) {
- fprintf(
- stderr,
- "Error: Failed to deduce output format from file extension\n");
- return 1;
+ gsr_capture *capture = create_capture_impl(window_str, screen_region, wayland, egl.gpu_info, egl, fps, overclock, video_codec, color_range, record_cursor);
+
+ // (Some?) livestreaming services require at least one audio track to work.
+ // If not audio is provided then create one silent audio track.
+ if(is_livestream && requested_audio_inputs.empty()) {
+ fprintf(stderr, "Info: live streaming but no audio track was added. Adding a silent audio track\n");
+ MergedAudioInputs mai;
+ mai.audio_inputs.push_back({ "", "gsr-silent" });
+ requested_audio_inputs.push_back(std::move(mai));
}
- av_format_context->flags |= AVFMT_FLAG_GENPTS;
- const AVOutputFormat *output_format = av_format_context->oformat;
+ if(is_livestream && framerate_mode != FramerateMode::CONSTANT) {
+ fprintf(stderr, "Info: framerate mode was forcefully set to \"cfr\" because live streaming was detected\n");
+ framerate_mode = FramerateMode::CONSTANT;
+ framerate_mode_str = "cfr";
+ }
- //bool use_hevc = strcmp(window_str, "screen") == 0 || strcmp(window_str, "screen-direct") == 0;
- bool use_hevc = true;
- if(use_hevc && strcmp(container_format, "flv") == 0) {
- use_hevc = false;
- fprintf(stderr, "Warning: hevc is not compatible with flv, falling back to h264 instead.\n");
+ if(is_livestream && recording_saved_script) {
+ fprintf(stderr, "Warning: live stream detected, -sc script is ignored\n");
+ recording_saved_script = nullptr;
}
AVStream *video_stream = nullptr;
- AVStream *audio_stream = nullptr;
+ std::vector<AudioTrack> audio_tracks;
+ const bool hdr = video_codec_is_hdr(video_codec);
- AVCodecContext *video_codec_context = create_video_codec_context(av_format_context, quality, record_width, record_height, fps, use_hevc);
+ AVCodecContext *video_codec_context = create_video_codec_context(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI, quality, fps, video_codec_f, is_livestream, egl.gpu_info.vendor, framerate_mode, hdr, color_range);
if(replay_buffer_size_secs == -1)
video_stream = create_stream(av_format_context, video_codec_context);
- AVBufferRef *device_ctx;
- CUgraphicsResource cuda_graphics_resource;
- open_video(video_codec_context, window_pixmap, &device_ctx, &cuda_graphics_resource, cu_ctx);
+ AVFrame *video_frame = av_frame_alloc();
+ if(!video_frame) {
+ fprintf(stderr, "Error: Failed to allocate video frame\n");
+ _exit(1);
+ }
+ video_frame->format = video_codec_context->pix_fmt;
+ video_frame->width = video_codec_context->width;
+ video_frame->height = video_codec_context->height;
+ video_frame->color_range = video_codec_context->color_range;
+ video_frame->color_primaries = video_codec_context->color_primaries;
+ video_frame->color_trc = video_codec_context->color_trc;
+ video_frame->colorspace = video_codec_context->colorspace;
+ video_frame->chroma_location = video_codec_context->chroma_sample_location;
+
+ int capture_result = gsr_capture_start(capture, video_codec_context, video_frame);
+ if(capture_result != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_start failed\n");
+ _exit(capture_result);
+ }
+
+ open_video(video_codec_context, quality, very_old_gpu, egl.gpu_info.vendor, pixel_format, hdr);
if(video_stream)
avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
- AVCodecContext *audio_codec_context = nullptr;
- AVFrame *audio_frame = nullptr;
- if(audio_input_arg.value) {
- audio_codec_context = create_audio_codec_context(av_format_context, fps);
+ int audio_stream_index = VIDEO_STREAM_INDEX + 1;
+ for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) {
+ const bool use_amix = merged_audio_inputs.audio_inputs.size() > 1;
+ AVCodecContext *audio_codec_context = create_audio_codec_context(fps, audio_codec, use_amix, audio_bitrate);
+
+ AVStream *audio_stream = nullptr;
if(replay_buffer_size_secs == -1)
audio_stream = create_stream(av_format_context, audio_codec_context);
- audio_frame = open_audio(audio_codec_context);
+ open_audio(audio_codec_context);
if(audio_stream)
avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
+
+ #if LIBAVCODEC_VERSION_MAJOR < 60
+ const int num_channels = audio_codec_context->channels;
+ #else
+ const int num_channels = audio_codec_context->ch_layout.nb_channels;
+ #endif
+
+ //audio_frame->sample_rate = audio_codec_context->sample_rate;
+
+ std::vector<AVFilterContext*> src_filter_ctx;
+ AVFilterGraph *graph = nullptr;
+ AVFilterContext *sink = nullptr;
+ if(use_amix) {
+ int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size());
+ if(err < 0) {
+ fprintf(stderr, "Error: failed to create audio filter\n");
+ _exit(1);
+ }
+ }
+
+ // TODO: Cleanup above
+
+ std::vector<AudioDevice> audio_devices;
+ for(size_t i = 0; i < merged_audio_inputs.audio_inputs.size(); ++i) {
+ auto &audio_input = merged_audio_inputs.audio_inputs[i];
+ AVFilterContext *src_ctx = nullptr;
+ if(use_amix)
+ src_ctx = src_filter_ctx[i];
+
+ AudioDevice audio_device;
+ audio_device.audio_input = audio_input;
+ audio_device.src_filter_ctx = src_ctx;
+
+ if(audio_input.name.empty()) {
+ audio_device.sound_device.handle = NULL;
+ audio_device.sound_device.frames = 0;
+ } else {
+ if(sound_device_get_by_name(&audio_device.sound_device, audio_input.name.c_str(), audio_input.description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) {
+ fprintf(stderr, "Error: failed to get \"%s\" sound device\n", audio_input.name.c_str());
+ _exit(1);
+ }
+ }
+
+ audio_device.frame = create_audio_frame(audio_codec_context);
+ audio_device.frame->pts = 0;
+
+ audio_devices.push_back(std::move(audio_device));
+ }
+
+ AudioTrack audio_track;
+ audio_track.codec_context = audio_codec_context;
+ audio_track.stream = audio_stream;
+ audio_track.audio_devices = std::move(audio_devices);
+ audio_track.graph = graph;
+ audio_track.sink = sink;
+ audio_track.stream_index = audio_stream_index;
+ audio_tracks.push_back(std::move(audio_track));
+ ++audio_stream_index;
}
//av_dump_format(av_format_context, 0, filename, 1);
@@ -1148,351 +2349,359 @@ int main(int argc, char **argv) {
int ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "Error: Could not open '%s': %s\n", filename, av_error_to_string(ret));
- return 1;
+ _exit(1);
}
}
- //video_stream->duration = AV_TIME_BASE * 15;
- //audio_stream->duration = AV_TIME_BASE * 15;
- //av_format_context->duration = AV_TIME_BASE * 15;
if(replay_buffer_size_secs == -1) {
- int ret = avformat_write_header(av_format_context, nullptr);
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+ //av_dict_set_int(&av_format_context->metadata, "video_full_range_flag", 1, 0);
+
+ int ret = avformat_write_header(av_format_context, &options);
if (ret < 0) {
fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret));
- return 1;
+ _exit(1);
}
- }
-
- // av_frame_free(&rgb_frame);
- // avcodec_close(av_codec_context);
- if(dpy)
- XSelectInput(dpy, src_window_id, StructureNotifyMask | VisibilityChangeMask);
-
- /*
- int damage_event;
- int damage_error;
- if (!XDamageQueryExtension(dpy, &damage_event, &damage_error)) {
- fprintf(stderr, "Error: XDamage is not supported by your X11 server\n");
- return 1;
+ av_dict_free(&options);
}
- Damage damage = XDamageCreate(dpy, src_window_id, XDamageReportNonEmpty);
- XDamageSubtract(dpy, damage,None,None);
- */
-
const double start_time_pts = clock_get_monotonic_seconds();
- CUcontext old_ctx;
- CUarray mapped_array;
- if(src_window_id) {
- res = cuCtxPopCurrent(&old_ctx);
- res = cuCtxPushCurrent(cu_ctx);
-
- // Get texture
- res = cuGraphicsResourceSetMapFlags(
- cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
- res = cuGraphicsMapResources(1, &cuda_graphics_resource, 0);
-
- // Map texture to cuda array
- res = cuGraphicsSubResourceGetMappedArray(&mapped_array,
- cuda_graphics_resource, 0, 0);
- }
-
- // Release texture
- // res = cuGraphicsUnmapResources(1, &cuda_graphics_resource, 0);
-
- double start_time = glfwGetTime();
- double frame_timer_start = start_time;
- double window_resize_timer = start_time;
- bool window_resized = false;
+ double start_time = clock_get_monotonic_seconds();
+ double frame_timer_start = start_time - target_fps; // We want to capture the first frame immediately
int fps_counter = 0;
- int current_fps = 30;
- AVFrame *frame = av_frame_alloc();
- if (!frame) {
- fprintf(stderr, "Error: Failed to allocate frame\n");
- exit(1);
- }
- frame->format = video_codec_context->pix_fmt;
- frame->width = video_codec_context->width;
- frame->height = video_codec_context->height;
-
- if (av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0) < 0) {
- fprintf(stderr, "Error: av_hwframe_get_buffer failed\n");
- exit(1);
- }
-
- if(window_pixmap.texture_width < record_width)
- frame->width = window_pixmap.texture_width & ~1;
- else
- frame->width = record_width & ~1;
-
- if(window_pixmap.texture_height < record_height)
- frame->height = window_pixmap.texture_height & ~1;
- else
- frame->height = record_height & ~1;
+ bool paused = false;
+ double paused_time_offset = 0.0;
+ double paused_time_start = 0.0;
std::mutex write_output_mutex;
- std::thread audio_thread;
+ std::mutex audio_filter_mutex;
- double record_start_time = glfwGetTime();
- std::deque<AVPacket> frame_data_queue;
+ const double record_start_time = clock_get_monotonic_seconds();
+ std::deque<std::shared_ptr<PacketData>> frame_data_queue;
bool frames_erased = false;
- SoundDevice sound_device;
- uint8_t *audio_frame_buf;
- if(audio_input_arg.value) {
- if(sound_device_get_by_name(&sound_device, audio_input_arg.value, audio_codec_context->channels, audio_codec_context->frame_size) != 0) {
- fprintf(stderr, "failed to get 'pulse' sound device\n");
- exit(1);
- }
-
- int audio_buffer_size = av_samples_get_buffer_size(NULL, audio_codec_context->channels, audio_codec_context->frame_size, audio_codec_context->sample_fmt, 1);
- audio_frame_buf = (uint8_t *)av_malloc(audio_buffer_size);
- avcodec_fill_audio_frame(audio_frame, audio_codec_context->channels, audio_codec_context->sample_fmt, (const uint8_t*)audio_frame_buf, audio_buffer_size, 1);
-
- audio_thread = std::thread([record_start_time, replay_buffer_size_secs, &frame_data_queue, &frames_erased, audio_codec_context, start_time_pts, fps](AVFormatContext *av_format_context, AVStream *audio_stream, uint8_t *audio_frame_buf, SoundDevice *sound_device, AVFrame *audio_frame, std::mutex *write_output_mutex) mutable {
-
- SwrContext *swr = swr_alloc();
- if(!swr) {
- fprintf(stderr, "Failed to create SwrContext\n");
- exit(1);
- }
- av_opt_set_int(swr, "in_channel_layout", audio_codec_context->channel_layout, 0);
- av_opt_set_int(swr, "out_channel_layout", audio_codec_context->channel_layout, 0);
- av_opt_set_int(swr, "in_sample_rate", audio_codec_context->sample_rate, 0);
- av_opt_set_int(swr, "out_sample_rate", audio_codec_context->sample_rate, 0);
- av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
- av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
- swr_init(swr);
-
- while(running) {
- void *sound_buffer;
- int sound_buffer_size = sound_device_read_next_chunk(sound_device, &sound_buffer);
- if(sound_buffer_size >= 0) {
- // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format?
- swr_convert(swr, &audio_frame_buf, audio_frame->nb_samples, (const uint8_t**)&sound_buffer, sound_buffer_size);
- audio_frame->extended_data = &audio_frame_buf;
- audio_frame->pts = (clock_get_monotonic_seconds() - start_time_pts) * AV_TIME_BASE;
-
- int ret = avcodec_send_frame(audio_codec_context, audio_frame);
- if(ret < 0){
- fprintf(stderr, "Failed to encode!\n");
- break;
+ const size_t audio_buffer_size = 1024 * 4 * 2; // max 4 bytes/sample, 2 channels
+ uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size);
+ if(!empty_audio) {
+ fprintf(stderr, "Error: failed to create empty audio\n");
+ _exit(1);
+ }
+ memset(empty_audio, 0, audio_buffer_size);
+
+ const double audio_startup_time_seconds = std::max(0.0, 0.089166 - target_fps);
+
+ for(AudioTrack &audio_track : audio_tracks) {
+ for(AudioDevice &audio_device : audio_track.audio_devices) {
+ audio_device.thread = std::thread([&]() mutable {
+ const AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(audio_track.codec_context));
+ // TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged
+ const bool needs_audio_conversion = true;//audio_track.codec_context->sample_fmt != sound_device_sample_format;
+ SwrContext *swr = nullptr;
+ if(needs_audio_conversion) {
+ swr = swr_alloc();
+ if(!swr) {
+ fprintf(stderr, "Failed to create SwrContext\n");
+ _exit(1);
}
- if(ret >= 0)
- receive_frames(audio_codec_context, AUDIO_STREAM_INDEX, audio_stream, audio_frame, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, *write_output_mutex);
- } else {
- fprintf(stderr, "failed to read sound from device, error: %d\n", sound_buffer_size);
+ #if LIBAVUTIL_VERSION_MAJOR <= 56
+ av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
+ av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
+ #else
+ av_opt_set_chlayout(swr, "in_channel_layout", &audio_track.codec_context->ch_layout, 0);
+ av_opt_set_chlayout(swr, "out_channel_layout", &audio_track.codec_context->ch_layout, 0);
+ #endif
+ av_opt_set_int(swr, "in_sample_rate", audio_track.codec_context->sample_rate, 0);
+ av_opt_set_int(swr, "out_sample_rate", audio_track.codec_context->sample_rate, 0);
+ av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0);
+ av_opt_set_sample_fmt(swr, "out_sample_fmt", audio_track.codec_context->sample_fmt, 0);
+ swr_init(swr);
}
- }
- swr_free(&swr);
- }, av_format_context, audio_stream, audio_frame_buf, &sound_device, audio_frame, &write_output_mutex);
- }
+ double received_audio_time = clock_get_monotonic_seconds();
+ const double timeout_sec = 1000.0 / (double)audio_track.codec_context->sample_rate;
+ const int64_t timeout_ms = std::round(timeout_sec * 1000.0);
+
+ while(running) {
+ void *sound_buffer;
+ int sound_buffer_size = -1;
+ //const double time_before_read_seconds = clock_get_monotonic_seconds();
+ if(audio_device.sound_device.handle) {
+ // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time,
+ // also it's 0.0 for some users???
+ double latency_seconds = 0.0;
+ sound_buffer_size = sound_device_read_next_chunk(&audio_device.sound_device, &sound_buffer, timeout_sec, &latency_seconds);
+ }
- handle_new_pid_file(replay_buffer_size_secs == -1 ? "record" : "replay");
- started = 1;
+ const bool got_audio_data = sound_buffer_size >= 0;
+ //const double time_after_read_seconds = clock_get_monotonic_seconds();
+ //const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds;
+ const double this_audio_frame_time = (clock_get_monotonic_seconds() - audio_startup_time_seconds) - paused_time_offset;
- bool redraw = true;
- XEvent e;
- while (running) {
- double frame_start = glfwGetTime();
- glfwPollEvents();
- if(window)
- glClear(GL_COLOR_BUFFER_BIT);
+ if(paused) {
+ if(got_audio_data)
+ received_audio_time = this_audio_frame_time;
- redraw = true;
+ if(!audio_device.sound_device.handle)
+ usleep(timeout_ms * 1000);
- if(src_window_id) {
- if (XCheckTypedWindowEvent(dpy, src_window_id, DestroyNotify, &e)) {
- running = 0;
- }
+ continue;
+ }
- if (XCheckTypedWindowEvent(dpy, src_window_id, VisibilityNotify, &e)) {
- window_resize_timer = glfwGetTime();
- window_resized = true;
- }
+ int ret = av_frame_make_writable(audio_device.frame);
+ if (ret < 0) {
+ fprintf(stderr, "Failed to make audio frame writable\n");
+ break;
+ }
- if (XCheckTypedWindowEvent(dpy, src_window_id, ConfigureNotify, &e) && e.xconfigure.window == src_window_id) {
- // Window resize
- if(e.xconfigure.width != window_width || e.xconfigure.height != window_height) {
- window_width = e.xconfigure.width;
- window_height = e.xconfigure.height;
- window_resize_timer = glfwGetTime();
- window_resized = true;
- }
- }
+ // TODO: Is this |received_audio_time| really correct?
+ const double prev_audio_time = received_audio_time;
+ const double audio_receive_time_diff = this_audio_frame_time - received_audio_time;
+ int64_t num_missing_frames = std::round(audio_receive_time_diff / timeout_sec);
+ if(got_audio_data)
+ num_missing_frames = std::max((int64_t)0, num_missing_frames - 1);
+
+ if(!audio_device.sound_device.handle)
+ num_missing_frames = std::max((int64_t)1, num_missing_frames);
+
+ if(got_audio_data)
+ received_audio_time = this_audio_frame_time;
+
+ // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW.
+ // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY.
+ // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!!
+ // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate
+ // videos because bad software such as video editing software and VLC do not support variable frame rate software,
+ // despite nvidia shadowplay and xbox game bar producing variable frame rate videos.
+ // So we have to make sure we produce frames at the same relative rate as the video.
+ if(num_missing_frames >= 5 || !audio_device.sound_device.handle) {
+ // TODO:
+ //audio_track.frame->data[0] = empty_audio;
+ received_audio_time = this_audio_frame_time;
+ if(needs_audio_conversion)
+ swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size);
+ else
+ audio_device.frame->data[0] = empty_audio;
+
+ // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ for(int i = 0; i < num_missing_frames; ++i) {
+ const int64_t new_pts = ((prev_audio_time - record_start_time) + timeout_sec * i) * AV_TIME_BASE;
+ if(new_pts == audio_device.frame->pts)
+ continue;
+
+ audio_device.frame->pts = new_pts;
+ if(audio_track.graph) {
+ // TODO: av_buffersrc_add_frame
+ if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) {
+ fprintf(stderr, "Error: failed to add audio frame to filter\n");
+ }
+ } else {
+ ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame);
+ if(ret >= 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, audio_device.frame->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ }
+ }
+ }
- const double window_resize_timeout = 1.0; // 1 second
- if(window_resized && glfwGetTime() - window_resize_timer >= window_resize_timeout) {
- window_resized = false;
- fprintf(stderr, "Resize window!\n");
- recreate_window_pixmap(dpy, src_window_id, window_pixmap);
- // Resolution must be a multiple of two
- //video_stream->codec->width = window_pixmap.texture_width & ~1;
- //video_stream->codec->height = window_pixmap.texture_height & ~1;
-
- cuGraphicsUnregisterResource(cuda_graphics_resource);
- res = cuGraphicsGLRegisterImage(
- &cuda_graphics_resource, window_pixmap.target_texture_id, GL_TEXTURE_2D,
- CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
- if (res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr,
- "Error: cuGraphicsGLRegisterImage failed, error %s, texture "
- "id: %u\n",
- err_str, window_pixmap.target_texture_id);
- running = false;
- break;
+ if(!audio_device.sound_device.handle)
+ usleep(timeout_ms * 1000);
+
+ if(got_audio_data) {
+ // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format?
+ if(needs_audio_conversion)
+ swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size);
+ else
+ audio_device.frame->data[0] = (uint8_t*)sound_buffer;
+
+ const int64_t new_pts = (this_audio_frame_time - record_start_time) * AV_TIME_BASE;
+ if(new_pts != audio_device.frame->pts) {
+ audio_device.frame->pts = new_pts;
+
+ if(audio_track.graph) {
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ // TODO: av_buffersrc_add_frame
+ if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) {
+ fprintf(stderr, "Error: failed to add audio frame to filter\n");
+ }
+ } else {
+ ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame);
+ if(ret >= 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, audio_device.frame->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ }
+ }
+ }
}
- res = cuGraphicsResourceSetMapFlags(
- cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
- res = cuGraphicsMapResources(1, &cuda_graphics_resource, 0);
- res = cuGraphicsSubResourceGetMappedArray(&mapped_array, cuda_graphics_resource, 0, 0);
+ if(swr)
+ swr_free(&swr);
+ });
+ }
+ }
- av_frame_free(&frame);
- frame = av_frame_alloc();
- if (!frame) {
- fprintf(stderr, "Error: Failed to allocate frame\n");
- running = false;
- break;
- }
- frame->format = video_codec_context->pix_fmt;
- frame->width = video_codec_context->width;
- frame->height = video_codec_context->height;
+ // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video.
+ const double update_fps = fps + 190;
+ bool should_stop_error = false;
- if (av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0) < 0) {
- fprintf(stderr, "Error: av_hwframe_get_buffer failed\n");
- running = false;
- break;
- }
+ AVFrame *aframe = av_frame_alloc();
- if(window_pixmap.texture_width < record_width)
- frame->width = window_pixmap.texture_width & ~1;
- else
- frame->width = record_width & ~1;
+ int64_t video_pts_counter = 0;
+ int64_t video_prev_pts = 0;
- if(window_pixmap.texture_height < record_height)
- frame->height = window_pixmap.texture_height & ~1;
- else
- frame->height = record_height & ~1;
+ while(running) {
+ double frame_start = clock_get_monotonic_seconds();
- cuMemsetD8((CUdeviceptr)frame->data[0], 0, record_width * record_height * 4);
- }
+ gsr_capture_tick(capture, video_codec_context);
+ should_stop_error = false;
+ if(gsr_capture_should_stop(capture, &should_stop_error)) {
+ running = 0;
+ break;
}
-
++fps_counter;
- double time_now = glfwGetTime();
+ {
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ for(AudioTrack &audio_track : audio_tracks) {
+ if(!audio_track.sink)
+ continue;
+
+ int err = 0;
+ while ((err = av_buffersink_get_frame(audio_track.sink, aframe)) >= 0) {
+ const double this_audio_frame_time = (clock_get_monotonic_seconds() - audio_startup_time_seconds) - paused_time_offset;
+ const int64_t new_pts = (this_audio_frame_time - record_start_time) * AV_TIME_BASE;
+ if(new_pts == aframe->pts)
+ continue;
+ aframe->pts = new_pts;
+ err = avcodec_send_frame(audio_track.codec_context, aframe);
+ if(err >= 0){
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, aframe->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ av_frame_unref(aframe);
+ }
+ }
+ }
+
+ double time_now = clock_get_monotonic_seconds();
double frame_timer_elapsed = time_now - frame_timer_start;
double elapsed = time_now - start_time;
if (elapsed >= 1.0) {
- fprintf(stderr, "update fps: %d\n", fps_counter);
+ if(verbose) {
+ fprintf(stderr, "update fps: %d\n", fps_counter);
+ }
start_time = time_now;
- current_fps = fps_counter;
fps_counter = 0;
}
double frame_time_overflow = frame_timer_elapsed - target_fps;
if (frame_time_overflow >= 0.0) {
+ frame_time_overflow = std::min(frame_time_overflow, target_fps);
frame_timer_start = time_now - frame_time_overflow;
- bool frame_captured = true;
- if(redraw) {
- redraw = false;
- if(src_window_id) {
- // TODO: Use a framebuffer instead. glCopyImageSubData requires
- // opengl 4.2
- glCopyImageSubData(
- window_pixmap.texture_id, GL_TEXTURE_2D, 0, 0, 0, 0,
- window_pixmap.target_texture_id, GL_TEXTURE_2D, 0, 0, 0, 0,
- window_pixmap.texture_width, window_pixmap.texture_height, 1);
- int err = glGetError();
- if(err != 0) {
- static bool error_shown = false;
- if(!error_shown) {
- error_shown = true;
- fprintf(stderr, "Error: glCopyImageSubData failed, gl error: %d\n", err);
- }
+ const double this_video_frame_time = clock_get_monotonic_seconds() - paused_time_offset;
+ const int64_t expected_frames = std::round((this_video_frame_time - start_time_pts) / target_fps);
+ const int num_frames = framerate_mode == FramerateMode::CONSTANT ? std::max((int64_t)0LL, expected_frames - video_pts_counter) : 1;
+
+ if(num_frames > 0 && !paused) {
+ gsr_capture_capture(capture, video_frame);
+
+ // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
+ for(int i = 0; i < num_frames; ++i) {
+ if(framerate_mode == FramerateMode::CONSTANT) {
+ video_frame->pts = video_pts_counter + i;
+ } else {
+ video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE;
+ const bool same_pts = video_frame->pts == video_prev_pts;
+ video_prev_pts = video_frame->pts;
+ if(same_pts)
+ continue;
}
- glfwSwapBuffers(window);
- // int err = glGetError();
- // fprintf(stderr, "error: %d\n", err);
-
- // TODO: Remove this copy, which is only possible by using nvenc directly and encoding window_pixmap.target_texture_id
-
- CUDA_MEMCPY2D memcpy_struct;
- memcpy_struct.srcXInBytes = 0;
- memcpy_struct.srcY = 0;
- memcpy_struct.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
-
- memcpy_struct.dstXInBytes = 0;
- memcpy_struct.dstY = 0;
- memcpy_struct.dstMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
- memcpy_struct.srcArray = mapped_array;
- memcpy_struct.dstDevice = (CUdeviceptr)frame->data[0];
- memcpy_struct.dstPitch = frame->linesize[0];
- memcpy_struct.WidthInBytes = frame->width * 4;
- memcpy_struct.Height = frame->height;
- cuMemcpy2D(&memcpy_struct);
-
- frame_captured = true;
- } else {
- // TODO: Check when src_cu_device_ptr changes and re-register resource
- uint32_t byte_size = 0;
- CUdeviceptr src_cu_device_ptr = 0;
- frame_captured = nv_fbc_library.capture(&src_cu_device_ptr, &byte_size);
- frame->data[0] = (uint8_t*)src_cu_device_ptr;
+ int ret = avcodec_send_frame(video_codec_context, video_frame);
+ if(ret == 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(video_codec_context, VIDEO_STREAM_INDEX, video_stream, video_frame->pts, av_format_context,
+ record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Error: avcodec_send_frame failed, error: %s\n", av_error_to_string(ret));
+ }
}
- // res = cuCtxPopCurrent(&old_ctx);
+
+ gsr_capture_end(capture, video_frame);
+ video_pts_counter += num_frames;
}
+ }
- frame->pts = (clock_get_monotonic_seconds() - start_time_pts) * AV_TIME_BASE;
- if (avcodec_send_frame(video_codec_context, frame) >= 0) {
- receive_frames(video_codec_context, VIDEO_STREAM_INDEX, video_stream, frame, av_format_context,
- record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex);
+ if(toggle_pause == 1) {
+ const bool new_paused_state = !paused;
+ if(new_paused_state) {
+ paused_time_start = clock_get_monotonic_seconds();
+ fprintf(stderr, "Paused\n");
} else {
- fprintf(stderr, "Error: avcodec_send_frame failed\n");
+ paused_time_offset += (clock_get_monotonic_seconds() - paused_time_start);
+ fprintf(stderr, "Unpaused\n");
}
+
+ toggle_pause = 0;
+ paused = !paused;
}
if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
save_replay_thread.get();
puts(save_replay_output_filepath.c_str());
- for(size_t i = 0; i < save_replay_packets.size(); ++i) {
- av_packet_unref(&save_replay_packets[i]);
- }
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ std::lock_guard<std::mutex> lock(write_output_mutex);
save_replay_packets.clear();
}
if(save_replay == 1 && !save_replay_thread.valid() && replay_buffer_size_secs != -1) {
save_replay = 0;
- save_replay_async(video_codec_context, audio_codec_context, VIDEO_STREAM_INDEX, AUDIO_STREAM_INDEX, frame_data_queue, frames_erased, filename, container_format);
+ save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, frame_data_queue, frames_erased, filename, container_format, file_extension, write_output_mutex, make_folders);
}
- // av_frame_free(&frame);
- double frame_end = glfwGetTime();
- double frame_sleep_fps = 1.0 / 250.0;
+ double frame_end = clock_get_monotonic_seconds();
+ double frame_sleep_fps = 1.0 / update_fps;
double sleep_time = frame_sleep_fps - (frame_end - frame_start);
if(sleep_time > 0.0)
usleep(sleep_time * 1000.0 * 1000.0);
}
- running = 0;
+ running = 0;
- if(save_replay_thread.valid())
+ if(save_replay_thread.valid()) {
save_replay_thread.get();
+ puts(save_replay_output_filepath.c_str());
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ std::lock_guard<std::mutex> lock(write_output_mutex);
+ save_replay_packets.clear();
+ }
- if(audio_input_arg.value) {
- audio_thread.join();
- sound_device_close(&sound_device);
+ for(AudioTrack &audio_track : audio_tracks) {
+ for(AudioDevice &audio_device : audio_track.audio_devices) {
+ audio_device.thread.join();
+ sound_device_close(&audio_device.sound_device);
+ }
}
+ av_frame_free(&aframe);
+
if (replay_buffer_size_secs == -1 && av_write_trailer(av_format_context) != 0) {
fprintf(stderr, "Failed to write trailer\n");
}
@@ -1500,10 +2709,24 @@ int main(int argc, char **argv) {
if(replay_buffer_size_secs == -1 && !(output_format->flags & AVFMT_NOFILE))
avio_close(av_format_context->pb);
+ gsr_capture_destroy(capture, video_codec_context);
+
+ if(replay_buffer_size_secs == -1 && recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filename, "regular");
+
if(dpy) {
- XCompositeUnredirectWindow(dpy, src_window_id, CompositeRedirectAutomatic);
- XCloseDisplay(dpy);
+ // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this???
+ //XCloseDisplay(dpy);
}
- unlink(pid_file);
+ //av_frame_free(&video_frame);
+ free((void*)window_str);
+ free(empty_audio);
+ // We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
+ // but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
+ // the at_exit registered functions.
+ // Cuda (cuvid library in this case) seems to be waiting for a thread that never finishes execution.
+ // Maybe this happens because we dont clean up all ffmpeg resources?
+ // TODO: Investigate this.
+ _exit(should_stop_error ? 3 : 0);
}
diff --git a/src/overclock.c b/src/overclock.c
new file mode 100644
index 0000000..2cba623
--- /dev/null
+++ b/src/overclock.c
@@ -0,0 +1,281 @@
+#include "../include/overclock.h"
+#include <X11/Xlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+// HACK!!!: When a program uses cuda (including nvenc) then the nvidia driver drops to performance level 2 (memory transfer rate is dropped and possibly graphics clock).
+// Nvidia does this because in some very extreme cases of cuda there can be memory corruption when running at max memory transfer rate.
+// So to get around this we overclock memory transfer rate (maybe this should also be done for graphics clock?) to the best performance level while GPU Screen Recorder is running.
+
+// TODO: Does it always drop to performance level 2?
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+// Fields are 0 if not set
+typedef struct {
+ int perf;
+
+ int nv_clock;
+ int nv_clock_min;
+ int nv_clock_max;
+
+ int mem_clock;
+ int mem_clock_min;
+ int mem_clock_max;
+
+ int mem_transfer_rate;
+ int mem_transfer_rate_min;
+ int mem_transfer_rate_max;
+} NVCTRLPerformanceLevel;
+
+#define MAX_PERFORMANCE_LEVELS 12
+typedef struct {
+ NVCTRLPerformanceLevel performance_level[MAX_PERFORMANCE_LEVELS];
+ int num_performance_levels;
+} NVCTRLPerformanceLevelQuery;
+
+typedef void (*split_callback)(const char *str, size_t size, void *userdata);
+static void split_by_delimiter(const char *str, size_t size, char delimiter, split_callback callback, void *userdata) {
+ const char *it = str;
+ while(it < str + size) {
+ const char *prev_it = it;
+ it = memchr(it, delimiter, (str + size) - it);
+ if(!it)
+ it = str + size;
+
+ callback(prev_it, it - prev_it, userdata);
+ it += 1; // skip delimiter
+ }
+}
+
+typedef enum {
+ NVCTRL_GPU_NVCLOCK,
+ NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE,
+} NvCTRLAttributeType;
+
+static unsigned int attribute_type_to_attribute_param(NvCTRLAttributeType attribute_type) {
+ switch(attribute_type) {
+ case NVCTRL_GPU_NVCLOCK:
+ return NV_CTRL_GPU_NVCLOCK_OFFSET;
+ case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE:
+ return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET;
+ }
+ return 0;
+}
+
+static unsigned int attribute_type_to_attribute_param_all_levels(NvCTRLAttributeType attribute_type) {
+ switch(attribute_type) {
+ case NVCTRL_GPU_NVCLOCK:
+ return NV_CTRL_GPU_NVCLOCK_OFFSET_ALL_PERFORMANCE_LEVELS;
+ case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE:
+ return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS;
+ }
+ return 0;
+}
+
+// Returns 0 on error
+static int xnvctrl_get_attribute_max_value(gsr_xnvctrl *xnvctrl, int num_performance_levels, NvCTRLAttributeType attribute_type) {
+ NVCTRLAttributeValidValuesRec valid;
+ if(xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), &valid)) {
+ return valid.u.range.max;
+ }
+
+ if(num_performance_levels > 0 && xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, num_performance_levels - 1, attribute_type_to_attribute_param(attribute_type), &valid)) {
+ return valid.u.range.max;
+ }
+
+ return 0;
+}
+
+static bool xnvctrl_set_attribute_offset(gsr_xnvctrl *xnvctrl, int num_performance_levels, int offset, NvCTRLAttributeType attribute_type) {
+ bool success = false;
+
+ // NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS works (or at least used to?) without Xorg running as root
+ // so we try that first. NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS also only works with GTX 1000+.
+ // TODO: Reverse engineer NVIDIA Xorg driver so we can set this always without root access.
+ if(xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), offset))
+ success = true;
+
+ for(int i = 0; i < num_performance_levels; ++i) {
+ success |= xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, i, attribute_type_to_attribute_param(attribute_type), offset);
+ }
+
+ return success;
+}
+
+static void strip(const char **str, int *size) {
+ const char *str_d = *str;
+ int s_d = *size;
+
+ const char *start = str_d;
+ const char *end = start + s_d;
+
+ while(str_d < end) {
+ char c = *str_d;
+ if(c != ' ' && c != '\t' && c != '\n')
+ break;
+ ++str_d;
+ }
+
+ int start_offset = str_d - start;
+ while(s_d > start_offset) {
+ char c = start[s_d];
+ if(c != ' ' && c != '\t' && c != '\n')
+ break;
+ --s_d;
+ }
+
+ *str = str_d;
+ *size = s_d;
+}
+
+static void attribute_callback(const char *str, size_t size, void *userdata) {
+ if(size > 255 - 1)
+ return;
+
+ int size_i = size;
+ strip(&str, &size_i);
+
+ char attribute[255];
+ memcpy(attribute, str, size_i);
+ attribute[size_i] = '\0';
+
+ const char *sep = strchr(attribute, '=');
+ if(!sep)
+ return;
+
+ const char *attribute_name = attribute;
+ size_t attribute_name_len = sep - attribute_name;
+ const char *attribute_value_str = sep + 1;
+
+ int attribute_value = 0;
+ if(sscanf(attribute_value_str, "%d", &attribute_value) != 1)
+ return;
+
+ NVCTRLPerformanceLevel *performance_level = userdata;
+ if(attribute_name_len == 4 && memcmp(attribute_name, "perf", 4) == 0)
+ performance_level->perf = attribute_value;
+ else if(attribute_name_len == 7 && memcmp(attribute_name, "nvclock", 7) == 0)
+ performance_level->nv_clock = attribute_value;
+ else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmin", 10) == 0)
+ performance_level->nv_clock_min = attribute_value;
+ else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmax", 10) == 0)
+ performance_level->nv_clock_max = attribute_value;
+ else if(attribute_name_len == 8 && memcmp(attribute_name, "memclock", 8) == 0)
+ performance_level->mem_clock = attribute_value;
+ else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmin", 11) == 0)
+ performance_level->mem_clock_min = attribute_value;
+ else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmax", 11) == 0)
+ performance_level->mem_clock_max = attribute_value;
+ else if(attribute_name_len == 15 && memcmp(attribute_name, "memTransferRate", 15) == 0)
+ performance_level->mem_transfer_rate = attribute_value;
+ else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemin", 18) == 0)
+ performance_level->mem_transfer_rate_min = attribute_value;
+ else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemax", 18) == 0)
+ performance_level->mem_transfer_rate_max = attribute_value;
+}
+
+static void attribute_line_callback(const char *str, size_t size, void *userdata) {
+ NVCTRLPerformanceLevelQuery *query = userdata;
+ if(query->num_performance_levels >= MAX_PERFORMANCE_LEVELS)
+ return;
+
+ NVCTRLPerformanceLevel *current_performance_level = &query->performance_level[query->num_performance_levels];
+ memset(current_performance_level, 0, sizeof(NVCTRLPerformanceLevel));
+ ++query->num_performance_levels;
+ split_by_delimiter(str, size, ',', attribute_callback, current_performance_level);
+}
+
+static bool xnvctrl_get_performance_levels(gsr_xnvctrl *xnvctrl, NVCTRLPerformanceLevelQuery *query) {
+ bool success = false;
+ memset(query, 0, sizeof(NVCTRLPerformanceLevelQuery));
+
+ char *attributes = NULL;
+ if(!xnvctrl->XNVCTRLQueryTargetStringAttribute(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, NV_CTRL_STRING_PERFORMANCE_MODES, &attributes)) {
+ success = false;
+ goto done;
+ }
+
+ split_by_delimiter(attributes, strlen(attributes), ';', attribute_line_callback, query);
+ success = true;
+
+ done:
+ if(attributes)
+ XFree(attributes);
+
+ return success;
+}
+
+static int compare_mem_transfer_rate_max_asc(const void *a, const void *b) {
+ const NVCTRLPerformanceLevel *perf_a = a;
+ const NVCTRLPerformanceLevel *perf_b = b;
+ return perf_a->mem_transfer_rate_max - perf_b->mem_transfer_rate_max;
+}
+
+bool gsr_overclock_load(gsr_overclock *self, Display *display) {
+ memset(self, 0, sizeof(gsr_overclock));
+ self->num_performance_levels = 0;
+
+ return gsr_xnvctrl_load(&self->xnvctrl, display);
+}
+
+void gsr_overclock_unload(gsr_overclock *self) {
+ gsr_xnvctrl_unload(&self->xnvctrl);
+}
+
+bool gsr_overclock_start(gsr_overclock *self) {
+ int basep = 0;
+ int errorp = 0;
+ if(!self->xnvctrl.XNVCTRLQueryExtension(self->xnvctrl.display, &basep, &errorp)) {
+ fprintf(stderr, "gsr warning: gsr_overclock_start: xnvctrl is not supported on your system, failed to overclock memory transfer rate\n");
+ return false;
+ }
+
+ NVCTRLPerformanceLevelQuery query;
+ if(!xnvctrl_get_performance_levels(&self->xnvctrl, &query) || query.num_performance_levels == 0) {
+ fprintf(stderr, "gsr warning: gsr_overclock_start: failed to get performance levels for overclocking\n");
+ return false;
+ }
+ self->num_performance_levels = query.num_performance_levels;
+
+ qsort(query.performance_level, query.num_performance_levels, sizeof(NVCTRLPerformanceLevel), compare_mem_transfer_rate_max_asc);
+
+ int target_transfer_rate_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE);
+ if(query.num_performance_levels > 1) {
+ const int transfer_rate_max_diff = query.performance_level[query.num_performance_levels - 1].mem_transfer_rate_max - query.performance_level[query.num_performance_levels - 2].mem_transfer_rate_max;
+ target_transfer_rate_offset = min_int(target_transfer_rate_offset, transfer_rate_max_diff);
+ if(target_transfer_rate_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_transfer_rate_offset, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE)) {
+ fprintf(stderr, "gsr info: gsr_overclock_start: sucessfully set memory transfer rate offset to %d\n", target_transfer_rate_offset);
+ } else {
+ fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock memory transfer rate offset to %d\n", target_transfer_rate_offset);
+ }
+ }
+
+ // TODO: Sort by nv_clock_max
+
+ // TODO: Enable. Crashes on my system (gtx 1080) so it's disabled for now. Seems to crash even if graphics clock is increasd by 1, let alone 1200
+ /*
+ int target_nv_clock_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_GPU_NVCLOCK);
+ if(query.num_performance_levels > 1) {
+ const int nv_clock_max_diff = query.performance_level[query.num_performance_levels - 1].nv_clock_max - query.performance_level[query.num_performance_levels - 2].nv_clock_max;
+ target_nv_clock_offset = min_int(target_nv_clock_offset, nv_clock_max_diff);
+ if(target_nv_clock_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_nv_clock_offset, NVCTRL_GPU_NVCLOCK)) {
+ fprintf(stderr, "gsr info: gsr_overclock_start: sucessfully set nv clock offset to %d\n", target_nv_clock_offset);
+ } else {
+ fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock nv clock offset to %d\n", target_nv_clock_offset);
+ }
+ }
+ */
+
+ XSync(self->xnvctrl.display, False);
+ return true;
+}
+
+void gsr_overclock_stop(gsr_overclock *self) {
+ xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE);
+ //xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_GPU_NVCLOCK);
+ XSync(self->xnvctrl.display, False);
+}
diff --git a/src/shader.c b/src/shader.c
new file mode 100644
index 0000000..dcb956b
--- /dev/null
+++ b/src/shader.c
@@ -0,0 +1,143 @@
+#include "../include/shader.h"
+#include "../include/egl.h"
+#include <stdio.h>
+#include <assert.h>
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+static unsigned int loader_shader(gsr_egl *egl, unsigned int type, const char *source) {
+ unsigned int shader_id = egl->glCreateShader(type);
+ if(shader_id == 0) {
+ fprintf(stderr, "gsr error: loader_shader: failed to create shader, error: %d\n", egl->glGetError());
+ return 0;
+ }
+
+ egl->glShaderSource(shader_id, 1, &source, NULL);
+ egl->glCompileShader(shader_id);
+
+ int compiled = 0;
+ egl->glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled);
+ if(!compiled) {
+ int info_length = 0;
+ egl->glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &info_length);
+
+ if(info_length > 1) {
+ char info_log[4096];
+ egl->glGetShaderInfoLog(shader_id, min_int(4096, info_length), NULL, info_log);
+ fprintf(stderr, "gsr error: loader shader: failed to compile shader, error:\n%s\nshader source:\n%s\n", info_log, source);
+ }
+
+ egl->glDeleteShader(shader_id);
+ return 0;
+ }
+
+ return shader_id;
+}
+
+static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) {
+ unsigned int vertex_shader_id = 0;
+ unsigned int fragment_shader_id = 0;
+ unsigned int program_id = 0;
+ int linked = 0;
+
+ if(vertex_shader) {
+ vertex_shader_id = loader_shader(egl, GL_VERTEX_SHADER, vertex_shader);
+ if(vertex_shader_id == 0)
+ goto err;
+ }
+
+ if(fragment_shader) {
+ fragment_shader_id = loader_shader(egl, GL_FRAGMENT_SHADER, fragment_shader);
+ if(fragment_shader_id == 0)
+ goto err;
+ }
+
+ program_id = egl->glCreateProgram();
+ if(program_id == 0) {
+ fprintf(stderr, "gsr error: load_program: failed to create shader program, error: %d\n", egl->glGetError());
+ goto err;
+ }
+
+ if(vertex_shader_id)
+ egl->glAttachShader(program_id, vertex_shader_id);
+
+ if(fragment_shader_id)
+ egl->glAttachShader(program_id, fragment_shader_id);
+
+ egl->glLinkProgram(program_id);
+
+ egl->glGetProgramiv(program_id, GL_LINK_STATUS, &linked);
+ if(!linked) {
+ int info_length = 0;
+ egl->glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &info_length);
+
+ if(info_length > 1) {
+ char info_log[4096];
+ egl->glGetProgramInfoLog(program_id, min_int(4096, info_length), NULL, info_log);
+ fprintf(stderr, "gsr error: load program: linking shader program failed, error:\n%s\n", info_log);
+ }
+
+ goto err;
+ }
+
+ if(fragment_shader_id)
+ egl->glDeleteShader(fragment_shader_id);
+ if(vertex_shader_id)
+ egl->glDeleteShader(vertex_shader_id);
+
+ return program_id;
+
+ err:
+ if(program_id)
+ egl->glDeleteProgram(program_id);
+ if(fragment_shader_id)
+ egl->glDeleteShader(fragment_shader_id);
+ if(vertex_shader_id)
+ egl->glDeleteShader(vertex_shader_id);
+ return 0;
+}
+
+int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) {
+ assert(egl);
+ self->egl = egl;
+ self->program_id = 0;
+
+ if(!vertex_shader && !fragment_shader) {
+ fprintf(stderr, "gsr error: gsr_shader_init: vertex shader and fragment shader can't be NULL at the same time\n");
+ return -1;
+ }
+
+ self->program_id = load_program(self->egl, vertex_shader, fragment_shader);
+ if(self->program_id == 0)
+ return -1;
+
+ return 0;
+}
+
+void gsr_shader_deinit(gsr_shader *self) {
+ if(!self->egl)
+ return;
+
+ if(self->program_id) {
+ self->egl->glDeleteProgram(self->program_id);
+ self->program_id = 0;
+ }
+
+ self->egl = NULL;
+}
+
+int gsr_shader_bind_attribute_location(gsr_shader *self, const char *attribute, int location) {
+ while(self->egl->glGetError()) {}
+ self->egl->glBindAttribLocation(self->program_id, location, attribute);
+ return self->egl->glGetError();
+}
+
+void gsr_shader_use(gsr_shader *self) {
+ self->egl->glUseProgram(self->program_id);
+}
+
+void gsr_shader_use_none(gsr_shader *self) {
+ self->egl->glUseProgram(0);
+}
diff --git a/src/sound.cpp b/src/sound.cpp
index 779aa87..53000bd 100644
--- a/src/sound.cpp
+++ b/src/sound.cpp
@@ -1,154 +1,375 @@
-/*
- Copyright (C) 2020 dec05eba
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
#include "../include/sound.hpp"
+extern "C" {
+#include "../include/utils.h"
+}
#include <stdlib.h>
#include <stdio.h>
+#include <string.h>
+#include <cmath>
+#include <time.h>
-#ifdef PULSEAUDIO
-#include <pulse/simple.h>
+#include <pulse/pulseaudio.h>
+#include <pulse/mainloop.h>
+#include <pulse/xmalloc.h>
#include <pulse/error.h>
-int sound_device_get_by_name(SoundDevice *device, const char *name, unsigned int num_channels, unsigned int period_frame_size) {
- pa_sample_spec ss;
- ss.format = PA_SAMPLE_S16LE;
- ss.rate = 48000;
- ss.channels = num_channels;
- int error;
+#define CHECK_DEAD_GOTO(p, rerror, label) \
+ do { \
+ if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \
+ !(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \
+ if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \
+ ((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \
+ if (rerror) \
+ *(rerror) = pa_context_errno((p)->context); \
+ } else \
+ if (rerror) \
+ *(rerror) = PA_ERR_BADSTATE; \
+ goto label; \
+ } \
+ } while(false);
- pa_simple *pa_handle = pa_simple_new(nullptr, "gpu-screen-recorder", PA_STREAM_RECORD, name, "record", &ss, nullptr, nullptr, &error);
- if(!pa_handle) {
- fprintf(stderr, "pa_simple_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), name);
- return -1;
+struct pa_handle {
+ pa_context *context;
+ pa_stream *stream;
+ pa_mainloop *mainloop;
+
+ const void *read_data;
+ size_t read_index, read_length;
+
+ uint8_t *output_data;
+ size_t output_index, output_length;
+
+ int operation_success;
+ double latency_seconds;
+};
+
+static void pa_sound_device_free(pa_handle *s) {
+ assert(s);
+
+ if (s->stream)
+ pa_stream_unref(s->stream);
+
+ if (s->context) {
+ pa_context_disconnect(s->context);
+ pa_context_unref(s->context);
+ }
+
+ if (s->mainloop)
+ pa_mainloop_free(s->mainloop);
+
+ if (s->output_data) {
+ free(s->output_data);
+ s->output_data = NULL;
}
- int buffer_size = period_frame_size * 2 * num_channels; // 2 bytes/sample, @num_channels channels
+ pa_xfree(s);
+}
+
+static pa_handle* pa_sound_device_new(const char *server,
+ const char *name,
+ const char *dev,
+ const char *stream_name,
+ const pa_sample_spec *ss,
+ const pa_buffer_attr *attr,
+ int *rerror) {
+ pa_handle *p;
+ int error = PA_ERR_INTERNAL, r;
+
+ p = pa_xnew0(pa_handle, 1);
+ p->read_data = NULL;
+ p->read_length = 0;
+ p->read_index = 0;
+ p->latency_seconds = 0.0;
+
+ const int buffer_size = attr->fragsize;
void *buffer = malloc(buffer_size);
if(!buffer) {
fprintf(stderr, "failed to allocate buffer for audio\n");
- pa_simple_free(pa_handle);
- return -1;
+ *rerror = -1;
+ return NULL;
}
- fprintf(stderr, "Using pulseaudio\n");
+ p->output_data = (uint8_t*)buffer;
+ p->output_length = buffer_size;
+ p->output_index = 0;
- device->handle = pa_handle;
- device->buffer = buffer;
- device->buffer_size = buffer_size;
- device->frames = period_frame_size;
- return 0;
-}
+ if (!(p->mainloop = pa_mainloop_new()))
+ goto fail;
-void sound_device_close(SoundDevice *device) {
- pa_simple_free((pa_simple*)device->handle);
- free(device->buffer);
+ if (!(p->context = pa_context_new(pa_mainloop_get_api(p->mainloop), name)))
+ goto fail;
+
+ if (pa_context_connect(p->context, server, PA_CONTEXT_NOFLAGS, NULL) < 0) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ for (;;) {
+ pa_context_state_t state = pa_context_get_state(p->context);
+
+ if (state == PA_CONTEXT_READY)
+ break;
+
+ if (!PA_CONTEXT_IS_GOOD(state)) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ pa_mainloop_iterate(p->mainloop, 1, NULL);
+ }
+
+ if (!(p->stream = pa_stream_new(p->context, stream_name, ss, NULL))) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ r = pa_stream_connect_record(p->stream, dev, attr,
+ (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE));
+
+ if (r < 0) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ for (;;) {
+ pa_stream_state_t state = pa_stream_get_state(p->stream);
+
+ if (state == PA_STREAM_READY)
+ break;
+
+ if (!PA_STREAM_IS_GOOD(state)) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ pa_mainloop_iterate(p->mainloop, 1, NULL);
+ }
+
+ return p;
+
+fail:
+ if (rerror)
+ *rerror = error;
+ pa_sound_device_free(p);
+ return NULL;
}
-int sound_device_read_next_chunk(SoundDevice *device, void **buffer) {
- int error;
- if(pa_simple_read((pa_simple*)device->handle, device->buffer, device->buffer_size, &error) < 0) {
- fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error));
- return -1;
+static int pa_sound_device_read(pa_handle *p, double timeout_seconds) {
+ assert(p);
+
+ const double start_time = clock_get_monotonic_seconds();
+
+ bool success = false;
+ int r = 0;
+ int *rerror = &r;
+ pa_usec_t latency = 0;
+ int negative = 0;
+
+ CHECK_DEAD_GOTO(p, rerror, fail);
+
+ while (p->output_index < p->output_length) {
+ if(clock_get_monotonic_seconds() - start_time >= timeout_seconds)
+ return -1;
+
+ if(!p->read_data) {
+ pa_mainloop_prepare(p->mainloop, 1 * 1000); // 1 ms
+ pa_mainloop_poll(p->mainloop);
+ pa_mainloop_dispatch(p->mainloop);
+
+ if(pa_stream_peek(p->stream, &p->read_data, &p->read_length) < 0)
+ goto fail;
+
+ if(!p->read_data && p->read_length == 0)
+ continue;
+
+ if(!p->read_data && p->read_length > 0) {
+ // There is a hole in the stream :( drop it. Maybe we should generate silence instead? TODO
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+ continue;
+ }
+
+ if(p->read_length <= 0) {
+ p->read_data = NULL;
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+
+ CHECK_DEAD_GOTO(p, rerror, fail);
+ continue;
+ }
+
+ pa_operation_unref(pa_stream_update_timing_info(p->stream, NULL, NULL));
+ // TODO: Deal with one pa_stream_peek not being enough. In that case we need to add multiple of these together(?)
+ if(pa_stream_get_latency(p->stream, &latency, &negative) >= 0) {
+ p->latency_seconds = negative ? -(double)latency : latency;
+ if(p->latency_seconds < 0.0)
+ p->latency_seconds = 0.0;
+ p->latency_seconds *= 0.0000001;
+ }
+ }
+
+ const size_t space_free_in_output_buffer = p->output_length - p->output_index;
+ if(space_free_in_output_buffer < p->read_length) {
+ memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, space_free_in_output_buffer);
+ p->output_index = 0;
+ p->read_index += space_free_in_output_buffer;
+ p->read_length -= space_free_in_output_buffer;
+ break;
+ } else {
+ memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, p->read_length);
+ p->output_index += p->read_length;
+ p->read_data = NULL;
+ p->read_length = 0;
+ p->read_index = 0;
+
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+
+ if(p->output_index == p->output_length) {
+ p->output_index = 0;
+ break;
+ }
+ }
}
- *buffer = device->buffer;
- return device->frames;
+
+ success = true;
+
+ fail:
+ return success ? 0 : -1;
}
-#else
-#define ALSA_PCM_NEW_HW_PARAMS_API
-#include <alsa/asoundlib.h>
-
-int sound_device_get_by_name(SoundDevice *device, const char *name, unsigned int num_channels, unsigned int period_frame_size) {
- int rc;
- snd_pcm_t *handle;
-
- rc = snd_pcm_open(&handle, name, SND_PCM_STREAM_CAPTURE, 0);
- if(rc < 0) {
- fprintf(stderr, "unable to open pcm device 'default', reason: %s\n", snd_strerror(rc));
- return rc;
+
+static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return PA_SAMPLE_S16LE;
+ case S32: return PA_SAMPLE_S32LE;
+ case F32: return PA_SAMPLE_FLOAT32LE;
}
+ assert(false);
+ return PA_SAMPLE_S16LE;
+}
- snd_pcm_hw_params_t *params;
- snd_pcm_hw_params_alloca(&params);
- // Fill the params with default values
- snd_pcm_hw_params_any(handle, params);
- // Interleaved mode
- snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
- // Signed 16--bit little-endian format
- snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
- snd_pcm_hw_params_set_channels(handle, params, num_channels);
-
- // 48000 bits/second samling rate (DVD quality)
- unsigned int val = 48000;
- int dir;
- snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir);
-
- snd_pcm_uframes_t frames = period_frame_size;
- snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir);
-
- // Write the parmeters to the driver
- rc = snd_pcm_hw_params(handle, params);
- if(rc < 0) {
- fprintf(stderr, "unable to set hw parameters, reason: %s\n", snd_strerror(rc));
- snd_pcm_close(handle);
- return rc;
+static int audio_format_to_get_bytes_per_sample(AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return 2;
+ case S32: return 4;
+ case F32: return 4;
}
+ assert(false);
+ return 2;
+}
- // Use a buffer large enough to hold one period
- snd_pcm_hw_params_get_period_size(params, &frames, &dir);
- int buffer_size = frames * 2 * num_channels; // 2 bytes/sample, @num_channels channels
- void *buffer = malloc(buffer_size);
- if(!buffer) {
- fprintf(stderr, "failed to allocate buffer for audio\n");
- snd_pcm_close(handle);
+int sound_device_get_by_name(SoundDevice *device, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format) {
+ pa_sample_spec ss;
+ ss.format = audio_format_to_pulse_audio_format(audio_format);
+ ss.rate = 48000;
+ ss.channels = num_channels;
+
+ pa_buffer_attr buffer_attr;
+ buffer_attr.fragsize = period_frame_size * audio_format_to_get_bytes_per_sample(audio_format) * num_channels; // 2/4 bytes/sample, @num_channels channels
+ buffer_attr.tlength = -1;
+ buffer_attr.prebuf = -1;
+ buffer_attr.minreq = -1;
+ buffer_attr.maxlength = buffer_attr.fragsize;
+
+ int error = 0;
+ pa_handle *handle = pa_sound_device_new(nullptr, description, device_name, description, &ss, &buffer_attr, &error);
+ if(!handle) {
+ fprintf(stderr, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), description);
return -1;
}
- fprintf(stderr, "Using alsa\n");
-
device->handle = handle;
- device->buffer = buffer;
- device->buffer_size = buffer_size;
- device->frames = frames;
+ device->frames = period_frame_size;
return 0;
}
void sound_device_close(SoundDevice *device) {
- /* TODO: Is this also needed in @sound_device_get_by_name on failure? */
- // TODO: This has been commented out since it causes the thread to block forever. Why?
- //snd_pcm_drain((snd_pcm_t*)device->handle);
- snd_pcm_close((snd_pcm_t*)device->handle);
- free(device->buffer);
+ if(device->handle)
+ pa_sound_device_free((pa_handle*)device->handle);
+ device->handle = NULL;
+}
+
+int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) {
+ pa_handle *pa = (pa_handle*)device->handle;
+ if(pa_sound_device_read(pa, timeout_sec) < 0) {
+ //fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error));
+ *latency_seconds = 0.0;
+ return -1;
+ }
+ *buffer = pa->output_data;
+ *latency_seconds = pa->latency_seconds;
+ return device->frames;
}
-int sound_device_read_next_chunk(SoundDevice *device, void **buffer) {
- int rc = snd_pcm_readi((snd_pcm_t*)device->handle, device->buffer, device->frames);
- if (rc == -EPIPE) {
- /* overrun */
- fprintf(stderr, "overrun occured\n");
- snd_pcm_prepare((snd_pcm_t*)device->handle);
- return rc;
- } else if(rc < 0) {
- fprintf(stderr, "failed to read from sound device, reason: %s\n", snd_strerror(rc));
- return rc;
- } else if (rc != (int)device->frames) {
- fprintf(stderr, "short read, read %d frames\n", rc);
+static void pa_state_cb(pa_context *c, void *userdata) {
+ pa_context_state state = pa_context_get_state(c);
+ int *pa_ready = (int*)userdata;
+ switch(state) {
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ default:
+ break;
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ *pa_ready = 2;
+ break;
+ case PA_CONTEXT_READY:
+ *pa_ready = 1;
+ break;
}
- *buffer = device->buffer;
- return rc;
}
-#endif
+
+static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) {
+ (void)ctx;
+ if(eol > 0)
+ return;
+
+ std::vector<AudioInput> *inputs = (std::vector<AudioInput>*)userdata;
+ inputs->push_back({ source_info->name, source_info->description });
+}
+
+std::vector<AudioInput> get_pulseaudio_inputs() {
+ std::vector<AudioInput> inputs;
+ pa_mainloop *main_loop = pa_mainloop_new();
+
+ pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder");
+ pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
+ int state = 0;
+ int pa_ready = 0;
+ pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready);
+
+ pa_operation *pa_op = NULL;
+
+ for(;;) {
+ // Not ready
+ if(pa_ready == 0) {
+ pa_mainloop_iterate(main_loop, 1, NULL);
+ continue;
+ }
+
+ switch(state) {
+ case 0: {
+ pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &inputs);
+ ++state;
+ break;
+ }
+ }
+
+ // Couldn't get connection to the server
+ if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) {
+ if(pa_op)
+ pa_operation_unref(pa_op);
+ pa_context_disconnect(ctx);
+ pa_context_unref(ctx);
+ break;
+ }
+
+ pa_mainloop_iterate(main_loop, 1, NULL);
+ }
+
+ pa_mainloop_free(main_loop);
+ return inputs;
+}
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..7dd1890
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,450 @@
+#include "../include/utils.h"
+#include <time.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <xf86drmMode.h>
+#include <xf86drm.h>
+#include <stdlib.h>
+#include <X11/Xatom.h>
+
+double clock_get_monotonic_seconds(void) {
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
+}
+
+static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) {
+ for(int i = 0; i < sr->nmode; ++i) {
+ if(sr->modes[i].id == id)
+ return &sr->modes[i];
+ }
+ return NULL;
+}
+
+static gsr_monitor_rotation x11_rotation_to_gsr_rotation(int rot) {
+ switch(rot) {
+ case RR_Rotate_0: return GSR_MONITOR_ROT_0;
+ case RR_Rotate_90: return GSR_MONITOR_ROT_90;
+ case RR_Rotate_180: return GSR_MONITOR_ROT_180;
+ case RR_Rotate_270: return GSR_MONITOR_ROT_270;
+ }
+ return GSR_MONITOR_ROT_0;
+}
+
+static gsr_monitor_rotation wayland_transform_to_gsr_rotation(int32_t rot) {
+ switch(rot) {
+ case 0: return GSR_MONITOR_ROT_0;
+ case 1: return GSR_MONITOR_ROT_90;
+ case 2: return GSR_MONITOR_ROT_180;
+ case 3: return GSR_MONITOR_ROT_270;
+ }
+ return GSR_MONITOR_ROT_0;
+}
+
+static uint32_t x11_output_get_connector_id(Display *dpy, RROutput output, Atom randr_connector_id_atom) {
+ Atom type = 0;
+ int format = 0;
+ unsigned long bytes_after = 0;
+ unsigned long nitems = 0;
+ unsigned char *prop = NULL;
+ XRRGetOutputProperty(dpy, output, randr_connector_id_atom, 0, 128, false, false, AnyPropertyType, &type, &format, &nitems, &bytes_after, &prop);
+
+ long result = 0;
+ if(type == XA_INTEGER && format == 32)
+ result = *(long*)prop;
+
+ free(prop);
+ return result;
+}
+
+void for_each_active_monitor_output_x11(Display *display, active_monitor_callback callback, void *userdata) {
+ XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display));
+ if(!screen_res)
+ return;
+
+ const Atom randr_connector_id_atom = XInternAtom(display, "CONNECTOR_ID", False);
+
+ char display_name[256];
+ for(int i = 0; i < screen_res->noutput; ++i) {
+ XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]);
+ if(out_info && out_info->crtc && out_info->connection == RR_Connected) {
+ XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc);
+ if(crt_info && crt_info->mode) {
+ const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode);
+ if(mode_info && out_info->nameLen < (int)sizeof(display_name)) {
+ memcpy(display_name, out_info->name, out_info->nameLen);
+ display_name[out_info->nameLen] = '\0';
+
+ const gsr_monitor monitor = {
+ .name = display_name,
+ .name_len = out_info->nameLen,
+ .pos = { .x = crt_info->x, .y = crt_info->y },
+ .size = { .x = (int)crt_info->width, .y = (int)crt_info->height },
+ .crt_info = crt_info,
+ .connector_id = x11_output_get_connector_id(display, screen_res->outputs[i], randr_connector_id_atom),
+ .rotation = x11_rotation_to_gsr_rotation(crt_info->rotation),
+ .monitor_identifier = 0
+ };
+ callback(&monitor, userdata);
+ }
+ }
+ if(crt_info)
+ XRRFreeCrtcInfo(crt_info);
+ }
+ if(out_info)
+ XRRFreeOutputInfo(out_info);
+ }
+
+ XRRFreeScreenResources(screen_res);
+}
+
+typedef struct {
+ int type;
+ int count;
+ int count_active;
+} drm_connector_type_count;
+
+#define CONNECTOR_TYPE_COUNTS 32
+
+static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
+ for(int i = 0; i < *num_type_counts; ++i) {
+ if(type_counts[i].type == connector_type)
+ return &type_counts[i];
+ }
+
+ if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
+ return NULL;
+
+ const int index = *num_type_counts;
+ type_counts[index].type = connector_type;
+ type_counts[index].count = 0;
+ type_counts[index].count_active = 0;
+ ++*num_type_counts;
+ return &type_counts[index];
+}
+
+static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
+ for(int i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]);
+ if(prop) {
+ if(strcmp(name, prop->name) == 0) {
+ *result = props->prop_values[i];
+ drmModeFreeProperty(prop);
+ return true;
+ }
+ drmModeFreeProperty(prop);
+ }
+ }
+ return false;
+}
+
+/* TODO: Support more connector types*/
+static int get_connector_type_by_name(const char *name) {
+ int len = strlen(name);
+ if(len >= 5 && strncmp(name, "HDMI-", 5) == 0)
+ return 1;
+ else if(len >= 3 && strncmp(name, "DP-", 3) == 0)
+ return 2;
+ else if(len >= 12 && strncmp(name, "DisplayPort-", 12) == 0)
+ return 3;
+ else if(len >= 4 && strncmp(name, "eDP-", 4) == 0)
+ return 4;
+ else
+ return -1;
+}
+
+static uint32_t monitor_identifier_from_type_and_count(int monitor_type_index, int monitor_type_count) {
+ return ((uint32_t)monitor_type_index << 16) | ((uint32_t)monitor_type_count);
+}
+
+static void for_each_active_monitor_output_wayland(const gsr_egl *egl, active_monitor_callback callback, void *userdata) {
+ drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
+ int num_type_counts = 0;
+
+ for(int i = 0; i < egl->wayland.num_outputs; ++i) {
+ const gsr_wayland_output *output = &egl->wayland.outputs[i];
+ if(!output->name)
+ continue;
+
+ const int connector_type_index = get_connector_type_by_name(output->name);
+ drm_connector_type_count *connector_type = NULL;
+ if(connector_type_index != -1)
+ connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector_type_index);
+
+ if(connector_type) {
+ ++connector_type->count;
+ ++connector_type->count_active;
+ }
+
+ const gsr_monitor monitor = {
+ .name = output->name,
+ .name_len = strlen(output->name),
+ .pos = { .x = output->pos.x, .y = output->pos.y },
+ .size = { .x = output->size.x, .y = output->size.y },
+ .crt_info = NULL,
+ .connector_id = 0,
+ .rotation = wayland_transform_to_gsr_rotation(output->transform),
+ .monitor_identifier = connector_type ? monitor_identifier_from_type_and_count(connector_type_index, connector_type->count_active) : 0
+ };
+ callback(&monitor, userdata);
+ }
+}
+
+static void for_each_active_monitor_output_drm(const gsr_egl *egl, active_monitor_callback callback, void *userdata) {
+ int fd = open(egl->card_path, O_RDONLY);
+ if(fd == -1)
+ return;
+
+ drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);
+
+ drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
+ int num_type_counts = 0;
+
+ char display_name[256];
+ drmModeResPtr resources = drmModeGetResources(fd);
+ if(resources) {
+ for(int i = 0; i < resources->count_connectors; ++i) {
+ drmModeConnectorPtr connector = drmModeGetConnectorCurrent(fd, resources->connectors[i]);
+ if(!connector)
+ continue;
+
+ drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
+ const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
+ const int connection_name_len = strlen(connection_name);
+ if(connector_type)
+ ++connector_type->count;
+
+ if(connector->connection != DRM_MODE_CONNECTED) {
+ drmModeFreeConnector(connector);
+ continue;
+ }
+
+ if(connector_type)
+ ++connector_type->count_active;
+
+ uint64_t crtc_id = 0;
+ connector_get_property_by_name(fd, connector, "CRTC_ID", &crtc_id);
+
+ drmModeCrtcPtr crtc = drmModeGetCrtc(fd, crtc_id);
+ if(connector_type && crtc_id > 0 && crtc && connection_name_len + 5 < (int)sizeof(display_name)) {
+ const int display_name_len = snprintf(display_name, sizeof(display_name), "%s-%d", connection_name, connector_type->count);
+ const int connector_type_index_name = get_connector_type_by_name(display_name);
+ const gsr_monitor monitor = {
+ .name = display_name,
+ .name_len = display_name_len,
+ .pos = { .x = crtc->x, .y = crtc->y },
+ .size = { .x = (int)crtc->width, .y = (int)crtc->height },
+ .crt_info = NULL,
+ .connector_id = connector->connector_id,
+ .rotation = GSR_MONITOR_ROT_0,
+ .monitor_identifier = connector_type_index_name != -1 ? monitor_identifier_from_type_and_count(connector_type_index_name, connector_type->count_active) : 0
+ };
+ callback(&monitor, userdata);
+ }
+
+ if(crtc)
+ drmModeFreeCrtc(crtc);
+
+ drmModeFreeConnector(connector);
+ }
+ drmModeFreeResources(resources);
+ }
+
+ close(fd);
+}
+
+void for_each_active_monitor_output(const gsr_egl *egl, gsr_connection_type connection_type, active_monitor_callback callback, void *userdata) {
+ switch(connection_type) {
+ case GSR_CONNECTION_X11:
+ for_each_active_monitor_output_x11(egl->x11.dpy, callback, userdata);
+ break;
+ case GSR_CONNECTION_WAYLAND:
+ for_each_active_monitor_output_wayland(egl, callback, userdata);
+ break;
+ case GSR_CONNECTION_DRM:
+ for_each_active_monitor_output_drm(egl, callback, userdata);
+ break;
+ }
+}
+
+static void get_monitor_by_name_callback(const gsr_monitor *monitor, void *userdata) {
+ get_monitor_by_name_userdata *data = (get_monitor_by_name_userdata*)userdata;
+ if(!data->found_monitor && strcmp(data->name, monitor->name) == 0) {
+ data->monitor->pos = monitor->pos;
+ data->monitor->size = monitor->size;
+ data->monitor->connector_id = monitor->connector_id;
+ data->monitor->rotation = monitor->rotation;
+ data->monitor->monitor_identifier = monitor->monitor_identifier;
+ data->found_monitor = true;
+ }
+}
+
+bool get_monitor_by_name(const gsr_egl *egl, gsr_connection_type connection_type, const char *name, gsr_monitor *monitor) {
+ get_monitor_by_name_userdata userdata;
+ userdata.name = name;
+ userdata.name_len = strlen(name);
+ userdata.monitor = monitor;
+ userdata.found_monitor = false;
+ for_each_active_monitor_output(egl, connection_type, get_monitor_by_name_callback, &userdata);
+ return userdata.found_monitor;
+}
+
+typedef struct {
+ const gsr_monitor *monitor;
+ gsr_monitor_rotation rotation;
+} get_monitor_by_connector_id_userdata;
+
+static void get_monitor_by_connector_id_callback(const gsr_monitor *monitor, void *userdata) {
+ get_monitor_by_connector_id_userdata *data = (get_monitor_by_connector_id_userdata*)userdata;
+ if(monitor->connector_id == data->monitor->connector_id ||
+ (!monitor->connector_id && monitor->monitor_identifier == data->monitor->monitor_identifier))
+ {
+ data->rotation = monitor->rotation;
+ }
+}
+
+gsr_monitor_rotation drm_monitor_get_display_server_rotation(const gsr_egl *egl, const gsr_monitor *monitor) {
+ if(egl->wayland.dpy) {
+ get_monitor_by_connector_id_userdata userdata;
+ userdata.monitor = monitor;
+ userdata.rotation = GSR_MONITOR_ROT_0;
+ for_each_active_monitor_output_wayland(egl, get_monitor_by_connector_id_callback, &userdata);
+ return userdata.rotation;
+ } else {
+ get_monitor_by_connector_id_userdata userdata;
+ userdata.monitor = monitor;
+ userdata.rotation = GSR_MONITOR_ROT_0;
+ for_each_active_monitor_output_x11(egl->x11.dpy, get_monitor_by_connector_id_callback, &userdata);
+ return userdata.rotation;
+ }
+
+ return GSR_MONITOR_ROT_0;
+}
+
+bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) {
+ const char *software_renderers[] = { "llvmpipe", "SWR", "softpipe", NULL };
+ bool supported = true;
+ const unsigned char *gl_vendor = egl->glGetString(GL_VENDOR);
+ const unsigned char *gl_renderer = egl->glGetString(GL_RENDERER);
+
+ info->gpu_version = 0;
+
+ if(!gl_vendor) {
+ fprintf(stderr, "gsr error: failed to get gpu vendor\n");
+ supported = false;
+ goto end;
+ }
+
+ if(gl_renderer) {
+ for(int i = 0; software_renderers[i]; ++i) {
+ if(strstr((const char*)gl_renderer, software_renderers[i])) {
+ fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]);
+ supported = false;
+ goto end;
+ }
+ }
+ }
+
+ if(strstr((const char*)gl_vendor, "AMD"))
+ info->vendor = GSR_GPU_VENDOR_AMD;
+ else if(strstr((const char*)gl_vendor, "Intel"))
+ info->vendor = GSR_GPU_VENDOR_INTEL;
+ else if(strstr((const char*)gl_vendor, "NVIDIA"))
+ info->vendor = GSR_GPU_VENDOR_NVIDIA;
+ else {
+ fprintf(stderr, "gsr error: unknown gpu vendor: %s\n", gl_vendor);
+ supported = false;
+ goto end;
+ }
+
+ if(gl_renderer) {
+ if(info->vendor == GSR_GPU_VENDOR_NVIDIA)
+ sscanf((const char*)gl_renderer, "%*s %*s %*s %d", &info->gpu_version);
+ }
+
+ end:
+ return supported;
+}
+
+static bool try_card_has_valid_plane(const char *card_path) {
+ drmVersion *ver = NULL;
+ drmModePlaneResPtr planes = NULL;
+ bool found_screen_card = false;
+
+ int fd = open(card_path, O_RDONLY);
+ if(fd == -1)
+ return false;
+
+ ver = drmGetVersion(fd);
+ if(!ver || strstr(ver->name, "nouveau"))
+ goto next;
+
+ drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
+
+ planes = drmModeGetPlaneResources(fd);
+ if(!planes)
+ goto next;
+
+ for(uint32_t j = 0; j < planes->count_planes; ++j) {
+ drmModePlanePtr plane = drmModeGetPlane(fd, planes->planes[j]);
+ if(!plane)
+ continue;
+
+ if(plane->fb_id)
+ found_screen_card = true;
+
+ drmModeFreePlane(plane);
+ if(found_screen_card)
+ break;
+ }
+
+ next:
+ if(planes)
+ drmModeFreePlaneResources(planes);
+ if(ver)
+ drmFreeVersion(ver);
+ close(fd);
+ if(found_screen_card)
+ return true;
+
+ return false;
+}
+
+bool gsr_get_valid_card_path(gsr_egl *egl, char *output) {
+ if(egl->dri_card_path) {
+ strncpy(output, egl->dri_card_path, 127);
+ return try_card_has_valid_plane(output);
+ }
+
+ for(int i = 0; i < 10; ++i) {
+ snprintf(output, 127, DRM_DEV_NAME, DRM_DIR_NAME, i);
+ if(try_card_has_valid_plane(output))
+ return true;
+ }
+ return false;
+}
+
+bool gsr_card_path_get_render_path(const char *card_path, char *render_path) {
+ int fd = open(card_path, O_RDONLY);
+ if(fd == -1)
+ return false;
+
+ char *render_path_tmp = drmGetRenderDeviceNameFromFd(fd);
+ if(render_path_tmp) {
+ strncpy(render_path, render_path_tmp, 127);
+ free(render_path_tmp);
+ close(fd);
+ return true;
+ }
+
+ close(fd);
+ return false;
+}
+
+int even_number_ceil(int value) {
+ return value + (value & 1);
+}
diff --git a/src/window_texture.c b/src/window_texture.c
new file mode 100644
index 0000000..0f4aa2c
--- /dev/null
+++ b/src/window_texture.c
@@ -0,0 +1,123 @@
+#include "../include/window_texture.h"
+#include <X11/extensions/Xcomposite.h>
+
+static int x11_supports_composite_named_window_pixmap(Display *display) {
+ int extension_major;
+ int extension_minor;
+ if(!XCompositeQueryExtension(display, &extension_major, &extension_minor))
+ return 0;
+
+ int major_version;
+ int minor_version;
+ return XCompositeQueryVersion(display, &major_version, &minor_version) && (major_version > 0 || minor_version >= 2);
+}
+
+int window_texture_init(WindowTexture *window_texture, Display *display, Window window, gsr_egl *egl) {
+ window_texture->display = display;
+ window_texture->window = window;
+ window_texture->pixmap = None;
+ window_texture->texture_id = 0;
+ window_texture->redirected = 0;
+ window_texture->egl = egl;
+
+ if(!x11_supports_composite_named_window_pixmap(display))
+ return 1;
+
+ XCompositeRedirectWindow(display, window, CompositeRedirectAutomatic);
+ window_texture->redirected = 1;
+ return window_texture_on_resize(window_texture);
+}
+
+static void window_texture_cleanup(WindowTexture *self, int delete_texture) {
+ if(delete_texture && self->texture_id) {
+ self->egl->glDeleteTextures(1, &self->texture_id);
+ self->texture_id = 0;
+ }
+
+ if(self->pixmap) {
+ XFreePixmap(self->display, self->pixmap);
+ self->pixmap = None;
+ }
+}
+
+void window_texture_deinit(WindowTexture *self) {
+ if(self->redirected) {
+ XCompositeUnredirectWindow(self->display, self->window, CompositeRedirectAutomatic);
+ self->redirected = 0;
+ }
+ window_texture_cleanup(self, 1);
+}
+
+int window_texture_on_resize(WindowTexture *self) {
+ window_texture_cleanup(self, 0);
+
+ int result = 0;
+ Pixmap pixmap = None;
+ unsigned int texture_id = 0;
+ EGLImage image = NULL;
+
+ const intptr_t pixmap_attrs[] = {
+ EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
+ EGL_NONE,
+ };
+
+ pixmap = XCompositeNameWindowPixmap(self->display, self->window);
+ if(!pixmap) {
+ result = 2;
+ goto cleanup;
+ }
+
+ if(self->texture_id == 0) {
+ self->egl->glGenTextures(1, &texture_id);
+ if(texture_id == 0) {
+ result = 3;
+ goto cleanup;
+ }
+ self->egl->glBindTexture(GL_TEXTURE_2D, texture_id);
+ } else {
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+ texture_id = self->texture_id;
+ }
+
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ while(self->egl->glGetError()) {}
+ while(self->egl->eglGetError() != EGL_SUCCESS) {}
+
+ image = self->egl->eglCreateImage(self->egl->egl_display, NULL, EGL_NATIVE_PIXMAP_KHR, (EGLClientBuffer)pixmap, pixmap_attrs);
+ if(!image) {
+ result = 4;
+ goto cleanup;
+ }
+
+ self->egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ if(self->egl->glGetError() != 0 || self->egl->eglGetError() != EGL_SUCCESS) {
+ result = 5;
+ goto cleanup;
+ }
+
+ self->pixmap = pixmap;
+ self->texture_id = texture_id;
+
+ cleanup:
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ if(image)
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+
+ if(result != 0) {
+ if(texture_id != 0)
+ self->egl->glDeleteTextures(1, &texture_id);
+ if(pixmap)
+ XFreePixmap(self->display, pixmap);
+ }
+
+ return result;
+}
+
+unsigned int window_texture_get_opengl_texture_id(WindowTexture *self) {
+ return self->texture_id;
+}
diff --git a/src/xnvctrl.c b/src/xnvctrl.c
new file mode 100644
index 0000000..b738455
--- /dev/null
+++ b/src/xnvctrl.c
@@ -0,0 +1,46 @@
+#include "../include/xnvctrl.h"
+#include "../include/library_loader.h"
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+bool gsr_xnvctrl_load(gsr_xnvctrl *self, Display *display) {
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ self->display = display;
+
+ dlerror(); /* clear */
+ void *lib = dlopen("libXNVCtrl.so.0", RTLD_LAZY);
+ if(!lib) {
+ fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: failed to load libXNVCtrl.so.0, error: %s\n", dlerror());
+ return false;
+ }
+
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->XNVCTRLQueryExtension, "XNVCTRLQueryExtension" },
+ { (void**)&self->XNVCTRLSetTargetAttributeAndGetStatus, "XNVCTRLSetTargetAttributeAndGetStatus" },
+ { (void**)&self->XNVCTRLQueryValidTargetAttributeValues, "XNVCTRLQueryValidTargetAttributeValues" },
+ { (void**)&self->XNVCTRLQueryTargetStringAttribute, "XNVCTRLQueryTargetStringAttribute" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(lib, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: missing required symbols in libXNVCtrl.so.0\n");
+ goto fail;
+ }
+
+ self->library = lib;
+ return true;
+
+ fail:
+ dlclose(lib);
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ return false;
+}
+
+void gsr_xnvctrl_unload(gsr_xnvctrl *self) {
+ if(self->library) {
+ dlclose(self->library);
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ }
+}