10 Commits

Author SHA1 Message Date
Yuxuan Shui
4574977287 vblank: winding down vblank events instead of stopping immediately
I noticed sometimes full frame rate video is rendered at half frame rate
sometimes. That is because the damage notify is sent very close to
vblank, and since we request vblank events when we get the damage, we
miss the vblank event immediately after, despite the damage happening
before the vblank.

       request  next  ......  next next
damage  vblank vblank          vblank
   |    |       |     ......      |
   v    v       v                 v
---------------------->>>>>>---------

`request vblank` is triggered by `damage`, but because it's too close to
`next vblank`, that vblank is missed despite we requested before it
happening, and we only get `next next vblank`. The result is we will
drop every other frame.

The solution in this commit is that we will keep requesting vblank
events, right after we received a vblank event, even when nobody is
asking for them. We would do that for a set number of vblanks before
stopping (currently 4).

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:58:11 +00:00
Yuxuan Shui
58150dfe55 x: fix x_request_vblank_event
present_notify_msc with divisor == 0 has undocumented special meaning,
it means async present notify, which means we could receive MSC
notifications from the past.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:58:10 +00:00
Yuxuan Shui
91a0ccc391 core: simplify pacing logic a bit more
Also, vblank event callback should call schedule_render to queue renders
instead of starting the draw timer directly, so that the CPU time
calculation will be correct.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:58:09 +00:00
Yuxuan Shui
8e35b33458 config: add debug options to enable timing based pacing
Disable timing estimation based pacing by default, as it might not work
well across drivers, and might have subtle bugs.

You can try setting `PICOM_DEBUG=smart_frame_pacing` if you want to try
it out.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:58:07 +00:00
Yuxuan Shui
5bcd34449c core: refactor frame pacing
Factored out vblank event generation, add abstraction over how vblank
events are generated. The goal is so we can request vblank events in
different ways based on the driver we are running on.

Tried to simplify the frame scheduling logic, we will see if I succeeded
or not.

Also, the logic to exclude vblank events for vblank interval estimation
when the screen off is dropped. It's too hard to get right, we need to
find something robust.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:57:56 +00:00
Yuxuan Shui
5d94b2a054 config: add a debug environment variable
This is where we keep temporary, short living, private debug options.

Adding and removing command line and config file options are
troublesome, and we don't want people adding these to their config
files.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 10:36:03 +00:00
Yuxuan Shui
3e8af9fb88 core: don't unredir when display is turned off
We unredirect because we receive bad vblank events, and also vblank
events at a different interval compared to when the screen is on. But it
is enough to just not record the vblank interval statistics when the
screen is off.

Although, unredirecting when display is off can also fix the problem
where use-damage causes the screen to flicker when the display is turned
off then back on. So we need something else for that.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 09:59:47 +00:00
Yuxuan Shui
2bc180c2a7 core: don't request vblank events when we are not rendering
Previously everytime we receive a vblank event, we always request a new
one. This made the logic somewhat simpler. But this generated many
useless vblank events, and wasted power. We only need vblank events for
two things:

1. after we rendered a frame, we need to know when it has been displayed
   on the screen.
2. estimating the refresh rate.

This commit makes sure we only request vblank events when it's actually
needed.

Fixes #1079

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 09:59:46 +00:00
Yuxuan Shui
580889488f core: simplify the pacing logic a little bit
Make it simpler to stop requesting PresentCompleteNotify when there is
nothing to render.

Related: #1079

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-19 09:59:43 +00:00
Yuxuan Shui
ce160cf432 core: don't call schedule_render too early
I mistakenly assumed that PresentCompleteNotify event signifies the end
of a vblank (or the start of scanout). But actually this event can in
theory in sent at any point during a vblank, with its timestamp pointing
to when the end of vblank is. (that's why we often find the timestamp to
be in the future).

Add a delay so schedule_render is actually called at the end of vblank,
so it doesn't mistakenly think the render is too slow to complete.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2023-12-18 04:37:03 +00:00
14 changed files with 720 additions and 287 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

