mirror of
https://github.com/hyprwm/xdg-desktop-portal-hyprland.git
synced 2024-11-25 15:35:58 +01:00
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:
parent
323d89ead7
commit
ab8ff54f4c
15 changed files with 317 additions and 15 deletions
|
@ -1,6 +1,7 @@
|
|||
image: freebsd/latest
|
||||
packages:
|
||||
- basu
|
||||
- libepoll-shim
|
||||
- meson
|
||||
- pipewire
|
||||
- pkgconf
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
[screencast]
|
||||
output=
|
||||
output_name=
|
||||
max_fps=30
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
struct config_screencast {
|
||||
char *output_name;
|
||||
double max_fps;
|
||||
};
|
||||
|
||||
struct xdpw_config {
|
||||
|
|
18
include/fps_limit.h
Normal file
18
include/fps_limit.h
Normal 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
|
|
@ -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
|
||||
|
@ -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
18
include/timespec_util.h
Normal 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
|
|
@ -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
|
||||
|
|
10
meson.build
10
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 <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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,6 +15,7 @@ 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";
|
||||
|
@ -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
69
src/core/timer.c
Normal 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
33
src/core/timespec_util.c
Normal 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;
|
||||
}
|
70
src/screencast/fps_limit.c
Normal file
70
src/screencast/fps_limit.c
Normal 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;
|
||||
}
|
|
@ -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,8 +52,14 @@ void xdpw_wlr_frame_free(struct xdpw_screencast_instance *cast) {
|
|||
return ;
|
||||
}
|
||||
|
||||
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) {
|
||||
char name[] = "/xdpw-shm-XXXXXX";
|
||||
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue