Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-114721

CAMetalLayer.nextDrawable() blocks QSGRenderThread, thus blocking Main Thread

    XMLWordPrintable

Details

    • Bug
    • Resolution: Unresolved
    • P2: Important
    • None
    • 6.3.0, 6.3.2
    • Quick: SceneGraph
    • None
    • - x86_64 Qt Metal app (VSync ON)
      - macOS Monterey or newer
      - ARM Macs (M1, M2, etc.)
    • macOS

    Description

      Hi Qt,

      We've experienced some GUI lag/frame drops when running x86_64 builds on ARM Macs (e.g. M1, M2) and also reproduced the same issue in basic Qt demo app using Metal.

      ====================================================================

      To reproduce the issue

      • Build Qt app using Metal for x86_64 with VSync ON
      • Upgrade ARM Macs to Monterey or newer (we also found that the QSGRenderThread runs around 4 times longer on each update after upgrading to Monterey or newer)
      • Run x86_64 builds on ARM Macs (Rosetta involved)

       

      Root cause and explanation

      CAMetalLayer.nextDrawable() blocks the QSGRenderThread for around 10 ms every update which retards the QSGRenderThread.

      And it ends up with the Main Thread being ahead and posting new WMSyncEvent while the QSGRenderThread is still rendering and then wait for the next Metal drawable for the previous event, so the Main Thread will keep blocking itself waiting for the delayed syncing to finish every time, which lasts around 16 ms and made the Main Thread have very limited time/chance to process other events.

      The issue looks like a combination of longer rendering time on Monterey or newer and Rosetta involvement ending up with asking too much from the CAMetalLayer and caused the waiting on the next Metal drawable.

       

      Suggested fix

      Skip the update request/WMSyncEvent if the QSGRenderThread is still busy (can check QSGRenderThread::sleeping == false) so the QSGRenderThread can get a chance to empty the event queue and catch up, otherwise the wait on the next Metal drawable will happen constantly as there are always new events to render.

      Note: Turning VSync off in CAMetalLayer can also eliminate the wait on drawable but will cause screen tearing sometimes which is not ideal.

       

      Backtraces

      Main Thread:

      __psynch_cvwait
      QWaitConditionPrivate::wait(QDeadlineTimer)
      QWaitConditionPrivate::wait(QDeadlineTimer)
      QWaitCondition::wait(QMutex*, QDeadlineTimer)
      QSGThreadedRenderLoop::polishAndSync(QSGThreadedRenderLoop::Window*, bool)
      QSGThreadedRenderLoop::handleUpdateRequest(QQuickWindow*)
      QQuickWindow::event(QEvent*)
      serato::gui::MainQtView::event(QEvent*)
      QCoreApplicationPrivate::setEventSpontaneous(QEvent*, bool)
      QApplicationPrivate::notify_helper(QObject*, QEvent*)
      QApplication::notify(QObject*, QEvent*)
      QCoreApplication::notifyInternal2(QObject*, QEvent*)
      QPlatformWindow::deliverUpdateRequest()
      QCocoaWindow::deliverUpdateRequest()
      QCocoaScreen::deliverUpdateRequests()
      _dispatch_client_callout
      _dispatch_continuation_pop
      _dispatch_source_invoke
      _dispatch_main_queue_drain
      _dispatch_main_queue_callback_4CF
      {}CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE{}
      __CFRunLoopRun
      CFRunLoopRunSpecific
      RunCurrentEventLoopInMode
      ReceiveNextEventCommon
      _BlockUntilNextEventMatchingListInModeWithFilter
      _DPSNextEvent
      -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:]
      -[NSApplication run]
      RunEventLoop()
      main
      start

       

      QSGRenderThread:

      semaphore_timedwait_trap    
      _dispatch_semaphore_wait_slow    
      _dispatch_semaphore_wait_slow    
      -[CAMetalLayer nextDrawable]    
      CAMetalLayer_nextDrawable    
      QRhiMetal::beginPass(QRhiCommandBuffer*, QRhiRenderTarget*, QColor const&, QRhiDepthStencilClearValue const&, QRhiResourceUpdateBatch*, QFlags<QRhiCommandBuffer::BeginPassFlag>)    
      QRhiCommandBuffer::beginPass(QRhiRenderTarget*, QColor const&, QRhiDepthStencilClearValue const&, QRhiResourceUpdateBatch*, QFlags<QRhiCommandBuffer::BeginPassFlag>)    
      QSGBatchRenderer::Renderer::beginRenderPass(QSGBatchRenderer::Renderer::RenderPassContext*)    
      QSGBatchRenderer::Renderer::render()    
      bool std::{}1::{}cxx_atomic_load<bool>(std::{}1::{}cxx_atomic_base_impl<bool> const*, std::{_}_1::memory_order)    
      std::{}1::{}atomic_base<bool, false>::load(std::{_}_1::memory_order) const    
      bool QAtomicOps<bool>::loadRelaxed<bool>(std::__1::atomic<bool> const&)    
      QBasicAtomicInteger<bool>::loadRelaxed() const    
      QLoggingCategory::isDebugEnabled() const    
      QSGRenderer::renderScene()    
      QQuickWindowPrivate::renderSceneGraph(QSize const&, QSize const&)    
      bool std::{}1::{}cxx_atomic_load<bool>(std::{}1::{}cxx_atomic_base_impl<bool> const*, std::{_}_1::memory_order)    
      std::{}1::{}atomic_base<bool, false>::load(std::{_}_1::memory_order) const    
      bool QAtomicOps<bool>::loadRelaxed<bool>(std::__1::atomic<bool> const&)    
      QBasicAtomicInteger<bool>::loadRelaxed() const    
      QLoggingCategory::isDebugEnabled() const    
      QSGRenderThread::syncAndRender()    
      QSGRenderThread::run()    
      QThreadPrivate::start(void*)    
      _pthread_start    
      thread_start    

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            qt.team.graphics.and.multimedia Qt Graphics Team
            percy.pan Percy Pan
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

            Dates

              Created:
              Updated:

              Gerrit Reviews

                There are no open Gerrit changes