aboutsummaryrefslogtreecommitdiff
path: root/src/Overlay.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/Overlay.cpp')
-rw-r--r--src/Overlay.cpp1184
1 files changed, 967 insertions, 217 deletions
diff --git a/src/Overlay.cpp b/src/Overlay.cpp
index 2475a77..919117d 100644
--- a/src/Overlay.cpp
+++ b/src/Overlay.cpp
@@ -7,20 +7,29 @@
#include "../include/gui/DropdownButton.hpp"
#include "../include/gui/CustomRendererWidget.hpp"
#include "../include/gui/SettingsPage.hpp"
+#include "../include/gui/GlobalSettingsPage.hpp"
#include "../include/gui/Utils.hpp"
#include "../include/gui/PageStack.hpp"
+#include "../include/gui/GsrPage.hpp"
+#include "../include/WindowUtils.hpp"
+#include "../include/GlobalHotkeys.hpp"
#include <string.h>
#include <assert.h>
#include <sys/wait.h>
#include <limits.h>
#include <fcntl.h>
+#include <poll.h>
#include <stdexcept>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
+#include <X11/extensions/Xfixes.h>
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/shape.h>
+#include <X11/Xcursor/Xcursor.h>
#include <mglpp/system/Rect.hpp>
#include <mglpp/window/Event.hpp>
@@ -32,65 +41,7 @@ namespace gsr {
static const mgl::Color bg_color(0, 0, 0, 100);
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.0;
-
- static bool window_has_atom(Display *dpy, Window window, Atom atom) {
- Atom type;
- unsigned long len, bytes_left;
- int format;
- unsigned char *properties = NULL;
- if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success)
- return false;
-
- if(properties)
- XFree(properties);
-
- return type != None;
- }
-
- static bool window_is_user_program(Display *dpy, Window window) {
- const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
- const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False);
- return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
- }
-
- static Window get_window_at_cursor_position(Display *dpy) {
- Window root_window = None;
- Window window = None;
- int dummy_i;
- unsigned int dummy_u;
- int cursor_pos_x = 0;
- int cursor_pos_y = 0;
- XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u);
- return window;
- }
-
- static Window get_focused_window(Display *dpy) {
- const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
- Window focused_window = None;
-
- // Atom type = None;
- // int format = 0;
- // unsigned long num_items = 0;
- // unsigned long bytes_left = 0;
- // unsigned char *data = NULL;
- // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data);
-
- // fprintf(stderr, "focused window: %p\n", (void*)data);
-
- // if(type == XA_WINDOW && num_items == 1 && data)
- // return *(Window*)data;
-
- int revert_to = 0;
- XGetInputFocus(dpy, &focused_window, &revert_to);
- if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
- return focused_window;
-
- focused_window = get_window_at_cursor_position(dpy);
- if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
- return focused_window;
-
- return None;
- }
+ static const double replay_saving_notification_timeout_seconds = 0.5;
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
@@ -117,6 +68,53 @@ namespace gsr {
return texture;
}
+ static bool texture_from_x11_cursor(XcursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) {
+ uint8_t *cursor_data = NULL;
+ uint8_t *out = NULL;
+ const unsigned int *pixels = NULL;
+ *visible = false;
+
+ if(!x11_cursor_image)
+ return false;
+
+ if(!x11_cursor_image->pixels)
+ return false;
+
+ hotspot->x = x11_cursor_image->xhot;
+ hotspot->y = x11_cursor_image->yhot;
+
+ pixels = x11_cursor_image->pixels;
+ cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4);
+ if(!cursor_data)
+ return false;
+
+ out = cursor_data;
+ /* Un-premultiply alpha */
+ for(uint32_t y = 0; y < x11_cursor_image->height; ++y) {
+ for(uint32_t x = 0; x < x11_cursor_image->width; ++x) {
+ uint32_t pixel = *pixels++;
+ uint8_t *in = (uint8_t*)&pixel;
+ uint8_t alpha = in[3];
+ if(alpha == 0) {
+ alpha = 1;
+ } else {
+ *visible = true;
+ }
+
+ out[0] = (float)in[2] * 255.0/(float)alpha;
+ out[1] = (float)in[1] * 255.0/(float)alpha;
+ out[2] = (float)in[0] * 255.0/(float)alpha;
+ out[3] = in[3];
+ out += 4;
+ in += 4;
+ }
+ }
+
+ texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA);
+ free(cursor_data);
+ return true;
+ }
+
static char hex_value_to_str(uint8_t v) {
if(v <= 9)
return '0' + v;
@@ -167,7 +165,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;
@@ -176,8 +174,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) {
@@ -231,15 +229,6 @@ namespace gsr {
return is_fullscreen;
}
- static void set_focused_window(Display *dpy, Window window) {
- XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime);
-
- const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
- XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1);
-
- XFlush(dpy);
- }
-
#define _NET_WM_STATE_REMOVE 0
#define _NET_WM_STATE_ADD 1
#define _NET_WM_STATE_TOGGLE 2
@@ -265,6 +254,14 @@ namespace gsr {
return True;
}
+ static void make_window_click_through(Display *display, Window window) {
+ XRectangle rect;
+ memset(&rect, 0, sizeof(rect));
+ XserverRegion region = XFixesCreateRegion(display, &rect, 1);
+ XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region);
+ XFixesDestroyRegion(display, region);
+ }
+
static Bool make_window_sticky(Display *dpy, Window window) {
return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False));
}
@@ -274,22 +271,13 @@ namespace gsr {
}
// Returns the first monitor if not found. Assumes there is at least one monitor connected.
- static const mgl_monitor* find_monitor_by_cursor_position(mgl::Window &window) {
- 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({ win->cursor_position.x, win->cursor_position.y }))
- 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];
- }
-
- static 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);
- return XGetSelectionOwner(dpy, prop_atom) != None;
+ return &monitors.front();
}
static std::string get_power_supply_online_filepath() {
@@ -320,17 +308,108 @@ namespace gsr {
return is_connected;
}
- Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs) :
+ static bool xinput_is_supported(Display *dpy, int *xi_opcode) {
+ *xi_opcode = 0;
+ int query_event = 0;
+ int query_error = 0;
+ if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) {
+ fprintf(stderr, "gsr-ui error: X Input extension not available\n");
+ return false;
+ }
+
+ int major = 2;
+ int minor = 1;
+ int retval = XIQueryVersion(dpy, &major, &minor);
+ if (retval != Success) {
+ fprintf(stderr, "gsr-ui error: XInput 2.1 is not supported\n");
+ return false;
+ }
+
+ return true;
+ }
+
+ static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) {
+ return {
+ (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key),
+ config_hotkey.modifiers
+ };
+ }
+
+ static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) {
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey),
+ "show_hide", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_show();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey),
+ "record", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_record();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey),
+ "pause", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_pause();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey),
+ "stream", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_stream();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey),
+ "replay_start", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_replay();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey),
+ "replay_save", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay();
+ });
+ }
+
+ static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) {
+ auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type);
+ if(!global_hotkeys->start())
+ fprintf(stderr, "error: failed to start global hotkeys\n");
+
+ bind_linux_hotkeys(global_hotkeys.get(), overlay);
+ return global_hotkeys;
+ }
+
+ static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) {
+ auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>();
+ if(!global_hotkeys_js->start())
+ fprintf(stderr, "Warning: failed to start joystick hotkeys\n");
+
+ global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay();
+ });
+
+ return global_hotkeys_js;
+ }
+
+ Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) :
resources_path(std::move(resources_path)),
- gsr_info(gsr_info),
+ gsr_info(std::move(gsr_info)),
egl_funcs(egl_funcs),
+ config(capture_options),
bg_screenshot_overlay({0.0f, 0.0f}),
top_bar_background({0.0f, 0.0f}),
- close_button_widget({0.0f, 0.0f}),
- config(gsr_info)
+ close_button_widget({0.0f, 0.0f})
{
- memset(&window_texture, 0, sizeof(window_texture));
-
key_bindings[0].key_event.code = mgl::Keyboard::Escape;
key_bindings[0].key_event.alt = false;
key_bindings[0].key_event.control = false;
@@ -340,19 +419,32 @@ namespace gsr {
page_stack.pop();
};
- std::optional<Config> new_config = read_config(gsr_info);
+ memset(&window_texture, 0, sizeof(window_texture));
+
+ std::optional<Config> new_config = read_config(capture_options);
if(new_config)
config = std::move(new_config.value());
- init_color_theme(gsr_info);
- // These environment variable are used by files in scripts/ folder
- const std::string notify_bg_color_str = color_to_hex_str(get_color_theme().tint_color);
- setenv("GSR_NOTIFY_BG_COLOR", notify_bg_color_str.c_str(), true);
+ init_color_theme(config, this->gsr_info);
power_supply_online_filepath = get_power_supply_online_filepath();
if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup")
on_press_start_replay(true);
+
+ if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
+ else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices")
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL);
+
+ if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys")
+ global_hotkeys_js = register_joystick_hotkeys(this);
+
+ x11_mapping_display = XOpenDisplay(nullptr);
+ if(x11_mapping_display)
+ XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify
+ else
+ fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n");
}
Overlay::~Overlay() {
@@ -377,6 +469,134 @@ namespace gsr {
}
gpu_screen_recorder_process = -1;
}
+
+ close_gpu_screen_recorder_output();
+ deinit_color_theme();
+
+ if(x11_mapping_display)
+ XCloseDisplay(x11_mapping_display);
+ }
+
+ void Overlay::xi_setup() {
+ xi_display = XOpenDisplay(nullptr);
+ if(!xi_display) {
+ fprintf(stderr, "gsr-ui error: failed to setup XI connection\n");
+ return;
+ }
+
+ if(!xinput_is_supported(xi_display, &xi_opcode))
+ goto error;
+
+ xi_input_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_input_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ xi_output_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_output_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ unsigned char mask[XIMaskLen(XI_LASTEVENT)];
+ memset(mask, 0, sizeof(mask));
+ XISetMask(mask, XI_Motion);
+ //XISetMask(mask, XI_RawMotion);
+ XISetMask(mask, XI_ButtonPress);
+ XISetMask(mask, XI_ButtonRelease);
+ XISetMask(mask, XI_KeyPress);
+ XISetMask(mask, XI_KeyRelease);
+
+ XIEventMask xi_masks;
+ xi_masks.deviceid = XIAllMasterDevices;
+ xi_masks.mask_len = sizeof(mask);
+ xi_masks.mask = mask;
+ if(XISelectEvents(xi_display, DefaultRootWindow(xi_display), &xi_masks, 1) != Success) {
+ fprintf(stderr, "gsr-ui error: XISelectEvents failed\n");
+ goto error;
+ }
+
+ XFlush(xi_display);
+ return;
+
+ error:
+ free(xi_input_xev);
+ xi_input_xev = nullptr;
+ free(xi_output_xev);
+ xi_output_xev = nullptr;
+ if(xi_display) {
+ XCloseDisplay(xi_display);
+ xi_display = nullptr;
+ }
+ }
+
+ void Overlay::close_gpu_screen_recorder_output() {
+ if(gpu_screen_recorder_process_output_file) {
+ fclose(gpu_screen_recorder_process_output_file);
+ gpu_screen_recorder_process_output_file = nullptr;
+ }
+
+ if(gpu_screen_recorder_process_output_fd > 0) {
+ close(gpu_screen_recorder_process_output_fd);
+ gpu_screen_recorder_process_output_fd = -1;
+ }
+ }
+
+ void Overlay::handle_xi_events() {
+ if(!xi_display)
+ return;
+
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ while(XPending(xi_display)) {
+ XNextEvent(xi_display, xi_input_xev);
+ XGenericEventCookie *cookie = &xi_input_xev->xcookie;
+ if(cookie->type == GenericEvent && cookie->extension == xi_opcode && XGetEventData(xi_display, cookie)) {
+ const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
+ if(cookie->evtype == XI_Motion) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = MotionNotify;
+ xi_output_xev->xmotion.display = display;
+ xi_output_xev->xmotion.window = window->get_system_handle();
+ xi_output_xev->xmotion.subwindow = window->get_system_handle();
+ xi_output_xev->xmotion.x = de->root_x - window_pos.x;
+ xi_output_xev->xmotion.y = de->root_y - window_pos.y;
+ xi_output_xev->xmotion.x_root = de->root_x;
+ xi_output_xev->xmotion.y_root = de->root_y;
+ //xi_output_xev->xmotion.state = // modifiers // TODO:
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease;
+ xi_output_xev->xbutton.display = display;
+ xi_output_xev->xbutton.window = window->get_system_handle();
+ xi_output_xev->xbutton.subwindow = window->get_system_handle();
+ xi_output_xev->xbutton.x = de->root_x - window_pos.x;
+ xi_output_xev->xbutton.y = de->root_y - window_pos.y;
+ xi_output_xev->xbutton.x_root = de->root_x;
+ xi_output_xev->xbutton.y_root = de->root_y;
+ //xi_output_xev->xbutton.state = // modifiers // TODO:
+ xi_output_xev->xbutton.button = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_KeyPress || cookie->evtype == XI_KeyRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease;
+ xi_output_xev->xkey.display = display;
+ xi_output_xev->xkey.window = window->get_system_handle();
+ xi_output_xev->xkey.subwindow = window->get_system_handle();
+ xi_output_xev->xkey.x = de->root_x - window_pos.x;
+ xi_output_xev->xkey.y = de->root_y - window_pos.y;
+ xi_output_xev->xkey.x_root = de->root_x;
+ xi_output_xev->xkey.y_root = de->root_y;
+ xi_output_xev->xkey.state = de->mods.effective;
+ xi_output_xev->xkey.keycode = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ }
+ //fprintf(stderr, "got xi event: %d\n", cookie->evtype);
+ XFreeEventData(xi_display, cookie);
+ }
+ }
}
static uint32_t key_event_to_bitmask(mgl::Event::KeyEvent key_event) {
@@ -397,11 +617,42 @@ namespace gsr {
}
}
+ void Overlay::handle_keyboard_mapping_event() {
+ if(!x11_mapping_display)
+ return;
+
+ bool mapping_updated = false;
+ while(XPending(x11_mapping_display)) {
+ XNextEvent(x11_mapping_display, &x11_mapping_xev);
+ if(x11_mapping_xev.type == MappingNotify) {
+ XRefreshKeyboardMapping(&x11_mapping_xev.xmapping);
+ mapping_updated = true;
+ }
+ }
+
+ if(mapping_updated)
+ rebind_all_keyboard_hotkeys();
+ }
+
void Overlay::handle_events() {
+ if(global_hotkeys)
+ global_hotkeys->poll_events();
+
+ if(global_hotkeys_js)
+ global_hotkeys_js->poll_events();
+
+ handle_keyboard_mapping_event();
+
if(!visible || !window)
return;
+ handle_xi_events();
+
while(window->poll_event(event)) {
+ if(global_hotkeys) {
+ if(!global_hotkeys->on_event(event))
+ continue;
+ }
on_event(event);
}
}
@@ -410,7 +661,9 @@ namespace gsr {
if(!visible || !window)
return;
- close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f));
+ if(!close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)))
+ return;
+
if(!page_stack.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)))
return;
@@ -419,6 +672,7 @@ namespace gsr {
bool Overlay::draw() {
update_notification_process_status();
+ update_gsr_replay_save();
update_gsr_process_status();
replay_status_update_status();
@@ -433,55 +687,229 @@ namespace gsr {
if(!window)
return false;
+ grab_mouse_and_keyboard();
+
//force_window_on_top();
- window->clear(bg_color);
+ const bool draw_ui = show_overlay_clock.get_elapsed_time_seconds() >= show_overlay_timeout_seconds;
- if(window_texture_sprite.get_texture() && window_texture.texture_id) {
- window->draw(window_texture_sprite);
- window->draw(bg_screenshot_overlay);
- } else if(screenshot_texture.is_valid()) {
- window->draw(screenshot_sprite);
- window->draw(bg_screenshot_overlay);
- }
+ window->clear(draw_ui ? bg_color : mgl::Color(0, 0, 0, 0));
+
+ if(draw_ui) {
+ if(window_texture_sprite.get_texture() && window_texture.texture_id) {
+ window->draw(window_texture_sprite);
+ window->draw(bg_screenshot_overlay);
+ } else if(screenshot_texture.is_valid()) {
+ window->draw(screenshot_sprite);
+ window->draw(bg_screenshot_overlay);
+ }
+
+ window->draw(top_bar_background);
+ window->draw(top_bar_text);
+ window->draw(logo_sprite);
- window->draw(top_bar_background);
- window->draw(top_bar_text);
- window->draw(logo_sprite);
+ close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
- close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
- page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ if(cursor_texture.is_valid()) {
+ cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
+ window->draw(cursor_sprite);
+ }
+
+ if(!drawn_first_frame) {
+ drawn_first_frame = true;
+ mgl::Event event;
+ event.type = mgl::Event::MouseMoved;
+ event.mouse_move.x = window->get_mouse_position().x;
+ event.mouse_move.y = window->get_mouse_position().y;
+ on_event(event);
+ }
+ }
window->display();
return true;
}
+ void Overlay::grab_mouse_and_keyboard() {
+ // TODO: Remove these grabs when debugging with a debugger, or your X11 session will appear frozen.
+ // There should be a debug mode to not use these
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ XGrabPointer(display, window->get_system_handle(), True,
+ ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
+ Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
+ ButtonMotionMask,
+ GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
+ // TODO: This breaks global hotkeys (when using x11 global hotkeys)
+ XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ XFlush(display);
+ }
+
+ void Overlay::xi_setup_fake_cursor() {
+ if(!xi_display)
+ return;
+
+ XFixesHideCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+
+ // TODO: XCURSOR_SIZE and XCURSOR_THEME environment variables
+ const char *cursor_theme = XcursorGetTheme(xi_display);
+ if(!cursor_theme) {
+ //fprintf(stderr, "Warning: failed to get cursor theme, using \"default\" theme instead\n");
+ cursor_theme = "default";
+ }
+
+ int cursor_size = XcursorGetDefaultSize(xi_display);
+ if(cursor_size <= 1)
+ cursor_size = 24;
+
+ XcursorImage *cursor_image = nullptr;
+ for(int cursor_size_test : {cursor_size, 24}) {
+ for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) {
+ for(unsigned int shape : {XC_left_ptr, XC_arrow}) {
+ cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test);
+ if(cursor_image)
+ goto done;
+ }
+ }
+ }
+
+ done:
+ if(!cursor_image) {
+ fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n");
+ const std::string default_cursor_path = resources_path + "images/default.cur";
+ for(int cursor_size_test : {cursor_size, 24}) {
+ cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test);
+ if(cursor_image)
+ break;
+ }
+ }
+
+ if(!cursor_image) {
+ fprintf(stderr, "Error: failed to get cursor\n");
+ XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+ return;
+ }
+
+ bool cursor_visible = false;
+ texture_from_x11_cursor(cursor_image, &cursor_visible, &cursor_hotspot, cursor_texture);
+ if(cursor_texture.is_valid())
+ cursor_sprite.set_texture(&cursor_texture);
+
+ XcursorImageDestroy(cursor_image);
+ }
+
+ static bool device_is_mouse(const XIDeviceInfo *dev) {
+ for(int i = 0; i < dev->num_classes; ++i) {
+ if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer)
+ return true;
+ }
+ return false;
+ }
+
+ void Overlay::xi_grab_all_mouse_devices() {
+ if(!xi_display)
+ return;
+
+ int num_devices = 0;
+ XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices);
+ if(!info)
+ return;
+
+ unsigned char mask[XIMaskLen(XI_LASTEVENT)];
+ memset(mask, 0, sizeof(mask));
+ XISetMask(mask, XI_Motion);
+ //XISetMask(mask, XI_RawMotion);
+ XISetMask(mask, XI_ButtonPress);
+ XISetMask(mask, XI_ButtonRelease);
+ XISetMask(mask, XI_KeyPress);
+ XISetMask(mask, XI_KeyRelease);
+
+ for (int i = 0; i < num_devices; ++i) {
+ const XIDeviceInfo *dev = &info[i];
+ if(!device_is_mouse(dev))
+ continue;
+
+ XIEventMask xi_masks;
+ xi_masks.deviceid = dev->deviceid;
+ xi_masks.mask_len = sizeof(mask);
+ xi_masks.mask = mask;
+ XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks);
+ }
+
+ XFlush(xi_display);
+ XIFreeDeviceInfo(info);
+ }
+
void Overlay::show() {
+ if(visible)
+ return;
+
+ drawn_first_frame = false;
window.reset();
window = std::make_unique<mgl::Window>();
deinit_theme();
- mgl::vec2i window_size = { 1280, 720 };
- mgl::vec2i window_pos = { 0, 0 };
+ 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);
+
+ // 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 || is_wlroots;
+
+ if(prevent_game_minimizing) {
+ window_pos = focused_monitor->position;
+ window_size = focused_monitor->size;
+ } else {
+ window_pos = {0, 0};
+ window_size = focused_monitor->size / 2;
+ }
mgl::Window::CreateParams window_create_params;
window_create_params.size = window_size;
- window_create_params.min_size = window_size;
- window_create_params.max_size = window_size;
- window_create_params.position = window_pos;
- window_create_params.hidden = true;
- window_create_params.override_redirect = true;
- window_create_params.background_color = bg_color;
+ if(prevent_game_minimizing) {
+ window_create_params.min_size = window_size;
+ window_create_params.max_size = window_size;
+ }
+ window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2;
+ window_create_params.hidden = prevent_game_minimizing;
+ window_create_params.override_redirect = prevent_game_minimizing;
+ window_create_params.background_color = mgl::Color(0, 0, 0, 0);
window_create_params.support_alpha = true;
- window_create_params.window_type = MGL_WINDOW_TYPE_NOTIFICATION;
- window_create_params.render_api = MGL_RENDER_API_EGL;
+ window_create_params.hide_decorations = true;
+ // 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;
+ // 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");
- mgl_context *context = mgl_get_context();
- Display *display = (Display*)context->connection;
+ //window->set_low_latency(true);
unsigned char data = 2; // Prefer being composed to allow transparency
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
@@ -489,30 +917,27 @@ namespace gsr {
data = 1;
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
+ const auto original_window_size = window_size;
+ 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_by_cursor_position(*window);
- 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);
- window->set_size(window_size);
- window->set_size_limits(window_size, window_size);
- window->set_position(window_pos);
+ if(prevent_game_minimizing) {
+ window->set_size(window_size);
+ window->set_size_limits(window_size, window_size);
+ }
+ window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2);
- update_compositor_texture(focused_monitor);
+ 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;
- 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());
@@ -545,6 +970,7 @@ namespace gsr {
const int button_width = button_height;
auto main_buttons_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
+ List * main_buttons_list_ptr = main_buttons_list.get();
main_buttons_list->set_spacing(0.0f);
{
auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture,
@@ -555,9 +981,14 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("save", &get_theme().save_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, gsr_info, config, &page_stack);
+ auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack);
+ replay_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::REPLAY)
+ show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ };
page_stack.push(std::move(replay_settings_page));
} else if(id == "save") {
on_press_save_replay();
@@ -576,9 +1007,14 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("pause", &get_theme().pause_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, gsr_info, config, &page_stack);
+ auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack);
+ record_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::RECORD)
+ show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ };
page_stack.push(std::move(record_settings_page));
} else if(id == "pause") {
toggle_pause();
@@ -595,9 +1031,14 @@ namespace gsr {
button->add_item("Start", "start", "Alt+F8");
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, gsr_info, config, &page_stack);
+ auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack);
+ stream_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::STREAM)
+ show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
+ };
page_stack.push(std::move(stream_settings_page));
} else if(id == "start") {
on_press_start_stream();
@@ -610,6 +1051,59 @@ namespace gsr {
main_buttons_list->set_position((mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.25f) - main_buttons_list_size * 0.5f).floor());
front_page_ptr->add_widget(std::move(main_buttons_list));
+ {
+ const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size();
+ const int settings_button_size = main_buttons_size.y * 0.2f;
+ auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180));
+ button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor());
+ button->set_bg_hover_color(mgl::Color(0, 0, 0, 255));
+ button->set_icon(&get_theme().settings_small_texture);
+ button->on_click = [&]() {
+ auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack);
+
+ settings_page->on_startup_changed = [&](bool enable, int exit_status) {
+ if(exit_status == 0)
+ return;
+
+ if(exit_status == 127) {
+ if(enable)
+ show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ } else {
+ if(enable)
+ show_notification("Failed to add GPU Screen Recorder to system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ else
+ show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ }
+ };
+
+ settings_page->on_click_exit_program_button = [this](const char *reason) {
+ do_exit = true;
+ exit_reason = reason;
+ };
+
+ settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) {
+ global_hotkeys.reset();
+ if(strcmp(hotkey_option, "enable_hotkeys") == 0)
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
+ else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0)
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL);
+ else if(strcmp(hotkey_option, "disable_hotkeys") == 0)
+ global_hotkeys.reset();
+ };
+
+ settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) {
+ global_hotkeys_js.reset();
+ if(strcmp(hotkey_option, "enable_hotkeys") == 0)
+ global_hotkeys_js = register_joystick_hotkeys(this);
+ else if(strcmp(hotkey_option, "disable_hotkeys") == 0)
+ global_hotkeys_js.reset();
+ };
+
+ page_stack.push(std::move(settings_page));
+ };
+ front_page_ptr->add_widget(std::move(button));
+ }
+
close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height);
const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height);
@@ -637,8 +1131,18 @@ namespace gsr {
return true;
};
- window->set_fullscreen(true);
+ // The focused application can be an xwayland application but the cursor can hover over a wayland application.
+ // This is even the case when hovering over the titlebar of the xwayland application.
+ const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing;
+ if(fake_cursor)
+ xi_setup();
+
+ //window->set_fullscreen(true);
+ if(gsr_info.system_info.display_server == DisplayServer::X11)
+ make_window_click_through(display, window->get_system_handle());
+
window->set_visible(true);
+
make_window_sticky(display, window->get_system_handle());
hide_window_from_taskbar(display, window->get_system_handle());
@@ -646,33 +1150,23 @@ namespace gsr {
XFreeCursor(display, default_cursor);
default_cursor = 0;
}
- default_cursor = XCreateFontCursor(display, XC_arrow);
+ default_cursor = XCreateFontCursor(display, XC_left_ptr);
+ XFlush(display);
- // TODO: Retry if these fail.
- // TODO: Hmm, these dont work in owlboy. Maybe owlboy uses xi2 and that breaks this (does it?).
- // Remove these grabs when debugging with a debugger, or your X11 session will appear frozen
+ grab_mouse_and_keyboard();
- // XGrabPointer(display, window->get_system_handle(), True,
- // ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
- // Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
- // ButtonMotionMask,
- // GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
- // TODO: This breaks global hotkeys
- //XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
+ xi_setup_fake_cursor();
- set_focused_window(display, window->get_system_handle());
- XFlush(display);
+ // We want to grab all devices to prevent any other application below the UI from receiving events.
+ // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
+ xi_grab_all_mouse_devices();
- //window->set_fullscreen(true);
+ if(!is_wlroots)
+ window->set_fullscreen(true);
visible = true;
- mgl::Event event;
- event.type = mgl::Event::MouseMoved;
- event.mouse_move.x = window->get_mouse_position().x;
- event.mouse_move.y = window->get_mouse_position().y;
- on_event(event);
-
if(gpu_screen_recorder_process > 0) {
switch(recording_status) {
case RecordingStatus::NONE:
@@ -691,9 +1185,18 @@ namespace gsr {
if(paused)
update_ui_recording_paused();
+
+ // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way
+ // without messing up window position.
+ show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15;
+ show_overlay_clock.restart();
+ draw();
}
void Overlay::hide() {
+ if(!visible)
+ return;
+
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -710,13 +1213,57 @@ namespace gsr {
XUngrabPointer(display, CurrentTime);
XFlush(display);
+ if(xi_display) {
+ cursor_texture.clear();
+ cursor_sprite.set_texture(nullptr);
+ }
+
window_texture_deinit(&window_texture);
window_texture_sprite.set_texture(nullptr);
screenshot_texture.clear();
screenshot_sprite.set_texture(nullptr);
visible = false;
+ drawn_first_frame = false;
+
+ if(xi_input_xev) {
+ free(xi_input_xev);
+ xi_input_xev = nullptr;
+ }
+
+ if(xi_output_xev) {
+ free(xi_output_xev);
+ xi_output_xev = nullptr;
+ }
+
+ if(xi_display) {
+ XCloseDisplay(xi_display);
+ xi_display = nullptr;
+
+ if(window) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position();
+ XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y);
+ XFlush(display);
+
+ XFixesShowCursor(display, DefaultRootWindow(display));
+ XFlush(display);
+ }
+ }
+
if(window) {
+ if(show_overlay_timeout_seconds > 0.0001) {
+ window->clear(mgl::Color(0, 0, 0, 0));
+ window->display();
+
+ mgl_context *context = mgl_get_context();
+ context->gl.glFlush();
+ context->gl.glFinish();
+ usleep(50 * 1000); // EGL doesn't do an immediate flush for some reason
+ }
+
window->set_visible(false);
window.reset();
}
@@ -725,10 +1272,16 @@ namespace gsr {
}
void Overlay::toggle_show() {
- if(visible)
- hide();
- else
+ if(visible) {
+ //hide();
+ // We dont want to hide immediately because hide is called in mgl event callback, in which it destroys the mgl window.
+ // Instead remove all pages and wait until next iteration to close the UI (which happens when there are no pages to render).
+ while(!page_stack.empty()) {
+ page_stack.pop();
+ }
+ } else {
show();
+ }
}
void Overlay::toggle_record() {
@@ -787,7 +1340,7 @@ namespace gsr {
const char *notification_type_str = notification_type_to_string(notification_type);
if(notification_type_str) {
notification_args[9] = "--icon";
- notification_args[10] = "record",
+ notification_args[10] = notification_type_str;
notification_args[11] = nullptr;
} else {
notification_args[9] = nullptr;
@@ -799,13 +1352,40 @@ namespace gsr {
waitpid(notification_process, &status, 0);
}
- notification_process = exec_program(notification_args);
+ notification_process = exec_program(notification_args, NULL);
}
bool Overlay::is_open() const {
return visible;
}
+ bool Overlay::should_exit(std::string &reason) const {
+ reason.clear();
+ if(do_exit)
+ reason = exit_reason;
+ return do_exit;
+ }
+
+ void Overlay::exit() {
+ do_exit = true;
+ }
+
+ const Config& Overlay::get_config() const {
+ return config;
+ }
+
+ void Overlay::unbind_all_keyboard_hotkeys() {
+ if(global_hotkeys)
+ global_hotkeys->unbind_all_keys();
+ }
+
+ void Overlay::rebind_all_keyboard_hotkeys() {
+ unbind_all_keyboard_hotkeys();
+ // TODO: Check if type is GlobalHotkeysLinux
+ if(global_hotkeys)
+ bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
+ }
+
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -819,32 +1399,128 @@ namespace gsr {
notification_process = -1;
}
+ static void string_replace_characters(char *str, const char *characters_to_replace, char new_character) {
+ for(; *str != '\0'; ++str) {
+ for(const char *p = characters_to_replace; *p != '\0'; ++p) {
+ if(*str == *p)
+ *str = new_character;
+ }
+ }
+ }
+
+ static std::string filepath_get_directory(const char *filepath) {
+ std::string result = filepath;
+ const size_t last_slash_index = result.rfind('/');
+ if(last_slash_index == std::string::npos)
+ result = ".";
+ else
+ result.erase(last_slash_index);
+ return result;
+ }
+
+ static std::string filepath_get_filename(const char *filepath) {
+ std::string result = filepath;
+ const size_t last_slash_index = result.rfind('/');
+ if(last_slash_index != std::string::npos)
+ result.erase(0, last_slash_index + 1);
+ return result;
+ }
+
+ static void truncate_string(std::string &str, int max_length) {
+ if((int)str.size() > max_length)
+ str.replace(str.begin() + max_length, str.end(), "...");
+ }
+
+ void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ const std::string video_filename = filepath_get_filename(video_filepath);
+
+ const Window gsr_ui_window = window ? window->get_system_handle() : None;
+ std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window);
+ if(focused_window_name.empty())
+ focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
+ if(focused_window_name.empty())
+ focused_window_name = "Game";
+
+ string_replace_characters(focused_window_name.data(), "/\\", '_');
+
+ std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name;
+ create_directory_recursive(video_directory.data());
+
+ const std::string new_video_filepath = video_directory + "/" + video_filename;
+ rename(video_filepath, new_video_filepath.c_str());
+
+ truncate_string(focused_window_name, 20);
+ std::string text;
+ switch(notification_type) {
+ case NotificationType::RECORD: {
+ if(!config.record_config.show_video_saved_notifications)
+ return;
+ text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'";
+ break;
+ }
+ case NotificationType::REPLAY: {
+ if(!config.replay_config.show_replay_saved_notifications)
+ return;
+ text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'";
+ break;
+ }
+ case NotificationType::NONE:
+ case NotificationType::STREAM:
+ break;
+ }
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type);
+ }
+
+ void Overlay::on_replay_saved(const char *replay_saved_filepath) {
+ replay_save_show_notification = false;
+ if(config.replay_config.save_video_in_game_folder) {
+ save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
+ } else {
+ const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'";
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ }
+ }
+
+ void Overlay::update_gsr_replay_save() {
+ if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
+ replay_save_show_notification = false;
+ show_notification("Saving replay, this might take some time", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ }
+
+ if(gpu_screen_recorder_process_output_file) {
+ char buffer[1024];
+ char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
+ if(!replay_saved_filepath || replay_saved_filepath[0] == '\0')
+ return;
+
+ const int line_len = strlen(replay_saved_filepath);
+ if(replay_saved_filepath[line_len - 1] == '\n')
+ replay_saved_filepath[line_len - 1] = '\0';
+
+ on_replay_saved(replay_saved_filepath);
+ } else if(gpu_screen_recorder_process_output_fd > 0) {
+ char buffer[1024];
+ read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
+ }
+ }
+
void Overlay::update_gsr_process_status() {
if(gpu_screen_recorder_process <= 0)
return;
- errno = 0;
int status;
if(waitpid(gpu_screen_recorder_process, &status, WNOHANG) == 0) {
// Still running
return;
}
+ close_gpu_screen_recorder_output();
+
int exit_code = -1;
- // The process is no longer a child process since gsr ui has restarted
- if(errno == ECHILD) {
- errno = 0;
- kill(gpu_screen_recorder_process, 0);
- if(errno != ESRCH) {
- // Still running
- return;
- }
- // We cant know the exit status, so we assume it succeeded
- exit_code = 0;
- } else {
- if(WIFEXITED(status))
- exit_code = WEXITSTATUS(status);
- }
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
switch(recording_status) {
case RecordingStatus::NONE:
@@ -856,16 +1532,13 @@ namespace gsr {
show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Replay stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
+ show_notification("Replay stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
}
break;
}
case RecordingStatus::RECORD: {
update_ui_recording_stopped();
- if(exit_code != 0) {
- fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Failed to start/save recording", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
- }
+ on_stop_recording(exit_code);
break;
}
case RecordingStatus::STREAM: {
@@ -875,7 +1548,7 @@ namespace gsr {
show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Streaming stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
+ show_notification("Streaming stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
}
break;
}
@@ -901,7 +1574,7 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
- const Window focused_window = get_focused_window(display);
+ const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
if(window && focused_window == window->get_system_handle())
return;
@@ -915,6 +1588,7 @@ namespace gsr {
}
}
+ // TODO: Instead of checking power supply status periodically listen to power supply event
void Overlay::update_power_supply_status() {
if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected")
return;
@@ -929,6 +1603,20 @@ namespace gsr {
}
}
+ void Overlay::on_stop_recording(int exit_code) {
+ if(exit_code == 0) {
+ if(config.record_config.save_video_in_game_folder) {
+ save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD);
+ } else {
+ const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'";
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ }
+ } else {
+ fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
+ show_notification("Failed to start/save recording. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
+ }
+ }
+
void Overlay::update_ui_recording_paused() {
if(!visible || recording_status != RecordingStatus::RECORD)
return;
@@ -1088,12 +1776,35 @@ namespace gsr {
args.push_back(audio_track.c_str());
}
}
+
+ if(record_options.restore_portal_session) {
+ args.push_back("-restore-portal-session");
+ args.push_back("yes");
+ }
+ }
+
+ static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number)
+ if(capture_target == "focused") {
+ return capture_options.focused;
+ } else if(capture_target == "portal") {
+ return capture_options.portal;
+ } else {
+ for(const GsrMonitor &monitor : capture_options.monitors) {
+ if(capture_target == monitor.name)
+ return true;
+ }
+ return false;
+ }
}
void Overlay::on_press_save_replay() {
if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
return;
+ replay_save_show_notification = true;
+ replay_save_clock.restart();
kill(gpu_screen_recorder_process, SIGUSR1);
}
@@ -1111,10 +1822,13 @@ namespace gsr {
}
paused = false;
+ replay_save_show_notification = false;
// window->close();
// usleep(1000 * 50); // 50 milliseconds
+ close_gpu_screen_recorder_output();
+
if(gpu_screen_recorder_process > 0) {
kill(gpu_screen_recorder_process, SIGINT);
int status;
@@ -1133,6 +1847,13 @@ namespace gsr {
return;
}
+ if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY);
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.replay_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
@@ -1149,7 +1870,9 @@ namespace gsr {
}
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.replay_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height);
if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height);
@@ -1169,16 +1892,16 @@ namespace gsr {
"-o", output_directory.c_str()
};
- add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
+ if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) {
+ args.push_back("-restart-replay-on-save");
+ args.push_back("yes");
+ }
- setenv("GSR_SHOW_SAVED_NOTIFICATION", config.replay_config.show_replay_saved_notifications ? "1" : "0", true);
- const std::string script_to_run_on_save = resources_path + (config.replay_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh");
- args.push_back("-sc");
- args.push_back(script_to_run_on_save.c_str());
+ add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1186,6 +1909,12 @@ namespace gsr {
update_ui_replay_started();
}
+ const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL);
+ fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK);
+ gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r");
+ if(gpu_screen_recorder_process_output_file)
+ gpu_screen_recorder_process_output_fd = -1;
+
// TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video.
// Make clear to the user that the recording starts after the notification is gone.
// Maybe have the option in notification to show timer until its getting hidden, then the notification can say:
@@ -1223,17 +1952,29 @@ namespace gsr {
if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
perror("waitpid failed");
/* Ignore... */
+ } else {
+ int exit_code = -1;
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
+ on_stop_recording(exit_code);
}
- // window->set_visible(false);
- // window->close();
- // return;
- //exit(0);
+
gpu_screen_recorder_process = -1;
recording_status = RecordingStatus::NONE;
update_ui_recording_stopped();
+ record_filepath.clear();
return;
}
+ if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD);
+ return;
+ }
+
+ record_filepath.clear();
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.record_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
@@ -1249,7 +1990,9 @@ namespace gsr {
}
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.record_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height);
if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height);
@@ -1270,14 +2013,10 @@ namespace gsr {
add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
- setenv("GSR_SHOW_SAVED_NOTIFICATION", config.record_config.show_video_saved_notifications ? "1" : "0", true);
- const std::string script_to_run_on_save = resources_path + (config.record_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh");
- args.push_back("-sc");
- args.push_back(script_to_run_on_save.c_str());
-
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ record_filepath = output_file;
+ gpu_screen_recorder_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1363,6 +2102,13 @@ namespace gsr {
return;
}
+ if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM);
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.streaming_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate);
@@ -1383,7 +2129,9 @@ namespace gsr {
const std::string url = streaming_get_url(config);
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.record_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height);
if(config.record_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height);
@@ -1397,16 +2145,16 @@ namespace gsr {
"-fm", framerate_mode.c_str(),
"-encoder", encoder,
"-f", fps.c_str(),
- "-f", fps.c_str(),
"-v", "no",
"-o", url.c_str()
};
+ config.streaming_config.record_options.merge_audio_tracks = true;
add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ gpu_screen_recorder_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1427,7 +2175,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();
@@ -1440,15 +2188,17 @@ namespace gsr {
return false;
bool window_texture_loaded = false;
- const Window window_at_cursor_position = get_window_at_cursor_position(display);
- if(is_window_fullscreen_on_monitor(display, window_at_cursor_position, monitor) && window_at_cursor_position)
- window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), window_at_cursor_position, egl_funcs) == 0;
+ Window focused_window = get_focused_window(display, WindowCaptureType::CURSOR);
+ if(!focused_window)
+ focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
+ if(focused_window && is_window_fullscreen_on_monitor(display, focused_window, monitor))
+ window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), focused_window, egl_funcs) == 0;
if(window_texture_loaded && window_texture.texture_id) {
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");
@@ -1474,4 +2224,4 @@ namespace gsr {
XFlush(display);
}
}
-} \ No newline at end of file
+}