Skip to content

Commit

Permalink
Control how many frames are captured per second
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zsolt-donca authored and emersion committed Mar 8, 2021
1 parent 323d89e commit ab8ff54
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 15 deletions.
1 change: 1 addition & 0 deletions .builds/freebsd.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
image: freebsd/latest
packages:
- basu
- libepoll-shim
- meson
- pipewire
- pkgconf
Expand Down
3 changes: 2 additions & 1 deletion contrib/config.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[screencast]
output=
output_name=
max_fps=30
1 change: 1 addition & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

struct config_screencast {
char *output_name;
double max_fps;
};

struct xdpw_config {
Expand Down
18 changes: 18 additions & 0 deletions include/fps_limit.h
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion include/screencast_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions include/timespec_util.h
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions include/xdpw.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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
10 changes: 10 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -76,6 +85,7 @@ executable(
pipewire,
rt,
iniparser,
epoll,
],
include_directories: [inc],
install: true,
Expand Down
9 changes: 9 additions & 0 deletions src/core/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
Expand Down
47 changes: 43 additions & 4 deletions src/core/main.c
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
#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"

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"
Expand All @@ -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";

Expand All @@ -46,20 +51,22 @@ 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 }
};

while (1) {
int c = getopt_long(argc, argv, shortopts, longopts, NULL);
if (c < 0)
if (c < 0) {
break;
}

switch (c) {
case 'l':
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
69 changes: 69 additions & 0 deletions src/core/timer.c
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit ab8ff54

Please sign in to comment.