mirror of https://github.com/yshui/picom.git
Merge pull request #1156 from yshui/pacing-fixes
This commit is contained in:
commit
148e61a0b2
|
@ -82,13 +82,17 @@ void handle_device_reset(session_t *ps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// paint all windows
|
/// 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();
|
struct timespec now = get_time_timespec();
|
||||||
auto paint_all_start_us =
|
auto paint_all_start_us =
|
||||||
(uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000;
|
(uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000;
|
||||||
if (ps->backend_data->ops->device_status &&
|
if (ps->backend_data->ops->device_status &&
|
||||||
ps->backend_data->ops->device_status(ps->backend_data) != DEVICE_STATUS_NORMAL) {
|
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->o.xrender_sync_fence) {
|
||||||
if (ps->xsync_exists && !x_fence_sync(&ps->c, ps->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(®_damage)) {
|
if (!pixman_region32_not_empty(®_damage)) {
|
||||||
pixman_region32_fini(®_damage);
|
pixman_region32_fini(®_damage);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DEBUG_REPAINT
|
#ifdef DEBUG_REPAINT
|
||||||
|
@ -190,7 +194,7 @@ 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;
|
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);
|
log_trace("Getting damage took %" PRIu64 " us", after_damage_us - after_sync_fence_us);
|
||||||
if (ps->next_render > 0) {
|
if (ps->next_render > 0) {
|
||||||
log_trace("Render schedule deviation: %ld us (%s) %" PRIu64 " %ld",
|
log_verbose("Render schedule deviation: %ld us (%s) %" PRIu64 " %ld",
|
||||||
labs((long)after_damage_us - (long)ps->next_render),
|
labs((long)after_damage_us - (long)ps->next_render),
|
||||||
after_damage_us < ps->next_render ? "early" : "late",
|
after_damage_us < ps->next_render ? "early" : "late",
|
||||||
after_damage_us, ps->next_render);
|
after_damage_us, ps->next_render);
|
||||||
|
@ -199,7 +203,6 @@ void paint_all_new(session_t *ps, struct managed_win *t) {
|
||||||
ps->last_schedule_delay = 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) {
|
if (ps->backend_data->ops->prepare) {
|
||||||
ps->backend_data->ops->prepare(ps->backend_data, ®_paint);
|
ps->backend_data->ops->prepare(ps->backend_data, ®_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.
|
// on top of that window. This is used to reduce the number of pixels painted.
|
||||||
//
|
//
|
||||||
// Whether this is beneficial is to be determined XXX
|
// 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(®_visible, &ps->screen_reg, w->reg_ignore);
|
pixman_region32_subtract(®_visible, &ps->screen_reg, w->reg_ignore);
|
||||||
assert(!(w->flags & WIN_FLAGS_IMAGE_ERROR));
|
assert(!(w->flags & WIN_FLAGS_IMAGE_ERROR));
|
||||||
assert(!(w->flags & WIN_FLAGS_PIXMAP_STALE));
|
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)
|
for (win *w = t; w; w = w->prev_trans)
|
||||||
log_trace(" %#010lx", w->id);
|
log_trace(" %#010lx", w->id);
|
||||||
#endif
|
#endif
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// vim: set noet sw=8 ts=8 :
|
// vim: set noet sw=8 ts=8 :
|
||||||
|
|
|
@ -366,4 +366,8 @@ struct backend_operations {
|
||||||
|
|
||||||
extern struct backend_operations *backend_list[];
|
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);
|
||||||
|
|
|
@ -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 detect_driver(xcb_connection_t *c, backend_t *backend_data, xcb_window_t window) {
|
||||||
enum driver ret = 0;
|
enum driver ret = 0;
|
||||||
// First we try doing backend agnostic detection using RANDR
|
// First we try doing backend agnostic detection using RANDR
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <xcb/xcb.h>
|
#include <xcb/xcb.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
struct session;
|
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.
|
/// Apply driver specified global workarounds. It's safe to call this multiple times.
|
||||||
void apply_driver_workarounds(struct session *ps, enum driver);
|
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
|
// Print driver names to stdout, for diagnostics
|
||||||
static inline void print_drivers(enum driver drivers) {
|
static inline void print_drivers(enum driver drivers) {
|
||||||
|
|
27
src/common.h
27
src/common.h
|
@ -139,8 +139,6 @@ typedef struct session {
|
||||||
// === Event handlers ===
|
// === Event handlers ===
|
||||||
/// ev_io for X connection
|
/// ev_io for X connection
|
||||||
ev_io xiow;
|
ev_io xiow;
|
||||||
/// Timer for checking DPMS power level
|
|
||||||
ev_timer dpms_check_timer;
|
|
||||||
/// Timeout for delayed unredirection.
|
/// Timeout for delayed unredirection.
|
||||||
ev_timer unredir_timer;
|
ev_timer unredir_timer;
|
||||||
/// Timer for fading
|
/// Timer for fading
|
||||||
|
@ -214,26 +212,19 @@ typedef struct session {
|
||||||
bool first_frame;
|
bool first_frame;
|
||||||
/// Whether screen has been turned off
|
/// Whether screen has been turned off
|
||||||
bool screen_is_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.
|
/// When last MSC event happened, in useconds.
|
||||||
uint64_t last_msc_instant;
|
uint64_t last_msc_instant;
|
||||||
/// The last MSC number
|
/// The last MSC number
|
||||||
uint64_t last_msc;
|
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 delay between when the last frame was scheduled to be rendered, and when
|
||||||
/// the render actually started.
|
/// the render actually started.
|
||||||
uint64_t last_schedule_delay;
|
uint64_t last_schedule_delay;
|
||||||
/// When do we want our next frame to start rendering.
|
/// When do we want our next frame to start rendering.
|
||||||
uint64_t next_render;
|
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.
|
/// Whether we can perform frame pacing.
|
||||||
bool frame_pacing;
|
bool frame_pacing;
|
||||||
|
/// Vblank event scheduler
|
||||||
|
struct vblank_scheduler *vblank_scheduler;
|
||||||
|
|
||||||
/// Render statistics
|
/// Render statistics
|
||||||
struct render_statistics render_stats;
|
struct render_statistics render_stats;
|
||||||
|
@ -245,8 +236,18 @@ typedef struct session {
|
||||||
options_t o;
|
options_t o;
|
||||||
/// Whether we have hit unredirection timeout.
|
/// Whether we have hit unredirection timeout.
|
||||||
bool tmout_unredir_hit;
|
bool tmout_unredir_hit;
|
||||||
/// Whether we need to redraw the screen
|
/// If the backend is busy. This means two things:
|
||||||
bool redraw_needed;
|
/// 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.
|
/// Cache a xfixes region so we don't need to allocate it every time.
|
||||||
/// A workaround for yshui/picom#301
|
/// A workaround for yshui/picom#301
|
||||||
|
|
79
src/config.c
79
src/config.c
|
@ -8,6 +8,8 @@
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
@ -617,6 +619,82 @@ char *locate_auxiliary_file(const char *scope, const char *path, const char *inc
|
||||||
return NULL;
|
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.
|
* 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)hasneg;
|
||||||
(void)winopt_mask;
|
(void)winopt_mask;
|
||||||
#endif
|
#endif
|
||||||
|
parse_debug_options(&opt->debug_options);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
23
src/config.h
23
src/config.h
|
@ -73,6 +73,27 @@ enum blur_method {
|
||||||
|
|
||||||
typedef struct _c2_lptr c2_lptr_t;
|
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.
|
/// Structure representing all options.
|
||||||
typedef struct options {
|
typedef struct options {
|
||||||
// === Debugging ===
|
// === Debugging ===
|
||||||
|
@ -262,6 +283,8 @@ typedef struct options {
|
||||||
c2_lptr_t *transparent_clipping_blacklist;
|
c2_lptr_t *transparent_clipping_blacklist;
|
||||||
|
|
||||||
bool dithered_present;
|
bool dithered_present;
|
||||||
|
|
||||||
|
struct debug_options debug_options;
|
||||||
} options_t;
|
} options_t;
|
||||||
|
|
||||||
extern const char *const BACKEND_STRS[NUM_BKEND + 1];
|
extern const char *const BACKEND_STRS[NUM_BKEND + 1];
|
||||||
|
|
|
@ -716,7 +716,8 @@ void ev_handle(session_t *ps, xcb_generic_event_t *ev) {
|
||||||
// XXX redraw needs to be more fine grained
|
// XXX redraw needs to be more fine grained
|
||||||
queue_redraw(ps);
|
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) {
|
switch (ev->response_type) {
|
||||||
case FocusIn: ev_focus_in(ps, (xcb_focus_in_event_t *)ev); break;
|
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;
|
case FocusOut: ev_focus_out(ps, (xcb_focus_out_event_t *)ev); break;
|
||||||
|
|
|
@ -9,7 +9,8 @@ base_deps = [
|
||||||
|
|
||||||
srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c',
|
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',
|
'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('.')
|
picom_inc = include_directories('.')
|
||||||
|
|
||||||
cflags = []
|
cflags = []
|
||||||
|
@ -58,7 +59,7 @@ endif
|
||||||
|
|
||||||
if get_option('opengl')
|
if get_option('opengl')
|
||||||
cflags += ['-DCONFIG_OPENGL', '-DGL_GLEXT_PROTOTYPES']
|
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' ]
|
srcs += [ 'opengl.c' ]
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
551
src/picom.c
551
src/picom.c
|
@ -16,11 +16,13 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
#include <math.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/resource.h>
|
#include <sys/resource.h>
|
||||||
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <xcb/composite.h>
|
#include <xcb/composite.h>
|
||||||
#include <xcb/damage.h>
|
#include <xcb/damage.h>
|
||||||
|
@ -64,6 +66,7 @@
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "statistics.h"
|
#include "statistics.h"
|
||||||
#include "uthash_extra.h"
|
#include "uthash_extra.h"
|
||||||
|
#include "vblank.h"
|
||||||
|
|
||||||
/// Get session_t pointer from a pointer to a member of session_t
|
/// Get session_t pointer from a pointer to a member of session_t
|
||||||
#define session_ptr(ptr, member) \
|
#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;
|
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.
|
* Find matched window.
|
||||||
*
|
*
|
||||||
|
@ -163,122 +145,248 @@ static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t
|
||||||
return w;
|
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.
|
/// How many seconds into the future should we start rendering the next frame.
|
||||||
///
|
///
|
||||||
/// Renders are scheduled like this:
|
/// Renders are scheduled like this:
|
||||||
///
|
///
|
||||||
/// 1. queue_redraw() registers the intention to render. redraw_needed is set to true to
|
/// 1. queue_redraw() queues a new render by calling schedule_render, if there
|
||||||
/// indicate what is on screen needs to be updated.
|
/// is no render currently scheduled. i.e. render_queued == false.
|
||||||
/// 2. then, we need to figure out the best time to start rendering. first, we need to
|
/// 2. then, we need to figure out the best time to start rendering. we need to
|
||||||
/// know when the next frame will be displayed on screen. we have this information from
|
/// at least know when the next vblank will start, as we can't start render
|
||||||
/// the Present extension: we know when was the last frame displayed, and we know the
|
/// before the current rendered frame is diplayed on screen. we have this
|
||||||
/// refresh rate. so we can calculate the next frame's display time. if our render time
|
/// information from the vblank scheduler, it will notify us when that happens.
|
||||||
/// estimation shows we could miss that target, we push the target back one frame.
|
/// we might also want to delay the rendering even further to reduce latency,
|
||||||
/// 3. if there is already render completed for that target frame, or there is a render
|
/// this is discussed below, in FUTURE WORKS.
|
||||||
/// currently underway, we don't do anything, and wait for the next Present Complete
|
/// 3. we schedule a render for that target point in time.
|
||||||
/// Notify event to try to schedule again.
|
/// 4. draw_callback() is called at the schedule time (i.e. when scheduled
|
||||||
/// 4. otherwise, we schedule a render for that target frame. we use past statistics about
|
/// vblank event is delivered). Backend APIs are called to issue render
|
||||||
/// how long our renders took to figure out when to start rendering. we start rendering
|
/// commands. render_queued is set to false, and backend_busy is set to true.
|
||||||
/// at the latest point of time possible to still hit the target frame.
|
|
||||||
///
|
///
|
||||||
/// The `triggered_by_timer` parameter is used to indicate whether this function is
|
/// There are some considerations in step 2:
|
||||||
/// 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
|
/// First of all, a vblank event being delivered
|
||||||
/// something changed and schedule_render is triggered by a DamageNotify. The idea is that
|
/// doesn't necessarily mean the frame has been displayed on screen. If a frame
|
||||||
/// when the schedule is triggered by a steady timer, schedule_render will be called at a
|
/// takes too long to render, it might miss the current vblank, and will be
|
||||||
/// predictable offset into each vblank.
|
/// 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;
|
double delay_s = 0;
|
||||||
ps->next_render = 0;
|
unsigned int divisor = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct timespec now;
|
struct timespec now;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
auto now_us = (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_nsec / 1000;
|
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 =
|
// if ps->o.debug_options.smart_frame_pacing is false, we won't have any render
|
||||||
(int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L);
|
// time or vblank interval estimates, so we would naturally fallback to schedule
|
||||||
if (ps->target_msc == ps->last_msc) {
|
// render immediately.
|
||||||
// The frame has just been displayed, record its render time;
|
auto render_budget = render_statistics_get_budget(&ps->render_stats);
|
||||||
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);
|
|
||||||
auto frame_time = render_statistics_get_vblank_time(&ps->render_stats);
|
auto frame_time = render_statistics_get_vblank_time(&ps->render_stats);
|
||||||
if (frame_time == 0) {
|
if (frame_time == 0) {
|
||||||
// We don't have enough data for render time estimates, maybe there's
|
// 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
|
// no frame rendered yet, or the backend doesn't support render timing
|
||||||
// information, schedule render immediately.
|
// information, schedule render immediately.
|
||||||
ps->target_msc = ps->last_msc + 1;
|
log_verbose("Not enough data for render time estimates.");
|
||||||
goto schedule;
|
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;
|
unsigned int available = 0;
|
||||||
if (deadline > now_us) {
|
if (deadline > now_us) {
|
||||||
available = (unsigned int)(deadline - now_us);
|
available = (unsigned int)(deadline - now_us);
|
||||||
}
|
}
|
||||||
|
|
||||||
ps->target_msc = ps->last_msc + divisor;
|
|
||||||
if (available > render_budget) {
|
if (available > render_budget) {
|
||||||
delay_s = (double)(available - render_budget) / 1000000.0;
|
delay_s = (double)(available - render_budget) / 1000000.0;
|
||||||
ps->next_render = deadline - render_budget;
|
ps->next_render = deadline - render_budget;
|
||||||
} else {
|
|
||||||
delay_s = 0;
|
|
||||||
ps->next_render = now_us;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delay_s > 1) {
|
if (delay_s > 1) {
|
||||||
log_warn("Delay too long: %f s, render_budget: %d us, frame_time: "
|
log_warn("Delay too long: %f s, render_budget: %d us, frame_time: "
|
||||||
"%" PRIu32 " us, now_us: %" PRIu64 " us, next_msc: %" PRIu64 " u"
|
"%" PRIu32 " us, now_us: %" PRIu64 " us, next_msc: %" PRIu64 " u"
|
||||||
|
@ -286,39 +394,33 @@ void schedule_render(session_t *ps, bool triggered_by_vblank) {
|
||||||
delay_s, render_budget, frame_time, now_us, deadline);
|
delay_s, render_budget, frame_time, now_us, deadline);
|
||||||
}
|
}
|
||||||
|
|
||||||
log_trace("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, frame_time: "
|
log_verbose("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, "
|
||||||
"%" PRIu32 ", now_us: %" PRIu64 ", next_msc: %" PRIu64 ", "
|
"frame_time: %" PRIu32 ", now_us: %" PRIu64 ", next_render: %" PRIu64
|
||||||
"target_msc: %" PRIu64 ", divisor: %d",
|
", next_msc: %" PRIu64 ", divisor: "
|
||||||
|
"%d",
|
||||||
delay_s, ps->last_msc_instant, render_budget, frame_time, now_us,
|
delay_s, ps->last_msc_instant, render_budget, frame_time, now_us,
|
||||||
deadline, ps->target_msc, divisor);
|
ps->next_render, deadline, divisor);
|
||||||
|
|
||||||
schedule:
|
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));
|
assert(!ev_is_active(&ps->draw_timer));
|
||||||
ev_timer_set(&ps->draw_timer, delay_s, 0);
|
ev_timer_set(&ps->draw_timer, delay_s, 0);
|
||||||
ev_timer_start(ps->loop, &ps->draw_timer);
|
ev_timer_start(ps->loop, &ps->draw_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void queue_redraw(session_t *ps) {
|
void queue_redraw(session_t *ps) {
|
||||||
if (ps->screen_is_off) {
|
log_verbose("Queue redraw, render_queued: %d, backend_busy: %d",
|
||||||
// The screen is off, if there is a draw queued for the next frame (i.e.
|
ps->render_queued, ps->backend_busy);
|
||||||
// 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
|
if (ps->render_queued) {
|
||||||
// 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Whether we have already rendered for the current frame.
|
ps->render_queued = true;
|
||||||
// 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);
|
schedule_render(ps, false);
|
||||||
}
|
}
|
||||||
ps->redraw_needed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a region of the screen size.
|
* Get a region of the screen size.
|
||||||
|
@ -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,
|
// If there's no window to paint, and the screen isn't redirected,
|
||||||
// don't redirect it.
|
// don't redirect it.
|
||||||
unredir_possible = true;
|
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 (unredir_possible) {
|
||||||
if (ps->redirected) {
|
if (ps->redirected) {
|
||||||
|
@ -1397,7 +1486,7 @@ static bool redirect_start(session_t *ps) {
|
||||||
pixman_region32_init(&ps->damage_ring[i]);
|
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) &&
|
if ((ps->o.legacy_backends || ps->o.benchmark || !ps->backend_data->ops->last_render_time) &&
|
||||||
ps->frame_pacing) {
|
ps->frame_pacing) {
|
||||||
// Disable frame pacing if we are using a legacy backend or if we are in
|
// 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;
|
ps->frame_pacing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps->present_exists && ps->frame_pacing) {
|
// Re-detect driver since we now have a backend
|
||||||
ps->present_event_id = x_new_id(&ps->c);
|
ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root);
|
||||||
auto select_input = xcb_present_select_input(
|
apply_driver_workarounds(ps, ps->drivers);
|
||||||
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);
|
|
||||||
|
|
||||||
|
if (ps->present_exists && ps->frame_pacing) {
|
||||||
// Initialize rendering and frame timing statistics, and frame pacing
|
// Initialize rendering and frame timing statistics, and frame pacing
|
||||||
// states.
|
// states.
|
||||||
ps->last_msc_instant = 0;
|
ps->last_msc_instant = 0;
|
||||||
ps->last_msc = 0;
|
ps->last_msc = 0;
|
||||||
ps->last_schedule_delay = 0;
|
ps->last_schedule_delay = 0;
|
||||||
ps->target_msc = 0;
|
|
||||||
render_statistics_reset(&ps->render_stats);
|
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) {
|
} else if (ps->frame_pacing) {
|
||||||
log_error("Present extension is not supported, frame pacing disabled.");
|
log_error("Present extension is not supported, frame pacing disabled.");
|
||||||
ps->frame_pacing = false;
|
ps->frame_pacing = false;
|
||||||
|
@ -1436,10 +1531,6 @@ static bool redirect_start(session_t *ps) {
|
||||||
ps->redirected = true;
|
ps->redirected = true;
|
||||||
ps->first_frame = 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);
|
root_damaged(ps);
|
||||||
|
|
||||||
// Repaint the whole screen
|
// Repaint the whole screen
|
||||||
|
@ -1472,12 +1563,9 @@ static void unredirect(session_t *ps) {
|
||||||
free(ps->damage_ring);
|
free(ps->damage_ring);
|
||||||
ps->damage_ring = ps->damage = NULL;
|
ps->damage_ring = ps->damage = NULL;
|
||||||
|
|
||||||
if (ps->present_event_id) {
|
if (ps->vblank_scheduler) {
|
||||||
xcb_present_select_input(ps->c.c, ps->present_event_id,
|
vblank_scheduler_free(ps->vblank_scheduler);
|
||||||
session_get_target_window(ps), 0);
|
ps->vblank_scheduler = NULL;
|
||||||
ps->present_event_id = XCB_NONE;
|
|
||||||
xcb_unregister_for_special_event(ps->c.c, ps->present_event);
|
|
||||||
ps->present_event = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must call XSync() here
|
// Must call XSync() here
|
||||||
|
@ -1487,92 +1575,12 @@ static void unredirect(session_t *ps) {
|
||||||
log_debug("Screen unredirected.");
|
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
|
// 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) {
|
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);
|
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;
|
xcb_generic_event_t *ev;
|
||||||
while ((ev = xcb_poll_for_queued_event(ps->c.c))) {
|
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) {
|
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;
|
struct timespec now;
|
||||||
int64_t draw_callback_enter_us;
|
int64_t draw_callback_enter_us;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
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",
|
log_trace("paint_preprocess took: %" PRIi64 " us",
|
||||||
after_preprocess_us - after_handle_pending_updates_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) {
|
if (ps->redirected && ps->o.stoppaint_force != ON) {
|
||||||
static int paint = 0;
|
static int paint = 0;
|
||||||
|
|
||||||
log_trace("Render start, frame %d", paint);
|
log_verbose("Render start, frame %d", paint);
|
||||||
if (!ps->o.legacy_backends) {
|
if (!ps->o.legacy_backends) {
|
||||||
paint_all_new(ps, bottom);
|
did_render = paint_all_new(ps, bottom);
|
||||||
} else {
|
} else {
|
||||||
paint_all(ps, bottom);
|
paint_all(ps, bottom);
|
||||||
}
|
}
|
||||||
log_trace("Render end");
|
log_verbose("Render end");
|
||||||
|
|
||||||
ps->first_frame = false;
|
ps->first_frame = false;
|
||||||
paint++;
|
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) {
|
if (!fade_running) {
|
||||||
ps->fade_time = 0L;
|
ps->fade_time = 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps->render_queued = false;
|
||||||
|
|
||||||
// TODO(yshui) Investigate how big the X critical section needs to be. There are
|
// 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.
|
// 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
|
// Queue redraw if animation is running. This should be picked up by next present
|
||||||
// event.
|
// 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) {
|
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_exists = 0,
|
||||||
.randr_event = 0,
|
.randr_event = 0,
|
||||||
.randr_error = 0,
|
.randr_error = 0,
|
||||||
.present_event_id = XCB_NONE,
|
|
||||||
.glx_exists = false,
|
.glx_exists = false,
|
||||||
.glx_event = 0,
|
.glx_event = 0,
|
||||||
.glx_error = 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);
|
ext_info = xcb_get_extension_data(ps->c.c, &xcb_dpms_id);
|
||||||
ps->dpms_exists = ext_info && ext_info->present;
|
ps->dpms_exists = ext_info && ext_info->present;
|
||||||
if (ps->dpms_exists) {
|
if (!ps->dpms_exists) {
|
||||||
auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL);
|
log_warn("No DPMS extension");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse configuration file
|
// Parse configuration file
|
||||||
|
@ -2727,7 +2749,6 @@ static void session_destroy(session_t *ps) {
|
||||||
// Stop libev event handlers
|
// Stop libev event handlers
|
||||||
ev_timer_stop(ps->loop, &ps->unredir_timer);
|
ev_timer_stop(ps->loop, &ps->unredir_timer);
|
||||||
ev_timer_stop(ps->loop, &ps->fade_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_timer_stop(ps->loop, &ps->draw_timer);
|
||||||
ev_prepare_stop(ps->loop, &ps->event_check);
|
ev_prepare_stop(ps->loop, &ps->event_check);
|
||||||
ev_signal_stop(ps->loop, &ps->usr1_signal);
|
ev_signal_stop(ps->loop, &ps->usr1_signal);
|
||||||
|
|
|
@ -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
|
/// 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
|
/// the number of vblanks we should wait between each frame. A divisor of 1 means
|
||||||
/// full framerate, 2 means half framerate, etc.
|
/// full framerate, 2 means half framerate, etc.
|
||||||
unsigned int
|
unsigned int render_statistics_get_budget(struct render_statistics *rs) {
|
||||||
render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor) {
|
|
||||||
if (rs->render_times.nelem < rs->render_times.window_size) {
|
if (rs->render_times.nelem < rs->render_times.window_size) {
|
||||||
// No valid render time estimates yet. Assume maximum budget.
|
// No valid render time estimates yet. Assume maximum budget.
|
||||||
*divisor = 1;
|
|
||||||
return UINT_MAX;
|
return UINT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// N-th percentile of render times, see render_statistics_init for N.
|
// N-th percentile of render times, see render_statistics_init for N.
|
||||||
auto render_time_percentile =
|
auto render_time_percentile =
|
||||||
rolling_quantile_estimate(&rs->render_time_quantile, &rs->render_times);
|
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;
|
return (unsigned int)render_time_percentile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
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.
|
/// How much time budget we should give to the backend for rendering, in microseconds.
|
||||||
///
|
unsigned int render_statistics_get_budget(struct render_statistics *rs);
|
||||||
/// 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);
|
|
||||||
|
|
||||||
|
/// 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);
|
unsigned int render_statistics_get_vblank_time(struct render_statistics *rs);
|
||||||
|
|
22
src/utils.h
22
src/utils.h
|
@ -125,14 +125,22 @@ safe_isnan(double a) {
|
||||||
* @param max maximum value
|
* @param max maximum value
|
||||||
* @return normalized value
|
* @return normalized value
|
||||||
*/
|
*/
|
||||||
static inline int attr_const normalize_i_range(int i, int min, int max) {
|
static inline int attr_const attr_unused normalize_i_range(int i, int min, int max) {
|
||||||
if (i > max)
|
if (i > max) {
|
||||||
return max;
|
return max;
|
||||||
if (i < min)
|
}
|
||||||
|
if (i < min) {
|
||||||
return min;
|
return min;
|
||||||
|
}
|
||||||
return i;
|
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 min2(a, b) ((a) > (b) ? (b) : (a))
|
||||||
#define max2(a, b) ((a) > (b) ? (a) : (b))
|
#define max2(a, b) ((a) > (b) ? (a) : (b))
|
||||||
#define min3(a, b, c) min2(a, min2(b, c))
|
#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
|
* @return normalized value
|
||||||
*/
|
*/
|
||||||
static inline double attr_const normalize_d_range(double d, double min, double max) {
|
static inline double attr_const normalize_d_range(double d, double min, double max) {
|
||||||
if (d > max)
|
if (d > max) {
|
||||||
return max;
|
return max;
|
||||||
if (d < min)
|
}
|
||||||
|
if (d < min) {
|
||||||
return min;
|
return min;
|
||||||
|
}
|
||||||
return d;
|
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
|
* @param d double value to normalize
|
||||||
* @return normalized value
|
* @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);
|
return normalize_d_range(d, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
27
src/x.c
|
@ -9,7 +9,9 @@
|
||||||
#include <pixman.h>
|
#include <pixman.h>
|
||||||
#include <xcb/composite.h>
|
#include <xcb/composite.h>
|
||||||
#include <xcb/damage.h>
|
#include <xcb/damage.h>
|
||||||
|
#include <xcb/dpms.h>
|
||||||
#include <xcb/glx.h>
|
#include <xcb/glx.h>
|
||||||
|
#include <xcb/present.h>
|
||||||
#include <xcb/randr.h>
|
#include <xcb/randr.h>
|
||||||
#include <xcb/render.h>
|
#include <xcb/render.h>
|
||||||
#include <xcb/sync.h>
|
#include <xcb/sync.h>
|
||||||
|
@ -777,6 +779,31 @@ err:
|
||||||
return false;
|
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
|
* 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,
|
* in the process. Allow the caller to specify the element at the center of the kernel,
|
||||||
|
|
8
src/x.h
8
src/x.h
|
@ -419,3 +419,11 @@ void x_update_monitors(struct x_connection *, struct x_monitors *);
|
||||||
void x_free_monitor_info(struct x_monitors *);
|
void x_free_monitor_info(struct x_monitors *);
|
||||||
|
|
||||||
uint32_t attr_deprecated xcb_generate_id(xcb_connection_t *c);
|
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);
|
||||||
|
|
Loading…
Reference in New Issue