Files
OpenVR-AdvancedSettings-frame/src/utils/setup.cpp
T
matzmanandClaude Opus 5.5 d664f86945 Add layout diagnosis: font metrics in the log, --snapshot option
On the Steam Frame, the fixed 1200x800 QML layout overflows: about the
bottom quarter is cut off and the navigation icons overlap their text.
Forcing 96 DPI and switching the Quick Controls style didn't help, so the
cause has to be measured rather than guessed.

- SetWidget() logs the application font (family, pt, px), what 20 pt
  (MyText) is in px, the screen's DPI and the device pixel ratio.
- --snapshot <file> writes the rendered overlay (RenderBackend::readback)
  as PNG at most once per second while it is visible. A time interval,
  because OVRAS only renders when the scene changes. Written to a
  temporary file and renamed, so readers never see a partial file.
- run.sh passes arguments after the graphics API to the program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:06:09 +02:00

403 lines
15 KiB
C++

#include "setup.h"
#include <QtLogging>
#include <QtDebug>
#include <QLoggingCategory>
#ifdef ENABLE_DEBUG_LOGGING
constexpr auto debugLoggingEnabled = true;
#else
constexpr auto debugLoggingEnabled = false;
#endif
QtMessageHandler originalMessageHandler = nullptr;
static FILE* f;
// The default Qt message handler prints to stdout on X11 and to the debugger on
// Windows. That is borderline useless for us, therefore we create our own
// message handler.
void mainQtMessageHandler( QtMsgType type,
const QMessageLogContext& context,
const QString& msg )
{
QString message = qFormatLogMessage( type, context, msg );
fprintf( f, "%s\n", qPrintable( message ) );
fflush( f );
if ( originalMessageHandler )
( *originalMessageHandler )( type, context, msg );
}
namespace argument
{
CommandLineOptions returnCommandLineParser( const MyQApplication& application )
{
QCommandLineParser parser;
parser.setApplicationDescription(
"Advanced Settings overlay for SteamVR." );
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption desktopMode( k_desktopMode, k_desktopModeDescription );
parser.addOption( desktopMode );
QCommandLineOption forceNoSound( k_forceNoSound,
k_forceNoSoundDescription );
parser.addOption( forceNoSound );
QCommandLineOption forceNoManifest( k_forceNoManifest,
k_forceNoManifestDescription );
parser.addOption( forceNoManifest );
QCommandLineOption forceInstallManifest(
k_forceInstallManifest, k_forceInstallManifestDescription );
parser.addOption( forceInstallManifest );
QCommandLineOption forceRemoveManifest( k_forceRemoveManifest,
k_forceRemoveManifestDescription );
parser.addOption( forceRemoveManifest );
QCommandLineOption resetSettings( k_resetSettings,
k_resetSettingsDescription );
parser.addOption( resetSettings );
QCommandLineOption graphicsApi( k_graphicsApi,
k_graphicsApiDescription,
k_graphicsApiValueName,
k_graphicsApiDefault );
parser.addOption( graphicsApi );
QCommandLineOption snapshot(
k_snapshot, k_snapshotDescription, k_snapshotValueName );
parser.addOption( snapshot );
parser.process( application );
const bool desktopModeEnabled = parser.isSet( desktopMode );
if ( desktopModeEnabled )
qInfo() << "Desktop mode enabled.";
const bool forceNoSoundEnabled = parser.isSet( forceNoSound );
if ( forceNoSoundEnabled )
qInfo() << "Sound effects disabled.";
const bool forceNoManifestEnabled = parser.isSet( forceNoManifest );
if ( forceNoManifestEnabled )
qInfo() << "Running without applications manifest.";
const bool forceInstallManifestEnabled
= parser.isSet( forceInstallManifest );
if ( forceInstallManifestEnabled )
qInfo() << "Forcing install of applications manifest.";
const bool forceRemoveManifestEnabled = parser.isSet( forceRemoveManifest );
if ( forceRemoveManifestEnabled )
qInfo() << "Forcing removal of applications manifest.";
const bool resetSettingsEnabled = parser.isSet( resetSettings );
if ( resetSettingsEnabled )
qInfo() << "Reset SteamVR Settings.";
const QString graphicsApiValue = parser.value( graphicsApi );
qInfo() << "Graphics API:" << graphicsApiValue;
const QString snapshotValue = parser.value( snapshot );
if ( !snapshotValue.isEmpty() )
qInfo() << "Overlay snapshots to:" << snapshotValue;
const CommandLineOptions commandLineArgs{
desktopModeEnabled, forceNoSoundEnabled,
forceNoManifestEnabled, forceInstallManifestEnabled,
forceRemoveManifestEnabled, resetSettingsEnabled,
graphicsApiValue, snapshotValue
};
qInfo() << "Command line arguments processed.";
return commandLineArgs;
}
} // namespace argument
namespace manifest
{
// Enables autostart of OpenVR Advanced Settings.
void enableApplicationAutostart()
{
const auto app_error = vr::VRApplications()->SetApplicationAutoLaunch(
application_strings::applicationKey, true );
if ( app_error != vr::VRApplicationError_None )
{
throw std::runtime_error(
std::string( "Could not set auto start: " )
+ std::string(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
app_error ) ) );
}
}
// Installs the application manifest. If a manifest with the same application
// key is already installed, nothing will happen.
// OpenVR must be initialized before calling this function.
void installApplicationManifest( const std::string manifestPath )
{
if ( vr::VRApplications()->IsApplicationInstalled(
application_strings::applicationKey ) )
{
qInfo() << "Manifest Is already Installed for Key "
<< application_strings::applicationKey;
}
else
{
const auto app_error = vr::VRApplications()->AddApplicationManifest(
manifestPath.c_str() );
qInfo() << "Manifest Installed at: " << manifestPath;
if ( app_error != vr::VRApplicationError_None )
{
throw std::runtime_error(
std::string( "Could not add application manifest: " )
+ std::string(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
app_error ) ) );
}
}
}
// Removes the application manifest.
void removeApplicationManifest( const std::string manifestPath )
{
if ( vr::VRApplications()->IsApplicationInstalled(
application_strings::applicationKey ) )
{
vr::VRApplications()->RemoveApplicationManifest( manifestPath.c_str() );
qInfo() << "Attempting to Remove Manifest At: " << manifestPath;
}
if ( vr::VRApplications()->IsApplicationInstalled(
application_strings::applicationKey ) )
{
qCritical() << "Removal Failed, run Advanced Settings from previous "
"install directory OR manually delete it in Steam "
"install at Steam\\config\\appconfig.json";
}
else
{
qInfo() << "Manifest Removed Successfully";
}
}
// Removes and then installs the application manifest.
void reinstallApplicationManifest( const std::string manifestPath )
{
if ( vr::VRApplications()->IsApplicationInstalled(
application_strings::applicationKey ) )
{
qInfo() << "Manifest Previously Installed";
// String size was arbitrarily chosen by original author.
constexpr auto kStringSize = 1024;
char oldApplicationWorkingDir[kStringSize] = { 0 };
auto app_error = vr::VRApplicationError_None;
vr::VRApplications()->GetApplicationPropertyString(
application_strings::applicationKey,
vr::VRApplicationProperty_WorkingDirectory_String,
oldApplicationWorkingDir,
kStringSize,
&app_error );
// Steam lists every VR app the user owns in its generated
// steamapps.vrmanifest, installed or not, so whoever owns OVRAS on
// Steam has this key registered with a steam:// URL and no working
// directory. There is no manifest file to remove then: add ours next
// to it (installApplicationManifest() would skip the installed key).
if ( app_error == vr::VRApplicationError_PropertyNotSet )
{
qInfo() << "Key registered without a manifest file (e.g. by "
"Steam), not removing it";
const auto add_error = vr::VRApplications()->AddApplicationManifest(
manifestPath.c_str() );
if ( add_error != vr::VRApplicationError_None )
{
throw std::runtime_error(
"Could not add application manifest: "
+ std::string(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
add_error ) ) );
}
qInfo() << "Manifest Installed at: " << manifestPath;
return;
}
if ( app_error != vr::VRApplicationError_None )
{
throw std::runtime_error(
"Could not find working directory of already "
"installed application: "
+ std::string(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
app_error ) ) );
}
const auto oldManifestPath
= QDir::toNativeSeparators(
QDir::cleanPath( QDir( oldApplicationWorkingDir )
.absoluteFilePath( kVRManifestName ) ) )
.toStdString();
removeApplicationManifest( oldManifestPath );
}
installApplicationManifest( manifestPath );
}
void forceRemoveApplicationManifest()
{
if ( vr::VRApplications()->IsApplicationInstalled(
application_strings::applicationKey ) )
{
// String size was arbitrarily chosen by original author.
constexpr auto kStringSize = 1024;
char oldApplicationWorkingDir[kStringSize] = { 0 };
auto app_error = vr::VRApplicationError_None;
vr::VRApplications()->GetApplicationPropertyString(
application_strings::applicationKey,
vr::VRApplicationProperty_WorkingDirectory_String,
oldApplicationWorkingDir,
kStringSize,
&app_error );
if ( app_error != vr::VRApplicationError_None )
{
throw std::runtime_error(
"Could not find working directory of already "
"installed application: "
+ std::string(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
app_error ) ) );
}
const auto oldManifestPath
= QDir::toNativeSeparators(
QDir::cleanPath( QDir( oldApplicationWorkingDir )
.absoluteFilePath( kVRManifestName ) ) )
.toStdString();
removeApplicationManifest( oldManifestPath );
}
else
{
qInfo() << "No Manifest Installed";
}
}
// Logs what SteamVR reports for OVRAS's app key, to show which entry it uses
// when several register the key (e.g. Steam's own, see
// reinstallApplicationManifest()).
void logApplicationEntry()
{
const auto property = []( vr::EVRApplicationProperty prop )
{
constexpr auto kStringSize = 1024;
char value[kStringSize] = { 0 };
auto error = vr::VRApplicationError_None;
vr::VRApplications()->GetApplicationPropertyString(
application_strings::applicationKey,
prop,
value,
kStringSize,
&error );
return error == vr::VRApplicationError_None
? QString( value )
: QString(
vr::VRApplications()->GetApplicationsErrorNameFromEnum(
error ) );
};
qInfo() << "SteamVR entry for" << application_strings::applicationKey
<< "- source:"
<< property( vr::VRApplicationProperty_Source_String )
<< "launch type:"
<< property( vr::VRApplicationProperty_LaunchType_String )
<< "working dir:"
<< property( vr::VRApplicationProperty_WorkingDirectory_String )
<< "binary:"
<< property( vr::VRApplicationProperty_BinaryPath_String )
<< "URL:" << property( vr::VRApplicationProperty_URL_String )
<< "auto-launch:"
<< vr::VRApplications()->GetApplicationAutoLaunch(
application_strings::applicationKey );
}
// Initializes OpenVR and calls the relevant manifest functions.
// The .vrmanifest is used by SteamVR however the exact functiontionality is not
// documented in the official documentation.
// The manifest is installed upon using the NSIS installer for windows (this
// program is called with "--force-install-manifest" by the installer) and every
// time the program is launched without both "--desktop-mode" and
// "--force-no-manifest". The OpenVR initialization is necessary for both
// removing and installing manifests.
[[noreturn]] void handleManifests( const bool installManifest,
const bool removeManifest )
{
// Connect as Utility app first: it works without a running SteamVR (e.g.
// from the Windows installer). The Steam Frame's SteamVR refuses Utility
// apps (VRInitError_Init_NotAvailableToUtilityApps); there, connect as
// Background app, which needs a running SteamVR and never starts it.
// VR_Init is called directly: initializeOpenVR() retries every error with
// waits, including the one expected here.
vr::EVRInitError initError = vr::VRInitError_None;
vr::VR_Init( &initError, vr::VRApplication_Utility );
if ( initError == vr::VRInitError_Init_NotAvailableToUtilityApps )
{
qInfo() << "SteamVR refuses Utility apps, connecting as Background "
"app.";
vr::VR_Init( &initError, vr::VRApplication_Background );
}
if ( initError != vr::VRInitError_None )
{
qCritical() << "Failed to connect to SteamVR for manifest handling:"
<< vr::VR_GetVRInitErrorAsEnglishDescription( initError );
exit( ReturnErrorCode::OPENVR_INIT_ERROR );
}
int exit_code = ReturnErrorCode::SUCCESS;
try
{
const auto manifestPath
= paths::binaryDirectoryFindFile( manifest::kVRManifestName );
if ( !manifestPath.has_value() )
{
throw std::runtime_error( "Could not find application manifest." );
}
if ( installManifest )
{
reinstallApplicationManifest( *manifestPath );
enableApplicationAutostart();
qInfo() << "Manifest reinstalled.";
logApplicationEntry();
}
else if ( removeManifest )
{
forceRemoveApplicationManifest();
qInfo() << "Manifest removed.";
}
}
catch ( std::exception& e )
{
exit_code = ReturnErrorCode::GENERAL_FAILURE;
qCritical() << e.what();
}
vr::VR_Shutdown();
exit( exit_code );
}
} // namespace manifest
void setUpLogging()
{
f = fopen( "AdvancedSettings.log", "a" );
qSetMessagePattern("[%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{time process}: %{message}");
originalMessageHandler = qInstallMessageHandler( mainQtMessageHandler );
// Debug output only in builds with debugSymbolsAndLogs, as on master
// (easylogging's Debug level was disabled otherwise). Some qDebug() calls
// run on every tick (e.g. "updateAllPulseData done."). The environment
// variable QT_LOGGING_RULES="*.debug=true" takes precedence over this and
// turns them on again without a rebuild.
if constexpr ( !debugLoggingEnabled )
{
QLoggingCategory::setFilterRules( "*.debug=false" );
}
}