diff --git a/src/common.h b/src/common.h index 889ca88..52967e6 100644 --- a/src/common.h +++ b/src/common.h @@ -134,17 +134,6 @@ struct shader_info { UT_hash_handle hh; }; -enum render_progress { - /// Render is finished and presented to the screen. - RENDER_IDLE = 0, - /// Rendering is queued, but not started yet. - RENDER_QUEUED, - /// Backend has been called, render commands have been issued. - RENDER_STARTED, - /// Backend reported render commands have been finished. (not actually used). - RENDER_FINISHED, -}; - /// Structure containing all necessary data for a session. typedef struct session { // === Event handlers === @@ -156,8 +145,6 @@ typedef struct session { ev_timer fade_timer; /// Use an ev_timer callback for drawing ev_timer draw_timer; - /// Timer for the end of each vblanks. Used for calling schedule_render. - ev_timer vblank_timer; /// Called every time we have timeouts or new data on socket, /// so we can be sure if xcb read from X socket at anytime during event /// handling, we will not left any event unhandled in the queue @@ -225,12 +212,6 @@ typedef struct session { bool first_frame; /// Whether screen has been turned off bool screen_is_off; - /// We asked X server to send us a event for the end of a vblank, and we haven't - /// received one yet. - bool vblank_event_requested; - /// 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 @@ -242,6 +223,8 @@ typedef struct session { uint64_t next_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; @@ -253,14 +236,18 @@ typedef struct session { options_t o; /// Whether we have hit unredirection timeout. bool tmout_unredir_hit; - /// Rendering is currently in progress. This means we are in any stage of - /// rendering a frame. The render could be queued but not yet started, or it could - /// have finished but not yet presented. - enum render_progress render_in_progress; - /// Whether there are changes pending for the next render. A render is currently - /// in progress, otherwise we would have started a new render instead of setting - /// this flag. - 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.h b/src/config.h index 93bede8..bdf384a 100644 --- a/src/config.h +++ b/src/config.h @@ -73,6 +73,16 @@ enum blur_method { typedef struct _c2_lptr c2_lptr_t; +enum vblank_scheduler_type { + /// X Present extension based vblank events + VBLANK_SCHEDULER_PRESENT, + /// GLX_SGI_video_sync based vblank events + VBLANK_SCHEDULER_SGI_VIDEO_SYNC, + /// An invalid scheduler, served as a scheduler count, and + /// as a sentinel value. + LAST_VBLANK_SCHEDULER, +}; + /// Internal, private options for debugging and development use. struct debug_options { diff --git a/src/meson.build b/src/meson.build index a608c57..51f9648 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 = [] diff --git a/src/picom.c b/src/picom.c index 2cfa08e..57ee4f0 100644 --- a/src/picom.c +++ b/src/picom.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +65,7 @@ #include "options.h" #include "statistics.h" #include "uthash_extra.h" +#include "vblank.h" /// Get session_t pointer from a pointer to a member of session_t #define session_ptr(ptr, member) \ @@ -142,43 +144,150 @@ static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t return w; } +void collect_vblank_interval_statistics(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + assert(ps->frame_pacing); + assert(ps->vblank_scheduler); + + if (ps->last_msc == e->msc) { + // Already collected statistics for this vblank + return; + } + + // TODO(yshui): this naive method of estimating vblank interval does not handle + // the variable refresh rate case very well. This includes the case + // of a VRR enabled monitor; or a monitor that's turned off, in which + // case the vblank events might slow down or stop all together. + // I tried using DPMS to detect monitor power state, and stop adding + // samples when the monitor is off, but I had a hard time to get it + // working reliably, there are just too many corner cases. + + if (ps->last_msc_instant != 0) { + auto frame_count = e->msc - ps->last_msc; + int frame_time = (int)((e->ust - ps->last_msc_instant) / frame_count); + if (frame_count == 1) { + render_statistics_add_vblank_time_sample(&ps->render_stats, frame_time); + log_trace("Frame count %lu, frame time: %d us, ust: %" PRIu64 "", + frame_count, frame_time, e->ust); + } else { + log_trace("Frame count %lu, frame time: %d us, msc: %" PRIu64 + ", not adding sample.", + frame_count, frame_time, e->ust); + } + } + ps->last_msc_instant = e->ust; + ps->last_msc = e->msc; + double vblank_interval = render_statistics_get_vblank_time(&ps->render_stats); + log_trace("Vblank interval estimate: %f us", vblank_interval); + if (vblank_interval == 0) { + // We don't have enough data for vblank interval estimate, schedule + // another vblank event. + vblank_scheduler_schedule(ps->vblank_scheduler, + collect_vblank_interval_statistics, ud); + } +} +/// vblank callback scheduled by schedule_render. +/// +/// Check if previously queued render has finished, and record the time it took. +void schedule_render_at_vblank(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + assert(ps->frame_pacing); + assert(ps->backend_busy); + assert(ps->render_queued); + assert(ps->vblank_scheduler); + + collect_vblank_interval_statistics(e, ud); + + struct timespec render_time; + bool completed = + ps->backend_data->ops->last_render_time(ps->backend_data, &render_time); + if (!completed) { + // Render hasn't completed yet, we can't start another render. + // Check again at the next vblank. + log_debug("Last render did not complete during vblank, msc: " + "%" PRIu64, + ps->last_msc); + vblank_scheduler_schedule(ps->vblank_scheduler, schedule_render_at_vblank, ud); + return; + } + + // The frame has been finished and presented, record its render time. + 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->last_schedule_delay = 0; + ps->backend_busy = false; + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + auto now_us = (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_nsec / 1000; + double delay_s = 0; + if (ps->next_render > now_us) { + delay_s = (double)(ps->next_render - now_us) / 1000000.0; + } + log_verbose("Prepare to start rendering: delay: %f s, next_render: %" PRIu64 + ", now_us: %" PRIu64, + delay_s, ps->next_render, now_us); + assert(!ev_is_active(&ps->draw_timer)); + ev_timer_set(&ps->draw_timer, delay_s, 0); + ev_timer_start(ps->loop, &ps->draw_timer); +} + /// How many seconds into the future should we start rendering the next frame. /// /// Renders are scheduled like this: /// -/// 1. queue_redraw() queues a new render by calling schedule_render, if there is no -/// render currently scheduled. i.e. render_in_progress == RENDER_IDLE. -/// 2. then, we need to figure out the best time to start rendering. first, we need to -/// know when the current vblank will end. we have this information from the Present -/// extension: we know when was the end of last vblank, and we know the refresh rate. -/// so we can calculate the end of the current vblank. if our render time estimation -/// shows we could miss that target, we push the target back an integer number of -/// frames. and we calculate the end of the target vblank similarly. -/// 3. We schedule a render for that target. we use past statistics about how long our -/// renders took to figure out when to start rendering. we start rendering as late as -/// possible, but not too late that we miss the target vblank. render_in_progress is -/// set to RENDER_QUEUED. -/// 4. draw_callback() is called at the schedule time. Backend APIs are called to issue -/// render commands. render_in_progress is set to RENDER_STARTED. -/// 5. PresentCompleteNotify is received, which gives us the actual time when the current -/// vblank will end/ended. We schedule a call to handle_end_of_vblank at the -/// appropriate time. -/// 6. in handle_end_of_vblank, we check the backend to see if the render has finished. if -/// not, render_in_progress is unchanged; otherwise, render_in_progress is set to -/// RENDER_IDLE, and the next frame can be scheduled. +/// 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 current 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. /// -/// This 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. +/// There is a small caveat in step 2. As 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. /// -/// 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. - +/// 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) { // 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. @@ -228,97 +337,37 @@ void schedule_render(session_t *ps, bool triggered_by_vblank attr_unused) { 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, frame_time: " "%" PRIu32 ", now_us: %" PRIu64 ", next_msc: %" PRIu64 ", " "divisor: %d", delay_s, ps->last_msc_instant, render_budget, frame_time, now_us, deadline, divisor); schedule: - ps->render_in_progress = RENDER_QUEUED; - ps->redraw_needed = false; - - x_request_vblank_event(ps, ps->last_msc + 1); - - assert(!ev_is_active(&ps->draw_timer)); - ev_timer_set(&ps->draw_timer, delay_s, 0); - ev_timer_start(ps->loop, &ps->draw_timer); -} - -/// Called after a vblank has ended -/// -/// Check if previously queued render has finished, and record the time it took. -void handle_end_of_vblank(session_t *ps) { - if (ps->render_in_progress == RENDER_IDLE) { - // We didn't start rendering for this vblank, no render time to record. - // But if we don't have a vblank estimate, we will ask for one more vblank - // event, so we can collect more data and get an estimate sooner. - if (render_statistics_get_vblank_time(&ps->render_stats) == 0) { - x_request_vblank_event(ps, ps->last_msc + 1); - } - return; - } - - // render_in_progress is either RENDER_STARTED or RENDER_QUEUED - struct timespec render_time; - bool completed; - if (ps->render_in_progress == RENDER_STARTED) { - completed = - ps->backend_data->ops->last_render_time(ps->backend_data, &render_time); + ps->render_queued = true; + // If the backend is not busy, we just need to schedule the render at the + // specified time; otherwise we need to wait for vblank events. + if (!ps->backend_busy) { + assert(!ev_is_active(&ps->draw_timer)); + ev_timer_set(&ps->draw_timer, delay_s, 0); + ev_timer_start(ps->loop, &ps->draw_timer); } else { - completed = false; - } - - // Do we want to be notified when the next vblank comes? First, if frame_pacing is - // disabled, we don't need vblank events; or if the screen is off, we cannot - // request vblank events. Otherwise, we need vblank events in these cases: - // 1) if we know we need to redraw for the next vblank. - // 2) previous render hasn't completed yet, so it will be presented during the - // next vblank. we need to ask for an event for that. - // 3) if we don't have enough data for a vblank interval estimate, see above. - bool need_vblank_events = - ps->frame_pacing && (ps->redraw_needed || !completed || - render_statistics_get_vblank_time(&ps->render_stats) == 0); - - if (need_vblank_events) { - x_request_vblank_event(ps, ps->last_msc + 1); - } - - if (!completed) { - // Render hasn't completed yet, keep render_in_progress unchanged. - log_debug("Last render did not complete during vblank, msc: %" PRIu64, - ps->last_msc); - return; - } - - int render_time_us = - (int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L); - // The frame has been finished and presented, record its render time. - 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->last_schedule_delay = 0; - ps->render_in_progress = RENDER_IDLE; - - if (ps->redraw_needed) { - schedule_render(ps, true); + // We should never set backend_busy to true unless frame_pacing is + // enabled. + assert(ps->vblank_scheduler); + if (!vblank_scheduler_schedule(ps->vblank_scheduler, + schedule_render_at_vblank, ps)) { + // TODO(yshui): handle error here + abort(); + } } } void queue_redraw(session_t *ps) { - // 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->render_in_progress == RENDER_IDLE && !ps->o.benchmark) { - schedule_render(ps, false); - } else if (ps->render_in_progress > RENDER_QUEUED) { - // render_in_progress > RENDER_QUEUED means we have already issued the - // render commands, so a new render must be scheduled to reflect new - // changes. Otherwise the queued render will include1 the new changes. - ps->redraw_needed = true; + if (ps->render_queued) { + return; } + schedule_render(ps, false); } /** @@ -1395,23 +1444,19 @@ static bool redirect_start(session_t *ps) { } if (ps->present_exists && ps->frame_pacing) { - ps->present_event_id = x_new_id(&ps->c); - auto select_input = xcb_present_select_input( - ps->c.c, ps->present_event_id, session_get_target_window(ps), - XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY); - auto notify_msc = xcb_present_notify_msc( - ps->c.c, session_get_target_window(ps), 0, 0, 1, 0); - set_cant_fail_cookie(&ps->c, select_input); - set_cant_fail_cookie(&ps->c, notify_msc); - ps->present_event = xcb_register_for_special_xge( - ps->c.c, &xcb_present_id, ps->present_event_id, NULL); - // Initialize rendering and frame timing statistics, and frame pacing // states. ps->last_msc_instant = 0; ps->last_msc = 0; ps->last_schedule_delay = 0; render_statistics_reset(&ps->render_stats); + ps->vblank_scheduler = + vblank_scheduler_new(ps->loop, &ps->c, session_get_target_window(ps)); + if (!ps->vblank_scheduler) { + return false; + } + vblank_scheduler_schedule(ps->vblank_scheduler, + collect_vblank_interval_statistics, ps); } else if (ps->frame_pacing) { log_error("Present extension is not supported, frame pacing disabled."); ps->frame_pacing = false; @@ -1459,12 +1504,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 @@ -1474,122 +1516,12 @@ static void unredirect(session_t *ps) { log_debug("Screen unredirected."); } -/// Handle PresentCompleteNotify events -/// -/// Record the MSC value and their timestamps, and schedule handle_end_of_vblank() at the -/// correct time. -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; - } - - assert(ps->frame_pacing); - assert(ps->vblank_event_requested); - ps->vblank_event_requested = false; - - // 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 <= ps->last_msc || cne->ust == 0; - if (event_is_invalid) { - log_debug("Invalid PresentCompleteNotify event, %" PRIu64 " %" PRIu64, - cne->msc, cne->ust); - x_request_vblank_event(ps, ps->last_msc + 1); - return; - } - - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - auto now_us = (int64_t)(now.tv_sec * 1000000L + now.tv_nsec / 1000); - auto drift = iabs((int64_t)cne->ust - now_us); - - if (ps->last_msc_instant == 0 && drift > 1000000) { - // 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?), %" PRIi64 " %" PRIu64, - now_us, ps->last_msc_instant); - if (ps->render_in_progress == RENDER_STARTED) { - // When frame_pacing is off, render_in_progress can't be - // RENDER_STARTED. See the comment on schedule_render(). - ps->render_in_progress = RENDER_IDLE; - } - ps->frame_pacing = false; - return; - } - - x_check_dpms_status(ps); - - 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); - if (frame_count == 1 && !ps->screen_is_off) { - 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 { - log_trace("Frame count %lu, frame time: %d us, msc: %" PRIu64 - ", offset: %d us, not adding sample.", - frame_count, frame_time, cne->ust, (int)drift); - } - } - ps->last_msc_instant = cne->ust; - ps->last_msc = cne->msc; - // Note we can't update ps->render_in_progress here because of this present - // complete notify, as we don't know if the render finished before the end of - // vblank or not. We schedule a call to handle_end_of_vblank() to figure out if we - // are still rendering, and update ps->render_in_progress accordingly. - if (now_us > (int64_t)cne->ust) { - handle_end_of_vblank(ps); - } else { - // Wait until the end of the current vblank to call - // handle_end_of_vblank. 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. - log_trace("The end of this vblank is %" PRIi64 " us into the " - "future", - (int64_t)cne->ust - now_us); - assert(!ev_is_active(&ps->vblank_timer)); - ev_timer_set(&ps->vblank_timer, - ((double)cne->ust - (double)now_us) / 1000000.0, 0); - ev_timer_start(ps->loop, &ps->vblank_timer); - } -} - -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))) { @@ -1711,6 +1643,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); @@ -1801,13 +1736,13 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { 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) { 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++; @@ -1816,30 +1751,30 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { } } - if (ps->frame_pacing && did_render) { - ps->render_in_progress = RENDER_STARTED; - } else { - // With frame pacing, we set render_in_progress to RENDER_IDLE after the - // end of vblank. Without frame pacing, we won't be receiving vblank - // events, so we set render_in_progress to RENDER_IDLE 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 render_in_progress to - // RENDER_IDLE. - ps->render_in_progress = RENDER_IDLE; - } + // With frame pacing, we set backend_busy to true after the end of + // vblank. Without frame pacing, we won't be receiving vblank events, so + // we set backend_busy to false here, right after we issue the render + // commands. + // The other case is if we decided there is no change to render, in that + // case no render command is issued, so we also set backend_busy to + // false. + ps->backend_busy = (ps->frame_pacing && did_render); ps->next_render = 0; if (!fade_running) { ps->fade_time = 0L; } + ps->render_queued = false; + // TODO(yshui) Investigate how big the X critical section needs to be. There are // suggestions that rendering should be in the critical section as well. // Queue redraw if animation is running. This should be picked up by next present // event. - ps->redraw_needed = animation; + if (animation) { + queue_redraw(ps); + } } static void draw_callback(EV_P_ ev_timer *w, int revents) { @@ -1855,13 +1790,6 @@ static void draw_callback(EV_P_ ev_timer *w, int revents) { } } -static void vblank_callback(EV_P_ ev_timer *w, int revents attr_unused) { - session_t *ps = session_ptr(w, vblank_timer); - ev_timer_stop(EV_A_ w); - - handle_end_of_vblank(ps); -} - static void x_event_callback(EV_P attr_unused, ev_io *w, int revents attr_unused) { session_t *ps = (session_t *)w; xcb_generic_event_t *ev = xcb_poll_for_event(ps->c.c); @@ -2013,7 +1941,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, @@ -2450,7 +2377,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ev_io_start(ps->loop, &ps->xiow); ev_init(&ps->unredir_timer, tmout_unredir_callback); ev_init(&ps->draw_timer, draw_callback); - ev_init(&ps->vblank_timer, vblank_callback); ev_init(&ps->fade_timer, fade_timer_callback); diff --git a/src/vblank.c b/src/vblank.c new file mode 100644 index 0000000..219e2cb --- /dev/null +++ b/src/vblank.c @@ -0,0 +1,246 @@ +#include + +#include +#include +#include +#include +#include + +#include "compiler.h" +#include "config.h" +#include "list.h" // for container_of +#include "log.h" +#include "vblank.h" +#include "x.h" + +struct vblank_callback { + vblank_callback_t fn; + void *user_data; +}; + +struct vblank_scheduler { + struct x_connection *c; + size_t callback_capacity, callback_count; + struct vblank_callback *callbacks; + struct ev_loop *loop; + xcb_window_t target_window; + enum vblank_scheduler_type type; + bool vblank_event_requested; +}; + +struct present_vblank_scheduler { + struct vblank_scheduler base; + + uint64_t last_msc; + /// The timestamp for the end of last vblank. + uint64_t last_ust; + ev_timer callback_timer; + xcb_present_event_t event_id; + xcb_special_event_t *event; +}; + +struct vblank_scheduler_ops { + void (*init)(struct vblank_scheduler *self); + void (*deinit)(struct vblank_scheduler *self); + void (*schedule)(struct vblank_scheduler *self); + bool (*handle_x_events)(struct vblank_scheduler *self); +}; + +static void +vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event); + +static void present_vblank_scheduler_schedule(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + log_verbose("Requesting vblank event for window 0x%08x, msc %" PRIu64, + base->target_window, self->last_msc + 1); + assert(!base->vblank_event_requested); + x_request_vblank_event(base->c, base->target_window, self->last_msc + 1); + base->vblank_event_requested = true; +} + +static void present_vblank_callback(EV_P attr_unused, ev_timer *w, int attr_unused revents) { + auto sched = container_of(w, struct present_vblank_scheduler, callback_timer); + auto event = (struct vblank_event){ + .msc = sched->last_msc, + .ust = sched->last_ust, + }; + sched->base.vblank_event_requested = false; + vblank_scheduler_invoke_callbacks(&sched->base, &event); +} + +static void present_vblank_scheduler_init(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + base->type = VBLANK_SCHEDULER_PRESENT; + ev_timer_init(&self->callback_timer, present_vblank_callback, 0, 0); + + self->event_id = x_new_id(base->c); + auto select_input = + xcb_present_select_input(base->c->c, self->event_id, base->target_window, + XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY); + set_cant_fail_cookie(base->c, select_input); + self->event = + xcb_register_for_special_xge(base->c->c, &xcb_present_id, self->event_id, NULL); +} + +static void present_vblank_scheduler_deinit(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + ev_timer_stop(base->loop, &self->callback_timer); + auto select_input = + xcb_present_select_input(base->c->c, self->event_id, base->target_window, 0); + set_cant_fail_cookie(base->c, select_input); + xcb_unregister_for_special_event(base->c->c, self->event); +} + +/// Handle PresentCompleteNotify events +/// +/// Schedule the registered callback to be called when the current vblank ends. +static void handle_present_complete_notify(struct present_vblank_scheduler *self, + xcb_present_complete_notify_event_t *cne) { + assert(self->base.type == VBLANK_SCHEDULER_PRESENT); + + if (cne->kind != XCB_PRESENT_COMPLETE_KIND_NOTIFY_MSC) { + return; + } + + assert(self->base.vblank_event_requested); + + // X sometimes sends duplicate/bogus MSC events, when screen has just been turned + // off. Don't use the msc value in these events. We treat this as not receiving a + // vblank event at all, and try to get a new one. + // + // See: + // https://gitlab.freedesktop.org/xorg/xserver/-/issues/1418 + bool event_is_invalid = cne->msc <= self->last_msc || cne->ust == 0; + if (event_is_invalid) { + log_debug("Invalid PresentCompleteNotify event, %" PRIu64 " %" PRIu64, + cne->msc, cne->ust); + x_request_vblank_event(self->base.c, cne->window, self->last_msc + 1); + return; + } + + self->last_ust = cne->ust; + self->last_msc = cne->msc; + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + auto now_us = (unsigned long)(now.tv_sec * 1000000L + now.tv_nsec / 1000); + double delay_sec = 0.0; + if (now_us < cne->ust) { + log_trace("The end of this vblank is %lu us into the " + "future", + cne->ust - now_us); + delay_sec = (double)(cne->ust - now_us) / 1000000.0; + } + // Wait until the end of the current vblank to invoke callbacks. If we + // call it too early, it can mistakenly think the render missed the + // vblank, and doesn't schedule render for the next vblank, causing frame + // drops. + assert(!ev_is_active(&self->callback_timer)); + ev_timer_set(&self->callback_timer, delay_sec, 0); + ev_timer_start(self->base.loop, &self->callback_timer); +} + +static bool handle_present_events(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + xcb_present_generic_event_t *ev; + while ((ev = (void *)xcb_poll_for_special_event(base->c->c, self->event))) { + if (ev->event != self->event_id) { + // This event doesn't have the right event context, it's not meant + // for us. + goto next; + } + + // We only subscribed to the complete notify event. + assert(ev->evtype == XCB_PRESENT_EVENT_COMPLETE_NOTIFY); + handle_present_complete_notify(self, (void *)ev); + next: + free(ev); + } + return true; +} + +static const struct vblank_scheduler_ops vblank_scheduler_ops[LAST_VBLANK_SCHEDULER] = { + [VBLANK_SCHEDULER_PRESENT] = + { + .init = present_vblank_scheduler_init, + .deinit = present_vblank_scheduler_deinit, + .schedule = present_vblank_scheduler_schedule, + .handle_x_events = handle_present_events, + }, +}; + +static void vblank_scheduler_schedule_internal(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].schedule; + assert(fn != NULL); + fn(self); +} + +bool vblank_scheduler_schedule(struct vblank_scheduler *self, + vblank_callback_t vblank_callback, void *user_data) { + if (self->callback_count == 0) { + vblank_scheduler_schedule_internal(self); + } + if (self->callback_count == self->callback_capacity) { + size_t new_capacity = + self->callback_capacity ? self->callback_capacity * 2 : 1; + void *new_buffer = + realloc(self->callbacks, new_capacity * sizeof(*self->callbacks)); + if (!new_buffer) { + return false; + } + self->callbacks = new_buffer; + self->callback_capacity = new_capacity; + } + self->callbacks[self->callback_count++] = (struct vblank_callback){ + .fn = vblank_callback, + .user_data = user_data, + }; + return true; +} + +static void +vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event) { + // callbacks might be added during callback invocation, so we need to + // copy the callback_count. + size_t count = self->callback_count; + for (size_t i = 0; i < count; i++) { + self->callbacks[i].fn(event, self->callbacks[i].user_data); + } + // remove the callbacks that we have called, keep the newly added ones. + memmove(self->callbacks, self->callbacks + count, + (self->callback_count - count) * sizeof(*self->callbacks)); + self->callback_count -= count; + if (self->callback_count) { + vblank_scheduler_schedule_internal(self); + } +} + +void vblank_scheduler_free(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].deinit; + if (fn != NULL) { + fn(self); + } + free(self->callbacks); + free(self); +} + +struct vblank_scheduler *vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c, + xcb_window_t target_window) { + struct vblank_scheduler *self = calloc(1, sizeof(struct present_vblank_scheduler)); + self->target_window = target_window; + self->c = c; + self->loop = loop; + vblank_scheduler_ops[VBLANK_SCHEDULER_PRESENT].init(self); + return self; +} + +bool vblank_handle_x_events(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].handle_x_events; + if (fn != NULL) { + return fn(self); + } + return true; +} \ No newline at end of file diff --git a/src/vblank.h b/src/vblank.h new file mode 100644 index 0000000..3f5c7d1 --- /dev/null +++ b/src/vblank.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "x.h" + +/// An object that schedule vblank events. +struct vblank_scheduler; + +struct vblank_event { + uint64_t msc; + uint64_t ust; +}; + +typedef void (*vblank_callback_t)(struct vblank_event *event, void *user_data); + +/// Schedule a vblank event. +/// +/// Schedule for `cb` to be called when the current vblank ends. If this is called +/// from a callback function for the current vblank, the newly scheduled callback +/// will be called in the next vblank. +/// +/// Returns whether the scheduling is successful. Scheduling can fail if there +/// is not enough memory. +bool vblank_scheduler_schedule(struct vblank_scheduler *self, vblank_callback_t cb, + void *user_data); +struct vblank_scheduler * +vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c, xcb_window_t target_window); +void vblank_scheduler_free(struct vblank_scheduler *); + +bool vblank_handle_x_events(struct vblank_scheduler *self); diff --git a/src/x.c b/src/x.c index 89786e1..45abe31 100644 --- a/src/x.c +++ b/src/x.c @@ -779,15 +779,10 @@ err: return false; } -void x_request_vblank_event(session_t *ps, uint64_t msc) { - if (ps->vblank_event_requested) { - return; - } - +void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc) { auto cookie = - xcb_present_notify_msc(ps->c.c, session_get_target_window(ps), 0, msc, 0, 0); - set_cant_fail_cookie(&ps->c, cookie); - ps->vblank_event_requested = true; + xcb_present_notify_msc(c->c, window, 0, msc, 0, 0); + set_cant_fail_cookie(c, cookie); } static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) { @@ -795,18 +790,19 @@ static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) { return info->state && (info->power_level != XCB_DPMS_DPMS_MODE_ON); } -void x_check_dpms_status(session_t *ps) { - auto r = xcb_dpms_info_reply(ps->c.c, xcb_dpms_info(ps->c.c), NULL); +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_fatal("Failed to query DPMS status."); - abort(); + log_error("Failed to query DPMS status."); + return false; } auto now_screen_is_off = dpms_screen_is_off(r); - if (ps->screen_is_off != now_screen_is_off) { + if (*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; + *screen_is_off = now_screen_is_off; } free(r); + return true; } /** diff --git a/src/x.h b/src/x.h index 60bcfef..df45b5c 100644 --- a/src/x.h +++ b/src/x.h @@ -421,7 +421,9 @@ 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(session_t *ps, uint64_t msc); +void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc); -/// Update ps->screen_is_off to reflect the current DPMS state. -void x_check_dpms_status(session_t *ps); +/// 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);