Compare commits

..

11 Commits
1.0.6 ... 1.0.7

Author SHA1 Message Date
dec05eba
e938241fe8 1.0.7 2025-01-17 14:00:50 +01:00
dec05eba
a9637f87e7 Fix background not being transparent on sway when a wayland application is focused and opening the ui 2025-01-17 13:18:40 +01:00
dec05eba
eb4ce76f01 Install flatpak version of the systemd service if missing or remove it if not using flatpak version 2025-01-16 19:34:47 +01:00
dec05eba
d4d61b8c93 Mention flatpak and non-flatpak conflict 2025-01-15 18:32:49 +01:00
dec05eba
35a2fcc615 Fix window/game name for games such as 'the finals' that have weird names with utf8-bom and zero width space characters 2025-01-14 22:34:25 +01:00
dec05eba
bb54b67956 sys/poll.h -> poll.h 2025-01-10 00:14:18 +01:00
dec05eba
35578e79ec M 2025-01-09 20:26:54 +01:00
dec05eba
ce6c924f58 Use glx on wayland to workaround buggy nvidia driver 2025-01-09 19:23:48 +01:00
dec05eba
77dd32a3ff Revert "Dont use dlopen in gsr-global-hotkeys"
This reverts commit b0def958c7.
2025-01-09 13:08:12 +01:00
dec05eba
b0def958c7 Dont use dlopen in gsr-global-hotkeys 2025-01-09 12:42:10 +01:00
dec05eba
698538ac84 Unset DRI_PRIME as well 2025-01-08 17:17:48 +01:00
13 changed files with 182 additions and 72 deletions

View File

