Move heavy SystemProperties initialization operations off the main thread

This dramatically improves startup performance on slow devices.
This commit is contained in:
Cameron Gutman
2026-01-08 19:09:54 -06:00
parent cda65ab58d
commit fad197fdce
3 changed files with 140 additions and 65 deletions

View File

@@ -12,6 +12,69 @@
#include <Windows.h>
#endif
class SystemPropertyQueryThread : public QThread
{
public:
SystemPropertyQueryThread(SystemProperties* properties)
: QThread(properties), m_Properties(properties)
{
setObjectName("System Properties Async Query Thread");
}
private:
void run() override
{
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool supportsHdr;
QSize maximumResolution;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
// Update display related attributes (max FPS, native resolution, etc).
m_Properties->refreshDisplays();
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
}
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
// Propagate the decoder properties to the SystemProperties singleton and emit any change signals on the main thread
QMetaObject::invokeMethod(m_Properties, "updateDecoderProperties",
Qt::QueuedConnection,
Q_ARG(bool, hasHardwareAcceleration),
Q_ARG(bool, rendererAlwaysFullScreen),
Q_ARG(QSize, maximumResolution),
Q_ARG(bool, supportsHdr));
}
private:
SystemProperties* m_Properties;
};
SystemProperties::SystemProperties()
{
versionString = QString(VERSION_STR);
@@ -68,71 +131,63 @@ SystemProperties::SystemProperties()
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
// Populate data that requires talking to SDL. We do it all in one shot
// and cache the results to speed up future queries on this data.
querySdlVideoInfo();
// These will be queried asynchronously to avoid blocking the UI
hasHardwareAcceleration = true;
rendererAlwaysFullScreen = false;
supportsHdr = true;
maximumResolution = QSize(0, 0);
Q_ASSERT(!monitorRefreshRates.isEmpty());
Q_ASSERT(!monitorNativeResolutions.isEmpty());
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
systemPropertyQueryThread = new SystemPropertyQueryThread(this);
systemPropertyQueryThread->start();
}
void SystemProperties::updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr)
{
if (hasHardwareAcceleration != this->hasHardwareAcceleration) {
this->hasHardwareAcceleration = hasHardwareAcceleration;
emit hasHardwareAccelerationChanged();
}
if (rendererAlwaysFullScreen != this->rendererAlwaysFullScreen) {
this->rendererAlwaysFullScreen = rendererAlwaysFullScreen;
emit rendererAlwaysFullScreenChanged();
}
if (maximumResolution != this->maximumResolution) {
this->maximumResolution = maximumResolution;
emit maximumResolutionChanged();
}
if (supportsHdr != this->supportsHdr) {
this->supportsHdr = supportsHdr;
emit supportsHdrChanged();
}
}
QRect SystemProperties::getNativeResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
systemPropertyQueryThread->wait();
Q_ASSERT(!monitorNativeResolutions.isEmpty());
return monitorNativeResolutions.value(displayIndex);
}
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
systemPropertyQueryThread->wait();
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
return monitorSafeAreaResolutions.value(displayIndex);
}
int SystemProperties::getRefreshRate(int displayIndex)
{
// Returns 0 if out of bounds
systemPropertyQueryThread->wait();
Q_ASSERT(!monitorRefreshRates.isEmpty());
return monitorRefreshRates.value(displayIndex);
}
void SystemProperties::querySdlVideoInfo()
{
hasHardwareAcceleration = false;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
// Update display related attributes (max FPS, native resolution, etc).
refreshDisplays();
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
}
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
void SystemProperties::refreshDisplays()
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {

View File

@@ -7,14 +7,12 @@ class SystemProperties : public QObject
{
Q_OBJECT
friend class QuerySdlVideoThread;
friend class RefreshDisplaysThread;
friend class SystemPropertyQueryThread;
public:
SystemProperties();
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
// Static properties queried synchronously during the constructor
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
@@ -22,11 +20,15 @@ public:
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
// Properties queried asynchronously
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration NOTIFY hasHardwareAccelerationChanged)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen NOTIFY rendererAlwaysFullScreenChanged)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution NOTIFY maximumResolutionChanged)
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr NOTIFY supportsHdrChanged)
Q_INVOKABLE void refreshDisplays();
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
@@ -35,12 +37,18 @@ public:
signals:
void unmappedGamepadsChanged();
void hasHardwareAccelerationChanged();
void rendererAlwaysFullScreenChanged();
void maximumResolutionChanged();
void supportsHdrChanged();
private slots:
void updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr);
private:
void querySdlVideoInfo();
QThread* systemPropertyQueryThread;
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
// Properties set by the constructor
bool isRunningWayland;
bool isRunningXWayland;
bool isWow64;
@@ -49,12 +57,18 @@ private:
bool hasBrowser;
bool hasDiscordIntegration;
QString unmappedGamepads;
QString versionString;
bool usesMaterial3Theme;
// Properties set by updateDecoderProperties()
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
QSize maximumResolution;
bool supportsHdr;
// Properties set by refreshDisplays()
QList<QRect> monitorNativeResolutions;
QList<QRect> monitorSafeAreaResolutions;
QList<int> monitorRefreshRates;
QString versionString;
bool supportsHdr;
bool usesMaterial3Theme;
};

View File

@@ -54,7 +54,18 @@ ApplicationWindow {
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
// Hardware acceleration is checked asynchronously
SystemProperties.hasHardwareAccelerationChanged.connect(hasHardwareAccelerationChanged)
}
function hasHardwareAccelerationChanged() {
if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
@@ -62,13 +73,8 @@ ApplicationWindow {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
// It would be better to use TextMetrics here, but it always lays out
// the text slightly more compactly than real Text does in ToolTip,
// causing unexpected line breaks to be inserted