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.
This commit is contained in:
Zsolt Donca 2020-12-23 20:47:10 +02:00 committed by Simon Ser
parent 323d89ead7
commit ab8ff54f4c
15 changed files with 317 additions and 15 deletions

View File

@ -1,6 +1,7 @@
image: freebsd/latest
packages:
- basu
- libepoll-shim
- meson
- pipewire
- pkgconf

View File

@ -1,2 +1,3 @@
[screencast]
output=
output_name=
max_fps=30

View File

@ -5,6 +5,7 @@
struct config_screencast {
char *output_name;
double max_fps;
};
struct xdpw_config {

18
include/fps_limit.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef FPS_LIMIT_H
#define FPS_LIMIT_H
#include <stdint.h>
#include <time.h>
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

View File

@ -5,6 +5,8 @@
#include <spa/param/video/format-utils.h>
#include <wayland-client-protocol.h>
#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 {

18
include/timespec_util.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef TIMESPEC_UTIL_H
#define TIMESPEC_UTIL_H
#include <time.h>
#include <stdbool.h>
#include <stdint.h>
#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

View File

@ -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

View File

@ -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 <sys/timerfd.h>') or
not cc.has_function('signalfd', prefix: '#include <sys/signalfd.h>'))
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,

View File

@ -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");

View File

@ -1,10 +1,13 @@
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/timerfd.h>
#include <getopt.h>
#include <poll.h>
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
#include <unistd.h>
#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=<loglevel> Select log level (default is ERROR).\n"
@ -27,6 +31,7 @@ static int xdpw_usage(FILE* stream, int rc) {
" -c, --config=<config file> 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=<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);

69
src/core/timer.c Normal file
View File

@ -0,0 +1,69 @@
#include <poll.h>
#include <wayland-util.h>
#include <sys/timerfd.h>
#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);
}

33
src/core/timespec_util.c Normal file
View File

@ -0,0 +1,33 @@
#include "timespec_util.h"
#include <time.h>
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;
}

View File

@ -0,0 +1,70 @@
#include "fps_limit.h"
#include "logger.h"
#include "timespec_util.h"
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#include <assert.h>
#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;
}

View File

@ -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);

View File

@ -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) {