Qt GUI burns most of a CPU core repeatedly decoding icons
Operating System Info
Other
Other OS
Arch Linux
OBS Studio Version
32.0.0
OBS Studio Version (Other)
32.0.1
OBS Studio Log URL
https://obsproject.com/logs/ptpJ2CITAoHKvmns
OBS Studio Crash Log URL
No response
Expected Behavior
OBS Studio does not burn a CPU core decoding icons.
Current Behavior
OBS Studio burns most of a CPU core. Samply profiler shows most of it is spent in QStyleSheetStyle::drawControl(QStyle::ControlElement, QStyleOption const*, QPainter*, QWidget const*) const ... QFusionStyle::standardIcon(QStyle::StandardPixmap, QStyleOption const*, QWidget const*) const ... QFusionStyle::iconFromTheme(QStyle::StandardPixmap) const ... QResourceFileEngine::open(QFlags<QIODeviceBase::OpenModeFlag>, std::optional<QFlags<QFileDevice::Permission> >).
Profiler screenshot:
Manual sampling by hitting Ctrl+C in gdb replicated these results; OBS used 60% of my CPU, and two out of four Ctrl+C backtraces were in QFusionStyle::iconFromTheme.
Example backtrace:
(gdb) bt
#0 simdSwapLoop<unsigned short> (src=0x7fffdda75626 <qt_resource_name+6> "", bytes=28, dst=0x555558665750 "q") at /usr/src/debug/qt6-base/qtbase/src/corelib/global/qendian.cpp:825
#1 bswapLoop<unsigned short> (src=0x7fffdda75626 <qt_resource_name+6> "", n=28, dst=0x555558665750 "q") at /usr/src/debug/qt6-base/qtbase/src/corelib/global/qendian.cpp:852
#2 qbswap<2> (source=0x7fffdda75626 <qt_resource_name+6>, n=<optimized out>, dest=0x555558665750) at /usr/src/debug/qt6-base/qtbase/src/corelib/global/qendian.cpp:865
#3 0x00007ffff2d3f941 in qFromBigEndian<char16_t> (source=<optimized out>, count=14, dest=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/corelib/global/qendian.h:213
#4 (anonymous namespace)::QResourceRoot::name (this=<optimized out>, node=1) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:788
#5 (anonymous namespace)::QResourceRoot::findNode (this=this@entry=0x555555adadd0, _path=..., locale=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:858
#6 0x00007ffff2d422b9 in QResourcePrivate::load (this=this@entry=0x555557dfdea0, file=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:339
#7 0x00007ffff2d42b1b in QResourcePrivate::ensureInitialized (this=this@entry=0x555557dfdea0) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:389
#8 0x00007ffff2d42f57 in QResource::fileName (this=0x555557dfd0d8) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:574
#9 QResourceFileEngine::open (this=<optimized out>, flags=..., permissions=std::optional [no contained value]) at /usr/src/debug/qt6-base/qtbase/src/corelib/io/qresource.cpp:1441
#10 0x00007ffff2d2121f in QFile::open (this=0x555557b7e000, mode=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/global/qflags.h:77
#11 0x00007ffff355b3c2 in QImageReaderPrivate::initHandler (this=0x555557df9570) at /usr/src/debug/qt6-base/qtbase/src/gui/image/qimagereader.cpp:513
#12 0x00007ffff355b6f9 in QImageReader::supportsOption (this=0x7fffffffae00, option=QImageIOHandler::ScaledSize) at /usr/src/debug/qt6-base/qtbase/src/gui/image/qimagereader.cpp:1410
#13 0x00007ffff35145a2 in (anonymous namespace)::ImageReader::ImageReader (this=0x7fffffffae00, fileName=..., size=...) at /usr/src/debug/qt6-base/qtbase/src/gui/image/qicon.cpp:44
#14 QPixmapIconEngine::addFile (this=0x555556112970, fileName=<optimized out>, size=..., mode=QIcon::Normal, state=QIcon::Off) at /usr/src/debug/qt6-base/qtbase/src/gui/image/qicon.cpp:460
#15 0x00007ffff35153b6 in QIcon::addFile (this=this@entry=0x7fffffffb208, fileName=..., size=..., mode=mode@entry=QIcon::Normal, state=state@entry=QIcon::Off) at /usr/src/debug/qt6-base/qtbase/src/gui/image/qicon.cpp:1195
#16 0x00007ffff40384c3 in operator() (prefix=..., icon=..., __closure=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qfusionstyle.cpp:3509
#17 0x00007ffff4038e0c in QFusionStyle::iconFromTheme (this=this@entry=0x555555cb8ec0, standardIcon=standardIcon@entry=QStyle::SP_TitleBarNormalButton) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qfusionstyle.cpp:3514
#18 0x00007ffff4038e69 in QFusionStyle::standardIcon (this=0x555555cb8ec0, standardIcon=QStyle::SP_TitleBarNormalButton, option=0x7fffffffbcf0, widget=0x55555611c7b0) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qfusionstyle.cpp:3538
#19 0x00007ffff3fb2711 in QProxyStyle::standardIcon (this=<optimized out>, standardIcon=QStyle::SP_TitleBarNormalButton, option=0x7fffffffbcf0, widget=0x55555611c7b0) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qproxystyle.cpp:380
#20 0x00007ffff3f8c27d in QCommonStyle::subElementRect (this=<optimized out>, sr=<optimized out>, opt=0x7fffffffbcf0, widget=0x55555611c7b0) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qcommonstyle.cpp:3074
#21 0x00007ffff4038145 in QFusionStyle::subElementRect (this=<optimized out>, sr=QStyle::SE_DockWidgetTitleBarText, opt=0x7fffffffbcf0, w=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qfusionstyle.cpp:3467
#22 0x00007ffff400855b in QStyleSheetStyle::subElementRect (this=0x555555ce9b60, se=QStyle::SE_DockWidgetTitleBarText, opt=0x7fffffffbcf0, w=0x55555611c7b0) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qstylesheetstyle.cpp:6402
#23 0x00007ffff3ffb3c0 in QStyleSheetStyle::drawControl (this=0x555555ce9b60, ce=<optimized out>, opt=0x7fffffffbcf0, p=0x7fffffffbcd0, w=0x55555611c7b0) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qstylesheetstyle.cpp:4442
#24 0x00007ffff409758e in QStylePainter::drawControl (this=0x7fffffffbcd0, ce=QStyle::CE_DockWidgetTitle, opt=...) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qstylepainter.h:51
#25 QDockWidget::paintEvent (this=<optimized out>, event=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/widgets/qdockwidget.cpp:1633
#26 0x00007ffff3f5d6f3 in QWidget::event (this=0x55555611c7b0, event=0x7fffffffbf40) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:9134
#27 0x00007ffff3f020a0 in QApplicationPrivate::notify_helper (this=<optimized out>, receiver=0x55555611c7b0, e=0x7fffffffbf40) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qapplication.cpp:3307
#28 0x00007ffff2d6a6c8 in QCoreApplication::notifyInternal2 (receiver=0x55555611c7b0, event=0x7fffffffbf40) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1109
#29 0x00007ffff2d6a71d in QCoreApplication::sendSpontaneousEvent (receiver=<optimized out>, event=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1563
#30 0x00007ffff3f4e96e in QWidgetPrivate::sendPaintEvent (this=this@entry=0x5555561c6380, toBePainted=...) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5661
#31 0x00007ffff3f50c27 in QWidgetPrivate::drawWidget (this=this@entry=0x5555561c6380, pdev=pdev@entry=0x555557df9428, rgn=..., offset=..., flags=flags@entry=..., sharedPainter=sharedPainter@entry=0x0, repaintManager=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5611
#32 0x00007ffff3f53a64 in QWidgetPrivate::paintSiblingsRecursive (this=this@entry=0x555555c6ee10, pdev=pdev@entry=0x555557df9428, siblings=<optimized out>, index=<optimized out>, rgn=..., offset=..., flags=..., sharedPainter=0x0, repaintManager=0x555558857670) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5790
#33 0x00007ffff3f51185 in QWidgetPrivate::drawWidget (this=0x555555c6ee10, pdev=0x555557df9428, rgn=<optimized out>, offset=<optimized out>, flags=..., sharedPainter=<optimized out>, repaintManager=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5652
#34 0x00007ffff3f75929 in QWidgetRepaintManager::paintAndFlush (this=0x555558857670) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidgetrepaintmanager.cpp:907
#35 0x00007ffff3f5d0f6 in QWidget::event (this=0x555555f04720, event=0x555555d326e0) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:9298
#36 0x00007ffff3f020a0 in QApplicationPrivate::notify_helper (this=<optimized out>, receiver=0x555555f04720, e=0x555555d326e0) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qapplication.cpp:3307
#37 0x00007ffff2d6a6c8 in QCoreApplication::notifyInternal2 (receiver=0x555555f04720, event=event@entry=0x555555d326e0) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1109
#38 0x00007ffff2d6aab2 in QCoreApplication::sendEvent (receiver=<optimized out>, event=0x555555d326e0) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1549
#39 QCoreApplicationPrivate::sendPostedEvents (receiver=0x0, event_type=0, data=0x555555aa5c20) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1904
#40 0x00007ffff304db18 in QCoreApplication::sendPostedEvents (receiver=0x0, event_type=0) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1757
#41 postEventSourceDispatch (s=0x555555aa5eb0) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qeventdispatcher_glib.cpp:246
#42 0x00007ffff1106f8d in g_main_dispatch (context=0x7fffd8000f60) at ../glib/glib/gmain.c:3565
#43 0x00007ffff1108657 in g_main_context_dispatch_unlocked (context=0x7fffd8000f60) at ../glib/glib/gmain.c:4425
#44 g_main_context_iterate_unlocked (context=context@entry=0x7fffd8000f60, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>) at ../glib/glib/gmain.c:4490
#45 0x00007ffff1108865 in g_main_context_iteration (context=0x7fffd8000f60, may_block=1) at ../glib/glib/gmain.c:4556
#46 0x00007ffff304a9d2 in QEventDispatcherGlib::processEvents (this=0x555555aa0c30, flags=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qeventdispatcher_glib.cpp:399
#47 0x00007ffff2d75a86 in QEventLoop::processEvents (this=0x7fffffffcb70, flags=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qeventloop.cpp:104
#48 QEventLoop::exec (this=0x7fffffffcb70, flags=...) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qeventloop.cpp:186
#49 0x00007ffff2d6f171 in QCoreApplication::exec () at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1452
#50 0x00007ffff3efd31a in QApplication::exec () at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qapplication.cpp:2574
#51 0x000055555583d173 in run_program (logFile=..., argc=<optimized out>, argc@entry=1, argv=argv@entry=0x7fffffffda38) at /usr/src/debug/obs-studio/obs-studio-32.0.1-sources/frontend/obs-main.cpp:690
#52 0x0000555555605649 in main (argc=1, argv=0x7fffffffda38) at /usr/src/debug/obs-studio/obs-studio-32.0.1-sources/frontend/obs-main.cpp:1035
Steps to Reproduce
- Launch OBS.
- (optional) Hide the preview.
Remarkably, even with preview shown, main thread CPU usage is dominated by repeatedly redrawing icons and trying to locate image decoders, rather than displaying video! (I enabled preview and ran a new profiling session, with results indistinguishable from my previous screenshot.)
Anything else we should know?
No response
If you disable the Audio Mixer dock via the Docks menu, does the issue go away? We do know rendering of the audio meters is currently quite inefficient, and that QWidgets aren't hardware accelerated.
The CPU usage appears to no longer happen.
I did notice the CPU burn was not in the audio mixer rendering (drawing rectangles? QPainter?), but rather icon decoding. Perhaps only the meters should be redrawn and not the mixer itself? Or throttle the repaint loop to 30 times a second or so? Though that wouldn't fix CPU usage...
I tried estimating the rate of function calls in gdb:
gdb obs
r (wait for program to show GUI) ^C
b QFusionStyle::iconFromTheme
ignore 1 1000
c
where I'd let the program start up, set a breakpoint in QFusionStyle::iconFromTheme, then run a stopwatch while waiting for the program to request 1000 icons. I had worried this would noticeably slow down the program (since I'd had slowdown in KDE apps even with breakpoints disabled), but OBS Studio seemed to be using the same 60-ish percent of CPU time.
- While actively interacting with the UI it took around 12 seconds, and 1000/12 = 83.3 Hz. If I left the UI idling in the background, it took 16.3 seconds (from stopwach), and 1000/16.3 = 61.3 Hz (within rounding error of 60 Hz). I suspect you're trying to decode an icon every video frame (or two icons at 30 Hz).
- There is the possibility gdb slowed the program down and made each function call more expensive, meaning icons were decoded at higher than 60 Hz. I don't know how to check this other than recompiling the program or running under valgrind callgrind to count function calls in KCachegrind (which definitely slows down the program massively).
Looking at the backtrace and layout asm, it seemed QWidget::event() was invoking syncBackingStore(). In the Qt source code this corresponds to QEvent::UpdateRequest.
- I tried and failed to grep the source code for a connection to update(), but
rg --multiline 'connect\(.*(\n.*)?::[uU]pdate'turned up only false positives. - Instead I used GammaRay to check for timers running at around 60 Hz, which revealed
VolumeMeterTimerat interval 16 ms. The source code shows you override QTimer::timerEvent instead of connecting a QTimer toupdate(), which explains why I couldn't find it in the code.
VolumeMeterTimer::timerEvent already tries to only update the bars instead of the widget. I can't add logging because the code won't build (CMake can't find target Qt::GuiPrivate).
- EDIT: https://old.reddit.com/r/QtFramework/comments/1o42h63/qtguiprivate_target_not_found/ "Oh, so the official instructions are wrong." 🤦
- It seems #12328 is trying to address this?
- If I breakpoint the method in gdb, stepping shows that needLayoutChange is false and execution does proceed into "Tell paintEvent to paint only the bars",
meter->update(meter->getBarRect());.
It seems when you ask a VolumeMeter to repaint a rectangle, at least one looks up icons anyway...
- I have 2 meters and
p volumeMetershas size 2, butQFusionStyle::iconFromThemeis only called once every 16 ms, dunno if they're coalesced.
EDIT: Now if we called update() on VolumeMeter, why is QDockWidget being repainted?
Who's being redrawn?
Attaching GammaRay to OBS started crashing again (https://github.com/KDAB/GammaRay/issues/1093). I was able to perform the following:
gammaray obs
(new tab) gdb -p (pgrep obs)
A stack trace I got is:
#19 0x00007f854fa9758e in QStylePainter::drawControl (this=0x7ffcb8fdd9b0, ce=QStyle::CE_DockWidgetTitle, opt=...) at /usr/src/debug/qt6-base/qtbase/src/widgets/styles/qstylepainter.h:51
#20 QDockWidget::paintEvent (this=<optimized out>, event=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/widgets/qdockwidget.cpp:1633
#21 0x00007f854f95d6f3 in QWidget::event (this=0x55f861df5300, event=0x7ffcb8fddc20) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:9134
#22 0x00007f854f9020a0 in QApplicationPrivate::notify_helper (this=<optimized out>, receiver=0x55f861df5300, e=0x7ffcb8fddc20) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qapplication.cpp:3307
#23 0x00007f854e76a6c8 in QCoreApplication::notifyInternal2 (receiver=0x55f861df5300, event=0x7ffcb8fddc20) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1109
#24 0x00007f854e76a71d in QCoreApplication::sendSpontaneousEvent (receiver=<optimized out>, event=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/corelib/kernel/qcoreapplication.cpp:1563
#25 0x00007f854f94e96e in QWidgetPrivate::sendPaintEvent (this=this@entry=0x55f861deb480, toBePainted=...) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5661
#26 0x00007f854f950c27 in QWidgetPrivate::drawWidget (this=this@entry=0x55f861deb480, pdev=pdev@entry=0x55f864f7bb58, rgn=..., offset=..., flags=flags@entry=..., sharedPainter=sharedPainter@entry=0x0, repaintManager=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5611
#27 0x00007f854f953a64 in QWidgetPrivate::paintSiblingsRecursive (this=this@entry=0x55f861aefff0, pdev=pdev@entry=0x55f864f7bb58, siblings=<optimized out>, index=<optimized out>, rgn=..., offset=..., flags=..., sharedPainter=0x0, repaintManager=0x55f862021020) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5790
#28 0x00007f854f951185 in QWidgetPrivate::drawWidget (this=0x55f861aefff0, pdev=0x55f864f7bb58, rgn=<optimized out>, offset=<optimized out>, flags=..., sharedPainter=<optimized out>, repaintManager=<optimized out>) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:5652
#29 0x00007f854f975929 in QWidgetRepaintManager::paintAndFlush (this=0x55f862021020) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidgetrepaintmanager.cpp:907
#30 0x00007f854f95d0f6 in QWidget::event (this=0x55f86186d320, event=0x55f8650af6e0) at /usr/src/debug/qt6-base/qtbase/src/widgets/kernel/qwidget.cpp:9298
Which widgets are being redrawn? GammaRay doesn't let you search named widgets by pointer values, so let's drag object names out of memory kicking and screaming:
(gdb) p ((QWidget*)0x55f86186d320)->objectName()
$2 = {d = {d = 0x55f8617520f0, ptr = 0x55f861752100 u"OBSBasic", size = 8}, static _empty = 0 u'\000'}
(gdb) p ((QWidgetPrivate*)0x55f861aefff0)->extraData->objectName
... u"OBSBasic" ...
(gdb) p ((QWidgetPrivate*)0x55f861deb480)->extraData->objectName
... u"mixerDock" ...
(gdb) p ((QWidget*)0x55f861df5300)->objectName()
$3 = {d = {d = 0x55f861e09210, ptr = 0x55f861e09220 u"mixerDock", size = 9}, static _empty = 0 u'\000'}
So VolumeMeterTimer::timerEvent() → VolumeMeter::update() is (indirectly?) sending event QEvent::UpdateRequest to OBSBasic, which repaints <widget class="OBSDock" name="mixerDock">
- It's possible the whole UI is being repainted, or that every UI element up to
VolumeMeter(or every element intersecting the rectangle?) is being repainted. - Looking in GammaRay,
mixerDockis the dock item surrounding the audio mixers being repainted. Since it's callingQStylePainter::drawControldirectly (not through a child widget), I suspected the icon being loaded is the "restore" icon on the top right (since the mixers and bottom toolbar live in a child widget). But it's actually aQDockWidgetTitleButton : QAbstractButton : QWidget? IDK.
The string passed to QIcon::addFile is fusion_normalizedockup-48.png. I found a similar file at https://github.com/qt/qtbase/blob/dev/src/widgets/styles/images/fusion_normalizedockup_48.png, but the icon orientation is different, and the name has an underscore instead of hyphen.
Solutions?
- I wonder if the problem is that one of
OBSDock -> QDockWidgetTitleButtonis provided a theme without the necessary icon, and Qt attempts to load the icon on every repaint. So if you supplied an icon, then repainting theOBSDock : QDockWidgetwouldn't burn this much CPU?- I have not researched how
QStyleSheetStyle::drawControl"decides" to fetch an icon of the above name. This is an avenue of future research.
- I have not researched how
- To avoid redrawing entirely, https://www.qtcentre.org/threads/9030-Repaint-child-without-having-the-parent-automatically-repainted suggests
QWidget::setAttribute(Qt::WA_OpaquePaintEvent)to paint the widget without its parents, but you have to paint the whole thing without letting the background color shine through (since you always paint on top of the previous paint).- The Qt docs are at https://doc.qt.io/qt-6/qwidget.html#transparency-and-double-buffering. They suggest using
Qt::WA_OpaquePaintEventor:
- The Qt docs are at https://doc.qt.io/qt-6/qwidget.html#transparency-and-double-buffering. They suggest using
define a suitable background color (using setBackgroundRole() with the QPalette::Window role), set the autoFillBackground property, and only implement the necessary drawing functionality in the widget's paintEvent().
I have not tested if either code change fixes the CPU burn.
EDIT: You already call setAttribute(Qt::WA_OpaquePaintEvent, true); and it doesn't work!
The problem
I think I know what's up. OBS's custom theme (I think) uses stylesheets to customize display over the Fusion(?) theme, and QStyleSheetStyle::polish contains:
QRenderRule rule = renderRule(w, PseudoElement_None, PseudoClass_Any);
...
if (rule.hasDrawable() || rule.hasBox()) {
...
if (!rule.hasBackground() || rule.background()->isTransparent() || rule.hasBox()
|| (!rule.hasNativeBorder() && !rule.border()->isOpaque()))
w->setAttribute(Qt::WA_OpaquePaintEvent, false);
So if you want opaque paint event enabled, you need an opaque background, no box, and a native or opaque widget border. I don't know which condition fails, and don't plan to find out right now (QRenderRule is declared in an internal Qt .cpp file, not even a private header!). Alternatively you can cheat (link):
If you need to do some delayed initialization use the Polish event delivered to the event() function.
bool QWidget::event(QEvent *event)
{
...switch (event->type()) {
...case QEvent::Polish: {
style()->polish(this);
setAttribute(Qt::WA_WState_Polished);
if (!QApplication::font(this).isCopyOf(QApplication::font()))
d->resolveFont();
if (!QApplication::palette(this).isCopyOf(QGuiApplication::palette()))
d->resolvePalette();
}
break;
}
...
return true;
So we'd have to override QWidget::event(), call the parent function, then in case of QEvent::Polish ensure our widget is opaque.
How did I find QStyleSheetStyle::polish was turning off opacity? I tried running OBS under gdb:
tb VolumeMeter::VolumeMeter
r (until break)
Thread 1 "obs" hit Breakpoint 1, VolumeMeter::VolumeMeter(QWidget*, obs_volmeter*, bool) [clone .constprop.0] (this=0x5555574117c0, obs_volmeter=0x555558639d60, vertical=false, parent=0x0) at /usr/src/debug/obs-studio/obs-studio-32.0.1-sources/frontend/widgets/VolumeMeter.cpp:386
- VolumeMeter = 0x5555574117c0
(gdb) n...
391 setAttribute(Qt::WA_OpaquePaintEvent, true);
(repeatedly type next until GDB advances to the next line instead of going backwards)
(gdb) p data->widget_attributes & (1<<Qt::WA_OpaquePaintEvent)
$5 = 16
(gdb) p &data->widget_attributes
$3 = (uint *) 0x555558798398
(gdb) watch *(uint *)0x555558798398
Hardware watchpoint 2: *(uint32_t)0x555558798398
(gdb) c
Continuing.
Then repeatedly continue, and every time it changes run p (new value) & (1<<Qt::WA_OpaquePaintEvent), until QStyleSheetStyle::polish makes it output 0.
Tracing icon CPU burn
Why is Fusion loading an icon? Is QFusionStyle::standardIcon() succeeding or failing? Why doesn't the Qt resource manager cache the icon it's loaded? I don't know yet.
I've written up a quick code patch to force WA_OpaquePaintEvent:
bool VolumeMeter::event(QEvent *event)
{
bool ret = QWidget::event(event);
switch (event->type()) {
case QEvent::Polish: {
// QWidget::event() calls QStyleSheetStyle::polish() which turns off opaque.
// Turn it back on.
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
default:
break;
}
return ret;
}
It works (in that CPU usage is now to 50% of a core and dominated by v4l2: capture│/usr/bin/obs), but has a rendering bug:
We probably need a background color.
The VolumeMeter and QFrame have a transparent background color (#00000000):
while the VolControl has an opaque background:
I don't know what causes only this widget to paint its own background. All 3 widgets have backgroundRole = QPalette::Window, and none have autoFillBackground set. Moreover, setBackgroundRole(QPalette::Window); setAutoFillBackground(true) does not change the widget's appearance, and GammaRay both before and after the change show a single transparent background FillRectColor. If I remove setAttribute(Qt::WA_OpaquePaintEvent, true); and keep the background role/autofill, the CPU burn returns.
Why is this->palette().color(this->backgroundRole()) different for different widgets, and VolumeMeter's background is transparent? I may not have the answer yet, but I know what's doing it.
I verified that VolControl does not have a stylesheet which could supply its own background color. Looking at https://doc.qt.io/qt-6/qwidget.html#palette-prop we see:
Finally, the style always has the option of polishing the palette as it's assigned (see QStyle::polish()).
Yep that's what's doing it. If I print << QWidget::backgroundRole() << "=" << palette().color(QWidget::backgroundRole()) before and after calling super::event() upon QEvent::Polish, I see the following:
warning: VolControl background color from QPalette::Window = QColor(ARGB 1, 0.192157, 0.211765, 0.231373)
warning: to QPalette::Window = QColor(ARGB 1, 0.137255, 0.14902, 0.160784)
warning: VolumeMeter background color from QPalette::Window = QColor(ARGB 1, 0.192157, 0.211765, 0.231373)
warning: to QPalette::Window = QColor(ARGB 0, 0, 0, 0)
To figure out what was clearing the background color, I added std::raise(SIGTRAP); before passing QEvent::Polish to the base class, spent hours debugging the wrong class (VolControl has background color, VolumeMeter doesn't), then found that in QStyleSheetStyle::polish(QWidget *w), baseStyle()->polish(w) (QFusionStyle) doesn't change the color, unsetPalette(w); doesn't, but setPalette(w); does.
- [ ] I'll have to trace what this method is doing, and why it makes VolumeMeter transparent but VolControl opaque, but that's a task for tomorrow.
I wonder if we can cheat and pull the color from the OBS theme CSS; this doesn't work in an arbitrary Qt app but does in OBS (fitting for a bug that only happens with stylesheets).
EDIT: It may be worthwhile editing the theme CSS to give this widget a background color.
Hey @nyanpasu64 thank you so much for this in-depth analysis. I am going to dig through all of this data today.
While I do that though, could you try testing out #12735? (Download available in Artifacts). I actually did a bit of adjustment to the volume meter rendering which includes how the background is done and I'm curious if it solves this indirectly.
To figure out what was clearing the background color, I added
std::raise(SIGTRAP);before passingQEvent::Polishto the base class, spent hours debugging the wrong class (VolControl has background color, VolumeMeter doesn't), then found that inQStyleSheetStyle::polish(QWidget *w),baseStyle()->polish(w)(QFusionStyle) doesn't change the color,unsetPalette(w);doesn't, butsetPalette(w);does.
The background color is being cleared by our stylesheet. We currently have background: transparent in the VolumeMeter {} style rule.
I do not get CPU burn on Windows. I don't know if VolumeMeter has Qt::WA_OpaquePaintEvent on Windows, or it's cleared but the parent dock widget doesn't burn CPU time loading icons at 60 Hz. I tried to find out but was unable to make it work.
There is no prebuilt GammaRay on Windows. I tried building OBS Studio master on Windows by following https://github.com/obsproject/obs-studio/wiki/build-instructions-for-windows, but CMake first required I install the older SDK 10.0.22621.0, then cmake --preset windows-x64 took 3 runs to succeed (the first couldn't find Detours and the second couldn't find SIMDe), and after building OBS Studio in Debug mode, pressing F5 failed to launch the program (instead popups about missing zlib.dll and zlibd1.dll).
On Linux, the Flatpak nightly build does not appear to have CPU burn (though I don't know if decoding icons still eats CPU time). I think the mainline Flatpak build had CPU burn initially (causing me to debug the issue on the Arch Linux build), so it may have been "fixed" as a side effect of the partial rewrite (though it's not yet merged).
testing out https://github.com/obsproject/obs-studio/pull/12735
I don't see any difference with this build vs 32.0.2 release flatpak (ca. 8x CPU jump with audio mixer visible).
It seems that CMake found that scoop -> vcpkg had installed zlib, linked to its zlib stub libraries, but couldn't find its DLLs when running from Visual Studio (both Debug and RelWithDebInfo!). I've uninstalled vcpkg's zlib (along with like 8 packages including dependencies), deleted CMakeCache, rebuilt (again cmake --preset windows-x64 took three runs to succeed), and managed I am this close to uninstalling vcpkg altogether (it had broken Dolphin Emulator builds in the past).
Do you think fixing these problems with the build instructions is worth fixing?
EDIT: I have confirmed that the widgets (on master) have Qt::WA_OpaquePaintEvent set to false upon polishing. It's a bit contradictory asking VolumeMeter to be opaque, then setting its background to transparent in Qt CSS?
If I change it to:
VolumeMeter {
background: var(--bg_base);
}
to match VolControl, then QEvent::Polish still turns off WA_OpaquePaintEvent, but if I force it on, it draws properly.
- [ ] Will you consider upstreaming this change?
As to why it doesn't burn CPU on Windows... I can't actually identify the call stack with QDockWidget::paintEvent in the VS profiler, and even after loading pdbs from https://github.com/obsproject/obs-deps/releases/tag/2025-08-23, VS ~~can't break on QDockWidget::paintEvent~~ now it can? I can also breakpoint QFusionStyle::iconFromTheme which doesn't get hit, but given the unreliability of C++ debuggers, absence of evidence is not sufficient evidence of absence.
VolumeMeter::paintEvent is instead invoked through Qt6Widgets.dll!QWidgetRepaintManager::paintAndFlush():
// Paint the rest with composition.
if (repaintAllWidgets || !dirtyCopy.isEmpty()) {
QWidgetPrivate::DrawWidgetFlags flags = QWidgetPrivate::DrawAsRoot | QWidgetPrivate::DrawRecursive
| QWidgetPrivate::UseEffectRegionBounds;
tlw->d_func()->drawWidget(store->paintDevice(), dirtyCopy, QPoint(), flags, nullptr, this);
}
- [x] On Linux, what's the call stack to
VolumeMeter::paintEvent, and does it match CPU burning?
EDIT: the dock and the volume meter share QWidgetRepaintManager::paintAndFlush > QWidgetPrivate::drawWidget and nothing else I know for sure.
- Hell, why is
QFusionStyle::iconFromThemecalled on Linux and is it not the case on Windows?
What's the function call chain leading up to icon loading?
QStyleSheetStyle::drawControlat Line 4442 is drawing aQStyle::ControlElement = CE_DockWidgetTitle.- We invoke
QStyleSheetStyle::subElementRectwithQStyle::SubElement = SE_DockWidgetTitleBarText.
- We invoke
QStyleSheetStyle::subElementRectat line 6402 delegates to the base style'sQFusionStyle::subElementRect.- Looking at https://doc.qt.io/qt-6/qstyle.html#subElementRect,
QStyleSheetStyle::subElementRectis supposed to be a pure function returning a rectangular region, not a repainting operation or decoding icons! Why is it burning CPU?
- Looking at https://doc.qt.io/qt-6/qstyle.html#subElementRect,
QFusionStyle::subElementRectat line 3467 invokes superclassQCommonStyle::subElementRectthen adjusts the output.QCommonStyle::subElementRectat line 3074 callsproxy()->standardIcon()(oops!) withQStyle::StandardPixmap = SP_TitleBarNormalButton.QProxyStyle::standardIconat line 380 delegates to the base style.- [ ] Is this called on Windows?
QFusionStyle::standardIconat line 3538 callsQFusionStyle::iconFromThemeand passes alongenum StandardPixmap = SP_TitleBarNormalButton.QFusionStyle::iconFromThemeat line 3500-3530 builds a QIcon and adds various sizes of image from the ":/qt-project.org/styles/fusionstyle/images/" resource path.- Because we're passed
SP_TitleBarNormalButton, this looks up ":/qt-project.org/styles/fusionstyle/images/fusion_normalizedockup-(sizes).png". - Many of the actual files have underscores instead of hyphens. Does Qt's resource system conflate underscores and hyphens? I don't know.
- Because we're passed
Which of these function calls don't happen on Windows without the CPU burn? I haven't looked yet...
EDIT: OBS's Windows builds uses a prebuilt Qt 6.8.3, while OBS on Arch Linux uses 6.10.0. I don't know if Qt version differences affect whether it burns CPU loading icons.
Tracing layout and icon loading on Windows
On Windows we see a convoluted Russian nesting doll of delegation and inheritance to calculate a simple subElementRect() region... and yet it still ends up burning less CPU than failing to load icons.
Analyzing the stack trace...
QStyleSheetStyle::subElementRect(QStyle::SubElement = SE_DockWidgetTitleBarText)at line 6429 delegates to baseQProxyStyle::subElementRect.QProxyStyle::subElementRectat line ~~219~~ 218 delegates to baseQWindows11Style::subElementRect.- This
QProxyStyleis either missing on Linux, or the stack frame was missing from the backtrace.
- This
QWindows11Style::subElementRectat line ~~1834~~ 1832 calls superclassQWindowsVistaStyle::subElementRect. (I didn't know Qt's Windows 11 style was a patch layer on the Vista Aero style?!)QWindowsVistaStyle::subElementRectat line ~~4262~~ 4258 calls superclassQWindowsStyle::subElementRect.QWindowsStyle::subElementRectat line 1774 calls superclassQCommonStyle::subElementRectbefore adjusting the output.- On Linux this is instead
QFusionStyle::subElementRect, but both invoke the superclass method.
- On Linux this is instead
QCommonStyle::subElementRectat line 3102 entersif (canFloat) {, then callsproxy()->standardIcon()(oops!) withQStyle::StandardPixmap = SP_TitleBarNormalButton.
Now we're trying to calculate QStyle::standardIcon()...
QProxyStyle::standardIconat line 380 delegates to baseQWindowsVistaStyle::standardIcon.QWindowsVistaStyle::standardIconat line 4949 tests ifd->dockFloat.isNull()(it's not), tests ifwidget && widget->isWindow()(it's not), then callsQWindowsStyle::standardIcon.- We skip altogether Linux's call to
QFusionStyle::iconFromTheme, which was responsible for burning CPU on icon loading. - So we already know why Windows doesn't burn CPU loading icons... but let's keep digging out of curiosity.
- We skip altogether Linux's call to
QWindowsStyle::standardIconat line 2292 calls superclassQCommonStyle::standardIcon. (Why is this function even present? Did it once do work or was planned to, and it's there for a stable ABI?)QCommonStyle::standardIconat line 6134 callsd->iconFromWindowsTheme()(null icon), callsd->iconFromApplicationTheme()(bails out because no theme name),d->iconFromMacTheme()(we're on Windows), thend->iconFromResourceTheme().
QCommonStylePrivate::iconFromResourceThemecallsaddIconFiles(u"normalizedockup-", dockTitleIconSizes, icon).template <typename T> static void addIconFiles()loops overQIcon::addFile()with arguments ":/qt-project.org/styles/commonstyle/images/normalizedockup-%d.png". It loads images of sizesdockTitleIconSizes = {10, 16, 20, 32, 48, 64}.
Why does addIconFiles() not burn CPU the way that QFusionStyle::iconFromTheme() does?
Comparing the Windows call stack to Linux...
QIcon::addFileat line 1173 finds thatalreadyAddedis false, and invokesQPixmapIconEngine::addFile.QPixmapIconEngine::addFileinitializes anImageReader(fileName)→QImageReader...- On Windows (OBS uses Qt 6.8.3),
QImageReader::supportsOptionis invoked fromQPixmapIconEngine::bestMatch→ImageReader::supportsReadSize().- On Arch Linux (Qt 6.10.0),
ImageReader::ImageReaderline 44 callsQImageReader::supportsOptionearlier in execution. This call was introduced in Qt 6.10.
- On Arch Linux (Qt 6.10.0),
- In any case, Windows still invokes
QImageReaderPrivate::initHandler, where Linux burns CPU at line 513. Windows never actually reaches the corresponding line 537. We do reach line 510 which invokesdevice->open(QIODevice::ReadOnly). QFile::openat line 926 invokesQResourceFileEngine::open.QResourceFileEngineimplementsQAbstractFileEngine, which effectively implements a file descriptor.
QResourceFileEngine::openat line 1431+ owns a singleQResource(docs, it's basically a virtual file/folder inode).- Every operation on a
QResourceinvokesQResourcePrivate::ensureInitialized.- Upon the initial call to
QResourcePrivate::ensureInitialized,this->datais nullptr, - Subsequently,
this->data(akaQResourceFileEngine.d.resource.d_ptr.d.data) holds an image starting with the‰PNGheader. (VS is interpreting binary data as Windows-1252.)
- Upon the initial call to
QResourcePrivate::ensureInitializedat line 392 does an unconst cast ofthisand invokesQResourcePrivate::load.- If
QResourcePrivate::loadsucceeds, it setsrelated.append(res);. Subsequent calls toQResourcePrivate::ensureInitialized()will seeif (!related.isEmpty())and return immediately.^ynybth
- If
QResourcePrivate::loadat line 338 iterates overresourceList()and invokesQResourceRoot::findNodeon eachQResourceRoot. This still matches the slow path from the Linux profiler.- On Windows,
findNodesucceeds on i=1 (the second resource root searched). (Somehow both roots have empty tree/names/payloads in the VS profiler.)
- On Windows,
After spending some time familiarizing myself with the Visual Studio profiler, I recorded a 10 second OBS trace to better pick up rare function calls.
QIcon::addFile() does eat about 5.89% of total used CPU time, though only 1.79% of used CPU time is in QResourcePrivate::ensureInitialized, and 1.39% of total CPU time is in subcall QResourceRoot::findNode.
- This indicates that
QStyle::subElementRect(SE_DockWidgetTitleBarText)is just expensive in general; even on Windows, a "good" platform with correct icon names, comparingQStyleSheetStyle::subElementRecttoQWidgetPrivate::drawWidget()shows300/1066of CPU time spent drawing widgets is spent processing dock widget icons alone! - I've attached a profiler trace: Report20251101-0025.diagsession.zip. You'll need the Qt symbols from https://github.com/obsproject/obs-deps/releases/tag/2025-08-23. After opening this file in Visual Studio, in the .diagsession tab click "⚙️ Settings" and uncheck "Show Just My Code".
- This is a new trace I made after debugging, so the numbers may not match. Irritatingly, in VS you can only save profiler traces made using Alt+F2, not the debugger's sidebar.
Causes and solutions for CPU burn on Linux
I think that loading icons while doing widget drawing is slow in general, and Qt shouldn't do it, but because Fusion has incorrect icon names, they fail to load and every QFile operation tries to look up the file again, wasting CPU in QResourceRoot::findNode. Evidently Qt doesn't detect the missing file and give up on loading icons. This is not a problem on Windows because the files are present, so QResourceRoot::findNode only gets called once per file (see ynybth).
- This theory explains my observations, but assumes that hyphens and underscores are not interchangeable in Qt resource files (I did not verify).
- [ ] We should report the bug to Qt, so they can check if fixing the icon names fixes CPU usage (while making sure icon rendering is still acceptable afterwards). I don't want to rebuild all of Qt on my own computer...
- [ ] Could Qt change their code to not construct QIcon then spend additional time in
QIcon::actualSize()just to find out how much space the icon takes away from the title bar text?- The actual title bar button is constructed in
QDockWidgetPrivate::updateButtons()line 689-691. - Looking at the callers to
QProxyStyle::standardIcon, it's exclusivelyQStyle::subElementRect(SE_DockWidgetTitleBarText)layout calculations. Icon loading fromQDockWidgetPrivate::updateButtons()doesn't even show in the callers, since it's called upfront before I profiled steady-state CPU usage. - To make this change, Qt could add a virtual
QDockWidgetLayoutitem for title bar text, make the layout compute the text rectangle, thenQCommonStyle::subElementRect()would look it up. AlternativelysubElementRect()could get a reference to thedwLayout->widgetForRole(QDockWidgetLayout::FloatButton)created inQDockWidgetPrivate::updateButtons().- This change is simple, but fully eliminating icon lookup during painting is a bigger task, since qcommonstyle.cpp has 19 occurrences of string
proxy()->standardIcon!
- This change is simple, but fully eliminating icon lookup during painting is a bigger task, since qcommonstyle.cpp has 19 occurrences of string
- The actual icon OBS uses for "pop out docked panel" doesn't even match either Qt's
normalizedockuporfusion_normalizedockupresources!
- The actual title bar button is constructed in
- [ ] Could Qt change
QFile::open()and callees to not burn CPU time repeatedly looking up a missing resource, but just remember it's missing until a new resource root is added? - [ ] IMO you still should give
VolumeMeteran opaquebackground: var(--bg_base)and force it asQt::WA_OpaquePaintEvent; drawing parent widgets at 60 Hz is wasteful. - Does anyone use OBS's ability to undock panels?
Why are some Fusion icons named with underscores, but Qt attempts to load them with hyphens?
- The files
fusion_normalizedockup{-_}.{svg,png}were last modified https://github.com/qt/qtbase/commit/dfe5118f927a45c56d508ffa8662e05c2f99d68d. - They were created in https://github.com/qt/qtbase/commit/acdef6669f8e179ac14ff1be1d974e1230879c51 (Qt 6.7), with comment "re-add" implying they existed before but were removed.
QFusionStyle::iconFromThemewas also not present before this commit or Qt 6.7.- In fact
QFusionStyle::iconFromThemeis a non-virtual method, and absent entirely from the base class and all of Qt 5!
- Both Qt 5 and 6 have icons for
normalizedockup-*.png(without the hyphen-underscore naming inconsistency), which look different from the Fusion variants.
- Does resizing a window on Linux burn more CPU than Windows or slow down the framerate, even with
Qt::WA_OpaquePaintEventforced on, because it's trying to load a missing icon?- Well on Linux, resizing with a 1000Hz mouse burns a full CPU core, with a good chunk of CPU in subElementRect. I haven't tested Windows.
- Why did I not get CPU burn in the PR flatpak, but Schlaefer did?
- I recorded a trace in sysprof (I can't use samply for Flatpak apps): System Capture from 2025-11-01 00:44:26.zip
- This says the flatpak does eat CPU, in the same places as before (
QFile::open). Then why didn't I see it? - On Arch Linux, htop doesn't include Flatpak apps when viewing CPU usage by thread! I did try
sudo htopwhich (sometimes?) showed OBS using CPU, though less than before the PR (15% of a core rather than 60%). - Is
resourceList()longer (or more files in each root) in Arch OBS than the flatpak, or isQResourcePrivate::loadcalled more times? I don't know and don't care to check.
This theory explains my observations, but assumes that hyphens and underscores are not interchangeable in Qt resource files (I did not verify).
I've made a test C++/Qt project at https://codeberg.org/nyanpasu64/fusion-resources, which confirms that QFile(":/qt-project.org/styles/fusionstyle/images/fusion_normalizedockup-%d.png") fails to open for %d != 16 or 32. This supports my theory for the cause of excessive CPU usage.
@nyanpasu64 Can you give https://github.com/obsproject/obs-studio/pull/12735 a try? The widget background should now be explicitly set and not overridden to transparent.
Sorry I haven't had a chance to retest that PR so far. I think the Flatpak build failed (the build logs just cut off at some point? I don't see an error message), and I didn't find the time to manually compile and run it locally. Additionally I think an opaque background would not resolve CPU burn, since Qt requires an opaque border as well as background for mysterious reasons. (I haven't found the time to trace that function with a debugger to find out which condition fails resulting in w->setAttribute(Qt::WA_OpaquePaintEvent, false);, but may do so at some point.)
I've reported the CPU burn upstream at https://qt-project.atlassian.net/browse/QTBUG-141673, but they closed the issue and told me to file individual bugs. I filed the icon filename mismatch at https://qt-project.atlassian.net/browse/QTBUG-142139, but did not report the "requires an opaque border" issue due to limited energy.