Merge pull request #1156 from yshui/pacing-fixes

This commit is contained in:
Yuxuan Shui 2024-01-14 15:46:24 +00:00 committed by GitHub
commit 148e61a0b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1080 additions and 317 deletions

View File

@ -82,13 +82,17 @@ void handle_device_reset(session_t *ps) {
}
/// paint all windows
void paint_all_new(session_t *ps, struct managed_win *t) {
///
/// Returns if any render command is issued. IOW if nothing on the screen has changed,
/// this function will return false.
bool paint_all_new(session_t *ps, struct managed_win *const t) {
struct timespec now = get_time_timespec();
auto paint_all_start_us =
(uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000;
if (ps->backend_data->ops->device_status &&
ps->backend_data->ops->device_status(ps->backend_data) != DEVICE_STATUS_NORMAL) {
return handle_device_reset(ps);
handle_device_reset(ps);
return false;
}
if (ps->o.xrender_sync_fence) {
if (ps->xsync_exists && !x_fence_sync(&ps->c, ps->sync_fence)) {
@ -114,7 +118,7 @@ void paint_all_new(session_t *ps, struct managed_win *t) {
if (!pixman_region32_not_empty(&reg_damage)) {
pixman_region32_fini(&reg_damage);
return;
return false;
}
#ifdef DEBUG_REPAINT
@ -190,16 +194,15 @@ void paint_all_new(session_t *ps, struct managed_win *t) {
auto after_damage_us = (uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000;
log_trace("Getting damage took %" PRIu64 " us", after_damage_us - after_sync_fence_us);
if (ps->next_render > 0) {
log_trace("Render schedule deviation: %ld us (%s) %" PRIu64 " %ld",
labs((long)after_damage_us - (long)ps->next_render),
after_damage_us < ps->next_render ? "early" : "late",
after_damage_us, ps->next_render);
log_verbose("Render schedule deviation: %ld us (%s) %" PRIu64 " %ld",
labs((long)after_damage_us - (long)ps->next_render),
after_damage_us < ps->next_render ? "early" : "late",
after_damage_us, ps->next_render);
ps->last_schedule_delay = 0;
if (after_damage_us > ps->next_render) {
ps->last_schedule_delay = after_damage_us - ps->next_render;
}
}
ps->did_render = true;
if (ps->backend_data->ops->prepare) {
ps->backend_data->ops->prepare(ps->backend_data, &reg_paint);
@ -219,7 +222,7 @@ void paint_all_new(session_t *ps, struct managed_win *t) {
// on top of that window. This is used to reduce the number of pixels painted.
//
// Whether this is beneficial is to be determined XXX
for (auto w = t; w; w = w->prev_trans) {
for (struct managed_win *w = t; w; w = w->prev_trans) {
pixman_region32_subtract(&reg_visible, &ps->screen_reg, w->reg_ignore);
assert(!(w->flags & WIN_FLAGS_IMAGE_ERROR));
assert(!(w->flags & WIN_FLAGS_PIXMAP_STALE));
@ -541,6 +544,7 @@ void paint_all_new(session_t *ps, struct managed_win *t) {
for (win *w = t; w; w = w->prev_trans)
log_trace(" %#010lx", w->id);
#endif
return true;
}
// vim: set noet sw=8 ts=8 :

View File

@ -366,4 +366,8 @@ struct backend_operations {
extern struct backend_operations *backend_list[];
void paint_all_new(session_t *ps, struct managed_win *const t) attr_nonnull(1);
/// paint all windows
///
/// Returns if any render command is issued. IOW if nothing on the screen has changed,
/// this function will return false.
bool paint_all_new(session_t *ps, struct managed_win *t) attr_nonnull(1);

View File

@ -19,6 +19,13 @@ void apply_driver_workarounds(struct session *ps, enum driver driver) {
}
}
enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver) {
if (driver & DRIVER_NVIDIA) {
return VBLANK_SCHEDULER_SGI_VIDEO_SYNC;
}
return VBLANK_SCHEDULER_PRESENT;
}
enum driver detect_driver(xcb_connection_t *c, backend_t *backend_data, xcb_window_t window) {
enum driver ret = 0;
// First we try doing backend agnostic detection using RANDR

View File

@ -7,6 +7,7 @@
#include <stdio.h>
#include <xcb/xcb.h>
#include "config.h"
#include "utils.h"
struct session;
@ -41,6 +42,8 @@ enum driver detect_driver(xcb_connection_t *, struct backend_base *, xcb_window_
/// Apply driver specified global workarounds. It's safe to call this multiple times.
void apply_driver_workarounds(struct session *ps, enum driver);
/// Choose a vblank scheduler based on the driver.
enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver);
// Print driver names to stdout, for diagnostics
static inline void print_drivers(enum driver drivers) {

View File

@ -139,8 +139,6 @@ typedef struct session {
// === Event handlers ===
/// ev_io for X connection
ev_io xiow;
/// Timer for checking DPMS power level
ev_timer dpms_check_timer;
/// Timeout for delayed unredirection.
ev_timer unredir_timer;
/// Timer for fading
@ -214,26 +212,19 @@ typedef struct session {
bool first_frame;
/// Whether screen has been turned off
bool screen_is_off;
/// Event context for X Present extension.
uint32_t present_event_id;
xcb_special_event_t *present_event;
/// When last MSC event happened, in useconds.
uint64_t last_msc_instant;
/// The last MSC number
uint64_t last_msc;
/// When the currently rendered frame will be displayed.
/// 0 means there is no pending frame.
uint64_t target_msc;
/// The delay between when the last frame was scheduled to be rendered, and when
/// the render actually started.
uint64_t last_schedule_delay;
/// When do we want our next frame to start rendering.
uint64_t next_render;
/// Did we actually render the last frame. Sometimes redraw will be scheduled only
/// to find out nothing has changed. In which case this will be set to false.
bool did_render;
/// Whether we can perform frame pacing.
bool frame_pacing;
/// Vblank event scheduler
struct vblank_scheduler *vblank_scheduler;
/// Render statistics
struct render_statistics render_stats;
@ -245,8 +236,18 @@ typedef struct session {
options_t o;
/// Whether we have hit unredirection timeout.
bool tmout_unredir_hit;
/// Whether we need to redraw the screen
bool redraw_needed;
/// If the backend is busy. This means two things:
/// Either the backend is currently rendering a frame, or a frame has been
/// rendered but has yet to be presented. In either case, we should not start
/// another render right now. As if we start issuing rendering commands now, we
/// will have to wait for either the the current render to finish, or the current
/// back buffer to be become available again. In either case, we will be wasting
/// time.
bool backend_busy;
/// Whether a render is queued. This generally means there are pending updates
/// to the screen that's neither included in the current render, nor on the
/// screen.
bool render_queued;
/// Cache a xfixes region so we don't need to allocate it every time.
/// A workaround for yshui/picom#301

View File

@ -8,6 +8,8 @@
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -617,6 +619,82 @@ char *locate_auxiliary_file(const char *scope, const char *path, const char *inc
return NULL;
}
struct debug_options_entry {
const char *name;
const char **choices;
size_t offset;
};
// clang-format off
const char *vblank_scheduler_str[] = {
[VBLANK_SCHEDULER_PRESENT] = "present",
[VBLANK_SCHEDULER_SGI_VIDEO_SYNC] = "sgi_video_sync",
[LAST_VBLANK_SCHEDULER] = NULL
};
static const struct debug_options_entry debug_options_entries[] = {
{"smart_frame_pacing", NULL, offsetof(struct debug_options, smart_frame_pacing)},
{"force_vblank_sched", vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)},
};
// clang-format on
void parse_debug_option_single(char *setting, struct debug_options *debug_options) {
char *equal = strchr(setting, '=');
size_t name_len = equal ? (size_t)(equal - setting) : strlen(setting);
for (size_t i = 0; i < ARR_SIZE(debug_options_entries); i++) {
if (strncmp(setting, debug_options_entries[i].name, name_len) != 0) {
continue;
}
if (debug_options_entries[i].name[name_len] != '\0') {
continue;
}
auto value = (int *)((void *)debug_options + debug_options_entries[i].offset);
if (equal) {
const char *const arg = equal + 1;
if (debug_options_entries[i].choices != NULL) {
for (size_t j = 0; debug_options_entries[i].choices[j]; j++) {
if (strcmp(arg, debug_options_entries[i].choices[j]) ==
0) {
*value = (int)j;
return;
}
}
}
if (!parse_int(arg, value)) {
log_error("Invalid value for debug option %s: %s, it "
"will be ignored.",
debug_options_entries[i].name, arg);
}
} else if (debug_options_entries[i].choices == NULL) {
*value = 1;
} else {
log_error(
"Missing value for debug option %s, it will be ignored.", setting);
}
return;
}
log_error("Invalid debug option: %s", setting);
}
/// Parse debug options from environment variable `PICOM_DEBUG`.
void parse_debug_options(struct debug_options *debug_options) {
const char *debug = getenv("PICOM_DEBUG");
const struct debug_options default_debug_options = {
.force_vblank_scheduler = LAST_VBLANK_SCHEDULER,
};
*debug_options = default_debug_options;
if (!debug) {
return;
}
scoped_charp debug_copy = strdup(debug);
char *tmp, *needle = strtok_r(debug_copy, ";", &tmp);
while (needle) {
parse_debug_option_single(needle, debug_options);
needle = strtok_r(NULL, ";", &tmp);
}
}
/**
* Parse a list of window shader rules.
*/
@ -819,5 +897,6 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable,
(void)hasneg;
(void)winopt_mask;
#endif
parse_debug_options(&opt->debug_options);
return ret;
}

View File

@ -73,6 +73,27 @@ enum blur_method {
typedef struct _c2_lptr c2_lptr_t;
enum vblank_scheduler_type {
/// X Present extension based vblank events
VBLANK_SCHEDULER_PRESENT,
/// GLX_SGI_video_sync based vblank events
VBLANK_SCHEDULER_SGI_VIDEO_SYNC,
/// An invalid scheduler, served as a scheduler count, and
/// as a sentinel value.
LAST_VBLANK_SCHEDULER,
};
extern const char *vblank_scheduler_str[];
/// Internal, private options for debugging and development use.
struct debug_options {
/// Try to reduce frame latency by using vblank interval and render time
/// estimates. Right now it's not working well across drivers.
int smart_frame_pacing;
/// Override the vblank scheduler chosen by the compositor.
int force_vblank_scheduler;
};
/// Structure representing all options.
typedef struct options {
// === Debugging ===
@ -262,6 +283,8 @@ typedef struct options {
c2_lptr_t *transparent_clipping_blacklist;
bool dithered_present;
struct debug_options debug_options;
} options_t;
extern const char *const BACKEND_STRS[NUM_BKEND + 1];

View File

@ -716,7 +716,8 @@ void ev_handle(session_t *ps, xcb_generic_event_t *ev) {
// XXX redraw needs to be more fine grained
queue_redraw(ps);
// the events sent from SendEvent will be ignored
// We intentionally ignore events sent via SendEvent. Those events has the 8th bit
// of response_type set, meaning they will match none of the cases below.
switch (ev->response_type) {
case FocusIn: ev_focus_in(ps, (xcb_focus_in_event_t *)ev); break;
case FocusOut: ev_focus_out(ps, (xcb_focus_out_event_t *)ev); break;

View File

@ -9,7 +9,8 @@ base_deps = [
srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c',
'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c',
'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c') ]
'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c',
'vblank.c') ]
picom_inc = include_directories('.')
cflags = []
@ -58,7 +59,7 @@ endif
if get_option('opengl')
cflags += ['-DCONFIG_OPENGL', '-DGL_GLEXT_PROTOTYPES']
deps += [dependency('gl', required: true), dependency('egl', required: true)]
deps += [dependency('gl', required: true), dependency('egl', required: true), dependency('threads', required:true)]
srcs += [ 'opengl.c' ]
endif

View File

@ -16,11 +16,13 @@
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <math.h>
#include <sched.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#include <xcb/composite.h>
#include <xcb/damage.h>
@ -64,6 +66,7 @@
#include "options.h"
#include "statistics.h"
#include "uthash_extra.h"
#include "vblank.h"
/// Get session_t pointer from a pointer to a member of session_t
#define session_ptr(ptr, member) \
@ -122,27 +125,6 @@ static inline int64_t get_time_ms(void) {
return (int64_t)tp.tv_sec * 1000 + (int64_t)tp.tv_nsec / 1000000;
}
static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) {
// state is a bool indicating whether dpms is enabled
return info->state && (info->power_level != XCB_DPMS_DPMS_MODE_ON);
}
void check_dpms_status(EV_P attr_unused, ev_timer *w, int revents attr_unused) {
auto ps = session_ptr(w, dpms_check_timer);
auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL);
if (!r) {
log_fatal("Failed to query DPMS status.");
abort();
}
auto now_screen_is_off = dpms_screen_is_off(r);
if (ps->screen_is_off != now_screen_is_off) {
log_debug("Screen is now %s", now_screen_is_off ? "off" : "on");
ps->screen_is_off = now_screen_is_off;
queue_redraw(ps);
}
free(r);
}
/**
* Find matched window.
*
@ -163,122 +145,248 @@ static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t
return w;
}
enum vblank_callback_action check_render_finish(struct vblank_event *e attr_unused, void *ud) {
auto ps = (session_t *)ud;
if (!ps->backend_busy) {
return VBLANK_CALLBACK_DONE;
}
struct timespec render_time;
bool completed =
ps->backend_data->ops->last_render_time(ps->backend_data, &render_time);
if (!completed) {
// Render hasn't completed yet, we can't start another render.
// Check again at the next vblank.
log_debug("Last render did not complete during vblank, msc: "
"%" PRIu64,
ps->last_msc);
return VBLANK_CALLBACK_AGAIN;
}
// The frame has been finished and presented, record its render time.
if (ps->o.debug_options.smart_frame_pacing) {
int render_time_us =
(int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L);
render_statistics_add_render_time_sample(
&ps->render_stats, render_time_us + (int)ps->last_schedule_delay);
log_verbose("Last render call took: %d (gpu) + %d (cpu) us, "
"last_msc: %" PRIu64,
render_time_us, (int)ps->last_schedule_delay, ps->last_msc);
}
ps->backend_busy = false;
return VBLANK_CALLBACK_DONE;
}
enum vblank_callback_action
collect_vblank_interval_statistics(struct vblank_event *e, void *ud) {
auto ps = (session_t *)ud;
double vblank_interval = NAN;
assert(ps->frame_pacing);
assert(ps->vblank_scheduler);
if (!ps->o.debug_options.smart_frame_pacing) {
// We don't need to collect statistics if we are not doing smart frame
// pacing.
return VBLANK_CALLBACK_DONE;
}
// TODO(yshui): this naive method of estimating vblank interval does not handle
// the variable refresh rate case very well. This includes the case
// of a VRR enabled monitor; or a monitor that's turned off, in which
// case the vblank events might slow down or stop all together.
// I tried using DPMS to detect monitor power state, and stop adding
// samples when the monitor is off, but I had a hard time to get it
// working reliably, there are just too many corner cases.
// Don't add sample again if we already collected statistics for this vblank
if (ps->last_msc < e->msc) {
if (ps->last_msc_instant != 0) {
auto frame_count = e->msc - ps->last_msc;
auto frame_time =
(int)((e->ust - ps->last_msc_instant) / frame_count);
if (frame_count == 1) {
render_statistics_add_vblank_time_sample(
&ps->render_stats, frame_time);
log_trace("Frame count %lu, frame time: %d us, ust: "
"%" PRIu64 "",
frame_count, frame_time, e->ust);
} else {
log_trace("Frame count %lu, frame time: %d us, msc: "
"%" PRIu64 ", not adding sample.",
frame_count, frame_time, e->ust);
}
}
ps->last_msc_instant = e->ust;
ps->last_msc = e->msc;
} else if (ps->last_msc > e->msc) {
log_warn("PresentCompleteNotify msc is going backwards, last_msc: "
"%" PRIu64 ", current msc: %" PRIu64,
ps->last_msc, e->msc);
ps->last_msc_instant = 0;
ps->last_msc = 0;
}
vblank_interval = render_statistics_get_vblank_time(&ps->render_stats);
log_trace("Vblank interval estimate: %f us", vblank_interval);
if (vblank_interval == 0) {
// We don't have enough data for vblank interval estimate, schedule
// another vblank event.
return VBLANK_CALLBACK_AGAIN;
}
return VBLANK_CALLBACK_DONE;
}
void schedule_render(session_t *ps, bool triggered_by_vblank);
/// vblank callback scheduled by schedule_render, when a render is ongoing.
///
/// Check if previously queued render has finished, and reschedule render if it has.
enum vblank_callback_action reschedule_render_at_vblank(struct vblank_event *e, void *ud) {
auto ps = (session_t *)ud;
assert(ps->frame_pacing);
assert(ps->render_queued);
assert(ps->vblank_scheduler);
log_verbose("Rescheduling render at vblank, msc: %" PRIu64, e->msc);
collect_vblank_interval_statistics(e, ud);
check_render_finish(e, ud);
if (ps->backend_busy) {
return VBLANK_CALLBACK_AGAIN;
}
schedule_render(ps, false);
return VBLANK_CALLBACK_DONE;
}
/// How many seconds into the future should we start rendering the next frame.
///
/// Renders are scheduled like this:
///
/// 1. queue_redraw() registers the intention to render. redraw_needed is set to true to
/// indicate what is on screen needs to be updated.
/// 2. then, we need to figure out the best time to start rendering. first, we need to
/// know when the next frame will be displayed on screen. we have this information from
/// the Present extension: we know when was the last frame displayed, and we know the
/// refresh rate. so we can calculate the next frame's display time. if our render time
/// estimation shows we could miss that target, we push the target back one frame.
/// 3. if there is already render completed for that target frame, or there is a render
/// currently underway, we don't do anything, and wait for the next Present Complete
/// Notify event to try to schedule again.
/// 4. otherwise, we schedule a render for that target frame. we use past statistics about
/// how long our renders took to figure out when to start rendering. we start rendering
/// at the latest point of time possible to still hit the target frame.
/// 1. queue_redraw() queues a new render by calling schedule_render, if there
/// is no render currently scheduled. i.e. render_queued == false.
/// 2. then, we need to figure out the best time to start rendering. we need to
/// at least know when the next vblank will start, as we can't start render
/// before the current rendered frame is diplayed on screen. we have this
/// information from the vblank scheduler, it will notify us when that happens.
/// we might also want to delay the rendering even further to reduce latency,
/// this is discussed below, in FUTURE WORKS.
/// 3. we schedule a render for that target point in time.
/// 4. draw_callback() is called at the schedule time (i.e. when scheduled
/// vblank event is delivered). Backend APIs are called to issue render
/// commands. render_queued is set to false, and backend_busy is set to true.
///
/// The `triggered_by_timer` parameter is used to indicate whether this function is
/// triggered by a steady timer, i.e. we are rendering for each vblank. The other case is
/// when we stop rendering for a while because there is no changes on screen, then
/// something changed and schedule_render is triggered by a DamageNotify. The idea is that
/// when the schedule is triggered by a steady timer, schedule_render will be called at a
/// predictable offset into each vblank.
/// There are some considerations in step 2:
///
/// First of all, a vblank event being delivered
/// doesn't necessarily mean the frame has been displayed on screen. If a frame
/// takes too long to render, it might miss the current vblank, and will be
/// displayed on screen during one of the subsequent vblanks. So in
/// schedule_render_at_vblank, we ask the backend to see if it has finished
/// rendering. if not, render_queued is unchanged, and another vblank is
/// scheduled; otherwise, draw_callback_impl will be scheduled to be call at
/// an appropriate time. Second, we might not have rendered for the previous vblank,
/// in which case the last vblank event we received could be many frames in the past,
/// so we can't make scheduling decisions based on that. So we always schedule
/// a vblank event when render is queued, and make scheduling decisions when the
/// event is delivered.
///
/// All of the above is what happens when frame_pacing is true. Otherwise
/// render_in_progress is either QUEUED or IDLE, and queue_redraw will always
/// schedule a render to be started immediately. PresentCompleteNotify will not
/// be received, and handle_end_of_vblank will not be called.
///
/// The `triggered_by_timer` parameter is used to indicate whether this function
/// is triggered by a steady timer, i.e. we are rendering for each vblank. The
/// other case is when we stop rendering for a while because there is no changes
/// on screen, then something changed and schedule_render is triggered by a
/// DamageNotify. The idea is that when the schedule is triggered by a steady
/// timer, schedule_render will be called at a predictable offset into each
/// vblank.
///
/// # FUTURE WORKS
///
/// As discussed in step 2 above, we might want to delay the rendering even
/// further. If we know the time it takes to render a frame, and the interval
/// between vblanks, we can try to schedule the render to start at a point in
/// time that's closer to the next vblank. We should be able to get this
/// information by doing statistics on the render time of previous frames, which
/// is available from the backends; and the interval between vblank events,
/// which is available from the vblank scheduler.
///
/// The code that does this is already implemented below, but disabled by
/// default. There are several problems with it, see bug #1072.
void schedule_render(session_t *ps, bool triggered_by_vblank attr_unused) {
// If the backend is busy, we will try again at the next vblank.
if (ps->backend_busy) {
// We should never have set backend_busy to true unless frame_pacing is
// enabled.
assert(ps->vblank_scheduler);
assert(ps->frame_pacing);
log_verbose("Backend busy, will reschedule render at next vblank.");
if (!vblank_scheduler_schedule(ps->vblank_scheduler,
reschedule_render_at_vblank, ps)) {
// TODO(yshui): handle error here
abort();
}
return;
}
void schedule_render(session_t *ps, bool triggered_by_vblank) {
// By default, we want to schedule render immediately, later in this function we
// might adjust that and move the render later, based on render timing statistics.
double delay_s = 0;
ps->next_render = 0;
if (!ps->frame_pacing || !ps->redirected) {
// Not doing frame pacing, schedule a render immediately, if not already
// scheduled.
// If not redirected, we schedule immediately to have a chance to
// redirect. We won't have frame or render timing information anyway.
if (!ev_is_active(&ps->draw_timer)) {
// We don't know the msc, so we set it to 1, because 0 is a
// special value
ps->target_msc = 1;
goto schedule;
}
return;
}
struct timespec render_time;
bool completed =
ps->backend_data->ops->last_render_time(ps->backend_data, &render_time);
if (!completed || ev_is_active(&ps->draw_timer)) {
// There is already a render underway (either just scheduled, or is
// rendered but awaiting completion), don't schedule another one.
if (ps->target_msc <= ps->last_msc) {
log_debug("Target frame %ld is in the past, but we are still "
"rendering",
ps->target_msc);
// We missed our target, push it back one frame
ps->target_msc = ps->last_msc + 1;
}
log_trace("Still rendering for target frame %ld, not scheduling another "
"render",
ps->target_msc);
return;
}
if (ps->target_msc > ps->last_msc) {
// Render for the target frame is completed, but is yet to be displayed.
// Don't schedule another render.
log_trace("Target frame %ld is in the future, and we have already "
"rendered, last msc: %d",
ps->target_msc, (int)ps->last_msc);
return;
}
unsigned int divisor = 0;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
auto now_us = (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_nsec / 1000;
if (triggered_by_vblank) {
log_trace("vblank schedule delay: %ld us", now_us - ps->last_msc_instant);
ps->next_render = now_us;
if (!ps->frame_pacing || !ps->redirected) {
// If not doing frame pacing, schedule a render immediately; if
// not redirected, we schedule immediately to have a chance to
// redirect. We won't have frame or render timing information
// anyway.
assert(!ev_is_active(&ps->draw_timer));
goto schedule;
}
int render_time_us =
(int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L);
if (ps->target_msc == ps->last_msc) {
// The frame has just been displayed, record its render time;
if (ps->did_render) {
log_trace("Last render call took: %d (gpu) + %d (cpu) us, "
"last_msc: %" PRIu64,
render_time_us, (int)ps->last_schedule_delay, ps->last_msc);
render_statistics_add_render_time_sample(
&ps->render_stats, render_time_us + (int)ps->last_schedule_delay);
}
ps->target_msc = 0;
ps->did_render = false;
ps->last_schedule_delay = 0;
}
unsigned int divisor = 0;
auto render_budget = render_statistics_get_budget(&ps->render_stats, &divisor);
// if ps->o.debug_options.smart_frame_pacing is false, we won't have any render
// time or vblank interval estimates, so we would naturally fallback to schedule
// render immediately.
auto render_budget = render_statistics_get_budget(&ps->render_stats);
auto frame_time = render_statistics_get_vblank_time(&ps->render_stats);
if (frame_time == 0) {
// We don't have enough data for render time estimates, maybe there's
// no frame rendered yet, or the backend doesn't support render timing
// information, schedule render immediately.
ps->target_msc = ps->last_msc + 1;
log_verbose("Not enough data for render time estimates.");
goto schedule;
}
auto const deadline = ps->last_msc_instant + (unsigned long)divisor * frame_time;
if (render_budget >= frame_time) {
// If the estimated render time is already longer than the estimated
// vblank interval, there is no way we can make it. Instead of always
// dropping frames, we try desperately to catch up and schedule a
// render immediately.
log_verbose("Render budget: %u us >= frame time: %" PRIu32 " us",
render_budget, frame_time);
goto schedule;
}
auto target_frame = (now_us + render_budget - ps->last_msc_instant) / frame_time + 1;
auto const deadline = ps->last_msc_instant + target_frame * frame_time;
unsigned int available = 0;
if (deadline > now_us) {
available = (unsigned int)(deadline - now_us);
}
ps->target_msc = ps->last_msc + divisor;
if (available > render_budget) {
delay_s = (double)(available - render_budget) / 1000000.0;
ps->next_render = deadline - render_budget;
} else {
delay_s = 0;
ps->next_render = now_us;
}
if (delay_s > 1) {
log_warn("Delay too long: %f s, render_budget: %d us, frame_time: "
"%" PRIu32 " us, now_us: %" PRIu64 " us, next_msc: %" PRIu64 " u"
@ -286,38 +394,32 @@ void schedule_render(session_t *ps, bool triggered_by_vblank) {
delay_s, render_budget, frame_time, now_us, deadline);
}
log_trace("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, frame_time: "
"%" PRIu32 ", now_us: %" PRIu64 ", next_msc: %" PRIu64 ", "
"target_msc: %" PRIu64 ", divisor: %d",
delay_s, ps->last_msc_instant, render_budget, frame_time, now_us,
deadline, ps->target_msc, divisor);
log_verbose("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, "
"frame_time: %" PRIu32 ", now_us: %" PRIu64 ", next_render: %" PRIu64
", next_msc: %" PRIu64 ", divisor: "
"%d",
delay_s, ps->last_msc_instant, render_budget, frame_time, now_us,
ps->next_render, deadline, divisor);
schedule:
// If the backend is not busy, we just need to schedule the render at the
// specified time; otherwise we need to wait for the next vblank event and
// reschedule.
ps->last_schedule_delay = 0;
assert(!ev_is_active(&ps->draw_timer));
ev_timer_set(&ps->draw_timer, delay_s, 0);
ev_timer_start(ps->loop, &ps->draw_timer);
}
void queue_redraw(session_t *ps) {
if (ps->screen_is_off) {
// The screen is off, if there is a draw queued for the next frame (i.e.
// ps->redraw_needed == true), it won't be triggered until the screen is
// on again, because the abnormal Present events we will receive from the
// X server when the screen is off. Yet we need the draw_callback to be
// called as soon as possible so the screen can be unredirected.
// So here we unconditionally start the draw timer.
ev_timer_stop(ps->loop, &ps->draw_timer);
ev_timer_set(&ps->draw_timer, 0, 0);
ev_timer_start(ps->loop, &ps->draw_timer);
log_verbose("Queue redraw, render_queued: %d, backend_busy: %d",
ps->render_queued, ps->backend_busy);
if (ps->render_queued) {
return;
}
// Whether we have already rendered for the current frame.
// If frame pacing is not enabled, pretend this is false.
// If --benchmark is used, redraw is always queued
if (!ps->redraw_needed && !ps->o.benchmark) {
schedule_render(ps, false);
}
ps->redraw_needed = true;
ps->render_queued = true;
schedule_render(ps, false);
}
/**
@ -1013,19 +1115,6 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation,
// If there's no window to paint, and the screen isn't redirected,
// don't redirect it.
unredir_possible = true;
} else if (ps->screen_is_off) {
// Screen is off, unredirect
// We do this unconditionally disregarding "unredir_if_possible"
// because it's important for correctness, because we need to
// workaround problems X server has around screen off.
//
// Known problems:
// 1. Sometimes OpenGL front buffer can lose content, and if we
// are doing partial updates (i.e. use-damage = true), the
// result will be wrong.
// 2. For frame pacing, X server sends bogus
// PresentCompleteNotify events when screen is off.
unredir_possible = true;
}
if (unredir_possible) {
if (ps->redirected) {
@ -1397,7 +1486,7 @@ static bool redirect_start(session_t *ps) {
pixman_region32_init(&ps->damage_ring[i]);
}
ps->frame_pacing = !ps->o.no_frame_pacing;
ps->frame_pacing = !ps->o.no_frame_pacing && ps->o.vsync;
if ((ps->o.legacy_backends || ps->o.benchmark || !ps->backend_data->ops->last_render_time) &&
ps->frame_pacing) {
// Disable frame pacing if we are using a legacy backend or if we are in
@ -1406,25 +1495,31 @@ static bool redirect_start(session_t *ps) {
ps->frame_pacing = false;
}
if (ps->present_exists && ps->frame_pacing) {
ps->present_event_id = x_new_id(&ps->c);
auto select_input = xcb_present_select_input(
ps->c.c, ps->present_event_id, session_get_target_window(ps),
XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY);
auto notify_msc = xcb_present_notify_msc(
ps->c.c, session_get_target_window(ps), 0, 0, 1, 0);
set_cant_fail_cookie(&ps->c, select_input);
set_cant_fail_cookie(&ps->c, notify_msc);
ps->present_event = xcb_register_for_special_xge(
ps->c.c, &xcb_present_id, ps->present_event_id, NULL);
// Re-detect driver since we now have a backend
ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root);
apply_driver_workarounds(ps, ps->drivers);
if (ps->present_exists && ps->frame_pacing) {
// Initialize rendering and frame timing statistics, and frame pacing
// states.
ps->last_msc_instant = 0;
ps->last_msc = 0;
ps->last_schedule_delay = 0;
ps->target_msc = 0;
render_statistics_reset(&ps->render_stats);
enum vblank_scheduler_type scheduler_type =
choose_vblank_scheduler(ps->drivers);
if (ps->o.debug_options.force_vblank_scheduler != LAST_VBLANK_SCHEDULER) {
scheduler_type =
(enum vblank_scheduler_type)ps->o.debug_options.force_vblank_scheduler;
}
log_info("Using vblank scheduler: %s.", vblank_scheduler_str[scheduler_type]);
ps->vblank_scheduler = vblank_scheduler_new(
ps->loop, &ps->c, session_get_target_window(ps), scheduler_type);
if (!ps->vblank_scheduler) {
return false;
}
vblank_scheduler_schedule(ps->vblank_scheduler,
collect_vblank_interval_statistics, ps);
} else if (ps->frame_pacing) {
log_error("Present extension is not supported, frame pacing disabled.");
ps->frame_pacing = false;
@ -1436,10 +1531,6 @@ static bool redirect_start(session_t *ps) {
ps->redirected = true;
ps->first_frame = true;
// Re-detect driver since we now have a backend
ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root);
apply_driver_workarounds(ps, ps->drivers);
root_damaged(ps);
// Repaint the whole screen
@ -1472,12 +1563,9 @@ static void unredirect(session_t *ps) {
free(ps->damage_ring);
ps->damage_ring = ps->damage = NULL;
if (ps->present_event_id) {
xcb_present_select_input(ps->c.c, ps->present_event_id,
session_get_target_window(ps), 0);
ps->present_event_id = XCB_NONE;
xcb_unregister_for_special_event(ps->c.c, ps->present_event);
ps->present_event = NULL;
if (ps->vblank_scheduler) {
vblank_scheduler_free(ps->vblank_scheduler);
ps->vblank_scheduler = NULL;
}
// Must call XSync() here
@ -1487,92 +1575,12 @@ static void unredirect(session_t *ps) {
log_debug("Screen unredirected.");
}
static void
handle_present_complete_notify(session_t *ps, xcb_present_complete_notify_event_t *cne) {
if (cne->kind != XCB_PRESENT_COMPLETE_KIND_NOTIFY_MSC) {
return;
}
bool event_is_invalid = false;
if (ps->frame_pacing) {
auto next_msc = cne->msc + 1;
if (cne->msc <= ps->last_msc || cne->ust == 0) {
// X sometimes sends duplicate/bogus MSC events, don't
// use the msc value. Also ignore these events.
//
// See:
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1418
next_msc = ps->last_msc + 1;
event_is_invalid = true;
}
auto cookie = xcb_present_notify_msc(
ps->c.c, session_get_target_window(ps), 0, next_msc, 0, 0);
set_cant_fail_cookie(&ps->c, cookie);
}
if (event_is_invalid) {
return;
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
uint64_t now_usec = (uint64_t)(now.tv_sec * 1000000 + now.tv_nsec / 1000);
uint64_t drift;
if (cne->ust > now_usec) {
drift = cne->ust - now_usec;
} else {
drift = now_usec - cne->ust;
}
if (ps->last_msc_instant != 0) {
auto frame_count = cne->msc - ps->last_msc;
int frame_time = (int)((cne->ust - ps->last_msc_instant) / frame_count);
render_statistics_add_vblank_time_sample(&ps->render_stats, frame_time);
log_trace("Frame count %lu, frame time: %d us, rolling average: %u us, "
"msc: %" PRIu64 ", offset: %d us",
frame_count, frame_time,
render_statistics_get_vblank_time(&ps->render_stats), cne->ust,
(int)drift);
} else if (drift > 1000000 && ps->frame_pacing) {
// This is the first MSC event we receive, let's check if the timestamps
// align with the monotonic clock. If not, disable frame pacing because we
// can't schedule frames reliably.
log_error("Temporal anomaly detected, frame pacing disabled. (Are we "
"running inside a time namespace?), %" PRIu64 " %" PRIu64,
now_usec, ps->last_msc_instant);
ps->frame_pacing = false;
}
ps->last_msc_instant = cne->ust;
ps->last_msc = cne->msc;
if (ps->redraw_needed) {
schedule_render(ps, true);
}
}
static void handle_present_events(session_t *ps) {
if (!ps->present_event) {
// Screen not redirected
return;
}
xcb_present_generic_event_t *ev;
while ((ev = (void *)xcb_poll_for_special_event(ps->c.c, ps->present_event))) {
if (ev->event != ps->present_event_id) {
// This event doesn't have the right event context, it's not meant
// for us.
goto next;
}
// We only subscribed to the complete notify event.
assert(ev->evtype == XCB_PRESENT_EVENT_COMPLETE_NOTIFY);
handle_present_complete_notify(ps, (void *)ev);
next:
free(ev);
}
}
// Handle queued events before we go to sleep
static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents attr_unused) {
session_t *ps = session_ptr(w, event_check);
handle_present_events(ps);
if (ps->vblank_scheduler) {
vblank_handle_x_events(ps->vblank_scheduler);
}
xcb_generic_event_t *ev;
while ((ev = xcb_poll_for_queued_event(ps->c.c))) {
@ -1694,6 +1702,9 @@ static void handle_pending_updates(EV_P_ struct session *ps) {
}
static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) {
assert(!ps->backend_busy);
assert(ps->render_queued);
struct timespec now;
int64_t draw_callback_enter_us;
clock_gettime(CLOCK_MONOTONIC, &now);
@ -1779,17 +1790,18 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) {
log_trace("paint_preprocess took: %" PRIi64 " us",
after_preprocess_us - after_handle_pending_updates_us);
// If the screen is unredirected, free all_damage to stop painting
// If the screen is unredirected, we don't render anything.
bool did_render = false;
if (ps->redirected && ps->o.stoppaint_force != ON) {
static int paint = 0;
log_trace("Render start, frame %d", paint);
log_verbose("Render start, frame %d", paint);
if (!ps->o.legacy_backends) {
paint_all_new(ps, bottom);
did_render = paint_all_new(ps, bottom);
} else {
paint_all(ps, bottom);
}
log_trace("Render end");
log_verbose("Render end");
ps->first_frame = false;
paint++;
@ -1798,16 +1810,36 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) {
}
}
// With frame pacing, we set backend_busy to true after the end of
// vblank. Without frame pacing, we won't be receiving vblank events, so
// we set backend_busy to false here, right after we issue the render
// commands.
// The other case is if we decided there is no change to render, in that
// case no render command is issued, so we also set backend_busy to
// false.
ps->backend_busy = (ps->frame_pacing && did_render);
ps->next_render = 0;
if (!fade_running) {
ps->fade_time = 0L;
}
ps->render_queued = false;
// TODO(yshui) Investigate how big the X critical section needs to be. There are
// suggestions that rendering should be in the critical section as well.
// Queue redraw if animation is running. This should be picked up by next present
// event.
ps->redraw_needed = animation;
if (animation) {
queue_redraw(ps);
}
if (ps->vblank_scheduler) {
// Even if we might not want to render during next vblank, we want to keep
// `backend_busy` up to date, so when the next render comes, we can
// immediately know if we can render.
vblank_scheduler_schedule(ps->vblank_scheduler, check_render_finish, ps);
}
}
static void draw_callback(EV_P_ ev_timer *w, int revents) {
@ -1974,7 +2006,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
.randr_exists = 0,
.randr_event = 0,
.randr_error = 0,
.present_event_id = XCB_NONE,
.glx_exists = false,
.glx_event = 0,
.glx_error = 0,
@ -2109,17 +2140,8 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
ext_info = xcb_get_extension_data(ps->c.c, &xcb_dpms_id);
ps->dpms_exists = ext_info && ext_info->present;
if (ps->dpms_exists) {
auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL);
if (!r) {
log_fatal("Failed to query DPMS info");
goto err;
}
ps->screen_is_off = dpms_screen_is_off(r);
// Check screen status every half second
ev_timer_init(&ps->dpms_check_timer, check_dpms_status, 0, 0.5);
ev_timer_start(ps->loop, &ps->dpms_check_timer);
free(r);
if (!ps->dpms_exists) {
log_warn("No DPMS extension");
}
// Parse configuration file
@ -2727,7 +2749,6 @@ static void session_destroy(session_t *ps) {
// Stop libev event handlers
ev_timer_stop(ps->loop, &ps->unredir_timer);
ev_timer_stop(ps->loop, &ps->fade_timer);
ev_timer_stop(ps->loop, &ps->dpms_check_timer);
ev_timer_stop(ps->loop, &ps->draw_timer);
ev_prepare_stop(ps->loop, &ps->event_check);
ev_signal_stop(ps->loop, &ps->usr1_signal);

View File

@ -55,26 +55,15 @@ void render_statistics_add_render_time_sample(struct render_statistics *rs, int
/// A `divisor` is also returned, indicating the target framerate. The divisor is
/// the number of vblanks we should wait between each frame. A divisor of 1 means
/// full framerate, 2 means half framerate, etc.
unsigned int
render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor) {
unsigned int render_statistics_get_budget(struct render_statistics *rs) {
if (rs->render_times.nelem < rs->render_times.window_size) {
// No valid render time estimates yet. Assume maximum budget.
*divisor = 1;
return UINT_MAX;
}
// N-th percentile of render times, see render_statistics_init for N.
auto render_time_percentile =
rolling_quantile_estimate(&rs->render_time_quantile, &rs->render_times);
auto vblank_time_us = render_statistics_get_vblank_time(rs);
if (vblank_time_us == 0) {
// We don't have a good estimate of the vblank time yet, so we
// assume we can finish in one vblank.
*divisor = 1;
} else {
*divisor =
(unsigned int)(render_time_percentile / rs->vblank_time_us.mean + 1);
}
return (unsigned int)render_time_percentile;
}

View File

@ -23,11 +23,8 @@ void render_statistics_add_vblank_time_sample(struct render_statistics *rs, int
void render_statistics_add_render_time_sample(struct render_statistics *rs, int time_us);
/// How much time budget we should give to the backend for rendering, in microseconds.
///
/// A `divisor` is also returned, indicating the target framerate. The divisor is
/// the number of vblanks we should wait between each frame. A divisor of 1 means
/// full framerate, 2 means half framerate, etc.
unsigned int
render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor);
unsigned int render_statistics_get_budget(struct render_statistics *rs);
/// Return the measured vblank interval in microseconds. Returns 0 if not enough
/// samples have been collected yet.
unsigned int render_statistics_get_vblank_time(struct render_statistics *rs);

View File

@ -125,14 +125,22 @@ safe_isnan(double a) {
* @param max maximum value
* @return normalized value
*/
static inline int attr_const normalize_i_range(int i, int min, int max) {
if (i > max)
static inline int attr_const attr_unused normalize_i_range(int i, int min, int max) {
if (i > max) {
return max;
if (i < min)
}
if (i < min) {
return min;
}
return i;
}
/// Generic integer abs()
#define iabs(val) \
({ \
__auto_type __tmp = (val); \
__tmp > 0 ? __tmp : -__tmp; \
})
#define min2(a, b) ((a) > (b) ? (b) : (a))
#define max2(a, b) ((a) > (b) ? (a) : (b))
#define min3(a, b, c) min2(a, min2(b, c))
@ -149,10 +157,12 @@ static inline int attr_const normalize_i_range(int i, int min, int max) {
* @return normalized value
*/
static inline double attr_const normalize_d_range(double d, double min, double max) {
if (d > max)
if (d > max) {
return max;
if (d < min)
}
if (d < min) {
return min;
}
return d;
}
@ -162,7 +172,7 @@ static inline double attr_const normalize_d_range(double d, double min, double m
* @param d double value to normalize
* @return normalized value
*/
static inline double attr_const normalize_d(double d) {
static inline double attr_const attr_unused normalize_d(double d) {
return normalize_d_range(d, 0.0, 1.0);
}

541
src/vblank.c Normal file
View File

@ -0,0 +1,541 @@
#include <assert.h>
#include <ev.h>
#include <inttypes.h>
#include <stdatomic.h>
#include <string.h>
#include <time.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include "config.h"
#ifdef CONFIG_OPENGL
// Enable sgi_video_sync_vblank_scheduler
#include <GL/glx.h>
#include <X11/X.h>
#include <X11/Xlib-xcb.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <pthread.h>
#include "backend/gl/glx.h"
#endif
#include "compiler.h"
#include "list.h" // for container_of
#include "log.h"
#include "vblank.h"
#include "x.h"
struct vblank_closure {
vblank_callback_t fn;
void *user_data;
};
#define VBLANK_WIND_DOWN 4
struct vblank_scheduler {
struct x_connection *c;
size_t callback_capacity, callback_count;
struct vblank_closure *callbacks;
struct ev_loop *loop;
/// Request extra vblank events even when no callbacks are scheduled.
/// This is because when callbacks are scheduled too close to a vblank,
/// we might send PresentNotifyMsc request too late and miss the vblank event.
/// So we request extra vblank events right after the last vblank event
/// to make sure this doesn't happen.
unsigned int wind_down;
xcb_window_t target_window;
enum vblank_scheduler_type type;
bool vblank_event_requested;
};
struct present_vblank_scheduler {
struct vblank_scheduler base;
uint64_t last_msc;
/// The timestamp for the end of last vblank.
uint64_t last_ust;
ev_timer callback_timer;
xcb_present_event_t event_id;
xcb_special_event_t *event;
};
struct vblank_scheduler_ops {
size_t size;
void (*init)(struct vblank_scheduler *self);
void (*deinit)(struct vblank_scheduler *self);
void (*schedule)(struct vblank_scheduler *self);
bool (*handle_x_events)(struct vblank_scheduler *self);
};
static void
vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event);
#ifdef CONFIG_OPENGL
struct sgi_video_sync_vblank_scheduler {
struct vblank_scheduler base;
// Since glXWaitVideoSyncSGI blocks, we need to run it in a separate thread.
// ... and all the thread shenanigans that come with it.
_Atomic unsigned int last_msc;
_Atomic uint64_t last_ust;
ev_async notify;
pthread_t sync_thread;
bool running, error;
/// Protects `running`, `error` and `base.vblank_event_requested`
pthread_mutex_t vblank_requested_mtx;
pthread_cond_t vblank_requested_cnd;
};
struct sgi_video_sync_thread_args {
struct sgi_video_sync_vblank_scheduler *self;
int start_status;
pthread_mutex_t start_mtx;
pthread_cond_t start_cnd;
};
static bool check_sgi_video_sync_extension(Display *dpy, int screen) {
const char *glx_ext = glXQueryExtensionsString(dpy, screen);
const char *needle = "GLX_SGI_video_sync";
char *found = strstr(glx_ext, needle);
if (!found) {
return false;
}
if (found != glx_ext && found[-1] != ' ') {
return false;
}
if (found[strlen(needle)] != ' ' && found[strlen(needle)] != '\0') {
return false;
}
glXWaitVideoSyncSGI = (PFNGLXWAITVIDEOSYNCSGIPROC)(void *)glXGetProcAddress(
(const GLubyte *)"glXWaitVideoSyncSGI");
if (!glXWaitVideoSyncSGI) {
return false;
}
return true;
}
static void *sgi_video_sync_thread(void *data) {
auto args = (struct sgi_video_sync_thread_args *)data;
auto self = args->self;
Display *dpy = XOpenDisplay(NULL);
int error_code = 0;
if (!dpy) {
error_code = 1;
goto start_failed;
}
Window root = DefaultRootWindow(dpy), dummy = None;
int screen = DefaultScreen(dpy);
int ncfg = 0;
GLXFBConfig *cfg_ = glXChooseFBConfig(
dpy, screen,
(int[]){GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, 0},
&ncfg);
GLXContext ctx = NULL;
GLXDrawable drawable = None;
if (!cfg_) {
error_code = 2;
goto start_failed;
}
GLXFBConfig cfg = cfg_[0];
XFree(cfg_);
XVisualInfo *vi = glXGetVisualFromFBConfig(dpy, cfg);
if (!vi) {
error_code = 3;
goto start_failed;
}
Visual *visual = vi->visual;
const int depth = vi->depth;
XFree(vi);
Colormap colormap = XCreateColormap(dpy, root, visual, AllocNone);
XSetWindowAttributes attributes;
attributes.colormap = colormap;
dummy = XCreateWindow(dpy, root, 0, 0, 1, 1, 0, depth, InputOutput, visual,
CWColormap, &attributes);
XFreeColormap(dpy, colormap);
if (dummy == None) {
error_code = 4;
goto start_failed;
}
drawable = glXCreateWindow(dpy, cfg, dummy, NULL);
if (drawable == None) {
error_code = 5;
goto start_failed;
}
ctx = glXCreateNewContext(dpy, cfg, GLX_RGBA_TYPE, 0, true);
if (ctx == NULL) {
error_code = 6;
goto start_failed;
}
if (!glXMakeContextCurrent(dpy, drawable, drawable, ctx)) {
error_code = 7;
goto start_failed;
}
if (!check_sgi_video_sync_extension(dpy, screen)) {
error_code = 8;
goto start_failed;
}
pthread_mutex_lock(&args->start_mtx);
args->start_status = 0;
pthread_cond_signal(&args->start_cnd);
pthread_mutex_unlock(&args->start_mtx);
pthread_mutex_lock(&self->vblank_requested_mtx);
while (self->running) {
if (!self->base.vblank_event_requested) {
pthread_cond_wait(&self->vblank_requested_cnd,
&self->vblank_requested_mtx);
continue;
}
pthread_mutex_unlock(&self->vblank_requested_mtx);
unsigned int last_msc;
glXWaitVideoSyncSGI(1, 0, &last_msc);
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
atomic_store(&self->last_msc, last_msc);
atomic_store(&self->last_ust,
(uint64_t)(now.tv_sec * 1000000 + now.tv_nsec / 1000));
ev_async_send(self->base.loop, &self->notify);
pthread_mutex_lock(&self->vblank_requested_mtx);
}
pthread_mutex_unlock(&self->vblank_requested_mtx);
goto cleanup;
start_failed:
pthread_mutex_lock(&args->start_mtx);
args->start_status = error_code;
pthread_cond_signal(&args->start_cnd);
pthread_mutex_unlock(&args->start_mtx);
cleanup:
if (dpy) {
glXMakeCurrent(dpy, None, NULL);
if (ctx) {
glXDestroyContext(dpy, ctx);
}
if (drawable) {
glXDestroyWindow(dpy, drawable);
}
if (dummy) {
XDestroyWindow(dpy, dummy);
}
XCloseDisplay(dpy);
}
return NULL;
}
static void sgi_video_sync_scheduler_schedule(struct vblank_scheduler *base) {
auto self = (struct sgi_video_sync_vblank_scheduler *)base;
log_verbose("Requesting vblank event for msc %d", self->last_msc + 1);
pthread_mutex_lock(&self->vblank_requested_mtx);
assert(!base->vblank_event_requested);
base->vblank_event_requested = true;
pthread_cond_signal(&self->vblank_requested_cnd);
pthread_mutex_unlock(&self->vblank_requested_mtx);
}
static void
sgi_video_sync_scheduler_callback(EV_P attr_unused, ev_async *w, int attr_unused revents) {
auto sched = container_of(w, struct sgi_video_sync_vblank_scheduler, notify);
auto event = (struct vblank_event){
.msc = atomic_load(&sched->last_msc),
.ust = atomic_load(&sched->last_ust),
};
sched->base.vblank_event_requested = false;
log_verbose("Received vblank event for msc %lu", event.msc);
vblank_scheduler_invoke_callbacks(&sched->base, &event);
}
static void sgi_video_sync_scheduler_init(struct vblank_scheduler *base) {
auto self = (struct sgi_video_sync_vblank_scheduler *)base;
auto args = (struct sgi_video_sync_thread_args){
.self = self,
.start_status = -1,
};
pthread_mutex_init(&args.start_mtx, NULL);
pthread_cond_init(&args.start_cnd, NULL);
base->type = VBLANK_SCHEDULER_SGI_VIDEO_SYNC;
ev_async_init(&self->notify, sgi_video_sync_scheduler_callback);
ev_async_start(base->loop, &self->notify);
pthread_mutex_init(&self->vblank_requested_mtx, NULL);
pthread_cond_init(&self->vblank_requested_cnd, NULL);
self->running = true;
pthread_create(&self->sync_thread, NULL, sgi_video_sync_thread, &args);
pthread_mutex_lock(&args.start_mtx);
while (args.start_status == -1) {
pthread_cond_wait(&args.start_cnd, &args.start_mtx);
}
if (args.start_status != 0) {
log_fatal("Failed to start sgi_video_sync_thread, error code: %d",
args.start_status);
abort();
}
pthread_mutex_destroy(&args.start_mtx);
pthread_cond_destroy(&args.start_cnd);
log_info("Started sgi_video_sync_thread");
}
static void sgi_video_sync_scheduler_deinit(struct vblank_scheduler *base) {
auto self = (struct sgi_video_sync_vblank_scheduler *)base;
ev_async_stop(base->loop, &self->notify);
pthread_mutex_lock(&self->vblank_requested_mtx);
self->running = false;
pthread_cond_signal(&self->vblank_requested_cnd);
pthread_mutex_unlock(&self->vblank_requested_mtx);
pthread_join(self->sync_thread, NULL);
pthread_mutex_destroy(&self->vblank_requested_mtx);
pthread_cond_destroy(&self->vblank_requested_cnd);
}
#endif
static void present_vblank_scheduler_schedule(struct vblank_scheduler *base) {
auto self = (struct present_vblank_scheduler *)base;
log_verbose("Requesting vblank event for window 0x%08x, msc %" PRIu64,
base->target_window, self->last_msc + 1);
assert(!base->vblank_event_requested);
x_request_vblank_event(base->c, base->target_window, self->last_msc + 1);
base->vblank_event_requested = true;
}
static void present_vblank_callback(EV_P attr_unused, ev_timer *w, int attr_unused revents) {
auto sched = container_of(w, struct present_vblank_scheduler, callback_timer);
auto event = (struct vblank_event){
.msc = sched->last_msc,
.ust = sched->last_ust,
};
sched->base.vblank_event_requested = false;
vblank_scheduler_invoke_callbacks(&sched->base, &event);
}
static void present_vblank_scheduler_init(struct vblank_scheduler *base) {
auto self = (struct present_vblank_scheduler *)base;
base->type = VBLANK_SCHEDULER_PRESENT;
ev_timer_init(&self->callback_timer, present_vblank_callback, 0, 0);
self->event_id = x_new_id(base->c);
auto select_input =
xcb_present_select_input(base->c->c, self->event_id, base->target_window,
XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY);
set_cant_fail_cookie(base->c, select_input);
self->event =
xcb_register_for_special_xge(base->c->c, &xcb_present_id, self->event_id, NULL);
}
static void present_vblank_scheduler_deinit(struct vblank_scheduler *base) {
auto self = (struct present_vblank_scheduler *)base;
ev_timer_stop(base->loop, &self->callback_timer);
auto select_input =
xcb_present_select_input(base->c->c, self->event_id, base->target_window, 0);
set_cant_fail_cookie(base->c, select_input);
xcb_unregister_for_special_event(base->c->c, self->event);
}
/// Handle PresentCompleteNotify events
///
/// Schedule the registered callback to be called when the current vblank ends.
static void handle_present_complete_notify(struct present_vblank_scheduler *self,
xcb_present_complete_notify_event_t *cne) {
assert(self->base.type == VBLANK_SCHEDULER_PRESENT);
if (cne->kind != XCB_PRESENT_COMPLETE_KIND_NOTIFY_MSC) {
return;
}
assert(self->base.vblank_event_requested);
// X sometimes sends duplicate/bogus MSC events, when screen has just been turned
// off. Don't use the msc value in these events. We treat this as not receiving a
// vblank event at all, and try to get a new one.
//
// See:
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1418
bool event_is_invalid = cne->msc <= self->last_msc || cne->ust == 0;
if (event_is_invalid) {
log_debug("Invalid PresentCompleteNotify event, %" PRIu64 " %" PRIu64,
cne->msc, cne->ust);
x_request_vblank_event(self->base.c, cne->window, self->last_msc + 1);
return;
}
self->last_ust = cne->ust;
self->last_msc = cne->msc;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
auto now_us = (unsigned long)(now.tv_sec * 1000000L + now.tv_nsec / 1000);
double delay_sec = 0.0;
if (now_us < cne->ust) {
log_trace("The end of this vblank is %lu us into the "
"future",
cne->ust - now_us);
delay_sec = (double)(cne->ust - now_us) / 1000000.0;
}
// Wait until the end of the current vblank to invoke callbacks. If we
// call it too early, it can mistakenly think the render missed the
// vblank, and doesn't schedule render for the next vblank, causing frame
// drops.
assert(!ev_is_active(&self->callback_timer));
ev_timer_set(&self->callback_timer, delay_sec, 0);
ev_timer_start(self->base.loop, &self->callback_timer);
}
static bool handle_present_events(struct vblank_scheduler *base) {
auto self = (struct present_vblank_scheduler *)base;
xcb_present_generic_event_t *ev;
while ((ev = (void *)xcb_poll_for_special_event(base->c->c, self->event))) {
if (ev->event != self->event_id) {
// This event doesn't have the right event context, it's not meant
// for us.
goto next;
}
// We only subscribed to the complete notify event.
assert(ev->evtype == XCB_PRESENT_EVENT_COMPLETE_NOTIFY);
handle_present_complete_notify(self, (void *)ev);
next:
free(ev);
}
return true;
}
static const struct vblank_scheduler_ops vblank_scheduler_ops[LAST_VBLANK_SCHEDULER] = {
[VBLANK_SCHEDULER_PRESENT] =
{
.size = sizeof(struct present_vblank_scheduler),
.init = present_vblank_scheduler_init,
.deinit = present_vblank_scheduler_deinit,
.schedule = present_vblank_scheduler_schedule,
.handle_x_events = handle_present_events,
},
#ifdef CONFIG_OPENGL
[VBLANK_SCHEDULER_SGI_VIDEO_SYNC] =
{
.size = sizeof(struct sgi_video_sync_vblank_scheduler),
.init = sgi_video_sync_scheduler_init,
.deinit = sgi_video_sync_scheduler_deinit,
.schedule = sgi_video_sync_scheduler_schedule,
.handle_x_events = NULL,
},
#endif
};
static void vblank_scheduler_schedule_internal(struct vblank_scheduler *self) {
assert(self->type < LAST_VBLANK_SCHEDULER);
auto fn = vblank_scheduler_ops[self->type].schedule;
assert(fn != NULL);
fn(self);
}
bool vblank_scheduler_schedule(struct vblank_scheduler *self,
vblank_callback_t vblank_callback, void *user_data) {
if (self->callback_count == 0 && self->wind_down == 0) {
vblank_scheduler_schedule_internal(self);
}
if (self->callback_count == self->callback_capacity) {
size_t new_capacity =
self->callback_capacity ? self->callback_capacity * 2 : 1;
void *new_buffer =
realloc(self->callbacks, new_capacity * sizeof(*self->callbacks));
if (!new_buffer) {
return false;
}
self->callbacks = new_buffer;
self->callback_capacity = new_capacity;
}
self->callbacks[self->callback_count++] = (struct vblank_closure){
.fn = vblank_callback,
.user_data = user_data,
};
return true;
}
static void
vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event) {
// callbacks might be added during callback invocation, so we need to
// copy the callback_count.
size_t count = self->callback_count, write_head = 0;
if (count == 0) {
self->wind_down--;
} else {
self->wind_down = VBLANK_WIND_DOWN;
}
for (size_t i = 0; i < count; i++) {
auto action = self->callbacks[i].fn(event, self->callbacks[i].user_data);
switch (action) {
case VBLANK_CALLBACK_AGAIN:
if (i != write_head) {
self->callbacks[write_head] = self->callbacks[i];
}
write_head++;
case VBLANK_CALLBACK_DONE:
default: // nothing to do
break;
}
}
memset(self->callbacks + write_head, 0,
(count - write_head) * sizeof(*self->callbacks));
assert(count == self->callback_count && "callbacks should not be added when "
"callbacks are being invoked.");
self->callback_count = write_head;
if (self->callback_count || self->wind_down) {
vblank_scheduler_schedule_internal(self);
}
}
void vblank_scheduler_free(struct vblank_scheduler *self) {
assert(self->type < LAST_VBLANK_SCHEDULER);
auto fn = vblank_scheduler_ops[self->type].deinit;
if (fn != NULL) {
fn(self);
}
free(self->callbacks);
free(self);
}
struct vblank_scheduler *
vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c,
xcb_window_t target_window, enum vblank_scheduler_type type) {
size_t object_size = vblank_scheduler_ops[type].size;
auto init_fn = vblank_scheduler_ops[type].init;
if (!object_size || !init_fn) {
log_error("Unsupported or invalid vblank scheduler type: %d", type);
return NULL;
}
assert(object_size >= sizeof(struct vblank_scheduler));
struct vblank_scheduler *self = calloc(1, object_size);
self->target_window = target_window;
self->c = c;
self->loop = loop;
init_fn(self);
return self;
}
bool vblank_handle_x_events(struct vblank_scheduler *self) {
assert(self->type < LAST_VBLANK_SCHEDULER);
auto fn = vblank_scheduler_ops[self->type].handle_x_events;
if (fn != NULL) {
return fn(self);
}
return true;
}

47
src/vblank.h Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <xcb/present.h>
#include <xcb/xcb.h>
#include <ev.h>
#include <xcb/xproto.h>
#include "config.h"
#include "x.h"
/// An object that schedule vblank events.
struct vblank_scheduler;
struct vblank_event {
uint64_t msc;
uint64_t ust;
};
enum vblank_callback_action {
/// The callback should be called again in the next vblank.
VBLANK_CALLBACK_AGAIN,
/// The callback is done and should not be called again.
VBLANK_CALLBACK_DONE,
};
typedef enum vblank_callback_action (*vblank_callback_t)(struct vblank_event *event,
void *user_data);
/// Schedule a vblank event.
///
/// Schedule for `cb` to be called when the current vblank ends. If this is called
/// from a callback function for the current vblank, the newly scheduled callback
/// will be called in the next vblank.
///
/// Returns whether the scheduling is successful. Scheduling can fail if there
/// is not enough memory.
bool vblank_scheduler_schedule(struct vblank_scheduler *self, vblank_callback_t cb,
void *user_data);
struct vblank_scheduler *
vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c,
xcb_window_t target_window, enum vblank_scheduler_type type);
void vblank_scheduler_free(struct vblank_scheduler *);
bool vblank_handle_x_events(struct vblank_scheduler *self);

27
src/x.c
View File

@ -9,7 +9,9 @@
#include <pixman.h>
#include <xcb/composite.h>
#include <xcb/damage.h>
#include <xcb/dpms.h>
#include <xcb/glx.h>
#include <xcb/present.h>
#include <xcb/randr.h>
#include <xcb/render.h>
#include <xcb/sync.h>
@ -777,6 +779,31 @@ err:
return false;
}
void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc) {
auto cookie = xcb_present_notify_msc(c->c, window, 0, msc, 1, 0);
set_cant_fail_cookie(c, cookie);
}
static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) {
// state is a bool indicating whether dpms is enabled
return info->state && (info->power_level != XCB_DPMS_DPMS_MODE_ON);
}
bool x_check_dpms_status(struct x_connection *c, bool *screen_is_off) {
auto r = xcb_dpms_info_reply(c->c, xcb_dpms_info(c->c), NULL);
if (!r) {
log_error("Failed to query DPMS status.");
return false;
}
auto now_screen_is_off = dpms_screen_is_off(r);
if (*screen_is_off != now_screen_is_off) {
log_debug("Screen is now %s", now_screen_is_off ? "off" : "on");
*screen_is_off = now_screen_is_off;
}
free(r);
return true;
}
/**
* Convert a struct conv to a X picture convolution filter, normalizing the kernel
* in the process. Allow the caller to specify the element at the center of the kernel,

View File

@ -419,3 +419,11 @@ void x_update_monitors(struct x_connection *, struct x_monitors *);
void x_free_monitor_info(struct x_monitors *);
uint32_t attr_deprecated xcb_generate_id(xcb_connection_t *c);
/// Ask X server to send us a notification for the next end of vblank.
void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc);
/// Update screen_is_off to reflect the current DPMS state.
///
/// Returns true if the DPMS state was successfully queried, false otherwise.
bool x_check_dpms_status(struct x_connection *c, bool *screen_is_off);