Android App VSync Lifecycle: Request, Scheduling, Dispatch, and Termination

  1. Initiating a VSync Request

An app triggers a VSync request via Binder IPC, ultimately invoking EventThread::requestNextVsync(). This method updates the associated EventThreadConnection’s internal request state and signals the event thread:

void EventThread::requestNextVsync(const sp<EventThreadConnection>& conn) {
    std::lock_guard<std::mutex> lock(mMutex);
    if (conn->vsyncRequest == VSyncRequest::None) {
        conn->vsyncRequest = VSyncRequest::Single;
        mCondition.notify_all(); // Wake up threadMain loop
    } else if (conn->vsyncRequest == VSyncRequest::SingleSuppressCallback) {
        conn->vsyncRequest = VSyncRequest::Single;
    }
}

The VSyncRequest enum governs dispatch behavior:

  • None: No active scheduling or delivery.
  • Single: Triggers two consecutive VSync deliveries (first as full event, second with suppressed callback).
  • SingleSuppressCallback: Final delivery; resets to None after consumption.
  1. Scheduling Logic and Timer Setup

Inside EventThread::threadMain(), the loop detects pending requests and transitions in to State::VSync. This activates the underlying mVSyncSource, which delegates timing to VSyncDispatchTimerQueue:

void CallbackRepeater::start(std::chrono::nanoseconds work, 
                              std::chrono::nanoseconds ready) {
    mStarted = true;
    mWorkDuration = work;
    mReadyDuration = ready;

    mRegistration.schedule({
        .workDuration = work.count(),
        .readyDuration = ready.count(),
        .earliestVsync = mLastCallTime.count()
    });
}

The scheduling flow proceeds as follows:

  1. Timing calculation: VSyncDispatchTimerQueueEntry::schedule() uses VSyncTracker to project the next software-synchronized timestamp, subtracting workDuration and readyDuration to determine the optimal wakeup time.

  2. Timer registration: The computed absolute nanosecond deadline is passed to Timer::alarmAt(), which configures a timerfd using timerfd_settime().

  3. Event loop integration: The timerfd is registered with an epoll instance in Timer::dispatch(). When the timer fires, epoll_wait() returns, and the stored mCallback is invoked.

  4. Callback Chain and Event Delivery


The callback path forms a tightly coupled chain:

  1. Timer::alarmAt() → invokes VSyncDispatchTimerQueue::timerCallback()
  2. → calls VSyncDispatchTimerQueueEntry::callback()
  3. → forwards to CallbackRepeater::callback()
  4. → delegates to DispSyncSource::onVsyncCallback()
  5. → routes to EventThread::onVSyncEvent()

In onVSyncEvent(), a new DisplayEventReceiver::Event is enqueued into mPendingEvents and mCondition is signaled:

void EventThread::onVSyncEvent(nsecs_t ts, VSyncData data) {
    mPendingEvents.push_back(makeVSync(mVSyncState->displayId, ts,
        ++mVSyncState->count, data.expectedPresentationTime,
        data.deadlineTimestamp));
    mCondition.notify_all();
}

Back in threadMain(), the loop consumes the front of mPendingEvents, filters eligible connections via shouldConsumeEvent(), and dispatches via dispatchEvent() — which writes the event over a Unix domain socket to the client’s DisplayEventReceiver.

  1. Request Termination and State Cleanup

VSync termination occurs implicitly when no connection has an active VSyncRequest. In threadMain(), vsyncRequested becomes false after shouldConsumeEvent() downgrades SingleSuppressCallback to None. This causes nextState to shift from State::VSync to State::Idle, triggering:

  • mVSyncSource->setVSyncEnabled(false), which cascades to CallbackRepeater::stop()
  • stop() sets mStarted = false and calls mRegistration.cancel() to disarm the timer
  • Subsequent invocations of CallbackRepeater::callback() exit early due to the !mStarted guard

This ensures no further timer rescheduling, cleanly halting the VSync pipeline without race conditions or resource leaks.

Tags: android-display vsync surfaceflinger disp-sync timerfd

Posted on Thu, 10 Sep 2026 16:43:53 +0000 by Alkimuz