From ab8ff54f4c3e564595afefbcbdd6c87e2c8188f5 Mon Sep 17 00:00:00 2001 From: Zsolt Donca Date: Wed, 23 Dec 2020 20:47:10 +0200 Subject: [PATCH] Control how many frames are captured per second The goal is to control the rate of capture while in screencast, as it can represent a performance issue and can cause input lag and the feeling of having a laggy mouse. This commit addresses the issue reported in #66. The code measures the time elapsed to make a single screen capture, and calculates how much to wait for the next capture to achieve the targeted frame rate. To delay the capturing of the next frame, the code introduces timers into the event loop based on the event loop in https://github.com/emersion/mako Added a command-line argument and an entry in the config file as well for the max FPS. The default value is 0, meaning no rate control. Added code to measure the average FPS every 5 seconds and print it with DEBUG level. --- .builds/freebsd.yml | 1 + contrib/config.sample | 3 +- include/config.h | 1 + include/fps_limit.h | 18 +++++++++ include/screencast_common.h | 7 +++- include/timespec_util.h | 18 +++++++++ include/xdpw.h | 18 +++++++++ meson.build | 10 +++++ src/core/config.c | 9 +++++ src/core/main.c | 47 ++++++++++++++++++++-- src/core/timer.c | 69 ++++++++++++++++++++++++++++++++ src/core/timespec_util.c | 33 ++++++++++++++++ src/screencast/fps_limit.c | 70 +++++++++++++++++++++++++++++++++ src/screencast/screencast.c | 8 ++-- src/screencast/wlr_screencast.c | 20 +++++++--- 15 files changed, 317 insertions(+), 15 deletions(-) create mode 100644 include/fps_limit.h create mode 100644 include/timespec_util.h create mode 100644 src/core/timer.c create mode 100644 src/core/timespec_util.c create mode 100644 src/screencast/fps_limit.c diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 0eda101..a1476d9 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,6 +1,7 @@ image: freebsd/latest packages: - basu + - libepoll-shim - meson - pipewire - pkgconf diff --git a/contrib/config.sample b/contrib/config.sample index 5daea46..57c8021 100644 --- a/contrib/config.sample +++ b/contrib/config.sample @@ -1,2 +1,3 @@ [screencast] -output= +output_name= +max_fps=30 diff --git a/include/config.h b/include/config.h index 9260a70..e8785d9 100644 --- a/include/config.h +++ b/include/config.h @@ -5,6 +5,7 @@ struct config_screencast { char *output_name; + double max_fps; }; struct xdpw_config { diff --git a/include/fps_limit.h b/include/fps_limit.h new file mode 100644 index 0000000..9312745 --- /dev/null +++ b/include/fps_limit.h @@ -0,0 +1,18 @@ +#ifndef FPS_LIMIT_H +#define FPS_LIMIT_H + +#include +#include + +struct fps_limit_state { + struct timespec frame_last_time; + + struct timespec fps_last_time; + uint64_t fps_frame_count; +}; + +void fps_limit_measure_start(struct fps_limit_state *state, double max_fps); + +uint64_t fps_limit_measure_end(struct fps_limit_state *state, double max_fps); + +#endif diff --git a/include/screencast_common.h b/include/screencast_common.h index 4aea195..4200800 100644 --- a/include/screencast_common.h +++ b/include/screencast_common.h @@ -5,6 +5,8 @@ #include #include +#include "fps_limit.h" + // this seems to be right based on // https://github.com/flatpak/xdg-desktop-portal/blob/309a1fc0cf2fb32cceb91dbc666d20cf0a3202c2/src/screen-cast.c#L955 #define XDP_CAST_PROTO_VER 2 @@ -54,7 +56,7 @@ struct xdpw_screencast_context { struct wl_list output_list; struct wl_registry *registry; struct zwlr_screencopy_manager_v1 *screencopy_manager; - struct zxdg_output_manager_v1* xdg_output_manager; + struct zxdg_output_manager_v1 *xdg_output_manager; struct wl_shm *shm; // sessions @@ -88,6 +90,9 @@ struct xdpw_screencast_instance { bool with_cursor; int err; bool quit; + + // fps limit + struct fps_limit_state fps_limit; }; struct xdpw_wlr_output { diff --git a/include/timespec_util.h b/include/timespec_util.h new file mode 100644 index 0000000..2ab27b8 --- /dev/null +++ b/include/timespec_util.h @@ -0,0 +1,18 @@ +#ifndef TIMESPEC_UTIL_H +#define TIMESPEC_UTIL_H + +#include +#include +#include + +#define TIMESPEC_NSEC_PER_SEC 1000000000L + +void timespec_add(struct timespec *t, int64_t delta_ns); + +bool timespec_less(struct timespec *t1, struct timespec *t2); + +bool timespec_is_zero(struct timespec *t); + +int64_t timespec_diff_ns(struct timespec *t1, struct timespec *t2); + +#endif diff --git a/include/xdpw.h b/include/xdpw.h index eeb81e7..105877d 100644 --- a/include/xdpw.h +++ b/include/xdpw.h @@ -23,6 +23,9 @@ struct xdpw_state { uint32_t screencast_cursor_modes; // bitfield of enum cursor_modes uint32_t screencast_version; struct xdpw_config *config; + int timer_poll_fd; + struct wl_list timers; + struct xdpw_timer *next_timer; }; struct xdpw_request { @@ -36,6 +39,16 @@ struct xdpw_session { struct xdpw_screencast_instance *screencast_instance; }; +typedef void (*xdpw_event_loop_timer_func_t)(void *data); + +struct xdpw_timer { + struct xdpw_state *state; + xdpw_event_loop_timer_func_t func; + void *user_data; + struct timespec at; + struct wl_list link; // xdpw_state::timers +}; + enum { PORTAL_RESPONSE_SUCCESS = 0, PORTAL_RESPONSE_CANCELLED = 1, @@ -51,4 +64,9 @@ void xdpw_request_destroy(struct xdpw_request *req); struct xdpw_session *xdpw_session_create(struct xdpw_state *state, sd_bus *bus, char *object_path); void xdpw_session_destroy(struct xdpw_session *req); +struct xdpw_timer *xdpw_add_timer(struct xdpw_state *state, + uint64_t delay_ns, xdpw_event_loop_timer_func_t func, void *data); + +void xdpw_destroy_timer(struct xdpw_timer *timer); + #endif diff --git a/meson.build b/meson.build index 125505e..a53e163 100644 --- a/meson.build +++ b/meson.build @@ -28,6 +28,12 @@ wayland_client = dependency('wayland-client') wayland_protos = dependency('wayland-protocols', version: '>=1.14') iniparser = cc.find_library('iniparser', dirs: [join_paths(get_option('prefix'),get_option('libdir'))]) +epoll = dependency('', required: false) +if (not cc.has_function('timerfd_create', prefix: '#include ') or + not cc.has_function('signalfd', prefix: '#include ')) + epoll = dependency('epoll-shim') +endif + if get_option('sd-bus-provider') == 'auto' assert(get_option('auto_features').auto(), 'sd-bus-provider must not be set to auto since auto_features != auto') sdbus = dependency('libsystemd', @@ -63,11 +69,14 @@ executable( 'src/core/config.c', 'src/core/request.c', 'src/core/session.c', + 'src/core/timer.c', + 'src/core/timespec_util.c', 'src/screenshot/screenshot.c', 'src/screencast/screencast.c', 'src/screencast/screencast_common.c', 'src/screencast/wlr_screencast.c', 'src/screencast/pipewire_screencast.c', + 'src/screencast/fps_limit.c' ]), dependencies: [ wayland_client, @@ -76,6 +85,7 @@ executable( pipewire, rt, iniparser, + epoll, ], include_directories: [inc], install: true, diff --git a/src/core/config.c b/src/core/config.c index 01dde46..6639dc1 100644 --- a/src/core/config.c +++ b/src/core/config.c @@ -39,6 +39,14 @@ static void getstring_from_conffile(dictionary *d, } } +static void getdouble_from_conffile(dictionary *d, + const char *key, double *dest, double fallback) { + if (*dest != 0) { + return; + } + *dest = iniparser_getdouble(d, key, fallback); +} + static bool file_exists(const char *path) { return path && access(path, R_OK) != -1; } @@ -70,6 +78,7 @@ static void config_parse_file(const char *configfile, struct xdpw_config *config // screencast getstring_from_conffile(d, "screencast:output_name", &config->screencast_conf.output_name, NULL); + getdouble_from_conffile(d, "screencast:max_fps", &config->screencast_conf.max_fps, 0); iniparser_freedict(d); logprint(DEBUG, "config: config file parsed"); diff --git a/src/core/main.c b/src/core/main.c index 3ec8e1a..e1173d0 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -1,10 +1,13 @@ #include #include #include +#include #include #include #include #include +#include + #include "xdpw.h" #include "logger.h" @@ -12,12 +15,13 @@ enum event_loop_fd { EVENT_LOOP_DBUS, EVENT_LOOP_WAYLAND, EVENT_LOOP_PIPEWIRE, + EVENT_LOOP_TIMER, }; static const char service_name[] = "org.freedesktop.impl.portal.desktop.wlr"; -static int xdpw_usage(FILE* stream, int rc) { - static const char* usage = +static int xdpw_usage(FILE *stream, int rc) { + static const char *usage = "Usage: xdg-desktop-portal-wlr [options]\n" "\n" " -l, --loglevel= Select log level (default is ERROR).\n" @@ -27,6 +31,7 @@ static int xdpw_usage(FILE* stream, int rc) { " -c, --config= Select config file.\n" " (default is $XDG_CONFIG_HOME/xdg-desktop-portal-wlr/config)\n" " -r, --replace Replace a running instance.\n" + " -f, --max-fps= Set the FPS limit (default 0, no limit).\n" " -h, --help Get help (this text).\n" "\n"; @@ -46,11 +51,12 @@ int main(int argc, char *argv[]) { enum LOGLEVEL loglevel = DEFAULT_LOGLEVEL; bool replace = false; - static const char* shortopts = "l:o:c:rh"; + static const char *shortopts = "l:o:c:f:rh"; static const struct option longopts[] = { { "loglevel", required_argument, NULL, 'l' }, { "output", required_argument, NULL, 'o' }, { "config", required_argument, NULL, 'c' }, + { "max-fps", required_argument, NULL, 'f' }, { "replace", no_argument, NULL, 'r' }, { "help", no_argument, NULL, 'h' }, { NULL, 0, NULL, 0 } @@ -58,8 +64,9 @@ int main(int argc, char *argv[]) { while (1) { int c = getopt_long(argc, argv, shortopts, longopts, NULL); - if (c < 0) + if (c < 0) { break; + } switch (c) { case 'l': @@ -74,6 +81,9 @@ int main(int argc, char *argv[]) { case 'r': replace = true; break; + case 'f': + config.screencast_conf.max_fps = atof(optarg); + break; case 'h': return xdpw_usage(stdout, EXIT_SUCCESS); default: @@ -166,6 +176,8 @@ int main(int argc, char *argv[]) { goto error; } + wl_list_init(&state.timers); + struct pollfd pollfds[] = { [EVENT_LOOP_DBUS] = { .fd = sd_bus_get_fd(state.bus), @@ -179,8 +191,14 @@ int main(int argc, char *argv[]) { .fd = pw_loop_get_fd(state.pw_loop), .events = POLLIN, }, + [EVENT_LOOP_TIMER] = { + .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC), + .events = POLLIN, + } }; + state.timer_poll_fd = pollfds[EVENT_LOOP_TIMER].fd; + while (1) { ret = poll(pollfds, sizeof(pollfds) / sizeof(pollfds[0]), -1); if (ret < 0) { @@ -217,6 +235,27 @@ int main(int argc, char *argv[]) { } } + if (pollfds[EVENT_LOOP_TIMER].revents & POLLIN) { + logprint(TRACE, "event-loop: got a timer event"); + + int timer_fd = pollfds[EVENT_LOOP_TIMER].fd; + uint64_t expirations; + ssize_t n = read(timer_fd, &expirations, sizeof(expirations)); + if (n < 0) { + logprint(ERROR, "failed to read from timer FD\n"); + goto error; + } + + struct xdpw_timer *timer = state.next_timer; + if (timer != NULL) { + xdpw_event_loop_timer_func_t func = timer->func; + void *user_data = timer->user_data; + xdpw_destroy_timer(timer); + + func(user_data); + } + } + do { ret = wl_display_dispatch_pending(state.wl_display); wl_display_flush(state.wl_display); diff --git a/src/core/timer.c b/src/core/timer.c new file mode 100644 index 0000000..4d21d29 --- /dev/null +++ b/src/core/timer.c @@ -0,0 +1,69 @@ +#include +#include +#include + +#include "xdpw.h" +#include "logger.h" +#include "timespec_util.h" + +static void update_timer(struct xdpw_state *state) { + int timer_fd = state->timer_poll_fd; + if (timer_fd < 0) { + return; + } + + bool updated = false; + struct xdpw_timer *timer; + wl_list_for_each(timer, &state->timers, link) { + if (state->next_timer == NULL || + timespec_less(&timer->at, &state->next_timer->at)) { + state->next_timer = timer; + updated = true; + } + } + + if (updated) { + struct itimerspec delay = { .it_value = state->next_timer->at }; + errno = 0; + int ret = timerfd_settime(timer_fd, TFD_TIMER_ABSTIME, &delay, NULL); + if (ret < 0) { + fprintf(stderr, "failed to timerfd_settime(): %s\n", + strerror(errno)); + } + } +} + +struct xdpw_timer *xdpw_add_timer(struct xdpw_state *state, + uint64_t delay_ns, xdpw_event_loop_timer_func_t func, void *data) { + struct xdpw_timer *timer = calloc(1, sizeof(struct xdpw_timer)); + if (timer == NULL) { + logprint(ERROR, "Timer allocation failed"); + return NULL; + } + timer->state = state; + timer->func = func; + timer->user_data = data; + wl_list_insert(&state->timers, &timer->link); + + clock_gettime(CLOCK_MONOTONIC, &timer->at); + timespec_add(&timer->at, delay_ns); + + update_timer(state); + return timer; +} + +void xdpw_destroy_timer(struct xdpw_timer *timer) { + if (timer == NULL) { + return; + } + struct xdpw_state *state = timer->state; + + if (state->next_timer == timer) { + state->next_timer = NULL; + } + + wl_list_remove(&timer->link); + free(timer); + + update_timer(state); +} diff --git a/src/core/timespec_util.c b/src/core/timespec_util.c new file mode 100644 index 0000000..b00c428 --- /dev/null +++ b/src/core/timespec_util.c @@ -0,0 +1,33 @@ +#include "timespec_util.h" +#include + +void timespec_add(struct timespec *t, int64_t delta_ns) { + int delta_ns_low = delta_ns % TIMESPEC_NSEC_PER_SEC; + int delta_s_high = delta_ns / TIMESPEC_NSEC_PER_SEC; + + t->tv_sec += delta_s_high; + + t->tv_nsec += (long)delta_ns_low; + if (t->tv_nsec >= TIMESPEC_NSEC_PER_SEC) { + t->tv_nsec -= TIMESPEC_NSEC_PER_SEC; + ++t->tv_sec; + } +} + +bool timespec_less(struct timespec *t1, struct timespec *t2) { + if (t1->tv_sec != t2->tv_sec) { + return t1->tv_sec < t2->tv_sec; + } + return t1->tv_nsec < t2->tv_nsec; +} + +bool timespec_is_zero(struct timespec *t) { + return t->tv_sec == 0 && t->tv_nsec == 0; +} + +int64_t timespec_diff_ns(struct timespec *t1, struct timespec *t2) { + int64_t s = t1->tv_sec - t2->tv_sec; + int64_t ns = t1->tv_nsec - t2->tv_nsec; + + return s * TIMESPEC_NSEC_PER_SEC + ns; +} diff --git a/src/screencast/fps_limit.c b/src/screencast/fps_limit.c new file mode 100644 index 0000000..f34c874 --- /dev/null +++ b/src/screencast/fps_limit.c @@ -0,0 +1,70 @@ +#include "fps_limit.h" +#include "logger.h" +#include "timespec_util.h" +#include +#include +#include +#include + +#define FPS_MEASURE_PERIOD_SEC 5.0 + +void measure_fps(struct fps_limit_state *state, struct timespec *now); + +void fps_limit_measure_start(struct fps_limit_state *state, double max_fps) { + if (max_fps <= 0.0) { + return; + } + + clock_gettime(CLOCK_MONOTONIC, &state->frame_last_time); +} + +uint64_t fps_limit_measure_end(struct fps_limit_state *state, double max_fps) { + if (max_fps <= 0.0) { + return 0; + } + + // `fps_limit_measure_start` was not called? + assert(!timespec_is_zero(&state->frame_last_time)); + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + int64_t elapsed_ns = timespec_diff_ns(&now, &state->frame_last_time); + + measure_fps(state, &now); + + int64_t target_ns = (1.0 / max_fps) * TIMESPEC_NSEC_PER_SEC; + int64_t delay_ns = target_ns - elapsed_ns; + if (delay_ns > 0) { + logprint(TRACE, "fps_limit: elapsed time since the last measurement: %u, " + "target %u, should delay for %u (ns)", elapsed_ns, target_ns, delay_ns); + return delay_ns; + } else { + logprint(TRACE, "fps_limit: elapsed time since the last measurement: %u, " + "target %u, target not met (ns)", elapsed_ns, target_ns); + return 0; + } +} + +void measure_fps(struct fps_limit_state *state, struct timespec *now) { + if (timespec_is_zero(&state->fps_last_time)) { + state->fps_last_time = *now; + return; + } + + state->fps_frame_count++; + + int64_t elapsed_ns = timespec_diff_ns(now, &state->fps_last_time); + + double elapsed_sec = (double) elapsed_ns / (double) TIMESPEC_NSEC_PER_SEC; + if (elapsed_sec < FPS_MEASURE_PERIOD_SEC) { + return; + } + + double avg_frames_per_sec = state->fps_frame_count / elapsed_sec; + + logprint(DEBUG, "fps_limit: average FPS in the last %0.2f seconds: %0.2f", + elapsed_sec, avg_frames_per_sec); + + state->fps_last_time = *now; + state->fps_frame_count = 0; +} diff --git a/src/screencast/screencast.c b/src/screencast/screencast.c index e4be866..a1acedf 100644 --- a/src/screencast/screencast.c +++ b/src/screencast/screencast.c @@ -134,7 +134,7 @@ static int method_screencast_create_session(sd_bus_message *msg, void *data, logprint(INFO, "dbus: session_handle: %s", session_handle); logprint(INFO, "dbus: app_id: %s", app_id); - char* key; + char *key; int innerRet = 0; while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { innerRet = sd_bus_message_read(msg, "s", &key); @@ -143,7 +143,7 @@ static int method_screencast_create_session(sd_bus_message *msg, void *data, } if (strcmp(key, "session_handle_token") == 0) { - char* token; + char *token; sd_bus_message_read(msg, "v", "s", &token); logprint(INFO, "dbus: option token: %s", token); } else { @@ -223,7 +223,7 @@ static int method_screencast_select_sources(sd_bus_message *msg, void *data, logprint(INFO, "dbus: session_handle: %s", session_handle); logprint(INFO, "dbus: app_id: %s", app_id); - char* key; + char *key; int innerRet = 0; while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { innerRet = sd_bus_message_read(msg, "s", &key); @@ -345,7 +345,7 @@ static int method_screencast_start(sd_bus_message *msg, void *data, logprint(INFO, "dbus: app_id: %s", app_id); logprint(INFO, "dbus: parent_window: %s", parent_window); - char* key; + char *key; int innerRet = 0; while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { innerRet = sd_bus_message_read(msg, "s", &key); diff --git a/src/screencast/wlr_screencast.c b/src/screencast/wlr_screencast.c index dcb35c6..29f89a5 100644 --- a/src/screencast/wlr_screencast.c +++ b/src/screencast/wlr_screencast.c @@ -18,6 +18,7 @@ #include "pipewire_screencast.h" #include "xdpw.h" #include "logger.h" +#include "fps_limit.h" static void wlr_frame_buffer_destroy(struct xdpw_screencast_instance *cast) { // Even though this check may be deemed unnecessary, @@ -51,7 +52,13 @@ void xdpw_wlr_frame_free(struct xdpw_screencast_instance *cast) { return ; } - xdpw_wlr_register_cb(cast); + uint64_t delay_ns = fps_limit_measure_end(&cast->fps_limit, cast->ctx->state->config->screencast_conf.max_fps); + if (delay_ns > 0) { + xdpw_add_timer(cast->ctx->state, delay_ns, + (xdpw_event_loop_timer_func_t) xdpw_wlr_register_cb, cast); + } else { + xdpw_wlr_register_cb(cast); + } } static int anonymous_shm_open(void) { @@ -133,8 +140,11 @@ static void wlr_frame_buffer_done(void *data, struct xdpw_screencast_instance *cast = data; logprint(TRACE, "wlroots: buffer_done event handler"); + zwlr_screencopy_frame_v1_copy_with_damage(frame, cast->simple_frame.buffer); logprint(TRACE, "wlroots: frame copied"); + + fps_limit_measure_start(&cast->fps_limit, cast->ctx->state->config->screencast_conf.max_fps); } static void wlr_frame_buffer(void *data, struct zwlr_screencopy_frame_v1 *frame, @@ -267,8 +277,8 @@ static const struct wl_output_listener wlr_output_listener = { .scale = wlr_output_handle_scale, }; -static void wlr_xdg_output_name(void* data, struct zxdg_output_v1* xdg_output, - const char* name) { +static void wlr_xdg_output_name(void *data, struct zxdg_output_v1 *xdg_output, + const char *name) { struct xdpw_wlr_output *output = data; output->name = strdup(name); @@ -287,7 +297,7 @@ static const struct zxdg_output_v1_listener wlr_xdg_output_listener = { }; static void wlr_add_xdg_output_listener(struct xdpw_wlr_output *output, - struct zxdg_output_v1* xdg_output) { + struct zxdg_output_v1 *xdg_output) { output->xdg_output = xdg_output; zxdg_output_v1_add_listener(output->xdg_output, &wlr_xdg_output_listener, output); @@ -312,7 +322,7 @@ struct xdpw_wlr_output *xdpw_wlr_output_first(struct wl_list *output_list) { } struct xdpw_wlr_output *xdpw_wlr_output_find_by_name(struct wl_list *output_list, - const char* name) { + const char *name) { struct xdpw_wlr_output *output, *tmp; wl_list_for_each_safe(output, tmp, output_list, link) { if (strcmp(output->name, name) == 0) {