8.Vsync 8.1概論 VSYNC(Vertical Synchronization)是一個相當古老的概念,對於游戲玩家,它有一個更加大名鼎鼎的中文名字—-垂直同步。 “垂直同步(vsync)”指的是顯卡的輸出幀數和屏幕的垂直刷新率相同,這完全是一個CRT顯示器上的概念。其實無論是VSYNC還是 ...
8.Vsync
8.1概論
VSYNC(Vertical Synchronization)是一個相當古老的概念,對於游戲玩家,它有一個更加大名鼎鼎的中文名字—-垂直同步。
“垂直同步(vsync)”指的是顯卡的輸出幀數和屏幕的垂直刷新率相同,這完全是一個CRT顯示器上的概念。其實無論是VSYNC還是垂直同步這個名字,
因為LCD根本就沒有垂直掃描的這種東西,因此這個名字本身已經沒有意義。但是基於歷史的原因,這個名稱在圖形圖像領域被沿襲下來。
在當下,垂直同步的含義我們可以理解為,使得顯卡生成幀的速度和屏幕刷新的速度的保持一致。舉例來說,如果屏幕的刷新率為60Hz,那麼生成幀
的速度就應該被固定在1/60 s。
8.2 VSync信號的產生和分發
VSync信號的產生在android_frameworks_native\services\surfaceflinger\DisplayHardware\HWComposer.cpp裡面定義:
HWComposer::HWComposer( const sp<SurfaceFlinger>& flinger, EventHandler& handler) : mFlinger(flinger), mFbDev(0), mHwc(0), mNumDisplays(1), mCBContext(new cb_context), mEventHandler(handler), mDebugForceFakeVSync(false), mVDSEnabled(false) { for (size_t i =0 ; i<MAX_HWC_DISPLAYS ; i++) { mLists[i] = 0; } for (size_t i=0 ; i<HWC_NUM_PHYSICAL_DISPLAY_TYPES ; i++) { mLastHwVSync[i] = 0; mVSyncCounts[i] = 0; } char value[PROPERTY_VALUE_MAX]; property_get("debug.sf.no_hw_vsync", value, "0"); mDebugForceFakeVSync = atoi(value); bool needVSyncThread = true; // Note: some devices may insist that the FB HAL be opened before HWC. int fberr = loadFbHalModule(); loadHwcModule(); if (mFbDev && mHwc && hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) { // close FB HAL if we don't needed it. // FIXME: this is temporary until we're not forced to open FB HAL // before HWC. framebuffer_close(mFbDev); mFbDev = NULL; } // If we have no HWC, or a pre-1.1 HWC, an FB dev is mandatory. if ((!mHwc || !hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) && !mFbDev) { ALOGE("ERROR: failed to open framebuffer (%s), aborting", strerror(-fberr)); abort(); } // these display IDs are always reserved for (size_t i=0 ; i<NUM_BUILTIN_DISPLAYS ; i++) { mAllocatedDisplayIDs.markBit(i); } if (mHwc) { ALOGI("Using %s version %u.%u", HWC_HARDWARE_COMPOSER, (hwcApiVersion(mHwc) >> 24) & 0xff, (hwcApiVersion(mHwc) >> 16) & 0xff); if (mHwc->registerProcs) { mCBContext->hwc = this; mCBContext->procs.invalidate = &hook_invalidate; mCBContext->procs.vsync = &hook_vsync; if (hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) mCBContext->procs.hotplug = &hook_hotplug; else mCBContext->procs.hotplug = NULL; memset(mCBContext->procs.zero, 0, sizeof(mCBContext->procs.zero)); mHwc->registerProcs(mHwc, &mCBContext->procs); } // don't need a vsync thread if we have a hardware composer needVSyncThread = false; // always turn vsync off when we start eventControl(HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, 0); // the number of displays we actually have depends on the // hw composer version if (hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_3)) { // 1.3 adds support for virtual displays mNumDisplays = MAX_HWC_DISPLAYS; } else if (hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) { // 1.1 adds support for multiple displays mNumDisplays = NUM_BUILTIN_DISPLAYS; } else { mNumDisplays = 1; } } if (mFbDev) { ALOG_ASSERT(!(mHwc && hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)), "should only have fbdev if no hwc or hwc is 1.0"); DisplayData& disp(mDisplayData[HWC_DISPLAY_PRIMARY]); disp.connected = true; disp.format = mFbDev->format; DisplayConfig config = DisplayConfig(); config.width = mFbDev->width; config.height = mFbDev->height; config.xdpi = mFbDev->xdpi; config.ydpi = mFbDev->ydpi; #ifdef QCOM_BSP config.secure = true; //XXX: Assuming primary is always true #endif config.refresh = nsecs_t(1e9 / mFbDev->fps); disp.configs.push_back(config); disp.currentConfig = 0; } else if (mHwc) { // here we're guaranteed to have at least HWC 1.1 for (size_t i =0 ; i<NUM_BUILTIN_DISPLAYS ; i++) { queryDisplayProperties(i); } } // read system property for VDS solution // This property is expected to be setup once during bootup if( (property_get("persist.hwc.enable_vds", value, NULL) > 0) && ((!strncmp(value, "1", strlen("1"))) || !strncasecmp(value, "true", strlen("true")))) { //HAL virtual display is using VDS based implementation mVDSEnabled = true; } if (needVSyncThread) { // we don't have VSYNC support, we need to fake it mVSyncThread = new VSyncThread(*this); } #ifdef QCOM_BSP // Threshold Area to enable GPU Tiled Rect. property_get("debug.hwc.gpuTiledThreshold", value, "1.9"); mDynThreshold = atof(value); #endif }HWComposer
bool needVSyncThread = true;
是否需要模擬產生VSync信號,預設是開啟的。
// Note: some devices may insist that the FB HAL be opened before HWC. int fberr = loadFbHalModule(); loadHwcModule();
打開fb & hwc設備的HAL模塊。
if (mHwc->registerProcs) { mCBContext->hwc = this; mCBContext->procs.invalidate = &hook_invalidate; mCBContext->procs.vsync = &hook_vsync; if (hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) mCBContext->procs.hotplug = &hook_hotplug; else mCBContext->procs.hotplug = NULL; memset(mCBContext->procs.zero, 0, sizeof(mCBContext->procs.zero)); mHwc->registerProcs(mHwc, &mCBContext->procs); } // don't need a vsync thread if we have a hardware composer needVSyncThread = false;
硬體VSync信號啟動,不需要軟體模擬。
if (needVSyncThread) { // we don't have VSYNC support, we need to fake it mVSyncThread = new VSyncThread(*this); }
如果需要,啟動VSyncThread線程來開啟軟體模擬。
8.2.1硬體產生
mHwc->registerProcs(mHwc, &mCBContext->procs);
hwc會產生回調:procs.vsync & procs.invalidate 信號。
void HWComposer::vsync(int disp, int64_t timestamp) { if (uint32_t(disp) < HWC_NUM_PHYSICAL_DISPLAY_TYPES) { { Mutex::Autolock _l(mLock); // There have been reports of HWCs that signal several vsync events // with the same timestamp when turning the display off and on. This // is a bug in the HWC implementation, but filter the extra events // out here so they don't cause havoc downstream. if (timestamp == mLastHwVSync[disp]) { ALOGW("Ignoring duplicate VSYNC event from HWC (t=%" PRId64 ")", timestamp); return; } mLastHwVSync[disp] = timestamp; } char tag[16]; snprintf(tag, sizeof(tag), "HW_VSYNC_%1u", disp); ATRACE_INT(tag, ++mVSyncCounts[disp] & 1); mEventHandler.onVSyncReceived(disp, timestamp); } }
最終會通知mEventHandler的消息,這個handler從那裡來的呢?
void SurfaceFlinger::init(){ mHwc = new HWComposer(this, *static_cast<HWComposer::EventHandler *>(this)); }
Yes,handler就是SurfaceFlinger,對嘛。SurfaceFlinger就是Surface合成的總管,所以這個信號一定會被它接收。
class SurfaceFlinger : public BnSurfaceComposer, private IBinder::DeathRecipient, private HWComposer::EventHandler
8.2.2軟體模擬信號
bool HWComposer::VSyncThread::threadLoop() { { // scope for lock Mutex::Autolock _l(mLock); while (!mEnabled) { mCondition.wait(mLock); } } const nsecs_t period = mRefreshPeriod; const nsecs_t now = systemTime(CLOCK_MONOTONIC); nsecs_t next_vsync = mNextFakeVSync; nsecs_t sleep = next_vsync - now; if (sleep < 0) { // we missed, find where the next vsync should be sleep = (period - ((now - next_vsync) % period)); next_vsync = now + sleep; } mNextFakeVSync = next_vsync + period; struct timespec spec; spec.tv_sec = next_vsync / 1000000000; spec.tv_nsec = next_vsync % 1000000000; int err; do { err = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &spec, NULL); } while (err<0 && errno == EINTR); if (err == 0) { mHwc.mEventHandler.onVSyncReceived(0, next_vsync); } return true; }
判斷系統Vsync信號開關。然後計算下一次刷新的時間點。
const nsecs_t period = mRefreshPeriod;
刷新間隔,CLOCK_MONOTONIC是從系統開機後的時間間隔(tick累加)
得到需要等待的時間sleep,和下一次vsync信號的時間點。
然後一個do while的操作,來等待信號時間點的到來。
最後,發出這個信號。
這裡還有個情況,就是一開始的地方,mEnable變數,系統可以設置enable來控制vsync信號的產生。
void HWComposer::VSyncThread::setEnabled(bool enabled)
8.3 SurfaceFlinger處理Vsync信號
在4.4以前,Vsync的處理通過EventThread就可以了。但是KK再次對這段邏輯進行細化和複雜化。Google真是費勁心思為了提升性能。
先來直接看下Surfaceflinger的onVSyncReceived函數:
void SurfaceFlinger::onVSyncReceived(int type, nsecs_t timestamp) { bool needsHwVsync = false; { // Scope for the lock Mutex::Autolock _l(mHWVsyncLock); if (type == 0 && mPrimaryHWVsyncEnabled) { needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);//mPromaryDisplays是什麼? } } if (needsHwVsync) { enableHardwareVsync();//做了什麼 } else { disableHardwareVsync(false);//做了什麼
}
}
雖然很短,但是乍一看還是一頭霧水
mPrimaryHWVsyncEnabled是什麼時候被賦值的?
mPrimaryDispSync是什麼,addResyncSample又做了什麼?
enableHardwareVsync &disableHardwareVsync在乾什麼?
要解答這些問題,就從SurfaceFlinger::init開始看
void SurfaceFlinger::init() { ALOGI( "SurfaceFlinger's main thread ready to run. " "Initializing graphics H/W..."); status_t err; Mutex::Autolock _l(mStateLock); /* Set the mask bit of the sigset to block the SIGPIPE signal */ sigset_t sigMask; sigemptyset (&sigMask); sigaddset(&sigMask, SIGPIPE); sigprocmask(SIG_BLOCK, &sigMask, NULL); // initialize EGL for the default display mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(mEGLDisplay, NULL, NULL); // Initialize the H/W composer object. There may or may not be an // actual hardware composer underneath. mHwc = new HWComposer(this, *static_cast<HWComposer::EventHandler *>(this)); // get a RenderEngine for the given display / config (can't fail) mRenderEngine = RenderEngine::create(mEGLDisplay, mHwc->getVisualID()); // retrieve the EGL context that was selected/created mEGLContext = mRenderEngine->getEGLContext(); LOG_ALWAYS_FATAL_IF(mEGLContext == EGL_NO_CONTEXT, "couldn't create EGLContext"); // initialize our non-virtual displays for (size_t i=0 ; i<DisplayDevice::NUM_BUILTIN_DISPLAY_TYPES ; i++) { DisplayDevice::DisplayType type((DisplayDevice::DisplayType)i); // set-up the displays that are already connected if (mHwc->isConnected(i) || type==DisplayDevice::DISPLAY_PRIMARY) { #ifdef QCOM_BSP // query from hwc if the non-virtual display is secure. bool isSecure = mHwc->isSecure(i);; #else // All non-virtual displays are currently considered secure bool isSecure = true; #endif createBuiltinDisplayLocked(type, isSecure); wp<IBinder> token = mBuiltinDisplays[i]; sp<IGraphicBufferProducer> producer; sp<IGraphicBufferConsumer> consumer; BufferQueue::createBufferQueue(&producer, &consumer, new GraphicBufferAlloc()); sp<FramebufferSurface> fbs = new FramebufferSurface(*mHwc, i, consumer); int32_t hwcId = allocateHwcDisplayId(type); sp<DisplayDevice> hw = new DisplayDevice(this, type, hwcId, mHwc->getFormat(hwcId), isSecure, token, fbs, producer, mRenderEngine->getEGLConfig()); if (i > DisplayDevice::DISPLAY_PRIMARY) { // FIXME: currently we don't get blank/unblank requests // for displays other than the main display, so we always // assume a connected display is unblanked. ALOGD("marking display %zu as acquired/unblanked", i); hw->setPowerMode(HWC_POWER_MODE_NORMAL); } mDisplays.add(token, hw); } } // make the GLContext current so that we can create textures when creating Layers // (which may happens before we render something) getDefaultDisplayDevice()->makeCurrent(mEGLDisplay, mEGLContext); // start the EventThread sp<VSyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync, vsyncPhaseOffsetNs, true, "app"); mEventThread = new EventThread(vsyncSrc); sp<VSyncSource> sfVsyncSrc = new DispSyncSource(&mPrimaryDispSync, sfVsyncPhaseOffsetNs, true, "sf"); mSFEventThread = new EventThread(sfVsyncSrc); mEventQueue.setEventThread(mSFEventThread); mEventControlThread = new EventControlThread(this); mEventControlThread->run("EventControl", PRIORITY_URGENT_DISPLAY); android_set_rt_ioprio(mEventControlThread->getTid(), 1); // set a fake vsync period if there is no HWComposer if (mHwc->initCheck() != NO_ERROR) { mPrimaryDispSync.setPeriod(16666667); } // initialize our drawing state mDrawingState = mCurrentState; // set initial conditions (e.g. unblank default device) initializeDisplays(); // start boot animation startBootAnim(); }SurfaceFlinger::init
有2個幾乎一樣的DispSyncSource,它的目的是提供了Vsync的虛擬化。關於這塊的分析,可以參考Android 4.4(KitKat)中VSync信號的虛擬化
在三緩衝的框架下,對於一幀內容,先等APP UI畫完了,SurfaceFlinger再出場整合到FrameBuffer
而現在google就是讓它們一起跑起來。
然後搞了2個延時,這樣就不會有問題。對應vsyncSrc(繪圖延時) & sfVsyncSrc(合成延時)
8.3.1 EventThread
bool EventThread::threadLoop() { DisplayEventReceiver::Event event; Vector< sp<EventThread::Connection> > signalConnections; signalConnections = waitForEvent(&event); // dispatch events to listeners... const size_t count = signalConnections.size(); for (size_t i=0 ; i<count ; i++) { const sp<Connection>& conn(signalConnections[i]); // now see if we still need to report this event status_t err = conn->postEvent(event); if (err == -EAGAIN || err == -EWOULDBLOCK) { // The destination doesn't accept events anymore, it's probably // full. For now, we just drop the events on the floor. // FIXME: Note that some events cannot be dropped and would have // to be re-sent later. // Right-now we don't have the ability to do this. ALOGW("EventThread: dropping event (%08x) for connection %p", event.header.type, conn.get()); } else if (err < 0) { // handle any other error on the pipe as fatal. the only // reasonable thing to do is to clean-up this connection. // The most common error we'll get here is -EPIPE. removeDisplayEventConnection(signalConnections[i]); } } return true; }
EventThread會發送消息到surfaceflinger裡面的MessageQueue。
MessageQueue處理消息:
int MessageQueue::eventReceiver(int /*fd*/, int /*events*/) { ssize_t n; DisplayEventReceiver::Event buffer[8]; while ((n = DisplayEventReceiver::getEvents(mEventTube, buffer, 8)) > 0) { for (int i=0 ; i<n ; i++) { if (buffer[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) { #if INVALIDATE_ON_VSYNC mHandler->dispatchInvalidate(); #else mHandler->dispatchRefresh(); #endif break; } } } return 1; }
如果Event的類型是
DisplayEventReceiver::DISPLAY_EVENT_VSYNC
正是我們需要的類型,所以,就有2中處理方式:
UI進程需要先準備好數據(invalidate),然後Vsync信號來了以後,就開始刷新屏幕。
SurfaceFlinger是在Vsync來臨之際準備數據然後刷新,還是平常就準備當VSync來臨是再刷新。
先來看dispatchInvalidate,最後處理的地方就是這裡。
case MessageQueue::INVALIDATE: { bool refreshNeeded = handleMessageTransaction(); refreshNeeded |= handleMessageInvalidate(); refreshNeeded |= mRepaintEverything; if (refreshNeeded) { // Signal a refresh if a transaction modified the window state, // a new buffer was latched, or if HWC has requested a full // repaint signalRefresh(); } break; }
我們來看下handleMessageRefresh:
void SurfaceFlinger::handleMessageRefresh() { ATRACE_CALL(); preComposition(); //合成前的準備 rebuildLayerStacks();//重新建立layer堆棧 setUpHWComposer();//HWComposer的設定 #ifdef QCOM_BSP setUpTiledDr(); #endif doDebugFlashRegions(); doComposition(); //正式合成工作 postComposition(); //合成的後期工作 }
8.3.2handleMessageTransaction
handleMessageTransaction在簡單判斷後直接調用handlerTransaction。
可以看到裡面的handleTransactionLocked才是代碼真正處理的地方。
void SurfaceFlinger::handleTransactionLocked(uint32_t transactionFlags) { const LayerVector& currentLayers(mCurrentState.layersSortedByZ); const size_t count = currentLayers.size(); /* * Traversal of the children * (perform the transaction for each of them if needed) */ if (transactionFlags & eTraversalNeeded) { for (size_t i=0 ; i<count ; i++) { const sp<Layer>& layer(currentLayers[i]); uint32_t trFlags = layer->getTransactionFlags(eTransactionNeeded); if (!trFlags) continue; const uint32_t flags = layer->doTransaction(0); if (flags & Layer::eVisibleRegion) mVisibleRegionsDirty = true; } } /* * Perform display own transactions if needed */ if (transactionFlags & eDisplayTransactionNeeded) { // here we take advantage of Vector's copy-on-write semantics to // improve performance by skipping the transaction entirely when // know that the lists are identical const KeyedVector< wp<IBinder>, DisplayDeviceState>& curr(mCurrentState.displays); const KeyedVector< wp<IBinder>, DisplayDeviceState>& draw(mDrawingState.displays); if (!curr.isIdenticalTo(draw)) { mVisibleRegionsDirty = true; const size_t cc = curr.size(); size_t dc = draw.size(); // find the displays that were removed // (ie: in drawing state but not in current state) // also handle displays that changed // (ie: displays that are in both lists) for (size_t i=0 ; i<dc ; i++) { const ssize_t j = curr.indexOfKey(draw.keyAt(i)); if (j < 0) { // in drawing state but not in current state if (!draw[i].isMainDisplay()) { // Call makeCurrent() on the primary display so we can // be sure that nothing associated with this display // is current. const sp<const DisplayDevice> defaultDisplay(getDefaultDisplayDevice()); defaultDisplay->makeCurrent(mEGLDisplay, mEGLContext); sp<DisplayDevice> hw(getDisplayDevice(draw.keyAt(i))); if (hw != NULL) hw->disconnect(getHwComposer()); if (draw[i].type < DisplayDevice::NUM_BUILTIN_DISPLAY_TYPES) mEventThread->onHotplugReceived(draw[i].type, false); mDisplays.removeItem(draw.keyAt(i)); } else { ALOGW("trying to remove the main display"); } } else { // this display is in both lists. see if something changed. const DisplayDeviceState& state(curr[j]); const wp<IBinder>& display(curr.keyAt(j)); if (state.surface->asBinder() != draw[i].surface->asBinder()) { // changing the surface is like destroying and // recreating the DisplayDevice, so we just remove it // from the drawing state, so that it get re-added // below. sp<DisplayDevice> hw(getDisplayDevice(display)); if (hw != NULL) hw->disconnect(getHwComposer()); mDisplays.removeItem(display); mDrawingState.displays.removeItemsAt(i); dc--; i--; // at this point we must loop to the next item continue; } const sp<DisplayDevice> disp(getDisplayDevice(display)); if (disp != NULL) { if (state.layerStack != draw[i].layerStack) { disp->setLayerStack(state.layerStack); } if ((state.orientation != draw[i].orientation) || (state.viewport != draw[i].viewport) || (state.frame != draw[i].frame)) { #ifdef QCOM_BSP int orient = state.orientation; // Honor the orientation change after boot // animation completes and make sure boot // animation is shown in panel orientation always. if(mBootFinished){ disp->setProjection(state.orientation, state.viewport, state.frame); orient = state.orientation; } else{ char property[PROPERTY_VALUE_MAX]; int panelOrientation = DisplayState::eOrientationDefault; if(property_get("persist.panel.orientation", property, "0") > 0){ panelOrientation = atoi(property) / 90; } disp->setProjection(panelOrientation, state.viewport, state.frame); orient = panelOrientation; } // Set the view frame of each display only of its // default orientation. if(orient == DisplayState::eOrientationDefault and state.frame.isValid()) { qdutils::setViewFrame(disp->getHwcDisplayId(), state.frame.left, state.frame.top, state.frame.right, state.frame.bottom); } #else disp->setProjection(state.orientation, state.viewport, state.frame); #endif } if (state.width != draw[i].width || state.height != draw[i].height) { disp->setDisplaySize(state.width, state.height); } } } } // find displays that were added // (ie: in current state but not in drawing state) for (size_t i=0 ; i<cc ; i++) { if (draw.indexOfKey(curr.keyAt(i)) < 0) { const DisplayDeviceState& state(curr[i]); sp<DisplaySurface> dispSurface; sp<IGraphicBufferProducer> producer; sp<IGraphicBufferProducer> bqProducer; sp<IGraphicBufferConsumer> bqConsumer; BufferQueue::createBufferQueue(&bqProducer, &bqConsumer, new GraphicBufferAlloc()); int32_t hwcDisplayId = -1; if (state.isVirtualDisplay()) { // Virtual displays without a surface are dormant: // they have external state (layer stack, projection, // etc.) but no internal state (i.e. a DisplayDevice). if (state.surface != NULL) { configureVirtualDisplay(hwcDisplayId, dispSurface, producer, state, bqProducer, bqConsumer); } } else { ALOGE_IF(state.surface!=NULL, "adding a supported display, but rendering " "surface is provided (%p), ignoring it", state.surface.get()); hwcDisplayId = allocateHwcDisplayId(state.type); // for supported (by hwc) displays we provide our // own rendering surface dispSurface = new FramebufferSurface(*mHwc, state.type, bqConsumer); producer = bqProducer; } const wp<IBinder>& display(curr.keyAt(i)); if (dispSurface != NULL && producer != NULL) { sp<DisplayDevice> hw = new DisplayDevice(this, state.type, hwcDisplayId, mHwc->getFormat(hwcDisplayId), state.isSecure, display, dispSurface, producer, mRenderEngine->getEGLConfig()); hw->setLayerStack(state.layerStack); hw->setProjection(state.orientation, state.viewport, state.frame); hw->setDisplayName(state.displayName); // When a new display device is added update the active // config by querying HWC otherwise the default config // (config 0) will be used. int activeConfig = mHwc->getActiveConfig(hwcDisplayId); if (activeConfig >= 0) { hw->setActiveConfig(activeConfig); } mDisplays.add(display, hw); if (state.isVirtualDisplay()) { if (hwcDisplayId >= 0) { mHwc->setVirtualDisplayProperties(hwcDisplayId, hw->getWidth(), hw->getHeight(), hw->getFormat()); } } else { mEventThread->onHotplugReceived(state.type, true); } } }