@@ -54,5 +54,19 @@ If you want to donate you can donate via bitcoin or monero.
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
# Known issues
* When the UI is open the wallpaper is shown instead of the game on Hyprland and Sway. This is an issue with Hyprland and Sway. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland and Sway. I believe this is an issue in Hyprland and Sway.
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
# FAQ
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then non-flatpak version of the systemd service will conflict with that.
Run these commands to first remove the flatpak version of the systemd service:
```
systemctl stop --user gpu-screen-recorder-ui
rm ~/.local/share/systemd/user/gpu-screen-recorder-ui.service
systemctl --user daemon-reload
```
and then start and enable the non-flatpak systemd service:
```
systemctl enable --now --user gpu-screen-recorder-ui`
```

7
TODO
View File

@@ -20,8 +20,6 @@ Make hotkeys configurable.
Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory.
Support wayland (excluding gnome, or force xwayland on gnome).
Restart replay on system start if monitor resolution changes.
Show warning when selecting hevc/av1 on amd because of amd driver/ffmpeg bug.
@@ -111,4 +109,7 @@ Re-enable hotkey disable option for flatpak.
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".
When enabling X11 global hotkey again only grab lalt, not ralt.
When enabling X11 global hotkey again only grab lalt, not ralt.
When adding window capture only add it to recording and streaming and do the window selection when recording starts, to make it more ergonomic with hotkeys.
If hotkey for recording/streaming start is pressed on the button for start is clicked then hide the ui if it's visible and show the window selection option (cursor).

View File

@@ -5,6 +5,7 @@
#include "GsrInfo.hpp"
#include "Config.hpp"
#include "window_texture.h"
#include "WindowUtils.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
@@ -97,7 +98,7 @@ namespace gsr {
void on_press_start_replay(bool disable_notification);
void on_press_start_record();
void on_press_start_stream();
bool update_compositor_texture(const mgl_monitor *monitor);
bool update_compositor_texture(const Monitor &monitor);
void force_window_on_top();
private:

View File

@@ -2,6 +2,7 @@
#include <mglpp/system/vec.hpp>
#include <string>
#include <vector>
#include <X11/Xlib.h>
namespace gsr {
@@ -10,10 +11,16 @@ namespace gsr {
CURSOR
};
struct Monitor {
mgl::vec2i position;
mgl::vec2i size;
};
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
mgl::vec2i create_window_get_center_position(Display *display);
std::string get_window_manager_name(Display *display);
bool is_compositor_running(Display *dpy, int screen);
std::vector<Monitor> get_monitors(Display *dpy);
}

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.0.6', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.0.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.0.6"
version = "1.0.7"
platforms = ["posix"]
[lang.cpp]

View File

@@ -164,7 +164,7 @@ namespace gsr {
return std::abs(a - b) <= difference;
}
static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitor) {
static bool is_window_fullscreen_on_monitor(Display *display, Window window, const Monitor &monitor) {
if(!window)
return false;
@@ -173,8 +173,8 @@ namespace gsr {
return false;
const int margin = 2;
return diff_int(geometry.x, monitor->pos.x, margin) && diff_int(geometry.y, monitor->pos.y, margin)
&& diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin);
return diff_int(geometry.x, monitor.position.x, margin) && diff_int(geometry.y, monitor.position.y, margin)
&& diff_int(geometry.width, monitor.size.x, margin) && diff_int(geometry.height, monitor.size.y, margin);
}
/*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) {
@@ -279,15 +279,13 @@ namespace gsr {
}
// Returns the first monitor if not found. Assumes there is at least one monitor connected.
static const mgl_monitor* find_monitor_at_position(mgl::Window &window, mgl::vec2i pos) {
const mgl_window *win = window.internal_window();
assert(win->num_monitors > 0);
for(int i = 0; i < win->num_monitors; ++i) {
const mgl_monitor *mon = &win->monitors[i];
if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains(pos))
return mon;
static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) {
assert(!monitors.empty());
for(const Monitor &monitor : monitors) {
if(mgl::IntRect(monitor.position, monitor.size).contains(pos))
return &monitor;
}
return &win->monitors[0];
return &monitors.front();
}
static std::string get_power_supply_online_filepath() {
@@ -712,25 +710,39 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::vector<Monitor> monitors = get_monitors(display);
if(monitors.empty()) {
fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
window.reset();
return;
}
const std::string wm_name = get_window_manager_name(display);
const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
// The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it
Window x11_cursor_window = None;
const mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
if(is_wlroots) {
window_pos = focused_monitor->position;
window_size = focused_monitor->size;
} else {
window_pos = {0, 0};
window_size = {32, 32};
}
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window;
window_size = { 32, 32 };
window_pos = { 0, 0 };
mgl::Window::CreateParams window_create_params;
window_create_params.size = window_size;
if(prevent_game_minimizing) {
if(is_wlroots || prevent_game_minimizing) {
window_create_params.min_size = window_size;
window_create_params.max_size = window_size;
}
@@ -743,7 +755,10 @@ namespace gsr {
// MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity
// or may not be visible at all
window_create_params.window_type = (is_kwin && gsr_info.system_info.display_server == DisplayServer::WAYLAND) ? MGL_WINDOW_TYPE_DIALOG : MGL_WINDOW_TYPE_NORMAL;
window_create_params.render_api = MGL_RENDER_API_EGL;
// Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering.
// Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia
// when a compositor isn't running.
window_create_params.render_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_RENDER_API_GLX : MGL_RENDER_API_EGL;
if(!window->create("gsr ui", window_create_params))
fprintf(stderr, "error: failed to create window\n");
@@ -754,36 +769,26 @@ namespace gsr {
data = 1;
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
window_pos = focused_monitor->position;
window_size = focused_monitor->size;
if(!init_theme(resources_path)) {
fprintf(stderr, "Error: failed to load theme\n");
::exit(1);
}
mgl_window *win = window->internal_window();
if(win->num_monitors == 0) {
fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
window.reset();
return;
}
const mgl_monitor *focused_monitor = find_monitor_at_position(*window, monitor_position_query_value);
window_pos = {focused_monitor->pos.x, focused_monitor->pos.y};
window_size = {focused_monitor->size.x, focused_monitor->size.y};
get_theme().set_window_size(window_size);
if(prevent_game_minimizing) {
if(is_wlroots || prevent_game_minimizing) {
window->set_size(window_size);
window->set_size_limits(window_size, window_size);
window->set_position(window_pos);
}
mgl_window *win = window->internal_window();
win->cursor_position.x = cursor_position.x - window_pos.x;
win->cursor_position.y = cursor_position.y - window_pos.y;
update_compositor_texture(focused_monitor);
top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font);
logo_sprite = mgl::Sprite(&get_theme().logo_texture);
update_compositor_texture(*focused_monitor);
bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height));
top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor());
@@ -991,7 +996,8 @@ namespace gsr {
// XFlush(display);
// }
window->set_fullscreen(true);
if(!is_wlroots)
window->set_fullscreen(true);
visible = true;
@@ -1954,7 +1960,7 @@ namespace gsr {
show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
}
bool Overlay::update_compositor_texture(const mgl_monitor *monitor) {
bool Overlay::update_compositor_texture(const Monitor &monitor) {
window_texture_deinit(&window_texture);
window_texture_sprite.set_texture(nullptr);
screenshot_texture.clear();
@@ -1977,7 +1983,7 @@ namespace gsr {
window_texture_texture = mgl::Texture(window_texture.texture_id, MGL_TEXTURE_FORMAT_RGB);
window_texture_sprite.set_texture(&window_texture_texture);
} else {
XImage *img = XGetImage(display, DefaultRootWindow(display), monitor->pos.x, monitor->pos.y, monitor->size.x, monitor->size.y, AllPlanes, ZPixmap);
XImage *img = XGetImage(display, DefaultRootWindow(display), monitor.position.x, monitor.position.y, monitor.size.x, monitor.size.y, AllPlanes, ZPixmap);
if(!img)
fprintf(stderr, "Error: failed to take a screenshot\n");
@@ -2003,4 +2009,4 @@ namespace gsr {
XFlush(display);
}
}
}
}

View File

@@ -6,7 +6,7 @@
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <fcntl.h>
namespace gsr {
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {

View File

@@ -4,11 +4,20 @@
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <mglpp/system/Utf8.hpp>
extern "C" {
#include <mgl/window/window.h>
}
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <optional>
#define MAX_PROPERTY_VALUE_LEN 4096
namespace gsr {
@@ -134,7 +143,26 @@ namespace gsr {
return None;
}
static char* get_window_title(Display *dpy, Window window) {
static std::string utf8_sanitize(const uint8_t *str, int size) {
const uint32_t zero_width_space_codepoint = 0x200b; // Some games such as the finals has zero-width space characters
std::string result;
for(int i = 0; i < size;) {
// Some games such as the finals has utf8-bom between each character, wtf?
if(i + 3 < size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
i += 3;
continue;
}
uint32_t codepoint = 0;
size_t codepoint_length = 1;
if(mgl::utf8_decode(str + i, size - i, &codepoint, &codepoint_length) && codepoint != zero_width_space_codepoint)
result.append((const char*)str + i, codepoint_length);
i += codepoint_length;
}
return result;
}
static std::optional<std::string> get_window_title(Display *dpy, Window window) {
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
@@ -147,7 +175,7 @@ namespace gsr {
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
if(type == utf8_string_atom && format == 8 && data)
return (char*)data;
return utf8_sanitize(data, num_items);
type = None;
format = 0;
@@ -157,16 +185,18 @@ namespace gsr {
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
if((type == XA_STRING || type == utf8_string_atom) && data)
return (char*)data;
return utf8_sanitize(data, num_items);
return NULL;
return std::nullopt;
}
static const char* strip(const char *str, int *len) {
int str_len = strlen(str);
static std::string strip(const std::string &str) {
int start_index = 0;
int str_len = str.size();
for(int i = 0; i < str_len; ++i) {
if(str[i] != ' ') {
str += i;
start_index += i;
str_len -= i;
break;
}
@@ -179,14 +209,7 @@ namespace gsr {
}
}
*len = str_len;
return str;
}
static std::string strip_strip(const char *str) {
int len = 0;
str = strip(str, &len);
return std::string(str, len);
return str.substr(start_index, str_len);
}
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
@@ -196,17 +219,16 @@ namespace gsr {
return result;
// Window title is not always ideal (for example for a browser), but for games its pretty much required
char *window_title = get_window_title(dpy, focused_window);
const std::optional<std::string> window_title = get_window_title(dpy, focused_window);
if(window_title) {
result = strip_strip(window_title);
XFree(window_title);
result = strip(window_title.value());
return result;
}
XClassHint class_hint = {nullptr, nullptr};
XGetClassHint(dpy, focused_window, &class_hint);
if(class_hint.res_class) {
result = strip_strip(class_hint.res_class);
result = strip(class_hint.res_class);
return result;
}
@@ -442,11 +464,9 @@ namespace gsr {
if(!window)
return wm_name;
char *window_title = get_window_title(display, window);
if(window_title) {
wm_name = strip_strip(window_title);
XFree(window_title);
}
const std::optional<std::string> window_title = get_window_title(display, window);
if(window_title)
wm_name = strip(window_title.value());
return wm_name;
}
@@ -454,7 +474,18 @@ namespace gsr {
bool is_compositor_running(Display *dpy, int screen) {
char prop_name[20];
snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
Atom prop_atom = XInternAtom(dpy, prop_name, False);
const Atom prop_atom = XInternAtom(dpy, prop_name, False);
return XGetSelectionOwner(dpy, prop_atom) != None;
}
static void get_monitors_callback(const mgl_monitor *monitor, void *userdata) {
std::vector<Monitor> *monitors = (std::vector<Monitor>*)userdata;
monitors->push_back({mgl::vec2i(monitor->pos.x, monitor->pos.y), mgl::vec2i(monitor->size.x, monitor->size.y)});
}
std::vector<Monitor> get_monitors(Display *dpy) {
std::vector<Monitor> monitors;
mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors);
return monitors;
}
}

View File

@@ -10,6 +10,7 @@
#include <signal.h>
#include <thread>
#include <string.h>
#include <limits.h>
#include <X11/keysym.h>
#include <mglpp/mglpp.hpp>
@@ -37,6 +38,7 @@ static void disable_prime_run() {
unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER");
unsetenv("__GLX_VENDOR_LIBRARY_NAME");
unsetenv("__VK_LAYER_NV_optimus");
unsetenv("DRI_PRIME");
}
static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay *overlay) {
@@ -188,6 +190,48 @@ static bool is_gsr_ui_virtual_keyboard_running() {
return virtual_keyboard_running;
}
static void install_flatpak_systemd_service() {
const bool systemd_service_exists = system(
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
"flatpak-spawn --host -- ls \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0;
if(systemd_service_exists)
return;
bool service_install_successful = (system(
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
"flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0);
service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0);
if(service_install_successful)
fprintf(stderr, "Info: the systemd service file was missing. It has now been installed\n");
else
fprintf(stderr, "Error: the systemd service file is missing and failed to install it again\n");
}
static void remove_flatpak_systemd_service() {
char systemd_service_path[PATH_MAX];
const char *xdg_data_home = getenv("XDG_DATA_HOME");
const char *home = getenv("HOME");
if(xdg_data_home) {
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/systemd/user/gpu-screen-recorder-ui.service", xdg_data_home);
} else if(home) {
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/.local/share/systemd/user/gpu-screen-recorder-ui.service", home);
} else {
fprintf(stderr, "Error: failed to get user home directory\n");
return;
}
if(access(systemd_service_path, F_OK) != 0)
return;
remove(systemd_service_path);
system("systemctl --user daemon-reload");
fprintf(stderr, "Info: conflicting flatpak version of the systemd service for gsr-ui was found at \"%s\", it has now been removed\n", systemd_service_path);
}
static bool is_flatpak() {
return getenv("FLATPAK_ID") != nullptr;
}
static void usage() {
printf("usage: gsr-ui [action]\n");
printf("OPTIONS:\n");
@@ -227,6 +271,11 @@ int main(int argc, char **argv) {
usage();
}
if(is_flatpak())
install_flatpak_systemd_service();
else
remove_flatpak_systemd_service();
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.

View File

@@ -11,7 +11,7 @@
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/poll.h>
#include <poll.h>
/* LINUX */
#include <linux/input.h>
@@ -184,6 +184,7 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
if(event.type == EV_KEY) {
keyboard_event_process_key_state_change(self, event, extra_data, fd);
/* We do this conversion from keycode to keysym back to keycode to support different keyboard layouts in the X server (which Wayland also uses to support Xwayland) */
uint32_t keycode = event.code;
const uint32_t keysym = keycode_to_keysym(self, event.code);
if(keysym)

View File

@@ -10,7 +10,7 @@
#include <stdint.h>
/* POSIX */
#include <sys/poll.h>
#include <poll.h>
/* LINUX */
#include <linux/input-event-codes.h>