Platform: prepare() hook and app-specific command-line options

prepare() runs once mode and app key are known, before engine, surfaces
and SteamVR; returning false ends exec() with exit code 1 (D21). The base
constructor takes the app's own QCommandLineOptions, parses them with its
own and exposes them via isOptionSet()/optionValue() (D38).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 22:06:09 +02:00
co-authored by Claude Opus 5.5
parent 6bdf0c2690
commit 4f47874a4b
4 changed files with 85 additions and 9 deletions
+21 -2
View File
@@ -128,6 +128,8 @@ struct DashboardApplication::Options {
int maxFps = 60;
QString appKey; // --app-key, empty: not given
QString appName; // --app-name, empty: not given
// Kept for the app's own options (isOptionSet() / optionValue()).
QCommandLineParser parser;
};
const char* dashboardModeName(DashboardMode mode) {
@@ -165,7 +167,8 @@ void DashboardApplication::setDefaultQmlModule(const char* uri) {
defaultQmlModule() = QString::fromUtf8(uri);
}
DashboardApplication::DashboardApplication(int& argc, char** argv)
DashboardApplication::DashboardApplication(
int& argc, char** argv, const QList<QCommandLineOption>& appOptions)
: m_options(std::make_unique<Options>()), m_qmlModule(defaultQmlModule()) {
// When launched by SteamVR, stderr is not a TTY and Qt would log to
// journald only; keep messages on stderr so they show up wherever the
@@ -189,7 +192,7 @@ DashboardApplication::DashboardApplication(int& argc, char** argv)
QCoreApplication::setApplicationVersion(
QString::fromLatin1(AppConfig::kVersion));
QCommandLineParser parser;
QCommandLineParser& parser = o.parser;
parser.setApplicationDescription(QStringLiteral("SteamVR dashboard overlay"));
parser.addHelpOption();
parser.addVersionOption();
@@ -247,6 +250,11 @@ DashboardApplication::DashboardApplication(int& argc, char** argv)
parser.addOptions({desktopOption, snapshotOption, benchmarkOption, apiOption,
keyOption, nameOption, fpsOption, installOption,
uninstallOption, manifestOption, launchOption, logOption});
for (const QCommandLineOption& option : appOptions) {
if (!parser.addOption(option))
o.error = QStringLiteral("App option --%1 clashes with another option")
.arg(option.names().constFirst());
}
parser.process(*m_app); // exits for --help / --version
if (parser.isSet(logOption)) {
@@ -319,6 +327,8 @@ void DashboardApplication::setQuickStyle(const QString& style) {
m_quickStyle = style;
}
bool DashboardApplication::prepare(QString*) { return true; }
void DashboardApplication::loading() {}
void DashboardApplication::started() {}
@@ -329,6 +339,14 @@ QList<Surface*> DashboardApplication::surfaces() const {
return result;
}
bool DashboardApplication::isOptionSet(const QString& name) const {
return m_options->parser.isSet(name);
}
QString DashboardApplication::optionValue(const QString& name) const {
return m_options->parser.value(name);
}
Surface* DashboardApplication::surface() const {
return m_surfaces.empty() ? nullptr : m_surfaces.front().get();
}
@@ -383,6 +401,7 @@ int DashboardApplication::exec() {
? m_configDisplayName
: QString::fromUtf8(AppConfig::kDisplayName);
QCoreApplication::setApplicationName(m_appName);
if (!prepare(&error)) return fail(error);
if (!m_quickStyle.isEmpty()) QQuickStyle::setStyle(m_quickStyle);
m_engine = std::make_unique<QQmlEngine>();
+14 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <QCommandLineOption>
#include <QImage>
#include <QList>
#include <QObject>
@@ -53,7 +54,11 @@ class DashboardApplication : public QObject {
Q_OBJECT
public:
// Creates the Qt application object and parses the command line.
DashboardApplication(int& argc, char** argv);
// `appOptions` are the app's own command-line options: parsed and listed
// in --help together with the built-in ones (names must not clash); read
// them with isOptionSet() / optionValue().
DashboardApplication(int& argc, char** argv,
const QList<QCommandLineOption>& appOptions = {});
~DashboardApplication() override;
// Runs the mode selected on the command line; returns the exit code.
@@ -81,6 +86,11 @@ class DashboardApplication : public QObject {
void setQuickStyle(const QString& style);
// --- Lifecycle hooks ---
// First hook, once mode and app key are known: before the QML engine, the
// surfaces and the SteamVR connection exist (not called for manifest
// commands). Return false and set *error to abort: exec() then logs the
// error and returns 1.
virtual bool prepare(QString* error);
// Before the QML scenes are created: the shared engine, the surfaces and
// (except in desktop preview) their render backends exist.
virtual void loading();
@@ -91,6 +101,9 @@ class DashboardApplication : public QObject {
DashboardMode mode() const { return m_mode; }
QString appKey() const { return m_appKey; }
QString appName() const { return m_appName; }
// The app's own command-line options (see the constructor), by name.
bool isOptionSet(const QString& name) const;
QString optionValue(const QString& name) const;
// Shared by all surfaces.
QQmlEngine* engine() const { return m_engine.get(); }
// All views, in addSurface() order; the primary (first) one; one by id.
+2
View File
@@ -31,5 +31,7 @@ add_test(NAME surfaces_opengl COMMAND tst_surfaces --graphics-api opengl)
add_test(NAME surfaces_vulkan COMMAND tst_surfaces --graphics-api vulkan)
add_test(NAME surfaces_cli_key COMMAND tst_surfaces --graphics-api opengl)
set_tests_properties(surfaces_cli_key PROPERTIES ENVIRONMENT "VRDASH_TEST_CLI_KEY=1")
add_test(NAME surfaces_prepare_fail COMMAND tst_surfaces --graphics-api opengl)
set_tests_properties(surfaces_prepare_fail PROPERTIES ENVIRONMENT "VRDASH_TEST_PREPARE_FAIL=1")
set_tests_properties(surfaces_opengl surfaces_vulkan surfaces_cli_key
PROPERTIES SKIP_RETURN_CODE 77)
+48 -6
View File
@@ -1,8 +1,12 @@
// An app with two surfaces, run in snapshot mode: checks surface set-up
// (overlay keys, names, sizes, shared engine, backends), the lifecycle hooks
// and that every surface renders its own scene. No SteamVR involved.
// (overlay keys, names, sizes, shared engine, backends), the lifecycle hooks,
// the app's own command-line options and that every surface renders its own
// scene. No SteamVR involved.
// VRDASH_TEST_PREPARE_FAIL=1: prepare() fails, so exec() must return 1
// without calling the other hooks.
// Exit codes: 0 pass, 1 fail, 77 skipped (no GPU context).
#include <QCommandLineOption>
#include <QDir>
#include <QImage>
#include <QTemporaryDir>
@@ -21,7 +25,14 @@ bool check(bool condition, const char* what) {
class TwoSurfaceApp : public DashboardApplication {
public:
TwoSurfaceApp(int& argc, char** argv) : DashboardApplication(argc, argv) {
TwoSurfaceApp(int& argc, char** argv)
: DashboardApplication(
argc, argv,
{QCommandLineOption(QStringLiteral("test-value"),
QStringLiteral("An app option."),
QStringLiteral("value")),
QCommandLineOption(QStringLiteral("test-flag"),
QStringLiteral("An app flag, never set."))}) {
setQmlModule(QStringLiteral("SurfacesTest"));
setDisplayName(QStringLiteral("Test App"));
setAppKey(QStringLiteral("config.key"));
@@ -35,11 +46,28 @@ class TwoSurfaceApp : public DashboardApplication {
}
QString expectedKey; // --app-key overrides setAppKey()
bool failPrepare = false;
int prepareCalls = 0;
int loadingCalls = 0;
int startedCalls = 0;
bool ok = true;
protected:
bool prepare(QString* error) override {
++prepareCalls;
ok &= check(loadingCalls == 0 && startedCalls == 0, "prepare() first");
ok &= check(engine() == nullptr && surfaces().isEmpty(),
"prepare() before engine and surfaces");
ok &= check(mode() == DashboardMode::Snapshot, "mode in prepare()");
ok &= check(appKey() == expectedKey, "app key in prepare()");
ok &= check(
isOptionSet(QStringLiteral("test-value")) &&
optionValue(QStringLiteral("test-value")) == QLatin1String("42"),
"app option value");
ok &= check(!isOptionSet(QStringLiteral("test-flag")), "app option unset");
if (failPrepare) *error = QStringLiteral("prepare() failed on purpose");
return !failPrepare;
}
void loading() override {
++loadingCalls;
Surface* red = surface(QStringLiteral("red"));
@@ -80,7 +108,10 @@ int main(int argc, char* argv[]) {
QDir(dir.path()).filePath(QStringLiteral("shot.png")).toStdString();
// VRDASH_TEST_CLI_KEY=1: also pass --app-key (must win over setAppKey()).
const bool cliKey = qEnvironmentVariableIntValue("VRDASH_TEST_CLI_KEY");
std::vector<std::string> args = {argv[0], "--snapshot", snapshot};
const bool failPrepare =
qEnvironmentVariableIntValue("VRDASH_TEST_PREPARE_FAIL");
std::vector<std::string> args = {argv[0], "--snapshot", snapshot,
"--test-value", "42"};
if (cliKey) {
args.emplace_back("--app-key");
args.emplace_back("cli.key");
@@ -93,15 +124,26 @@ int main(int argc, char* argv[]) {
TwoSurfaceApp app(cargc, cargs.data());
app.expectedKey =
cliKey ? QStringLiteral("cli.key") : QStringLiteral("config.key");
app.failPrepare = failPrepare;
const int result = app.exec();
if (failPrepare) {
bool ok = app.ok;
ok &= check(result == 1, "exec() returns 1 when prepare() fails");
ok &= check(
app.prepareCalls == 1 && app.loadingCalls == 0 && app.startedCalls == 0,
"no hooks after a failed prepare()");
fprintf(stderr, ok ? "PASS\n" : "FAILED\n");
return ok ? 0 : 1;
}
if (app.loadingCalls == 0) {
fprintf(stderr, "SKIP: no render backend (no GPU context?)\n");
return 77;
}
bool ok = app.ok;
ok &= check(result == 0, "exec() succeeded");
ok &= check(app.loadingCalls == 1 && app.startedCalls == 1,
"each hook called once");
ok &= check(
app.prepareCalls == 1 && app.loadingCalls == 1 && app.startedCalls == 1,
"each hook called once");
const QImage red(QDir(dir.path()).filePath(QStringLiteral("shot-red.png")));
const QImage blue(QDir(dir.path()).filePath(QStringLiteral("shot-blue.png")));