diff --git a/src/backend/backend.c b/src/backend/backend.c index 33a3d8bc..5120672b 100644 --- a/src/backend/backend.c +++ b/src/backend/backend.c @@ -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(®_damage)) { pixman_region32_fini(®_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, ®_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(®_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 : diff --git a/src/backend/backend.h b/src/backend/backend.h index f572a935..1b4df729 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -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); diff --git a/src/backend/driver.c b/src/backend/driver.c index b909a622..f17743a4 100644 --- a/src/backend/driver.c +++ b/src/backend/driver.c @@ -19,6 +19,13 @@ void apply_driver_workarounds(struct session *ps, enum driver driver) { } } +enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver) { + if (driver & DRIVER_NVIDIA) { + return VBLANK_SCHEDULER_SGI_VIDEO_SYNC; + } + return VBLANK_SCHEDULER_PRESENT; +} + enum driver detect_driver(xcb_connection_t *c, backend_t *backend_data, xcb_window_t window) { enum driver ret = 0; // First we try doing backend agnostic detection using RANDR diff --git a/src/backend/driver.h b/src/backend/driver.h index e4cc3981..1b0877c1 100644 --- a/src/backend/driver.h +++ b/src/backend/driver.h @@ -7,6 +7,7 @@ #include #include +#include "config.h" #include "utils.h" struct session; @@ -41,6 +42,8 @@ enum driver detect_driver(xcb_connection_t *, struct backend_base *, xcb_window_ /// Apply driver specified global workarounds. It's safe to call this multiple times. void apply_driver_workarounds(struct session *ps, enum driver); +/// Choose a vblank scheduler based on the driver. +enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver); // Print driver names to stdout, for diagnostics static inline void print_drivers(enum driver drivers) { diff --git a/src/common.h b/src/common.h index 707d00e7..52967e60 100644 --- a/src/common.h +++ b/src/common.h @@ -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 diff --git a/src/config.c b/src/config.c index f5764c5c..22774f7f 100644 --- a/src/config.c +++ b/src/config.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -617,6 +619,82 @@ char *locate_auxiliary_file(const char *scope, const char *path, const char *inc return NULL; } +struct debug_options_entry { + const char *name; + const char **choices; + size_t offset; +}; + +// clang-format off +const char *vblank_scheduler_str[] = { + [VBLANK_SCHEDULER_PRESENT] = "present", + [VBLANK_SCHEDULER_SGI_VIDEO_SYNC] = "sgi_video_sync", + [LAST_VBLANK_SCHEDULER] = NULL +}; +static const struct debug_options_entry debug_options_entries[] = { + {"smart_frame_pacing", NULL, offsetof(struct debug_options, smart_frame_pacing)}, + {"force_vblank_sched", vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)}, +}; +// clang-format on + +void parse_debug_option_single(char *setting, struct debug_options *debug_options) { + char *equal = strchr(setting, '='); + size_t name_len = equal ? (size_t)(equal - setting) : strlen(setting); + for (size_t i = 0; i < ARR_SIZE(debug_options_entries); i++) { + if (strncmp(setting, debug_options_entries[i].name, name_len) != 0) { + continue; + } + if (debug_options_entries[i].name[name_len] != '\0') { + continue; + } + auto value = (int *)((void *)debug_options + debug_options_entries[i].offset); + if (equal) { + const char *const arg = equal + 1; + if (debug_options_entries[i].choices != NULL) { + for (size_t j = 0; debug_options_entries[i].choices[j]; j++) { + if (strcmp(arg, debug_options_entries[i].choices[j]) == + 0) { + *value = (int)j; + return; + } + } + } + if (!parse_int(arg, value)) { + log_error("Invalid value for debug option %s: %s, it " + "will be ignored.", + debug_options_entries[i].name, arg); + } + } else if (debug_options_entries[i].choices == NULL) { + *value = 1; + } else { + log_error( + "Missing value for debug option %s, it will be ignored.", setting); + } + return; + } + log_error("Invalid debug option: %s", setting); +} + +/// Parse debug options from environment variable `PICOM_DEBUG`. +void parse_debug_options(struct debug_options *debug_options) { + const char *debug = getenv("PICOM_DEBUG"); + const struct debug_options default_debug_options = { + .force_vblank_scheduler = LAST_VBLANK_SCHEDULER, + }; + + *debug_options = default_debug_options; + if (!debug) { + return; + } + + scoped_charp debug_copy = strdup(debug); + char *tmp, *needle = strtok_r(debug_copy, ";", &tmp); + while (needle) { + parse_debug_option_single(needle, debug_options); + needle = strtok_r(NULL, ";", &tmp); + } +} + /** * Parse a list of window shader rules. */ @@ -819,5 +897,6 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, (void)hasneg; (void)winopt_mask; #endif + parse_debug_options(&opt->debug_options); return ret; } diff --git a/src/config.h b/src/config.h index 31e6774c..d91dbb45 100644 --- a/src/config.h +++ b/src/config.h @@ -73,6 +73,27 @@ enum blur_method { typedef struct _c2_lptr c2_lptr_t; +enum vblank_scheduler_type { + /// X Present extension based vblank events + VBLANK_SCHEDULER_PRESENT, + /// GLX_SGI_video_sync based vblank events + VBLANK_SCHEDULER_SGI_VIDEO_SYNC, + /// An invalid scheduler, served as a scheduler count, and + /// as a sentinel value. + LAST_VBLANK_SCHEDULER, +}; + +extern const char *vblank_scheduler_str[]; + +/// Internal, private options for debugging and development use. +struct debug_options { + /// Try to reduce frame latency by using vblank interval and render time + /// estimates. Right now it's not working well across drivers. + int smart_frame_pacing; + /// Override the vblank scheduler chosen by the compositor. + int force_vblank_scheduler; +}; + /// Structure representing all options. typedef struct options { // === Debugging === @@ -262,6 +283,8 @@ typedef struct options { c2_lptr_t *transparent_clipping_blacklist; bool dithered_present; + + struct debug_options debug_options; } options_t; extern const char *const BACKEND_STRS[NUM_BKEND + 1]; diff --git a/src/event.c b/src/event.c index 807cf4ed..739540dc 100644 --- a/src/event.c +++ b/src/event.c @@ -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; diff --git a/src/meson.build b/src/meson.build index a608c575..a2f9c36c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,7 +9,8 @@ base_deps = [ srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c', 'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c', - 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c') ] + 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c', + 'vblank.c') ] picom_inc = include_directories('.') cflags = [] @@ -58,7 +59,7 @@ endif if get_option('opengl') cflags += ['-DCONFIG_OPENGL', '-DGL_GLEXT_PROTOTYPES'] - deps += [dependency('gl', required: true), dependency('egl', required: true)] + deps += [dependency('gl', required: true), dependency('egl', required: true), dependency('threads', required:true)] srcs += [ 'opengl.c' ] endif diff --git a/src/picom.c b/src/picom.c index 6f3c2908..044a9eb1 100644 --- a/src/picom.c +++ b/src/picom.c @@ -16,11 +16,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -64,6 +66,7 @@ #include "options.h" #include "statistics.h" #include "uthash_extra.h" +#include "vblank.h" /// Get session_t pointer from a pointer to a member of session_t #define session_ptr(ptr, member) \ @@ -122,27 +125,6 @@ static inline int64_t get_time_ms(void) { return (int64_t)tp.tv_sec * 1000 + (int64_t)tp.tv_nsec / 1000000; } -static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) { - // state is a bool indicating whether dpms is enabled - return info->state && (info->power_level != XCB_DPMS_DPMS_MODE_ON); -} - -void check_dpms_status(EV_P attr_unused, ev_timer *w, int revents attr_unused) { - auto ps = session_ptr(w, dpms_check_timer); - auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL); - if (!r) { - log_fatal("Failed to query DPMS status."); - abort(); - } - auto now_screen_is_off = dpms_screen_is_off(r); - if (ps->screen_is_off != now_screen_is_off) { - log_debug("Screen is now %s", now_screen_is_off ? "off" : "on"); - ps->screen_is_off = now_screen_is_off; - queue_redraw(ps); - } - free(r); -} - /** * Find matched window. * @@ -163,122 +145,248 @@ static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t return w; } +enum vblank_callback_action check_render_finish(struct vblank_event *e attr_unused, void *ud) { + auto ps = (session_t *)ud; + if (!ps->backend_busy) { + return VBLANK_CALLBACK_DONE; + } + + struct timespec render_time; + bool completed = + ps->backend_data->ops->last_render_time(ps->backend_data, &render_time); + if (!completed) { + // Render hasn't completed yet, we can't start another render. + // Check again at the next vblank. + log_debug("Last render did not complete during vblank, msc: " + "%" PRIu64, + ps->last_msc); + return VBLANK_CALLBACK_AGAIN; + } + + // The frame has been finished and presented, record its render time. + if (ps->o.debug_options.smart_frame_pacing) { + int render_time_us = + (int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L); + render_statistics_add_render_time_sample( + &ps->render_stats, render_time_us + (int)ps->last_schedule_delay); + log_verbose("Last render call took: %d (gpu) + %d (cpu) us, " + "last_msc: %" PRIu64, + render_time_us, (int)ps->last_schedule_delay, ps->last_msc); + } + ps->backend_busy = false; + return VBLANK_CALLBACK_DONE; +} + +enum vblank_callback_action +collect_vblank_interval_statistics(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + double vblank_interval = NAN; + assert(ps->frame_pacing); + assert(ps->vblank_scheduler); + + if (!ps->o.debug_options.smart_frame_pacing) { + // We don't need to collect statistics if we are not doing smart frame + // pacing. + return VBLANK_CALLBACK_DONE; + } + + // TODO(yshui): this naive method of estimating vblank interval does not handle + // the variable refresh rate case very well. This includes the case + // of a VRR enabled monitor; or a monitor that's turned off, in which + // case the vblank events might slow down or stop all together. + // I tried using DPMS to detect monitor power state, and stop adding + // samples when the monitor is off, but I had a hard time to get it + // working reliably, there are just too many corner cases. + + // Don't add sample again if we already collected statistics for this vblank + if (ps->last_msc < e->msc) { + if (ps->last_msc_instant != 0) { + auto frame_count = e->msc - ps->last_msc; + auto frame_time = + (int)((e->ust - ps->last_msc_instant) / frame_count); + if (frame_count == 1) { + render_statistics_add_vblank_time_sample( + &ps->render_stats, frame_time); + log_trace("Frame count %lu, frame time: %d us, ust: " + "%" PRIu64 "", + frame_count, frame_time, e->ust); + } else { + log_trace("Frame count %lu, frame time: %d us, msc: " + "%" PRIu64 ", not adding sample.", + frame_count, frame_time, e->ust); + } + } + ps->last_msc_instant = e->ust; + ps->last_msc = e->msc; + } else if (ps->last_msc > e->msc) { + log_warn("PresentCompleteNotify msc is going backwards, last_msc: " + "%" PRIu64 ", current msc: %" PRIu64, + ps->last_msc, e->msc); + ps->last_msc_instant = 0; + ps->last_msc = 0; + } + + vblank_interval = render_statistics_get_vblank_time(&ps->render_stats); + log_trace("Vblank interval estimate: %f us", vblank_interval); + if (vblank_interval == 0) { + // We don't have enough data for vblank interval estimate, schedule + // another vblank event. + return VBLANK_CALLBACK_AGAIN; + } + return VBLANK_CALLBACK_DONE; +} + +void schedule_render(session_t *ps, bool triggered_by_vblank); + +/// vblank callback scheduled by schedule_render, when a render is ongoing. +/// +/// Check if previously queued render has finished, and reschedule render if it has. +enum vblank_callback_action reschedule_render_at_vblank(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + assert(ps->frame_pacing); + assert(ps->render_queued); + assert(ps->vblank_scheduler); + + log_verbose("Rescheduling render at vblank, msc: %" PRIu64, e->msc); + + collect_vblank_interval_statistics(e, ud); + check_render_finish(e, ud); + + if (ps->backend_busy) { + return VBLANK_CALLBACK_AGAIN; + } + + schedule_render(ps, false); + return VBLANK_CALLBACK_DONE; +} + /// How many seconds into the future should we start rendering the next frame. /// /// Renders are scheduled like this: /// -/// 1. queue_redraw() registers the intention to render. redraw_needed is set to true to -/// indicate what is on screen needs to be updated. -/// 2. then, we need to figure out the best time to start rendering. first, we need to -/// know when the next frame will be displayed on screen. we have this information from -/// the Present extension: we know when was the last frame displayed, and we know the -/// refresh rate. so we can calculate the next frame's display time. if our render time -/// estimation shows we could miss that target, we push the target back one frame. -/// 3. if there is already render completed for that target frame, or there is a render -/// currently underway, we don't do anything, and wait for the next Present Complete -/// Notify event to try to schedule again. -/// 4. otherwise, we schedule a render for that target frame. we use past statistics about -/// how long our renders took to figure out when to start rendering. we start rendering -/// at the latest point of time possible to still hit the target frame. +/// 1. queue_redraw() queues a new render by calling schedule_render, if there +/// is no render currently scheduled. i.e. render_queued == false. +/// 2. then, we need to figure out the best time to start rendering. we need to +/// at least know when the next vblank will start, as we can't start render +/// before the current rendered frame is diplayed on screen. we have this +/// information from the vblank scheduler, it will notify us when that happens. +/// we might also want to delay the rendering even further to reduce latency, +/// this is discussed below, in FUTURE WORKS. +/// 3. we schedule a render for that target point in time. +/// 4. draw_callback() is called at the schedule time (i.e. when scheduled +/// vblank event is delivered). Backend APIs are called to issue render +/// commands. render_queued is set to false, and backend_busy is set to true. /// -/// The `triggered_by_timer` parameter is used to indicate whether this function is -/// triggered by a steady timer, i.e. we are rendering for each vblank. The other case is -/// when we stop rendering for a while because there is no changes on screen, then -/// something changed and schedule_render is triggered by a DamageNotify. The idea is that -/// when the schedule is triggered by a steady timer, schedule_render will be called at a -/// predictable offset into each vblank. +/// There are some considerations in step 2: +/// +/// First of all, a vblank event being delivered +/// doesn't necessarily mean the frame has been displayed on screen. If a frame +/// takes too long to render, it might miss the current vblank, and will be +/// displayed on screen during one of the subsequent vblanks. So in +/// schedule_render_at_vblank, we ask the backend to see if it has finished +/// rendering. if not, render_queued is unchanged, and another vblank is +/// scheduled; otherwise, draw_callback_impl will be scheduled to be call at +/// an appropriate time. Second, we might not have rendered for the previous vblank, +/// in which case the last vblank event we received could be many frames in the past, +/// so we can't make scheduling decisions based on that. So we always schedule +/// a vblank event when render is queued, and make scheduling decisions when the +/// event is delivered. +/// +/// All of the above is what happens when frame_pacing is true. Otherwise +/// render_in_progress is either QUEUED or IDLE, and queue_redraw will always +/// schedule a render to be started immediately. PresentCompleteNotify will not +/// be received, and handle_end_of_vblank will not be called. +/// +/// The `triggered_by_timer` parameter is used to indicate whether this function +/// is triggered by a steady timer, i.e. we are rendering for each vblank. The +/// other case is when we stop rendering for a while because there is no changes +/// on screen, then something changed and schedule_render is triggered by a +/// DamageNotify. The idea is that when the schedule is triggered by a steady +/// timer, schedule_render will be called at a predictable offset into each +/// vblank. +/// +/// # FUTURE WORKS +/// +/// As discussed in step 2 above, we might want to delay the rendering even +/// further. If we know the time it takes to render a frame, and the interval +/// between vblanks, we can try to schedule the render to start at a point in +/// time that's closer to the next vblank. We should be able to get this +/// information by doing statistics on the render time of previous frames, which +/// is available from the backends; and the interval between vblank events, +/// which is available from the vblank scheduler. +/// +/// The code that does this is already implemented below, but disabled by +/// default. There are several problems with it, see bug #1072. +void schedule_render(session_t *ps, bool triggered_by_vblank attr_unused) { + // If the backend is busy, we will try again at the next vblank. + if (ps->backend_busy) { + // We should never have set backend_busy to true unless frame_pacing is + // enabled. + assert(ps->vblank_scheduler); + assert(ps->frame_pacing); + log_verbose("Backend busy, will reschedule render at next vblank."); + if (!vblank_scheduler_schedule(ps->vblank_scheduler, + reschedule_render_at_vblank, ps)) { + // TODO(yshui): handle error here + abort(); + } + return; + } -void schedule_render(session_t *ps, bool triggered_by_vblank) { + // By default, we want to schedule render immediately, later in this function we + // might adjust that and move the render later, based on render timing statistics. double delay_s = 0; - ps->next_render = 0; - if (!ps->frame_pacing || !ps->redirected) { - // Not doing frame pacing, schedule a render immediately, if not already - // scheduled. - // If not redirected, we schedule immediately to have a chance to - // redirect. We won't have frame or render timing information anyway. - if (!ev_is_active(&ps->draw_timer)) { - // We don't know the msc, so we set it to 1, because 0 is a - // special value - ps->target_msc = 1; - goto schedule; - } - return; - } - struct timespec render_time; - bool completed = - ps->backend_data->ops->last_render_time(ps->backend_data, &render_time); - if (!completed || ev_is_active(&ps->draw_timer)) { - // There is already a render underway (either just scheduled, or is - // rendered but awaiting completion), don't schedule another one. - if (ps->target_msc <= ps->last_msc) { - log_debug("Target frame %ld is in the past, but we are still " - "rendering", - ps->target_msc); - // We missed our target, push it back one frame - ps->target_msc = ps->last_msc + 1; - } - log_trace("Still rendering for target frame %ld, not scheduling another " - "render", - ps->target_msc); - return; - } - if (ps->target_msc > ps->last_msc) { - // Render for the target frame is completed, but is yet to be displayed. - // Don't schedule another render. - log_trace("Target frame %ld is in the future, and we have already " - "rendered, last msc: %d", - ps->target_msc, (int)ps->last_msc); - return; - } - + unsigned int divisor = 0; struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); auto now_us = (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_nsec / 1000; - if (triggered_by_vblank) { - log_trace("vblank schedule delay: %ld us", now_us - ps->last_msc_instant); + + ps->next_render = now_us; + + if (!ps->frame_pacing || !ps->redirected) { + // If not doing frame pacing, schedule a render immediately; if + // not redirected, we schedule immediately to have a chance to + // redirect. We won't have frame or render timing information + // anyway. + assert(!ev_is_active(&ps->draw_timer)); + goto schedule; } - int render_time_us = - (int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L); - if (ps->target_msc == ps->last_msc) { - // The frame has just been displayed, record its render time; - if (ps->did_render) { - log_trace("Last render call took: %d (gpu) + %d (cpu) us, " - "last_msc: %" PRIu64, - render_time_us, (int)ps->last_schedule_delay, ps->last_msc); - render_statistics_add_render_time_sample( - &ps->render_stats, render_time_us + (int)ps->last_schedule_delay); - } - ps->target_msc = 0; - ps->did_render = false; - ps->last_schedule_delay = 0; - } - - unsigned int divisor = 0; - auto render_budget = render_statistics_get_budget(&ps->render_stats, &divisor); + // if ps->o.debug_options.smart_frame_pacing is false, we won't have any render + // time or vblank interval estimates, so we would naturally fallback to schedule + // render immediately. + auto render_budget = render_statistics_get_budget(&ps->render_stats); auto frame_time = render_statistics_get_vblank_time(&ps->render_stats); if (frame_time == 0) { // We don't have enough data for render time estimates, maybe there's // no frame rendered yet, or the backend doesn't support render timing // information, schedule render immediately. - ps->target_msc = ps->last_msc + 1; + log_verbose("Not enough data for render time estimates."); goto schedule; } - auto const deadline = ps->last_msc_instant + (unsigned long)divisor * frame_time; + if (render_budget >= frame_time) { + // If the estimated render time is already longer than the estimated + // vblank interval, there is no way we can make it. Instead of always + // dropping frames, we try desperately to catch up and schedule a + // render immediately. + log_verbose("Render budget: %u us >= frame time: %" PRIu32 " us", + render_budget, frame_time); + goto schedule; + } + + auto target_frame = (now_us + render_budget - ps->last_msc_instant) / frame_time + 1; + auto const deadline = ps->last_msc_instant + target_frame * frame_time; unsigned int available = 0; if (deadline > now_us) { available = (unsigned int)(deadline - now_us); } - ps->target_msc = ps->last_msc + divisor; if (available > render_budget) { delay_s = (double)(available - render_budget) / 1000000.0; ps->next_render = deadline - render_budget; - } else { - delay_s = 0; - ps->next_render = now_us; } + if (delay_s > 1) { log_warn("Delay too long: %f s, render_budget: %d us, frame_time: " "%" PRIu32 " us, now_us: %" PRIu64 " us, next_msc: %" PRIu64 " u" @@ -286,38 +394,32 @@ void schedule_render(session_t *ps, bool triggered_by_vblank) { delay_s, render_budget, frame_time, now_us, deadline); } - log_trace("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, frame_time: " - "%" PRIu32 ", now_us: %" PRIu64 ", next_msc: %" PRIu64 ", " - "target_msc: %" PRIu64 ", divisor: %d", - delay_s, ps->last_msc_instant, render_budget, frame_time, now_us, - deadline, ps->target_msc, divisor); + log_verbose("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, " + "frame_time: %" PRIu32 ", now_us: %" PRIu64 ", next_render: %" PRIu64 + ", next_msc: %" PRIu64 ", divisor: " + "%d", + delay_s, ps->last_msc_instant, render_budget, frame_time, now_us, + ps->next_render, deadline, divisor); schedule: + // If the backend is not busy, we just need to schedule the render at the + // specified time; otherwise we need to wait for the next vblank event and + // reschedule. + ps->last_schedule_delay = 0; assert(!ev_is_active(&ps->draw_timer)); ev_timer_set(&ps->draw_timer, delay_s, 0); ev_timer_start(ps->loop, &ps->draw_timer); } void queue_redraw(session_t *ps) { - if (ps->screen_is_off) { - // The screen is off, if there is a draw queued for the next frame (i.e. - // ps->redraw_needed == true), it won't be triggered until the screen is - // on again, because the abnormal Present events we will receive from the - // X server when the screen is off. Yet we need the draw_callback to be - // called as soon as possible so the screen can be unredirected. - // So here we unconditionally start the draw timer. - ev_timer_stop(ps->loop, &ps->draw_timer); - ev_timer_set(&ps->draw_timer, 0, 0); - ev_timer_start(ps->loop, &ps->draw_timer); + log_verbose("Queue redraw, render_queued: %d, backend_busy: %d", + ps->render_queued, ps->backend_busy); + + if (ps->render_queued) { return; } - // Whether we have already rendered for the current frame. - // If frame pacing is not enabled, pretend this is false. - // If --benchmark is used, redraw is always queued - if (!ps->redraw_needed && !ps->o.benchmark) { - schedule_render(ps, false); - } - ps->redraw_needed = true; + ps->render_queued = true; + schedule_render(ps, false); } /** @@ -1013,19 +1115,6 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, // If there's no window to paint, and the screen isn't redirected, // don't redirect it. unredir_possible = true; - } else if (ps->screen_is_off) { - // Screen is off, unredirect - // We do this unconditionally disregarding "unredir_if_possible" - // because it's important for correctness, because we need to - // workaround problems X server has around screen off. - // - // Known problems: - // 1. Sometimes OpenGL front buffer can lose content, and if we - // are doing partial updates (i.e. use-damage = true), the - // result will be wrong. - // 2. For frame pacing, X server sends bogus - // PresentCompleteNotify events when screen is off. - unredir_possible = true; } if (unredir_possible) { if (ps->redirected) { @@ -1397,7 +1486,7 @@ static bool redirect_start(session_t *ps) { pixman_region32_init(&ps->damage_ring[i]); } - ps->frame_pacing = !ps->o.no_frame_pacing; + ps->frame_pacing = !ps->o.no_frame_pacing && ps->o.vsync; if ((ps->o.legacy_backends || ps->o.benchmark || !ps->backend_data->ops->last_render_time) && ps->frame_pacing) { // Disable frame pacing if we are using a legacy backend or if we are in @@ -1406,25 +1495,31 @@ static bool redirect_start(session_t *ps) { ps->frame_pacing = false; } - if (ps->present_exists && ps->frame_pacing) { - ps->present_event_id = x_new_id(&ps->c); - auto select_input = xcb_present_select_input( - ps->c.c, ps->present_event_id, session_get_target_window(ps), - XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY); - auto notify_msc = xcb_present_notify_msc( - ps->c.c, session_get_target_window(ps), 0, 0, 1, 0); - set_cant_fail_cookie(&ps->c, select_input); - set_cant_fail_cookie(&ps->c, notify_msc); - ps->present_event = xcb_register_for_special_xge( - ps->c.c, &xcb_present_id, ps->present_event_id, NULL); + // Re-detect driver since we now have a backend + ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root); + apply_driver_workarounds(ps, ps->drivers); + if (ps->present_exists && ps->frame_pacing) { // Initialize rendering and frame timing statistics, and frame pacing // states. ps->last_msc_instant = 0; ps->last_msc = 0; ps->last_schedule_delay = 0; - ps->target_msc = 0; render_statistics_reset(&ps->render_stats); + enum vblank_scheduler_type scheduler_type = + choose_vblank_scheduler(ps->drivers); + if (ps->o.debug_options.force_vblank_scheduler != LAST_VBLANK_SCHEDULER) { + scheduler_type = + (enum vblank_scheduler_type)ps->o.debug_options.force_vblank_scheduler; + } + log_info("Using vblank scheduler: %s.", vblank_scheduler_str[scheduler_type]); + ps->vblank_scheduler = vblank_scheduler_new( + ps->loop, &ps->c, session_get_target_window(ps), scheduler_type); + if (!ps->vblank_scheduler) { + return false; + } + vblank_scheduler_schedule(ps->vblank_scheduler, + collect_vblank_interval_statistics, ps); } else if (ps->frame_pacing) { log_error("Present extension is not supported, frame pacing disabled."); ps->frame_pacing = false; @@ -1436,10 +1531,6 @@ static bool redirect_start(session_t *ps) { ps->redirected = true; ps->first_frame = true; - // Re-detect driver since we now have a backend - ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root); - apply_driver_workarounds(ps, ps->drivers); - root_damaged(ps); // Repaint the whole screen @@ -1472,12 +1563,9 @@ static void unredirect(session_t *ps) { free(ps->damage_ring); ps->damage_ring = ps->damage = NULL; - if (ps->present_event_id) { - xcb_present_select_input(ps->c.c, ps->present_event_id, - session_get_target_window(ps), 0); - ps->present_event_id = XCB_NONE; - xcb_unregister_for_special_event(ps->c.c, ps->present_event); - ps->present_event = NULL; + if (ps->vblank_scheduler) { + vblank_scheduler_free(ps->vblank_scheduler); + ps->vblank_scheduler = NULL; } // Must call XSync() here @@ -1487,92 +1575,12 @@ static void unredirect(session_t *ps) { log_debug("Screen unredirected."); } -static void -handle_present_complete_notify(session_t *ps, xcb_present_complete_notify_event_t *cne) { - if (cne->kind != XCB_PRESENT_COMPLETE_KIND_NOTIFY_MSC) { - return; - } - - bool event_is_invalid = false; - if (ps->frame_pacing) { - auto next_msc = cne->msc + 1; - if (cne->msc <= ps->last_msc || cne->ust == 0) { - // X sometimes sends duplicate/bogus MSC events, don't - // use the msc value. Also ignore these events. - // - // See: - // https://gitlab.freedesktop.org/xorg/xserver/-/issues/1418 - next_msc = ps->last_msc + 1; - event_is_invalid = true; - } - auto cookie = xcb_present_notify_msc( - ps->c.c, session_get_target_window(ps), 0, next_msc, 0, 0); - set_cant_fail_cookie(&ps->c, cookie); - } - if (event_is_invalid) { - return; - } - - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - uint64_t now_usec = (uint64_t)(now.tv_sec * 1000000 + now.tv_nsec / 1000); - uint64_t drift; - if (cne->ust > now_usec) { - drift = cne->ust - now_usec; - } else { - drift = now_usec - cne->ust; - } - - if (ps->last_msc_instant != 0) { - auto frame_count = cne->msc - ps->last_msc; - int frame_time = (int)((cne->ust - ps->last_msc_instant) / frame_count); - render_statistics_add_vblank_time_sample(&ps->render_stats, frame_time); - log_trace("Frame count %lu, frame time: %d us, rolling average: %u us, " - "msc: %" PRIu64 ", offset: %d us", - frame_count, frame_time, - render_statistics_get_vblank_time(&ps->render_stats), cne->ust, - (int)drift); - } else if (drift > 1000000 && ps->frame_pacing) { - // This is the first MSC event we receive, let's check if the timestamps - // align with the monotonic clock. If not, disable frame pacing because we - // can't schedule frames reliably. - log_error("Temporal anomaly detected, frame pacing disabled. (Are we " - "running inside a time namespace?), %" PRIu64 " %" PRIu64, - now_usec, ps->last_msc_instant); - ps->frame_pacing = false; - } - ps->last_msc_instant = cne->ust; - ps->last_msc = cne->msc; - if (ps->redraw_needed) { - schedule_render(ps, true); - } -} - -static void handle_present_events(session_t *ps) { - if (!ps->present_event) { - // Screen not redirected - return; - } - xcb_present_generic_event_t *ev; - while ((ev = (void *)xcb_poll_for_special_event(ps->c.c, ps->present_event))) { - if (ev->event != ps->present_event_id) { - // This event doesn't have the right event context, it's not meant - // for us. - goto next; - } - - // We only subscribed to the complete notify event. - assert(ev->evtype == XCB_PRESENT_EVENT_COMPLETE_NOTIFY); - handle_present_complete_notify(ps, (void *)ev); - next: - free(ev); - } -} - // Handle queued events before we go to sleep static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents attr_unused) { session_t *ps = session_ptr(w, event_check); - handle_present_events(ps); + if (ps->vblank_scheduler) { + vblank_handle_x_events(ps->vblank_scheduler); + } xcb_generic_event_t *ev; while ((ev = xcb_poll_for_queued_event(ps->c.c))) { @@ -1694,6 +1702,9 @@ static void handle_pending_updates(EV_P_ struct session *ps) { } static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { + assert(!ps->backend_busy); + assert(ps->render_queued); + struct timespec now; int64_t draw_callback_enter_us; clock_gettime(CLOCK_MONOTONIC, &now); @@ -1779,17 +1790,18 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { log_trace("paint_preprocess took: %" PRIi64 " us", after_preprocess_us - after_handle_pending_updates_us); - // If the screen is unredirected, free all_damage to stop painting + // If the screen is unredirected, we don't render anything. + bool did_render = false; if (ps->redirected && ps->o.stoppaint_force != ON) { static int paint = 0; - log_trace("Render start, frame %d", paint); + log_verbose("Render start, frame %d", paint); if (!ps->o.legacy_backends) { - paint_all_new(ps, bottom); + did_render = paint_all_new(ps, bottom); } else { paint_all(ps, bottom); } - log_trace("Render end"); + log_verbose("Render end"); ps->first_frame = false; paint++; @@ -1798,16 +1810,36 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { } } + // With frame pacing, we set backend_busy to true after the end of + // vblank. Without frame pacing, we won't be receiving vblank events, so + // we set backend_busy to false here, right after we issue the render + // commands. + // The other case is if we decided there is no change to render, in that + // case no render command is issued, so we also set backend_busy to + // false. + ps->backend_busy = (ps->frame_pacing && did_render); + ps->next_render = 0; + if (!fade_running) { ps->fade_time = 0L; } + ps->render_queued = false; + // TODO(yshui) Investigate how big the X critical section needs to be. There are // suggestions that rendering should be in the critical section as well. // Queue redraw if animation is running. This should be picked up by next present // event. - ps->redraw_needed = animation; + if (animation) { + queue_redraw(ps); + } + if (ps->vblank_scheduler) { + // Even if we might not want to render during next vblank, we want to keep + // `backend_busy` up to date, so when the next render comes, we can + // immediately know if we can render. + vblank_scheduler_schedule(ps->vblank_scheduler, check_render_finish, ps); + } } static void draw_callback(EV_P_ ev_timer *w, int revents) { @@ -1974,7 +2006,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy, .randr_exists = 0, .randr_event = 0, .randr_error = 0, - .present_event_id = XCB_NONE, .glx_exists = false, .glx_event = 0, .glx_error = 0, @@ -2109,17 +2140,8 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ext_info = xcb_get_extension_data(ps->c.c, &xcb_dpms_id); ps->dpms_exists = ext_info && ext_info->present; - if (ps->dpms_exists) { - auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL); - if (!r) { - log_fatal("Failed to query DPMS info"); - goto err; - } - ps->screen_is_off = dpms_screen_is_off(r); - // Check screen status every half second - ev_timer_init(&ps->dpms_check_timer, check_dpms_status, 0, 0.5); - ev_timer_start(ps->loop, &ps->dpms_check_timer); - free(r); + if (!ps->dpms_exists) { + log_warn("No DPMS extension"); } // Parse configuration file @@ -2727,7 +2749,6 @@ static void session_destroy(session_t *ps) { // Stop libev event handlers ev_timer_stop(ps->loop, &ps->unredir_timer); ev_timer_stop(ps->loop, &ps->fade_timer); - ev_timer_stop(ps->loop, &ps->dpms_check_timer); ev_timer_stop(ps->loop, &ps->draw_timer); ev_prepare_stop(ps->loop, &ps->event_check); ev_signal_stop(ps->loop, &ps->usr1_signal); diff --git a/src/statistics.c b/src/statistics.c index 5e3d9249..11c7466e 100644 --- a/src/statistics.c +++ b/src/statistics.c @@ -55,26 +55,15 @@ void render_statistics_add_render_time_sample(struct render_statistics *rs, int /// A `divisor` is also returned, indicating the target framerate. The divisor is /// the number of vblanks we should wait between each frame. A divisor of 1 means /// full framerate, 2 means half framerate, etc. -unsigned int -render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor) { +unsigned int render_statistics_get_budget(struct render_statistics *rs) { if (rs->render_times.nelem < rs->render_times.window_size) { // No valid render time estimates yet. Assume maximum budget. - *divisor = 1; return UINT_MAX; } // N-th percentile of render times, see render_statistics_init for N. auto render_time_percentile = rolling_quantile_estimate(&rs->render_time_quantile, &rs->render_times); - auto vblank_time_us = render_statistics_get_vblank_time(rs); - if (vblank_time_us == 0) { - // We don't have a good estimate of the vblank time yet, so we - // assume we can finish in one vblank. - *divisor = 1; - } else { - *divisor = - (unsigned int)(render_time_percentile / rs->vblank_time_us.mean + 1); - } return (unsigned int)render_time_percentile; } diff --git a/src/statistics.h b/src/statistics.h index 67d02119..a1114861 100644 --- a/src/statistics.h +++ b/src/statistics.h @@ -23,11 +23,8 @@ void render_statistics_add_vblank_time_sample(struct render_statistics *rs, int void render_statistics_add_render_time_sample(struct render_statistics *rs, int time_us); /// How much time budget we should give to the backend for rendering, in microseconds. -/// -/// A `divisor` is also returned, indicating the target framerate. The divisor is -/// the number of vblanks we should wait between each frame. A divisor of 1 means -/// full framerate, 2 means half framerate, etc. -unsigned int -render_statistics_get_budget(struct render_statistics *rs, unsigned int *divisor); +unsigned int render_statistics_get_budget(struct render_statistics *rs); +/// Return the measured vblank interval in microseconds. Returns 0 if not enough +/// samples have been collected yet. unsigned int render_statistics_get_vblank_time(struct render_statistics *rs); diff --git a/src/utils.h b/src/utils.h index fc6dd306..446fba8b 100644 --- a/src/utils.h +++ b/src/utils.h @@ -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); } diff --git a/src/vblank.c b/src/vblank.c new file mode 100644 index 00000000..ff83a9a7 --- /dev/null +++ b/src/vblank.c @@ -0,0 +1,541 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include "config.h" + +#ifdef CONFIG_OPENGL +// Enable sgi_video_sync_vblank_scheduler +#include +#include +#include +#include +#include +#include + +#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; +} \ No newline at end of file diff --git a/src/vblank.h b/src/vblank.h new file mode 100644 index 00000000..8050f487 --- /dev/null +++ b/src/vblank.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#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); diff --git a/src/x.c b/src/x.c index 1840ebb1..06c8b939 100644 --- a/src/x.c +++ b/src/x.c @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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, diff --git a/src/x.h b/src/x.h index b5bd1a59..df45b5cc 100644 --- a/src/x.h +++ b/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 *); 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);