@@ -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>
@@ -615,6 +617,74 @@ char *locate_auxiliary_file(const char *scope, const char *path, const char *inc
return ret;
}
struct debug_options_entry {
const char *name;
const char **choices;
size_t offset;
};
static const struct debug_options_entry debug_options_entries[] = {
"smart_frame_pacing",
NULL,
offsetof(struct debug_options, smart_frame_pacing),
};
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 = {};
*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.
*/
@@ -817,5 +887,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,23 @@ 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,
};
/// 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;
};
/// Structure representing all options.
typedef struct options {
// === Debugging ===
@@ -262,6 +279,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 = []

View File

@@ -21,6 +21,7 @@
#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 +65,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 +124,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,105 +144,205 @@ static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t
return w;
}
void collect_vblank_interval_statistics(struct vblank_event *e, void *ud) {
auto ps = (session_t *)ud;
assert(ps->frame_pacing);
assert(ps->vblank_scheduler);
if (ps->last_msc == e->msc) {
// Already collected statistics for this vblank
return;
}
if (!ps->o.debug_options.smart_frame_pacing) {
// We don't need to collect statistics if we are not doing smart frame
// pacing.
return;
}
// 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.
if (ps->last_msc_instant != 0) {
auto frame_count = e->msc - ps->last_msc;
int 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;
double 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.
vblank_scheduler_schedule(ps->vblank_scheduler,
collect_vblank_interval_statistics, ud);
}
}
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.
void 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);
if (ps->backend_busy) {
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);
vblank_scheduler_schedule(ps->vblank_scheduler,
reschedule_render_at_vblank, ud);
return;
}
// 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;
}
schedule_render(ps, false);
}
/// 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;
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;
ps->next_render = now_us;
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 not doing frame pacing, schedule a render immediately unless it's
// 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;
}
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);
}
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;
// 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, &divisor);
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;
goto schedule;
}
@@ -271,14 +352,11 @@ void schedule_render(session_t *ps, bool triggered_by_vblank) {
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 +364,42 @@ 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) {
ps->render_queued = true;
if (ps->o.debug_options.smart_frame_pacing && ps->vblank_scheduler) {
// Make we schedule_render call is synced with vblank events.
// See the comment on schedule_render for more details.
if (!vblank_scheduler_schedule(ps->vblank_scheduler,
reschedule_render_at_vblank, ps)) {
// TODO(yshui): handle error here
abort();
}
} else {
schedule_render(ps, false);
}
ps->redraw_needed = true;
}
/**
@@ -1013,19 +1095,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) {
@@ -1407,24 +1476,19 @@ static bool redirect_start(session_t *ps) {
}
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);
// 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);
ps->vblank_scheduler =
vblank_scheduler_new(ps->loop, &ps->c, session_get_target_window(ps));
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;
@@ -1472,12 +1536,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 +1548,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 +1675,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 +1763,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 +1783,30 @@ 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);
}
}
static void draw_callback(EV_P_ ev_timer *w, int revents) {
@@ -1974,7 +1973,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 +2107,9 @@ 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_fatal("No DPMS extension");
exit(1);
}
// Parse configuration file
@@ -2727,7 +2717,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

@@ -30,4 +30,6 @@ void render_statistics_add_render_time_sample(struct render_statistics *rs, int
unsigned int
render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor);
/// 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);
}

259
src/vblank.c Normal file
View File

@@ -0,0 +1,259 @@
#include <assert.h>
#include <ev.h>
#include <inttypes.h>
#include <string.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include "compiler.h"
#include "config.h"
#include "list.h" // for container_of
#include "log.h"
#include "vblank.h"
#include "x.h"
struct vblank_callback {
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_callback *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 {
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);
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] =
{
.init = present_vblank_scheduler_init,
.deinit = present_vblank_scheduler_deinit,
.schedule = present_vblank_scheduler_schedule,
.handle_x_events = handle_present_events,
},
};
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_callback){
.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;
if (count == 0) {
self->wind_down--;
} else {
self->wind_down = VBLANK_WIND_DOWN;
}
for (size_t i = 0; i < count; i++) {
self->callbacks[i].fn(event, self->callbacks[i].user_data);
}
// remove the callbacks that we have called, keep the newly added ones.
memmove(self->callbacks, self->callbacks + count,
(self->callback_count - count) * sizeof(*self->callbacks));
self->callback_count -= count;
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) {
struct vblank_scheduler *self = calloc(1, sizeof(struct present_vblank_scheduler));
self->target_window = target_window;
self->c = c;
self->loop = loop;
vblank_scheduler_ops[VBLANK_SCHEDULER_PRESENT].init(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;
}

37
src/vblank.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <xcb/present.h>
#include <xcb/xcb.h>
#include <ev.h>
#include <xcb/xproto.h>
#include "x.h"
/// An object that schedule vblank events.
struct vblank_scheduler;
struct vblank_event {
uint64_t msc;
uint64_t ust;
};
typedef void (*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);
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);