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