From 022df2542baa057b1965a7c7ee9c32e738f637d2 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 25 Sep 2019 13:58:27 +0300 Subject: [PATCH] output: add max_render_time --- include/sway/commands.h | 1 + include/sway/config.h | 1 + include/sway/output.h | 7 ++ sway/commands/output.c | 1 + sway/commands/output/max_render_time.c | 28 ++++++++ sway/config/output.c | 33 +++++++--- sway/desktop/output.c | 91 ++++++++++++++++++++++---- sway/meson.build | 1 + sway/sway-output.5.scd | 24 +++++++ sway/tree/output.c | 1 + 10 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 sway/commands/output/max_render_time.c diff --git a/include/sway/commands.h b/include/sway/commands.h index d994ae9bc..5d468a421 100644 --- a/include/sway/commands.h +++ b/include/sway/commands.h @@ -271,6 +271,7 @@ sway_cmd output_cmd_background; sway_cmd output_cmd_disable; sway_cmd output_cmd_dpms; sway_cmd output_cmd_enable; +sway_cmd output_cmd_max_render_time; sway_cmd output_cmd_mode; sway_cmd output_cmd_position; sway_cmd output_cmd_scale; diff --git a/include/sway/config.h b/include/sway/config.h index 6892c9464..457e0a984 100644 --- a/include/sway/config.h +++ b/include/sway/config.h @@ -212,6 +212,7 @@ struct output_config { float scale; int32_t transform; enum wl_output_subpixel subpixel; + int max_render_time; // In milliseconds char *background; char *background_option; diff --git a/include/sway/output.h b/include/sway/output.h index 7d7057e10..741f5b5e7 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -51,6 +51,11 @@ struct sway_output { struct { struct wl_signal destroy; } events; + + struct timespec last_presentation; + uint32_t refresh_nsec; + int max_render_time; // In milliseconds + struct wl_event_source *repaint_timer; }; struct sway_output *output_create(struct wlr_output *wlr_output); @@ -71,6 +76,8 @@ typedef void (*sway_surface_iterator_func_t)(struct sway_output *output, struct wlr_surface *surface, struct wlr_box *box, float rotation, void *user_data); +int output_repaint_timer_handler(void *data); + void output_damage_whole(struct sway_output *output); void output_damage_surface(struct sway_output *output, double ox, double oy, diff --git a/sway/commands/output.c b/sway/commands/output.c index 59bc598c8..db2acb50e 100644 --- a/sway/commands/output.c +++ b/sway/commands/output.c @@ -12,6 +12,7 @@ static struct cmd_handler output_handlers[] = { { "disable", output_cmd_disable }, { "dpms", output_cmd_dpms }, { "enable", output_cmd_enable }, + { "max_render_time", output_cmd_max_render_time }, { "mode", output_cmd_mode }, { "pos", output_cmd_position }, { "position", output_cmd_position }, diff --git a/sway/commands/output/max_render_time.c b/sway/commands/output/max_render_time.c new file mode 100644 index 000000000..2d3cebe30 --- /dev/null +++ b/sway/commands/output/max_render_time.c @@ -0,0 +1,28 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_max_render_time(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing max render time argument."); + } + + int max_render_time; + if (!strcmp(*argv, "off")) { + max_render_time = 0; + } else { + char *end; + max_render_time = strtol(*argv, &end, 10); + if (*end || max_render_time <= 0) { + return cmd_results_new(CMD_INVALID, "Invalid max render time."); + } + } + config->handler_context.output_config->max_render_time = max_render_time; + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/config/output.c b/sway/config/output.c index 25956face..1d5f81da0 100644 --- a/sway/config/output.c +++ b/sway/config/output.c @@ -47,6 +47,7 @@ struct output_config *new_output_config(const char *name) { oc->scale = -1; oc->transform = -1; oc->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + oc->max_render_time = -1; return oc; } @@ -81,6 +82,9 @@ void merge_output_config(struct output_config *dst, struct output_config *src) { if (src->transform != -1) { dst->transform = src->transform; } + if (src->max_render_time != -1) { + dst->max_render_time = src->max_render_time; + } if (src->background) { free(dst->background); dst->background = strdup(src->background); @@ -153,11 +157,12 @@ static void merge_id_on_name(struct output_config *oc) { list_add(config->output_configs, ion_oc); sway_log(SWAY_DEBUG, "Generated id on name output config \"%s\"" " (enabled: %d) (%dx%d@%fHz position %d,%d scale %f " - "transform %d) (bg %s %s) (dpms %d)", ion_oc->name, - ion_oc->enabled, ion_oc->width, ion_oc->height, + "transform %d) (bg %s %s) (dpms %d) (max render time: %d)", + ion_oc->name, ion_oc->enabled, ion_oc->width, ion_oc->height, ion_oc->refresh_rate, ion_oc->x, ion_oc->y, ion_oc->scale, ion_oc->transform, ion_oc->background, - ion_oc->background_option, ion_oc->dpms_state); + ion_oc->background_option, ion_oc->dpms_state, + ion_oc->max_render_time); } } free(id_on_name); @@ -197,10 +202,12 @@ struct output_config *store_output_config(struct output_config *oc) { } sway_log(SWAY_DEBUG, "Config stored for output %s (enabled: %d) (%dx%d@%fHz " - "position %d,%d scale %f subpixel %s transform %d) (bg %s %s) (dpms %d)", + "position %d,%d scale %f subpixel %s transform %d) (bg %s %s) (dpms %d) " + "(max render time: %d)", oc->name, oc->enabled, oc->width, oc->height, oc->refresh_rate, oc->x, oc->y, oc->scale, sway_wl_output_subpixel_to_string(oc->subpixel), - oc->transform, oc->background, oc->background_option, oc->dpms_state); + oc->transform, oc->background, oc->background_option, oc->dpms_state, + oc->max_render_time); return oc; } @@ -325,6 +332,12 @@ bool apply_output_config(struct output_config *oc, struct sway_output *output) { wlr_output_enable(wlr_output, false); } + if (oc && oc->max_render_time >= 0) { + sway_log(SWAY_DEBUG, "Set %s max render time to %d", + oc->name, oc->max_render_time); + output->max_render_time = oc->max_render_time; + } + return true; } @@ -343,6 +356,7 @@ static void default_output_config(struct output_config *oc, oc->subpixel = output->detected_subpixel; oc->transform = WL_OUTPUT_TRANSFORM_NORMAL; oc->dpms_state = DPMS_ON; + oc->max_render_time = 0; } static struct output_config *get_output_config(char *identifier, @@ -396,10 +410,11 @@ static struct output_config *get_output_config(char *identifier, sway_log(SWAY_DEBUG, "Generated output config \"%s\" (enabled: %d)" " (%dx%d@%fHz position %d,%d scale %f transform %d) (bg %s %s)" - " (dpms %d)", result->name, result->enabled, result->width, - result->height, result->refresh_rate, result->x, result->y, - result->scale, result->transform, result->background, - result->background_option, result->dpms_state); + " (dpms %d) (max render time: %d)", result->name, result->enabled, + result->width, result->height, result->refresh_rate, + result->x, result->y, result->scale, result->transform, + result->background, result->background_option, result->dpms_state, + result->max_render_time); } else if (oc_name) { // No identifier config, just return a copy of the name config free(result->name); diff --git a/sway/desktop/output.c b/sway/desktop/output.c index 05b5cd44f..0f715c19d 100644 --- a/sway/desktop/output.c +++ b/sway/desktop/output.c @@ -480,19 +480,13 @@ static bool scan_out_fullscreen_view(struct sway_output *output, return wlr_output_commit(wlr_output); } -static void damage_handle_frame(struct wl_listener *listener, void *data) { - struct sway_output *output = - wl_container_of(listener, output, damage_frame); - if (!output->enabled || !output->wlr_output->enabled) { - return; - } - - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); +int output_repaint_timer_handler(void *data) { + struct sway_output *output = data; + output->wlr_output->block_idle_frame = false; struct sway_workspace *workspace = output->current.active_workspace; if (workspace == NULL) { - return; + return 0; } struct sway_container *fullscreen_con = root->fullscreen_global; @@ -515,7 +509,7 @@ static void damage_handle_frame(struct wl_listener *listener, void *data) { last_scanned_out = scanned_out; if (scanned_out) { - goto frame_done; + return 0; } } @@ -524,17 +518,82 @@ static void damage_handle_frame(struct wl_listener *listener, void *data) { pixman_region32_init(&damage); if (!wlr_output_damage_attach_render(output->damage, &needs_frame, &damage)) { - return; + return 0; } if (needs_frame) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + output_render(output, &now, &damage); } pixman_region32_fini(&damage); -frame_done: + return 0; +} + +static void damage_handle_frame(struct wl_listener *listener, void *data) { + struct sway_output *output = + wl_container_of(listener, output, damage_frame); + if (!output->enabled || !output->wlr_output->enabled) { + return; + } + + // Compute predicted milliseconds until the next refresh. It's used for + // delaying both output rendering and surface frame callbacks. + int msec_until_refresh = 0; + + if (output->max_render_time != 0) { + struct timespec now; + clockid_t presentation_clock + = wlr_backend_get_presentation_clock(server.backend); + clock_gettime(presentation_clock, &now); + + const long NSEC_IN_SECONDS = 1000000000; + struct timespec predicted_refresh = output->last_presentation; + predicted_refresh.tv_nsec += output->refresh_nsec % NSEC_IN_SECONDS; + predicted_refresh.tv_sec += output->refresh_nsec / NSEC_IN_SECONDS; + if (predicted_refresh.tv_nsec >= NSEC_IN_SECONDS) { + predicted_refresh.tv_sec += 1; + predicted_refresh.tv_nsec -= NSEC_IN_SECONDS; + } + + // If the predicted refresh time is before the current time then + // there's no point in delaying. + // + // We only check tv_sec because if the predicted refresh time is less + // than a second before the current time, then msec_until_refresh will + // end up slightly below zero, which will effectively disable the delay + // without potential disasterous negative overflows that could occur if + // tv_sec was not checked. + if (predicted_refresh.tv_sec >= now.tv_sec) { + long nsec_until_refresh + = (predicted_refresh.tv_sec - now.tv_sec) * NSEC_IN_SECONDS + + (predicted_refresh.tv_nsec - now.tv_nsec); + + // We want msec_until_refresh to be conservative, that is, floored. + // If we have 7.9 msec until refresh, we better compute the delay + // as if we had only 7 msec, so that we don't accidentally delay + // more than necessary and miss a frame. + msec_until_refresh = nsec_until_refresh / 1000000; + } + } + + int delay = msec_until_refresh - output->max_render_time; + + // If the delay is less than 1 millisecond (which is the least we can wait) + // then just render right away. + if (delay < 1) { + output_repaint_timer_handler(output); + } else { + output->wlr_output->block_idle_frame = true; + wl_event_source_timer_update(output->repaint_timer, delay); + } + // Send frame done to all visible surfaces + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); send_frame_done(output, &now); } @@ -768,6 +827,9 @@ static void handle_present(struct wl_listener *listener, void *data) { return; } + output->last_presentation = *output_event->when; + output->refresh_nsec = output_event->refresh; + struct wlr_presentation_event event = { .output = output->wlr_output, .tv_sec = (uint64_t)output_event->when->tv_sec, @@ -806,6 +868,9 @@ void handle_new_output(struct wl_listener *listener, void *data) { wl_signal_add(&output->damage->events.destroy, &output->damage_destroy); output->damage_destroy.notify = damage_handle_destroy; + output->repaint_timer = wl_event_loop_add_timer(server->wl_event_loop, + output_repaint_timer_handler, output); + struct output_config *oc = find_output_config(output); if (!oc || oc->enabled) { output_enable(output, oc); diff --git a/sway/meson.build b/sway/meson.build index 8fcf841c3..76a31d739 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -175,6 +175,7 @@ sway_sources = files( 'commands/output/disable.c', 'commands/output/dpms.c', 'commands/output/enable.c', + 'commands/output/max_render_time.c', 'commands/output/mode.c', 'commands/output/position.c', 'commands/output/scale.c', diff --git a/sway/sway-output.5.scd b/sway/sway-output.5.scd index ae6ced241..01496f194 100644 --- a/sway/sway-output.5.scd +++ b/sway/sway-output.5.scd @@ -107,6 +107,30 @@ must be separated by one space. For example: Enables or disables the specified output via DPMS. To turn an output off (ie. blank the screen but keep workspaces as-is), one can set DPMS to off. +*output* max_render_time off| + When set to a positive number of milliseconds, enables delaying output + rendering to reduce latency. The rendering is delayed in such a way as + to leave the specified number of milliseconds before the next + presentation for rendering. + + The output rendering normally takes place immediately after a + presentation (vblank, buffer flip, etc.) and the frame callbacks are + sent to surfaces immediately after the rendering to give surfaces the + most time to draw their next frame. This results in slightly below 2 + frames of latency between the surface rendering and committing new + contents, and the contents being shown on screen, on average. When the + output rendering is delayed, the frame callbacks are sent immediately + after presentation, and the surfaces have a small timespan (1 / + (refresh rate) - max_render_time) to render and commit new contents to + be shown on the next presentation, resulting in below 1 frame of + latency. + + To set this up for optimal latency: + . Launch some _full-screen_ application that renders continuously, like + *glxgears*. + . Start with *max_render_time 1*. Increment by *1* if you see frame + drops. + # SEE ALSO *sway*(5) *sway-input*(5) diff --git a/sway/tree/output.c b/sway/tree/output.c index 07f04ee55..c4ec6eec6 100644 --- a/sway/tree/output.c +++ b/sway/tree/output.c @@ -242,6 +242,7 @@ void output_destroy(struct sway_output *output) { } list_free(output->workspaces); list_free(output->current.workspaces); + wl_event_source_remove(output->repaint_timer); free(output); }