From 4fa497f9d70f8ff3c1a06090e135205eb568b692 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sun, 5 Apr 2026 16:17:11 +0000 Subject: [PATCH 1/3] output: use wlr_frame_scheduler Update for: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5330 --- include/sway/output.h | 1 + sway/desktop/output.c | 10 ++-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/include/sway/output.h b/include/sway/output.h index ae2e50d36..bd48155b5 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "config.h" diff --git a/sway/desktop/output.c b/sway/desktop/output.c index 12dc9cc7a..0327498fb 100644 --- a/sway/desktop/output.c +++ b/sway/desktop/output.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -270,7 +271,6 @@ static bool output_can_tear(struct sway_output *output) { static int output_repaint_timer_handler(void *data) { struct sway_output *output = data; - output->wlr_output->frame_pending = false; if (!output->wlr_output->enabled) { return 0; } @@ -281,11 +281,6 @@ static int output_repaint_timer_handler(void *data) { .color_transform = output->color_transform, }; - struct wlr_scene_output *scene_output = output->scene_output; - if (!wlr_scene_output_needs_frame(scene_output)) { - return 0; - } - struct wlr_output_state pending; wlr_output_state_init(&pending); if (!wlr_scene_output_build_state(output->scene_output, &pending, &opts)) { @@ -362,7 +357,6 @@ static void handle_frame(struct wl_listener *listener, void *user_data) { if (delay < 1) { output_repaint_timer_handler(output); } else { - output->wlr_output->frame_pending = true; wl_event_source_timer_update(output->repaint_timer, delay); } @@ -585,7 +579,7 @@ void handle_new_output(struct wl_listener *listener, void *data) { output->destroy.notify = handle_destroy; wl_signal_add(&wlr_output->events.present, &output->present); output->present.notify = handle_present; - wl_signal_add(&wlr_output->events.frame, &output->frame); + wl_signal_add(&scene_output->frame_scheduler->events.frame, &output->frame); output->frame.notify = handle_frame; wl_signal_add(&wlr_output->events.request_state, &output->request_state); output->request_state.notify = handle_request_state; From 2ae7af184426fb28ac2eb635af9cee6644ed7dbe Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sun, 5 Apr 2026 16:24:50 +0000 Subject: [PATCH 2/3] output: implement max_render_time via wlr_frame_scheduler --- include/sway/output.h | 5 +- sway/config/output.c | 3 +- sway/desktop/output.c | 248 +++++++++++++++++++++++++++++++----------- 3 files changed, 186 insertions(+), 70 deletions(-) diff --git a/include/sway/output.h b/include/sway/output.h index bd48155b5..ea59c9cd3 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -66,10 +66,7 @@ struct sway_output { struct wlr_color_transform *color_transform; struct wlr_ext_workspace_group_handle_v1 *ext_workspace_group; - struct timespec last_presentation; - uint32_t refresh_nsec; int max_render_time; // In milliseconds - struct wl_event_source *repaint_timer; bool allow_tearing; bool hdr; @@ -117,6 +114,8 @@ void output_enable(struct sway_output *output); void output_disable(struct sway_output *output); +void output_set_max_render_time(struct sway_output *output, int max_render_time); + struct sway_workspace *output_get_active_workspace(struct sway_output *output); void output_for_each_workspace(struct sway_output *output, diff --git a/sway/config/output.c b/sway/config/output.c index 6d6afdc25..5983f2e5d 100644 --- a/sway/config/output.c +++ b/sway/config/output.c @@ -684,7 +684,8 @@ static bool finalize_output_config(struct output_config *oc, struct sway_output } output->color_transform = config_applied->color_transform; - output->max_render_time = oc && oc->max_render_time > 0 ? oc->max_render_time : 0; + output_set_max_render_time(output, + oc && oc->max_render_time > 0 ? oc->max_render_time : 0); output->allow_tearing = oc && oc->allow_tearing > 0; output->hdr = applied->image_description != NULL; diff --git a/sway/desktop/output.c b/sway/desktop/output.c index 0327498fb..2d25cac88 100644 --- a/sway/desktop/output.c +++ b/sway/desktop/output.c @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include #include @@ -268,13 +268,158 @@ static bool output_can_tear(struct sway_output *output) { return false; } -static int output_repaint_timer_handler(void *data) { - struct sway_output *output = data; +static int get_msec_until_refresh(const struct wlr_output_event_present *event) { + // 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->wlr_output->enabled) { - return 0; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + const long NSEC_IN_SECONDS = 1000000000; + struct timespec predicted_refresh = event->when; + predicted_refresh.tv_nsec += event->refresh % NSEC_IN_SECONDS; + predicted_refresh.tv_sec += event->refresh / 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 disastrous 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; + } + + return msec_until_refresh; +} + +struct sway_timer_frame_scheduler { + struct wlr_frame_scheduler base; + + int max_render_time; + + struct wl_event_source *idle; + struct wl_event_source *timer; + + bool frame_pending; + bool needs_frame; + + struct wl_listener present; +}; + +static void timed_frame_scheduler_destroy(struct wlr_frame_scheduler *wlr_scheduler) { + struct sway_timer_frame_scheduler *scheduler = + wl_container_of(wlr_scheduler, scheduler, base); + if (scheduler->idle != NULL) { + wl_event_source_remove(scheduler->idle); + } + wl_event_source_remove(scheduler->timer); + wl_list_remove(&scheduler->present.link); + free(scheduler); +} + +static void timed_frame_scheduler_trigger_frame( + struct sway_timer_frame_scheduler *scheduler) { + if (!scheduler->needs_frame) { + return; + } + scheduler->needs_frame = false; + wl_signal_emit_mutable(&scheduler->base.events.frame, NULL); +} + +static void timed_frame_scheduler_handle_idle(void *data) { + struct sway_timer_frame_scheduler *scheduler = data; + scheduler->idle = NULL; + timed_frame_scheduler_trigger_frame(scheduler); +} + +static void timed_frame_scheduler_schedule_frame(struct wlr_frame_scheduler *wlr_scheduler) { + struct sway_timer_frame_scheduler *scheduler = + wl_container_of(wlr_scheduler, scheduler, base); + scheduler->needs_frame = true; + if (scheduler->idle != NULL || scheduler->frame_pending) { + return; + } + scheduler->idle = wl_event_loop_add_idle(scheduler->base.output->event_loop, + timed_frame_scheduler_handle_idle, scheduler); +} + +static const struct wlr_frame_scheduler_impl timed_frame_scheduler_impl = { + .schedule_frame = timed_frame_scheduler_schedule_frame, + .destroy = timed_frame_scheduler_destroy, +}; + +static int timed_frame_scheduler_handle_timer(void *data) { + struct sway_timer_frame_scheduler *scheduler = data; + scheduler->frame_pending = false; + timed_frame_scheduler_trigger_frame(scheduler); + return 0; +} + +static void timed_frame_scheduler_handle_present(struct wl_listener *listener, void *data) { + struct sway_timer_frame_scheduler *scheduler = + wl_container_of(listener, scheduler, present); + const struct wlr_output_event_present *event = data; + + if (!event->presented) { + return; + } + + scheduler->frame_pending = false; + + if (scheduler->idle != NULL) { + wl_event_source_remove(scheduler->idle); + scheduler->idle = NULL; + } + + int msec_until_refresh = get_msec_until_refresh(event); + int delay = msec_until_refresh - scheduler->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) { + timed_frame_scheduler_trigger_frame(scheduler); + } else { + scheduler->frame_pending = true; + wl_event_source_timer_update(scheduler->timer, delay); + } +} + +static struct wlr_frame_scheduler *timed_frame_scheduler_create(struct wlr_output *output, + int max_render_time) { + struct sway_timer_frame_scheduler *scheduler = calloc(1, sizeof(*scheduler)); + if (scheduler == NULL) { + return NULL; + } + wlr_frame_scheduler_init(&scheduler->base,&timed_frame_scheduler_impl, output); + + scheduler->max_render_time = max_render_time; + + scheduler->timer = wl_event_loop_add_timer(server.wl_event_loop, + timed_frame_scheduler_handle_timer, scheduler); + + scheduler->present.notify = timed_frame_scheduler_handle_present; + wl_signal_add(&output->events.present, &scheduler->present); + + return &scheduler->base; +} + +static void output_redraw(struct sway_output *output) { output_configure_scene(output, &root->root_scene->tree.node, 1.0f); struct wlr_scene_output_state_options opts = { @@ -285,7 +430,7 @@ static int output_repaint_timer_handler(void *data) { wlr_output_state_init(&pending); if (!wlr_scene_output_build_state(output->scene_output, &pending, &opts)) { wlr_output_state_finish(&pending); - return 0; + return; } if (output_can_tear(output)) { @@ -302,7 +447,6 @@ static int output_repaint_timer_handler(void *data) { sway_log(SWAY_ERROR, "Page-flip failed on output %s", output->wlr_output->name); } wlr_output_state_finish(&pending); - return 0; } static void handle_frame(struct wl_listener *listener, void *user_data) { @@ -312,60 +456,32 @@ static void handle_frame(struct wl_listener *listener, void *user_data) { 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; + output_redraw(output); +} - if (output->max_render_time != 0) { - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); +static void output_set_frame_scheduler(struct sway_output *output, + struct wlr_frame_scheduler *scheduler) { + wl_list_remove(&output->frame.link); + wlr_scene_output_set_frame_scheduler(output->scene_output, scheduler); + output->frame.notify = handle_frame; + wl_signal_add(&output->scene_output->frame_scheduler->events.frame, &output->frame); +} - 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; - } +void output_set_max_render_time(struct sway_output *output, int max_render_time) { + output->max_render_time = max_render_time; - // 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 disastrous 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); + struct wlr_frame_scheduler *scheduler; + if (max_render_time != 0) { + scheduler = timed_frame_scheduler_create(output->wlr_output, max_render_time); } else { - wl_event_source_timer_update(output->repaint_timer, delay); + scheduler = wlr_frame_scheduler_autocreate(output->wlr_output); + } + if (scheduler == NULL) { + sway_log(SWAY_ERROR, "Failed to create output frame scheduler"); + return; } - // Send frame done to all visible surfaces - struct send_frame_done_data data = {0}; - clock_gettime(CLOCK_MONOTONIC, &data.when); - data.msec_until_refresh = msec_until_refresh; - data.output = output; - wlr_scene_output_for_each_buffer(output->scene_output, send_frame_done_iterator, &data); + output_set_frame_scheduler(output, scheduler); } void update_output_manager_config(struct sway_server *server) { @@ -444,9 +560,6 @@ static void begin_destroy(struct sway_output *output) { output->wlr_output->data = NULL; output->wlr_output = NULL; - wl_event_source_remove(output->repaint_timer); - output->repaint_timer = NULL; - request_modeset(); } @@ -462,14 +575,19 @@ static void handle_layout_destroy(struct wl_listener *listener, void *data) { static void handle_present(struct wl_listener *listener, void *data) { struct sway_output *output = wl_container_of(listener, output, present); - struct wlr_output_event_present *output_event = data; + struct wlr_output_event_present *event = data; - if (!output->enabled || !output_event->presented) { + if (!output->enabled || !event->presented) { return; } - output->last_presentation = output_event->when; - output->refresh_nsec = output_event->refresh; + // Send frame done to all visible surfaces + struct send_frame_done_data frame_done_data = {0}; + clock_gettime(CLOCK_MONOTONIC, &frame_done_data.when); + frame_done_data.msec_until_refresh = get_msec_until_refresh(event); + frame_done_data.output = output; + wlr_scene_output_for_each_buffer(output->scene_output, + send_frame_done_iterator, &frame_done_data); } static void handle_request_state(struct wl_listener *listener, void *data) { @@ -579,13 +697,11 @@ void handle_new_output(struct wl_listener *listener, void *data) { output->destroy.notify = handle_destroy; wl_signal_add(&wlr_output->events.present, &output->present); output->present.notify = handle_present; - wl_signal_add(&scene_output->frame_scheduler->events.frame, &output->frame); - output->frame.notify = handle_frame; wl_signal_add(&wlr_output->events.request_state, &output->request_state); output->request_state.notify = handle_request_state; - output->repaint_timer = wl_event_loop_add_timer(server->wl_event_loop, - output_repaint_timer_handler, output); + wl_list_init(&output->frame.link); + output_set_max_render_time(output, 0); if (server->session_lock.lock) { sway_session_lock_add_output(server->session_lock.lock, output); From 856e4f108bd5773111ec04e40eeb85399ae10d65 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Mon, 6 Apr 2026 16:42:19 +0000 Subject: [PATCH 3/3] output: Add support for max_render_time auto When set to auto, sway will use the new predictive frame scheduler. --- include/sway/output.h | 4 +++- sway/commands/output/max_render_time.c | 3 +++ sway/config/output.c | 7 +++++-- sway/desktop/output.c | 21 ++++++--------------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/include/sway/output.h b/include/sway/output.h index ea59c9cd3..7a27a224e 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -19,6 +19,8 @@ struct sway_output_state { struct sway_workspace *active_workspace; }; +#define MAX_RENDER_TIME_AUTO -2 + struct sway_output { struct sway_node node; @@ -66,7 +68,7 @@ struct sway_output { struct wlr_color_transform *color_transform; struct wlr_ext_workspace_group_handle_v1 *ext_workspace_group; - int max_render_time; // In milliseconds + int max_render_time; bool allow_tearing; bool hdr; diff --git a/sway/commands/output/max_render_time.c b/sway/commands/output/max_render_time.c index 2d3cebe30..303490109 100644 --- a/sway/commands/output/max_render_time.c +++ b/sway/commands/output/max_render_time.c @@ -1,6 +1,7 @@ #include #include "sway/commands.h" #include "sway/config.h" +#include "sway/output.h" struct cmd_results *output_cmd_max_render_time(int argc, char **argv) { if (!config->handler_context.output_config) { @@ -13,6 +14,8 @@ struct cmd_results *output_cmd_max_render_time(int argc, char **argv) { int max_render_time; if (!strcmp(*argv, "off")) { max_render_time = 0; + } else if (!strcmp(*argv, "auto")) { + max_render_time = MAX_RENDER_TIME_AUTO; } else { char *end; max_render_time = strtol(*argv, &end, 10); diff --git a/sway/config/output.c b/sway/config/output.c index 5983f2e5d..0e1b53aba 100644 --- a/sway/config/output.c +++ b/sway/config/output.c @@ -684,8 +684,11 @@ static bool finalize_output_config(struct output_config *oc, struct sway_output } output->color_transform = config_applied->color_transform; - output_set_max_render_time(output, - oc && oc->max_render_time > 0 ? oc->max_render_time : 0); + int max_render_time = 0; + if (oc && (oc->max_render_time > 0 || oc->max_render_time == MAX_RENDER_TIME_AUTO)) { + max_render_time = oc->max_render_time; + } + output_set_max_render_time(output, max_render_time); output->allow_tearing = oc && oc->allow_tearing > 0; output->hdr = applied->image_description != NULL; diff --git a/sway/desktop/output.c b/sway/desktop/output.c index 2d25cac88..fc384f678 100644 --- a/sway/desktop/output.c +++ b/sway/desktop/output.c @@ -317,7 +317,6 @@ struct sway_timer_frame_scheduler { struct wl_event_source *timer; bool frame_pending; - bool needs_frame; struct wl_listener present; }; @@ -333,25 +332,15 @@ static void timed_frame_scheduler_destroy(struct wlr_frame_scheduler *wlr_schedu free(scheduler); } -static void timed_frame_scheduler_trigger_frame( - struct sway_timer_frame_scheduler *scheduler) { - if (!scheduler->needs_frame) { - return; - } - scheduler->needs_frame = false; - wl_signal_emit_mutable(&scheduler->base.events.frame, NULL); -} - static void timed_frame_scheduler_handle_idle(void *data) { struct sway_timer_frame_scheduler *scheduler = data; scheduler->idle = NULL; - timed_frame_scheduler_trigger_frame(scheduler); + wlr_frame_scheduler_emit_frame(&scheduler->base); } static void timed_frame_scheduler_schedule_frame(struct wlr_frame_scheduler *wlr_scheduler) { struct sway_timer_frame_scheduler *scheduler = wl_container_of(wlr_scheduler, scheduler, base); - scheduler->needs_frame = true; if (scheduler->idle != NULL || scheduler->frame_pending) { return; } @@ -367,7 +356,7 @@ static const struct wlr_frame_scheduler_impl timed_frame_scheduler_impl = { static int timed_frame_scheduler_handle_timer(void *data) { struct sway_timer_frame_scheduler *scheduler = data; scheduler->frame_pending = false; - timed_frame_scheduler_trigger_frame(scheduler); + wlr_frame_scheduler_emit_frame(&scheduler->base); return 0; } @@ -393,7 +382,7 @@ static void timed_frame_scheduler_handle_present(struct wl_listener *listener, v // If the delay is less than 1 millisecond (which is the least we can wait) // then just render right away. if (delay < 1) { - timed_frame_scheduler_trigger_frame(scheduler); + wlr_frame_scheduler_emit_frame(&scheduler->base); } else { scheduler->frame_pending = true; wl_event_source_timer_update(scheduler->timer, delay); @@ -471,7 +460,9 @@ void output_set_max_render_time(struct sway_output *output, int max_render_time) output->max_render_time = max_render_time; struct wlr_frame_scheduler *scheduler; - if (max_render_time != 0) { + if (max_render_time == MAX_RENDER_TIME_AUTO) { + scheduler = wlr_predictive_frame_scheduler_create(output->wlr_output); + } else if (max_render_time > 0) { scheduler = timed_frame_scheduler_create(output->wlr_output, max_render_time); } else { scheduler = wlr_frame_scheduler_autocreate(output->wlr_output);