wlr_frame_scheduler: Add predictive frame scheduler

The predictive frame scheduler builds upon the presentation scheduler,
adding a delay based on the measured CPU and GPU time required to
prepare a frame to place the frame event as close as safely possible to
the commit deadline.
This commit is contained in:
Kenny Levinsen 2026-04-06 16:42:02 +00:00
parent e70de3ffaa
commit df562a8e70
5 changed files with 246 additions and 48 deletions

View file

@ -10,9 +10,11 @@
#define WLR_TYPES_WLR_FRAME_SCHEDULER_H
#include <stdbool.h>
#include <stdint.h>
#include <wayland-server-core.h>
struct wlr_frame_scheduler_impl;
struct wlr_render_timer;
struct wlr_frame_scheduler {
struct wlr_output *output;
@ -65,4 +67,23 @@ void wlr_frame_scheduler_schedule_frame(struct wlr_frame_scheduler *scheduler);
void wlr_frame_scheduler_emit_frame(struct wlr_frame_scheduler *scheduler);
void wlr_frame_scheduler_destroy(struct wlr_frame_scheduler *scheduler);
/**
* The predictive scheduler maintains a render loop based on `wlr_output.events.present`, and
* schedules frame signals to arrive just before the estimated render deadline. It learns from
* historic render times provided via wlr_frame_scheduler_inform_render().
*/
struct wlr_frame_scheduler *wlr_predictive_frame_scheduler_create(struct wlr_output *output);
/**
* Provide render timing feedback to the scheduler. Must be called after each output commit.
*
* pre_render_duration_ns is the wall time from the frame signal to the start of the render pass.
*
* render_timer is the GPU render timer for this frame. May be NULL if no rendering was performed
* (e.g. direct scanout), in which case GPU render time is assumed to be zero.
*
* This is a no-op for non-predictive schedulers.
*/
void wlr_frame_scheduler_inform_render(struct wlr_frame_scheduler *scheduler,
int64_t pre_render_duration_ns, struct wlr_render_timer *render_timer);
#endif

View file

@ -268,12 +268,9 @@ struct wlr_scene_output {
uint64_t in_point;
struct wlr_drm_syncobj_timeline *out_timeline;
uint64_t out_point;
} WLR_PRIVATE;
};
struct wlr_scene_timer {
int64_t pre_render_duration;
struct wlr_render_timer *render_timer;
struct wlr_render_timer *render_timer;
} WLR_PRIVATE;
};
/** A layer shell scene helper */
@ -601,8 +598,6 @@ void wlr_scene_output_set_frame_scheduler(struct wlr_scene_output *scene_output,
struct wlr_frame_scheduler *scheduler);
struct wlr_scene_output_state_options {
struct wlr_scene_timer *timer;
/**
* Color transform to apply before the output's color transform. Cannot be
* used when the output has a non-NULL image description set.
@ -629,15 +624,6 @@ bool wlr_scene_output_commit(struct wlr_scene_output *scene_output,
bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
struct wlr_output_state *state, const struct wlr_scene_output_state_options *options);
/**
* Retrieve the duration in nanoseconds between the last wlr_scene_output_commit() call and the end
* of its operations, including those on the GPU that may have finished after the call returned.
*
* Returns -1 if the duration is unavailable.
*/
int64_t wlr_scene_timer_get_duration_ns(struct wlr_scene_timer *timer);
void wlr_scene_timer_finish(struct wlr_scene_timer *timer);
/**
* Call wlr_surface_send_frame_done() on all surfaces in the scene rendered by
* wlr_scene_output_commit() for which wlr_scene_surface.primary_output

View file

@ -329,6 +329,9 @@ int wlr_render_timer_get_duration_ns(struct wlr_render_timer *timer) {
}
void wlr_render_timer_destroy(struct wlr_render_timer *timer) {
if (!timer) {
return;
}
if (!timer->impl->destroy) {
return;
}

View file

@ -1781,6 +1781,8 @@ struct wlr_scene_output *wlr_scene_output_create(struct wlr_scene *scene,
scene_output_update_geometry(scene_output, false);
scene_output->render_timer = wlr_render_timer_create(output->renderer);
return scene_output;
}
@ -1818,6 +1820,7 @@ void wlr_scene_output_destroy(struct wlr_scene_output *scene_output) {
}
wlr_addon_finish(&scene_output->addon);
wlr_render_timer_destroy(scene_output->render_timer);
wlr_frame_scheduler_destroy(scene_output->frame_scheduler);
wlr_damage_ring_finish(&scene_output->damage_ring);
pixman_region32_fini(&scene_output->pending_commit_damage);
@ -2279,13 +2282,8 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
if (!options) {
options = &default_options;
}
struct wlr_scene_timer *timer = options->timer;
struct timespec start_time;
if (timer) {
clock_gettime(CLOCK_MONOTONIC, &start_time);
wlr_scene_timer_finish(timer);
*timer = (struct wlr_scene_timer){0};
}
clock_gettime(CLOCK_MONOTONIC, &start_time);
if ((state->committed & WLR_OUTPUT_STATE_ENABLED) && !state->enabled) {
// if the state is being disabled, do nothing.
@ -2432,12 +2430,11 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
if (scanout) {
scene_output_state_attempt_gamma(scene_output, state);
if (timer) {
struct timespec end_time, duration;
clock_gettime(CLOCK_MONOTONIC, &end_time);
timespec_sub(&duration, &end_time, &start_time);
timer->pre_render_duration = timespec_to_nsec(&duration);
}
struct timespec end_time, duration;
clock_gettime(CLOCK_MONOTONIC, &end_time);
timespec_sub(&duration, &end_time, &start_time);
wlr_frame_scheduler_inform_render(scene_output->frame_scheduler,
timespec_to_nsec(&duration), NULL);
return true;
}
@ -2457,13 +2454,12 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
assert(buffer->width == resolution_width && buffer->height == resolution_height);
if (timer) {
timer->render_timer = wlr_render_timer_create(output->renderer);
int64_t pre_render_duration_ns;
{
struct timespec end_time, duration;
clock_gettime(CLOCK_MONOTONIC, &end_time);
timespec_sub(&duration, &end_time, &start_time);
timer->pre_render_duration = timespec_to_nsec(&duration);
pre_render_duration_ns = timespec_to_nsec(&duration);
}
if ((render_gamma_lut
@ -2482,7 +2478,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
scene_output->in_point++;
struct wlr_render_pass *render_pass = wlr_renderer_begin_buffer_pass(output->renderer, buffer,
&(struct wlr_buffer_pass_options){
.timer = timer ? timer->render_timer : NULL,
.timer = scene_output->render_timer,
.color_transform = scene_output->combined_color_transform,
.signal_timeline = scene_output->in_timeline,
.signal_point = scene_output->in_point,
@ -2606,24 +2602,11 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
scene_output_state_attempt_gamma(scene_output, state);
}
wlr_frame_scheduler_inform_render(scene_output->frame_scheduler,
pre_render_duration_ns, scene_output->render_timer);
return true;
}
int64_t wlr_scene_timer_get_duration_ns(struct wlr_scene_timer *timer) {
int64_t pre_render = timer->pre_render_duration;
if (!timer->render_timer) {
return pre_render;
}
int64_t render = wlr_render_timer_get_duration_ns(timer->render_timer);
return render != -1 ? pre_render + render : -1;
}
void wlr_scene_timer_finish(struct wlr_scene_timer *timer) {
if (timer->render_timer) {
wlr_render_timer_destroy(timer->render_timer);
}
}
static void scene_node_send_frame_done(struct wlr_scene_node *node,
struct wlr_scene_output *scene_output, struct timespec *now) {
if (!node->enabled) {

View file

@ -1,6 +1,7 @@
#include <assert.h>
#include <backend/headless.h>
#include <stdlib.h>
#include <time.h>
#include <types/wlr_output.h>
#include <wayland-client-protocol.h>
#include <wayland-server-core.h>
@ -8,6 +9,7 @@
#include <wlr/backend/wayland.h>
#include <wlr/interfaces/wlr_frame_scheduler.h>
#include <wlr/interfaces/wlr_output.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_frame_scheduler.h>
#include <wlr/types/wlr_output.h>
#include <wlr/util/log.h>
@ -283,6 +285,209 @@ struct wlr_frame_scheduler *wlr_interval_scheduler_create(struct wlr_output *out
return &scheduler->base.base;
}
// The predictive scheduler schedules frame signals based on historic render times, aiming to
// start rendering as late as possible while still meeting the next vblank deadline.
struct predictive_scheduler {
struct wlr_frame_scheduler base;
struct wl_event_source *idle;
struct wl_event_source *timer;
struct wl_listener commit;
struct wl_listener present;
bool frame_pending;
int64_t estimated_frame_time_ns;
bool has_estimate;
int64_t pending_pre_render_ns;
struct wlr_render_timer *pending_render_timer;
bool has_pending_feedback;
};
static const struct wlr_frame_scheduler_impl predictive_scheduler_impl;
static int64_t compute_margin_ns(int refresh_ns) {
int64_t margin = refresh_ns / 20;
if (margin < 1500000) {
margin = 1500000;
}
return margin;
}
static void update_estimate(struct predictive_scheduler *scheduler, int64_t sample_ns) {
if (!scheduler->has_estimate) {
scheduler->estimated_frame_time_ns = sample_ns;
scheduler->has_estimate = true;
return;
}
if (sample_ns > scheduler->estimated_frame_time_ns) {
// React quickly to spikes
scheduler->estimated_frame_time_ns =
(9 * sample_ns + scheduler->estimated_frame_time_ns) / 10;
} else {
// Decay slowly
scheduler->estimated_frame_time_ns =
(sample_ns + 9 * scheduler->estimated_frame_time_ns) / 10;
}
}
static void predictive_scheduler_consume_feedback(struct predictive_scheduler *scheduler) {
if (!scheduler->has_pending_feedback) {
return;
}
scheduler->has_pending_feedback = false;
int64_t gpu_ns = 0;
if (scheduler->pending_render_timer != NULL) {
int duration = wlr_render_timer_get_duration_ns(scheduler->pending_render_timer);
if (duration > 0) {
gpu_ns = duration;
}
}
update_estimate(scheduler, scheduler->pending_pre_render_ns + gpu_ns);
}
static void predictive_scheduler_handle_idle(void *data) {
struct predictive_scheduler *scheduler = data;
scheduler->idle = NULL;
scheduler->frame_pending = false;
wlr_frame_scheduler_emit_frame(&scheduler->base);
}
static int predictive_scheduler_handle_timer(void *data) {
struct predictive_scheduler *scheduler = data;
scheduler->frame_pending = false;
wlr_frame_scheduler_emit_frame(&scheduler->base);
return 0;
}
static void predictive_scheduler_set_timer(struct predictive_scheduler *scheduler, int ms) {
if (scheduler->idle != NULL) {
wl_event_source_remove(scheduler->idle);
scheduler->idle = NULL;
}
wl_event_source_timer_update(scheduler->timer, ms);
}
static void predictive_scheduler_set_idle(struct predictive_scheduler *scheduler) {
wl_event_source_timer_update(scheduler->timer, 0);
if (!scheduler->idle) {
scheduler->idle = wl_event_loop_add_idle(
scheduler->base.output->event_loop,
predictive_scheduler_handle_idle, scheduler);
}
}
static void predictive_scheduler_cancel(struct predictive_scheduler *scheduler) {
if (scheduler->idle != NULL) {
wl_event_source_remove(scheduler->idle);
scheduler->idle = NULL;
}
wl_event_source_timer_update(scheduler->timer, 0);
}
static void predictive_scheduler_handle_commit(struct wl_listener *listener, void *data) {
struct predictive_scheduler *scheduler = wl_container_of(listener, scheduler, commit);
if (scheduler->base.output->enabled) {
scheduler->frame_pending = true;
predictive_scheduler_cancel(scheduler);
}
}
static void predictive_scheduler_handle_present(struct wl_listener *listener, void *data) {
struct predictive_scheduler *scheduler = wl_container_of(listener, scheduler, present);
struct wlr_output_event_present *event = data;
if (!event->presented) {
scheduler->frame_pending = false;
return;
}
predictive_scheduler_consume_feedback(scheduler);
if (!scheduler->has_estimate || event->refresh == 0) {
predictive_scheduler_set_idle(scheduler);
return;
}
int64_t refresh_ns = event->refresh;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t present_ns = (int64_t)event->when.tv_sec * 1000000000 + event->when.tv_nsec;
int64_t now_ns = (int64_t)now.tv_sec * 1000000000 + now.tv_nsec;
int64_t next_vblank_ns = present_ns + refresh_ns;
int64_t margin = compute_margin_ns(refresh_ns);
int64_t deadline_ns = next_vblank_ns - scheduler->estimated_frame_time_ns - margin;
int64_t delay_ns = deadline_ns - now_ns;
if (delay_ns < 1000000) {
predictive_scheduler_set_idle(scheduler);
} else {
int delay_ms = (int)(delay_ns / 1000000);
predictive_scheduler_set_timer(scheduler, delay_ms);
}
}
static void predictive_scheduler_schedule_frame(struct wlr_frame_scheduler *wlr_scheduler) {
struct predictive_scheduler *scheduler =
wl_container_of(wlr_scheduler, scheduler, base);
if (scheduler->idle != NULL || scheduler->frame_pending) {
return;
}
predictive_scheduler_set_idle(scheduler);
}
static void predictive_scheduler_destroy(struct wlr_frame_scheduler *wlr_scheduler) {
struct predictive_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->commit.link);
wl_list_remove(&scheduler->present.link);
free(scheduler);
}
static const struct wlr_frame_scheduler_impl predictive_scheduler_impl = {
.schedule_frame = predictive_scheduler_schedule_frame,
.destroy = predictive_scheduler_destroy,
};
struct wlr_frame_scheduler *wlr_predictive_frame_scheduler_create(struct wlr_output *output) {
struct predictive_scheduler *scheduler = calloc(1, sizeof(*scheduler));
if (!scheduler) {
return NULL;
}
wlr_frame_scheduler_init(&scheduler->base, &predictive_scheduler_impl, output);
scheduler->timer = wl_event_loop_add_timer(output->event_loop,
predictive_scheduler_handle_timer, scheduler);
scheduler->commit.notify = predictive_scheduler_handle_commit;
wl_signal_add(&output->events.commit, &scheduler->commit);
scheduler->present.notify = predictive_scheduler_handle_present;
wl_signal_add(&output->events.present, &scheduler->present);
return &scheduler->base;
}
void wlr_frame_scheduler_inform_render(struct wlr_frame_scheduler *scheduler,
int64_t pre_render_duration_ns, struct wlr_render_timer *render_timer) {
if (scheduler->impl != &predictive_scheduler_impl) {
return;
}
struct predictive_scheduler *predictive =
wl_container_of(scheduler, predictive, base);
predictive->pending_pre_render_ns = pre_render_duration_ns;
predictive->pending_render_timer = render_timer;
predictive->has_pending_feedback = true;
}
struct wlr_frame_scheduler *wlr_frame_scheduler_autocreate(struct wlr_output *output) {
if (wlr_output_is_wl(output) && !wlr_wl_backend_has_presentation_time(output->backend)) {
wlr_log(WLR_INFO, "wp_presentation not available, falling back to frame callbacks");