9 Commits
Author SHA1 Message Date
matzmanandClaude Opus 5.5 5a10cdf7ca Wrap-up documentation after the first tests on the Steam Frame
README: status, new Steam Frame facts (Steam's generated app manifest,
OpenVR library, display and font metrics) and open points. PROGRESS:
partial results of T1, T2 and T4, and what is open next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:21:52 +02:00
matzmanandClaude Opus 5.5 7f14551a6a Record the Frame test of the Fusion style fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:20:05 +02:00
matzmanandClaude Opus 5.5 ff4ea8f455 Use the Fusion Quick Controls style; drop the Material import
On the Steam Frame, the fixed 1200x800 layout lost about its bottom
quarter. Measured with --snapshot: fonts and DPI are as intended, but
the controls ran in the Material style. The qt6 branch changed
mainwidget.qml's "import QtQuick.Controls" into
"import QtQuick.Controls.Material", which fixes Material for all
controls and even overrides QT_QUICK_CONTROLS_STYLE. Material's insets
and padding make the navigation buttons 65 px tall instead of 45 px
(Fusion) or 49 px (Basic, Qt 5's default style renamed); nothing in the
file uses Material.

mainwidget.qml imports QtQuick.Controls again, and main.cpp sets Fusion
explicitly, so the sizes are the same on every platform instead of
depending on Qt 6's per-platform default. With Basic the last navigation
button is still cut off on the Frame; with Fusion everything fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:14:57 +02:00
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
matzmanandClaude Opus 5.5 bbb3d1ba0e Log debug output only in builds with debugSymbolsAndLogs
On master, easylogging's Debug level was disabled unless OVRAS was built
with debugSymbolsAndLogs (ENABLE_DEBUG_LOGGING). The qt6 branch switched
to qDebug() and lost that: debugLoggingEnabled was left unused, and all
debug output reached the log. On the Steam Frame it filled with
"updateAllPulseData done." about every 11 ms, as the audio tab reads the
microphone's mute state on every tick.

setUpLogging() now disables debug output through a logging filter rule.
QT_LOGGING_RULES="*.debug=true" takes precedence and turns it on again
for troubleshooting without a rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:32:00 +02:00
matzmanandClaude Opus 5.5 ec82b5fead Register the manifest when Steam already lists the app key
Steam writes every VR app the user owns into its generated
steamapps.vrmanifest, installed or not. Whoever owns OVRAS on Steam
therefore always has steam.overlay.1009850 registered, as a steam:// URL
without a working directory, and --force-install-manifest failed with
"Could not find working directory of already installed application:
VRApplicationError_PropertyNotSet" (found on the Steam Frame).

Such an entry has no manifest file to remove, so OVRAS now leaves it
alone and adds its own manifest. Whether SteamVR accepts a second
manifest for the key, and which entry it then uses, is not documented:
logApplicationEntry() logs what SteamVR reports for the key after
installing, so a test on the Frame shows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:25:57 +02:00
matzmanandClaude Opus 5.5 317db6831d Use the OpenVR SDK's bin/linuxarm64 library on aarch64
On the Steam Frame, VR_Init failed with "Installation path could not be
located": the SDK's lib/linuxarm64/libopenvr_api.so (2.15.6) reads the VR
path registry from the fixed path /data/work/openvrpaths.vrpath instead
of ~/.config/openvr. The SDK's bin/linuxarm64 library of the same version
uses ~/.config/openvr; the vrdashboard template ships it and was verified
on the Frame. It is added as a new file (third-party/openvr had no
bin/linuxarm64, and "bin" is gitignored, hence force-added), and aarch64
builds link against and ship it. x86_64 and Windows are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:13:47 +02:00
matzmanandClaude Opus 5.5 537539ab5e Run the manifest commands without a display
The first deploy to the Steam Frame aborted in install.sh: OVRAS creates
its QApplication before it handles --force-install-manifest, and Qt's
default platform plugin (xcb) aborts when there is no display, as over
SSH. Registering and unregistering draw nothing, so the generated
install.sh and uninstall.sh now run them with QT_QPA_PLATFORM=offscreen.
The normal start keeps DISPLAY=:0, as rendering needs a real context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:08:10 +02:00
Matthias JanetschekandClaude Opus 5.5 7c5a9a7a1e Handover documentation for continuing on another computer
- docs/steamframe/README.md: entry point with status, branches/remotes, setup,
  commands, Steam Frame facts, open points.
- CLAUDE.md: working rules and code conventions (fork-only).
- docs/steamframe/OVRAS-FEATURES.md: OVRAS's features, how they are
  implemented, issues found while reading.
- PROGRESS.md: status and wrap-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:57:20 +02:00
14 changed files with 910 additions and 18 deletions
+96
View File
@@ -0,0 +1,96 @@
# CLAUDE.md
Guidance for Claude Code (and humans) working in this fork. Fork-only file: not part of
upstream pull requests.
**Start with [docs/steamframe/README.md](docs/steamframe/README.md):** goal, status, setup,
commands, Steam Frame facts. Then [REQUIREMENTS.md](docs/steamframe/REQUIREMENTS.md)
(decisions), [DESIGN.md](docs/steamframe/DESIGN.md) and
[PROGRESS.md](docs/steamframe/PROGRESS.md) (status, Frame test checklist).
## Working rules (agreed with the user, always apply)
- **Proper software engineering:** follow established procedures and processes, not ad-hoc
hacking.
- **Nothing without explicit permission.**
- Propose first, then act only on the user's go-ahead.
- Permission covers only what was asked. No extra changes, renames, rewrites or "while I'm
at it" fixes: suggest those instead.
- **Ask, don't assume.** When a requirement, decision or detail is unclear, ask. Give a
recommendation with the question.
- **Iterations in phases.** Each phase ends with the user's OK (informal is enough):
1. requirements
2. design
3. implementation
4. tests
5. assessment
- Keep iterations small.
- **Document everything** in `docs/steamframe/`: requirements, decisions (with date), design,
progress, test results. Report results faithfully, including what was *not* tested. Keep
the docs in sync with every change.
- **Communication:** the user writes German or English; answer in the user's language. Docs
and code comments are in English.
## Goal and constraints (details: REQUIREMENTS.md)
- OVRAS runs natively on the Steam Frame (SteamOS, aarch64). Windows is out of scope for now.
- **Minimal change set against upstream's `qt6` branch**, because changes are meant to go
upstream later.
- Change existing code only where needed.
- Bring new parts in as self-contained files.
- **Only public Qt API.** No `GuiPrivate`/QRhi, and QML is compiled to bytecode only.
`objdump -T … | grep Qt_6_PRIVATE_API` may only show `QVulkan*`.
- **Frame differences:**
- Switched by the CMake option `OVRAS_STEAM_FRAME`.
- It is evaluated **only** in `src/utils/platform_capabilities.h`, which turns it into
capability constants named by capability (e.g. `k_chaperoneCommitSupported`).
- The code asks for capabilities, never for the platform.
## Code conventions
- **Comments:** every change gets comments that explain it fully (what and why, e.g. which
Frame or SteamVR fact). Keep them direct and concise. Trivial mechanical changes such as
`LOG(INFO)` → `qInfo()` are explained in the commit message instead.
- **Style:**
- OVRAS style (`.clang-format`, since 2018) everywhere, including code taken over from the
template.
- **Format only changed lines:** `git add …; git clang-format; git add …`. Never
reformat whole existing files, because a dozen files already deviate.
- New files are formatted completely (`clang-format -i`).
- Separate new comments from existing commented-out code with a blank line. Otherwise
clang-format reflows the old block and the diff grows.
- **Commits:**
- One commit per step / topic, with a message that explains the why.
- End every commit message with `Co-Authored-By: Claude …`.
- Push to `origin` (the fork); `upstream` is the GitHub OVRAS repository.
- **Template code** (`src/render/`, `build_scripts/steamframe/`) carries a note about its
origin (vrdashboard template, FrameAssistant `4a5b090`).
## Architecture notes (what is easy to break)
- **`main.cpp` order:**
1. parse the command line
2. choose the render backend (`--graphics-api`)
3. `initializeOpenVR(Overlay)`
4. `backend->initialize(openvr_init::renderInterop())`
5. `QQuickWindow::setGraphicsApi()`
6. `OverlayController`
`setGraphicsApi` must come before any `QQuickWindow` exists, and `OverlayController` owns
one.
- **`OverlayController`:**
- Owns `m_renderBackend`, declared *before* `m_renderControl` / `m_window`: Qt's device
comes from the backend.
- `SetWidget()` and `renderOverlay()` follow the call order in `src/render/RenderBackend.h`.
- **Shutdown:**
- OVRAS ends with `exit()`.
- `Shutdown()` (once only) calls `vr::VR_Shutdown()` *before* `releaseRenderTarget()` /
`detachWindow()`.
- **Tick:**
- `m_pumpEventsTimer`: 1 ms polling in "vsync" mode; in "vsync disabled" mode it runs
directly at `customTickRateMs` (`applyTickInterval()`).
- Frame defaults: vsync disabled, 11 ms.
- **Mouse:** SteamVR reports y from the bottom, so it is flipped on every platform.
- **Logging:** OVRAS writes `AdvancedSettings.log` into the working directory.
- **SteamVR Input:** `action_manifest.json` and `default_action_manifests/` (incl.
`…_default_frame.json` for `frame_controller`) lie next to the binary.
+6 -1
View File
@@ -343,7 +343,12 @@ endif ()
if (WIN32)
set(OPENVR_LIB_DIR ${CMAKE_SOURCE_DIR}/third-party/openvr/lib/win64)
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(OPENVR_LIB_DIR ${CMAKE_SOURCE_DIR}/third-party/openvr/lib/linuxarm64)
# bin/, not lib/: the SDK's lib/linuxarm64/libopenvr_api.so looks for the
# VR path registry in the fixed path /data/work/openvrpaths.vrpath instead
# of ~/.config/openvr, so on the Steam Frame it never finds SteamVR
# ("Installation path could not be located"). bin/linuxarm64 (SDK 2.15.6)
# uses ~/.config/openvr.
set(OPENVR_LIB_DIR ${CMAKE_SOURCE_DIR}/third-party/openvr/bin/linuxarm64)
else ()
set(OPENVR_LIB_DIR ${CMAKE_SOURCE_DIR}/third-party/openvr/lib/linux64)
endif ()
+14 -6
View File
@@ -45,7 +45,10 @@ cat > "$out/install.sh" <<EOF
cd "\$(dirname "\$0")" || exit 1
# An update replaces the files of a running instance: stop it first.
pkill -f "/$exe( |\\\$)" 2>/dev/null && sleep 1
./$exe --force-install-manifest || exit 1
# OVRAS creates its QApplication before handling the manifest options, and
# Qt's default platform plugin (xcb) aborts without a display, e.g. over SSH.
# Registering draws nothing, so use the display-less "offscreen" plugin.
QT_QPA_PLATFORM=offscreen ./$exe --force-install-manifest || exit 1
# Start now. Over SSH there is no display; Qt needs one even for off-screen
# rendering, so use the Frame's (:0).
DISPLAY="\${DISPLAY:-:0}" nohup ./$exe >/dev/null 2>&1 &
@@ -58,17 +61,22 @@ cat > "$out/uninstall.sh" <<EOF
# be deleted afterwards.
cd "\$(dirname "\$0")" || exit 1
pkill -f "/$exe( |\\\$)" 2>/dev/null
exec ./$exe --force-remove-manifest
# No display needed for unregistering (see install.sh).
exec env QT_QPA_PLATFORM=offscreen ./$exe --force-remove-manifest
EOF
cat > "$out/run.sh" <<EOF
#!/bin/sh
# Restarts OVR Advanced Settings with the given graphics API, for testing:
# ./run.sh [opengl|vulkan] (default: opengl)
# Restarts OVR Advanced Settings with the given graphics API, for testing.
# Further arguments go to the program:
# ./run.sh [opengl|vulkan] [options] (default: opengl)
# e.g. ./run.sh opengl --snapshot /tmp/ovras.png
cd "\$(dirname "\$0")" || exit 1
api="\${1:-opengl}"
[ \$# -gt 0 ] && shift
pkill -f "/$exe( |\\\$)" 2>/dev/null && sleep 1
DISPLAY="\${DISPLAY:-:0}" nohup ./$exe --graphics-api "\${1:-opengl}" >/dev/null 2>&1 &
echo "Started with \${1:-opengl}; log: \$(pwd)/AdvancedSettings.log"
DISPLAY="\${DISPLAY:-:0}" nohup ./$exe --graphics-api "\$api" "\$@" >/dev/null 2>&1 &
echo "Started with \$api; log: \$(pwd)/AdvancedSettings.log"
EOF
chmod +x "$out/install.sh" "$out/uninstall.sh" "$out/run.sh"
+75
View File
@@ -75,6 +75,36 @@ Private Qt symbols in the binary built that way (`objdump -T`, `Qt_6_PRIVATE_API
- It calls `VR_Init` directly, without `initializeOpenVR`'s retry loop. That loop waits
and retries on every error, including the one we expect here.
### 2a. App key already registered by Steam (approved 2026-10-07)
**Finding (Frame test T1):** Steam writes `~/.local/share/Steam/config/steamapps.vrmanifest`
(`"source": "steam"`) with every VR app the user **owns**, installed or not (Frame: 154
entries, 25 installed). It is regenerated by Steam. Whoever owns OVRAS on Steam therefore
always has `steam.overlay.1009850` registered, as `launch_type: url`
(`steam://launch/1009850/openvroverlay`), `is_hidden`. Such an entry has no working
directory. `reinstallApplicationManifest()` treats that as an error ("Could not find working
directory of already installed application: VRApplicationError_PropertyNotSet") and stops.
**Open question:** does SteamVR accept a second manifest for the same key, and which entry
wins? `AddApplicationManifest` may return `VRApplicationError_AppKeyAlreadyExists`. If
Steam's entry wins, auto-start and the dashboard would launch the Steam (Proton) version.
**Change (`setup.cpp`, platform-neutral, also useful upstream):**
- `reinstallApplicationManifest()`: if the key is installed but has no working directory
(`VRApplicationError_PropertyNotSet`), there is no manifest file to remove. Log it with
the entry's source and don't remove anything. Then call `AddApplicationManifest` directly
(`installApplicationManifest()` skips installed keys) and fail with the error's name if it
is refused.
- New helper `logApplicationEntry()`, called at the end of `--force-install-manifest`: logs
what SteamVR now reports for the key (source, launch type, working directory, binary path,
URL, auto-launch). This answers the open question on the Frame.
- Unchanged: the path for a regular earlier installation (with working directory), and
`installApplicationManifest()` on normal starts.
**Follow-up depending on the result:** if SteamVR refuses our manifest or Steam's entry
wins, the native build needs its own app key (option 2, separate design: the key is also in
the manifest, 7 default action manifests and `SteamVRTabController.h`).
## 3. Rendering through public API (R1, R3)
**From the template** (`4a5b090`, verified on the Frame), adopted largely unchanged:
@@ -230,6 +260,51 @@ existing code:
private API.
- RPATH is only `$ORIGIN` (`BUILD_WITH_INSTALL_RPATH`): the build tree gets packaged.
## 10. Layout diagnosis: font log and `--snapshot` (approved 2026-10-08)
**Why:** on the Frame the fixed 1200×800 layout overflows: the bottom quarter is cut off
(left: Statistics, Bindings, Settings; right: Microphone, Push-to-Talk), and the navigation
icons overlap their text. `QT_FONT_DPI=96`, `QT_QUICK_CONTROLS_STYLE=Basic` and the Material
"Dense" variant didn't fix it. Font metrics (Noto Sans instead of Segoe UI) are the main
suspect; measuring needs the unwarped rendered image.
**Changes (platform-neutral):**
- `OverlayController::SetWidget()`: logs once the application font (family, pt, px), what
20 pt (`MyText`) is in px, the screen's logical/physical DPI and the window's device pixel
ratio.
- Option `--snapshot <file>` (`setup.*`, `main.cpp`, `overlaycontroller.*`): while the
overlay is visible and rendered, `renderOverlay()` writes `RenderBackend::readback()` as
PNG to `<file>`, at most once per second (OVRAS only renders when the scene changes, so a
frame count could never trigger on a still page). Written to `<file>.tmp` and renamed.
`main.cpp` passes it through a setter, not a new constructor parameter, to keep the diff
small. Without the option nothing changes.
- `run.sh`: arguments after the graphics API go to the program
(`./run.sh opengl --snapshot /tmp/ovras.png`).
## 11. Quick Controls style: Fusion, no Material import (approved 2026-10-08)
**Finding (measured with `--snapshot`, DESIGN.md section 10):** the font is as intended (20 pt
= 27 px, 96 DPI, device pixel ratio 1). The controls run in the Material style: `qt6`
(`93c0851`) changed `mainwidget.qml`'s `import QtQuick.Controls` into
`import QtQuick.Controls.Material`. That import fixes Material for all controls and even
overrides `QT_QUICK_CONTROLS_STYLE`. Material's insets and padding make the navigation
buttons 65 px tall (75 px pitch), so the fixed 1200×800 root page loses about its bottom
quarter. Nothing in the file uses Material.
| Style (root page, navigation buttons) | Button | Pitch | Result |
|---|---|---|---|
| Material (`qt6` as is) | 65 px | 75 px | 9 of 12 buttons visible |
| Basic (Qt 5's "Default" renamed) | 49 px | 59 px | last button cut off (18 of 49 px) |
| Fusion (Qt 6's Linux default) | 45 px | 55 px | everything fits |
**Changes:**
- `mainwidget.qml`: `import QtQuick.Controls` again, as before `93c0851`, with a comment.
- `main.cpp`: `QQuickStyle::setStyle("Fusion")` after `QQuickWindow::setGraphicsApi()`,
before any QML is loaded. Explicit, so the look and sizes are the same on every platform
(without it Qt 6 picks Windows' own style there). Links `Qt6QuickControls2` directly
(already a dependency; present on the Frame: `/usr/lib/libQt6QuickControls2.so.6.8.0`).
- Upstream note: with Material, the layout overflows on Windows too.
## Tests
- **x86_64 locally:** the build succeeds; `objdump` check (R1).
+197
View File
@@ -0,0 +1,197 @@
# OVRAS: features and how they are implemented
Analysis of 2026-10-07, based on upstream `master` (`31df8b4`, 5.8.17). Line numbers refer to
that state. `qt6` has the same features, so the numbers are close. Paths are relative to
`src/`. Which of these features SteamVR now offers itself is in
[REQUIREMENTS.md](REQUIREMENTS.md), "Feature classification".
## Architecture
- **Dashboard overlay:**
- `VR_Init(VRApplication_Overlay)`, app key `steam.overlay.1009850`.
- QML UI (1200×800) rendered off-screen via `QQuickRenderControl`.
- The texture goes to `SetOverlayTexture`. On `steamframe` the render backends in `render/`
do this; before, it was an OpenGL FBO, and on `qt6` QRhi.
- **Input:** SteamVR mouse/scroll events are turned into Qt events (`overlaycontroller.cpp`,
`mainEventLoop()`).
- **Main loop (`OnTimeoutPumpEvents` → `mainEventLoop()`):**
- Timer; "vsync" mode once per compositor frame (`GetTimeSinceLastVsync`).
- Per tick:
1. input actions
2. VR events
3. poses
4. `eventLoopTick` of the tab controllers (always)
5. `dashboardLoopTick` (only while the dashboard is visible)
- **SteamVR Input:** `package_files/action_manifest.json`, 5 action sets:
- `motion`: space drag/turn, gravity, snap/smooth turn, height toggle, offsets, auto-turn
- `music`: media keys
- `misc`: axis locks, chaperone toggle, keyboard shortcuts
- `system`: push-to-talk, exclusive input, haptic clicks
- `haptic`: vibration, proximity sensor
Default bindings exist for Vive, Index, WMR, HP, Touch, generic HMD and Frame controllers.
- **Persistence:**
- App settings in a `QSettings` INI file (`settings/`). Profiles are stored as arrays
(`ISettingsObject`).
- SteamVR's own settings go directly through `vr::VRSettings()` (`openvr/ovr_settings_wrapper`).
- **Platform:**
- Windows: WASAPI / `IPolicyConfig`, `SendInput`.
- Linux: PulseAudio, X11 XTest, D-Bus MPRIS.
- Elsewhere: dummies.
## Features
### Playspace movement (`tabcontrollers/MoveCenterTabController.cpp`)
Almost everything shifts or rotates the **working standing zero pose** through
`IVRChaperoneSetup` and shows it live with `ShowWorkingSetPreview()`. Nothing is committed
per frame. This mechanism does **not work on the Frame** (inside-out tracking).
- **Core, `updateSpace()` (:2497–2721):**
- Offsets X/Y/Z and the rotation (centidegrees) are multiplied with the stored basis.
- The result goes to `SetWorkingStandingZeroPoseToRawTrackingPose`, in seated mode
additionally to the seated pose.
- Safety limit: about 40 km.
- **Space drag:**
- The controller's position delta, multiplied by `dragMult`, is added to the offsets.
- Axis locks; the speed is kept for fling.
- Comfort mode skips frames.
- **Space turn:** the change of the yaw angle via quaternions
(`q·conj(q_last)`, `quaternion/quaternion.h`). Rotation pivots on the HMD by default.
- **Snap/smooth turn:** fixed angle per press resp. per tick, so smooth turn depends on the
frame rate.
- **Gravity/momentum:** Euler integration with friction, terminal velocity 50 m/s, landing on
the floor, reversible.
- **Height toggle:** Y offset plus a new gravity floor.
- **Offset profiles, apply/reset:**
- "Apply" commits with `CommitWorkingCopy(Live)` (limit 990 m).
- `zeroOffsets()` / `updateChaperoneResetData()` also commit at start-up. On the Frame
this shifts the play area, hence R7.
### Rotation (`tabcontrollers/RotationTabController.cpp`, via `setRotation`)
- **Auto-turn:**
- Rotates the space when the user gets near a wall, either as a snap or smoothly.
- Direction: from the corner shared with the last wall, otherwise cord untangling (HMD
total rotation count).
- **Vestibular motion:** rotation as if walking on a circle of radius r
(`2·asin(d/2r)`).
- **View ratchetting:** amplifies head turns away from the wall.
### Fix floor / recenter (`tabcontrollers/FixFloorTabController.cpp`)
- **Fix floor:**
- Measures the lower controller over 25 frames.
- Subtracts the controller thickness per model and orientation (Vive / Knuckles).
- Commits the result as a Y offset; undo is possible.
- **Recenter:** `VRChaperone()->ResetZeroPose(...)`.
### Chaperone (`tabcontrollers/ChaperoneTabController.cpp`)
- **Settings:**
- Visibility, color, fade distance, wall height, center marker, floor bounds and style.
- These are keys in `k_pch_CollisionBounds_Section`.
- "Disable" sets the fade distance to 0 and remembers the old value.
- **Own center marker overlay:** shows the rotation count (cord).
- **Dim when crouching:** below a height, the fade distance goes to 0 over 10 s.
- **Proximity warnings:**
- Distance of HMD and controllers to the walls (`utils/ChaperoneUtils.cpp`: projection onto
each wall segment).
- Reactions: switch to the beginner style, vibration (own thread,
`TriggerHapticVibrationAction`), alarm sound with distance-dependent volume, open the
dashboard.
- **Chaperone profiles:**
- Save the geometry (`GetLiveCollisionBoundsInfo`, zero pose, size) and the settings.
- Apply them with `SetWorking…` + `CommitWorkingCopy`.
- Autosave profile.
### Video (`tabcontrollers/VideoTabController.cpp`)
- **Brightness:** a black overlay right in front of the HMD. Its alpha `(1−x)^(1/3)`
compensates for perception.
- **Color tint:**
- either a colored overlay (`SetOverlayColor`)
- or SteamVR's `HmdDisplayColorGainR/G/B`
- **SteamVR keys:** supersampling (incl. manual override), motion smoothing, supersample
filtering.
- **Video profiles.**
### Audio (`tabcontrollers/AudioTabController.cpp`, `audiomanager/`)
- **Interface:** `AudioManager` with a Windows, a PulseAudio and a dummy implementation.
- **Windows:**
- WASAPI/MMDevice, `IAudioEndpointVolume`.
- The default device is set via the undocumented `IPolicyConfig`.
- External changes come in via `IMMNotificationClient`.
- **Linux:**
- PulseAudio: `pa_context_set_default_sink/source`; existing source outputs are moved to
the new source.
- The original state is restored at exit.
- **No mirror device.**
- **Mirror device and device override:** these are SteamVR keys (`k_pch_audio_*`), not
OVRAS's own routing.
- **Push-to-talk / -mute:** SteamVR Input action → mic mute + notification overlay on the
HMD.
- **Mute the mic when the HMD is taken off:** proximity sensor action.
- **Audio profiles.**
### SteamVR tab (`tabcontrollers/SteamVRTabController.cpp`)
- **Toggles:** each is a SteamVR setting.
- performance graph, multiple drivers, do not disturb, no-HMD mode
- camera options
- system button to all apps, don't fade to grid, controller power auto-launch
- **Restart SteamVR:** Windows only, via `.bat`.
- **Per-game controller bindings:**
- Use SteamVR's web server `localhost:27062`, with a spoofed `Referer`.
- Saved as JSON and applied when the scene app changes.
- **Receiver/tracker pairing:** WebSocket to the lighthouse web interface (`start_pairing`).
- **`openvr/lh_console_util.cpp`** (`lighthouse_console`) is compiled but **not used**.
- There is no base-station power management.
### Utilities (`tabcontrollers/UtilitiesTabController.cpp`, `keyboard_input/`, `media_keys/`, `alarm_clock/`)
- **Keyboard:**
- Buttons (Enter, Alt-Tab, Ctrl-C/V, …) and 3 freely defined shortcuts.
- A parser for the syntax `^`=Ctrl, `*`=Alt, `>`=Shift, `#`=Super.
- Sent via Windows `SendInput` or X11 XTest. On the Frame (gamescope) the X11 path probably
doesn't reach games.
- **Media keys:**
- Windows: `VK_MEDIA_*`.
- Linux: D-Bus to every `org.mpris.MediaPlayer2.*`.
- **Tracker battery:** icon overlay on each tracker (`Prop_DeviceBatteryPercentage_Float`),
only while the dashboard is visible.
- **Alarm clock:** HMD overlay at a set time, shown for 3 s.
### Statistics (`tabcontrollers/StatisticsTabController.cpp`)
- **Frame counters:** `VRCompositor()->GetCumulativeStats()` gives presented, dropped,
reprojected and timed-out frames. "Reset" only stores an offset.
- **Distance traveled:** horizontal HMD movement, every 11th tick, steps ≥ 1 cm.
- **Rotation count, maximum controller speed.**
### Settings tab (`tabcontrollers/SettingsTabController.cpp`)
Autostart (`SetApplicationAutoLaunch`), native chaperone import, block Oculus SDK, desktop
mode, tick rate (vsync / custom tick rate).
## Issues found while reading (not verified)
| Place (master) | Issue |
|---|---|
| `MoveCenterTabController.cpp:1198` | `updateChaperoneResetData()` allocates `new vr::HmdQuad_t[]` on every call and never frees the old array (memory leak) |
| `MoveCenterTabController.cpp:2512` | `if ( m_chaperoneHasCommit ) { m_chaperoneHasCommit = true; … }` looks like a slip |
| `MoveCenterTabController.cpp:1332` | `leftHandSpaceDrag`: the final `else` overwrites the swap/override choice |
| `MoveCenterTabController` | Rotation is wrapped inconsistently (snap/smooth/auto-turn 0–36000 vs. ±18000 elsewhere) |
| `VideoTabController.cpp:114` | Sets the sort order of the brightness overlay instead of the color overlay |
| `ChaperoneTabController.cpp:903` | Floor bounds are read as Bool but written as Int32 |
| `MoveCenterTabController::zeroOffsets` | If `GetCalibrationState() == 200`, init never completes and the commit repeats every tick (with commits enabled) |
| `utils/FrameRateUtils.cpp`, `openvr/lh_console_util.cpp` | Compiled but unused |
Found on the `qt6` branch and fixed on `steamframe` (see PROGRESS.md):
- the build fails on Linux
- QRhi Vulkan path: wrong `m_nFormat` (Qt enum instead of `VkFormat`); no layout transition,
no SteamVR extensions, no choice of the HMD's GPU
- mouse y not flipped on Linux
- no `VR_Shutdown`
- hard-coded version 5.7.4
+177 -3
View File
@@ -1,5 +1,8 @@
# Steam Frame port: progress
**Status (2026-10-07): iteration 1 implemented. Next: test checklist T1–T11 on the Frame
(at the end of this file), then assessment. Getting started: [README.md](README.md).**
## Iteration 1
### Step 1: build fixes, bytecode-only QML (2026-10-07)
@@ -169,16 +172,156 @@
- **Result:** `APPLICATION_VERSION="5.8.17-release"`, `CMAKE_PROJECT_VERSION=5.8.17`, package
`dist/AdvancedSettings-5.8.17-linux-arm64.tar.gz` (3.0 MB).
### Fix: manifest commands without a display (2026-10-07)
- **Problem (first deploy to the Frame):** `install.sh` aborted with a core dump in
`./AdvancedSettings_app --force-install-manifest`. Log: "could not connect to display",
then "no Qt platform plugin could be initialized" (fatal).
- **Cause:** `main.cpp` creates the `QApplication` before it handles the manifest options.
Qt's default platform plugin (`xcb`) aborts without a display, and over SSH there is
none. `install.sh` set `DISPLAY=:0` only for the start, not for registering.
`uninstall.sh` (`--force-remove-manifest`) had the same problem.
- **Fix (`package-release.sh`, generated scripts only):** both manifest calls run with
`QT_QPA_PLATFORM=offscreen`. Registering draws nothing, so it needs no display at all.
The normal start keeps `DISPLAY=:0` (`xcb`), because rendering needs a real OpenGL/Vulkan
context. No change to the program.
- **Result:** package rebuilt (`--no-build`), generated scripts checked with `sh -n`.
- **Not tested yet:** the fix itself on the Frame (T1, T11). No local x86 build was at hand
for a test without a display.
### Fix: OpenVR library for aarch64 (2026-10-07)
- **Problem (second deploy):** with the display fix, `--force-install-manifest` got as far
as `VR_Init` and failed there: "Installation path could not be located (110)", and on
stderr "Unable to read VR Path Registry from /data/work/openvrpaths.vrpath".
`~/.config/openvr/openvrpaths.vrpath` on the Frame is correct (runtime `/opt/steamvr`).
- **Cause:** the package shipped `third-party/openvr/lib/linuxarm64/libopenvr_api.so`. It is
byte-identical to the OpenVR SDK 2.15.6's `lib/linuxarm64` and looks for the path registry
in the fixed path `/data/work/openvrpaths.vrpath`. The SDK's `bin/linuxarm64` library
(used by the template, verified on the Frame) and SteamVR's own one in
`/opt/steamvr/bin/linuxarm64` don't contain that path. OVRAS's `third-party/openvr` had no
`bin/linuxarm64`.
- **Fix:**
- New file `third-party/openvr/bin/linuxarm64/libopenvr_api.so` from the SDK v2.15.6
(`raw.githubusercontent.com/ValveSoftware/openvr/v2.15.6/bin/linuxarm64/…`, SHA-256
`cc3671d2…47e9`).
- `CMakeLists.txt`: `OPENVR_LIB_DIR` = `bin/linuxarm64` for aarch64 only. x86_64 and
Windows are unchanged.
- Rejected: setting `VR_PATHREG_OVERRIDE` in the scripts. It wouldn't help when SteamVR
autostarts OVRAS.
- **Result:** cross build and package succeed. The package contains the new library (no
`/data/work`), `Qt_6_PRIVATE_API` without `QVulkan*`: 0, RUNPATH `$ORIGIN`. Package
1.6 MB (was 3.0 MB, the old library was 5.4 MB).
- **Not tested yet:** on the Frame (T1).
### Finding: app key already registered by Steam (2026-10-07)
- **Third try of T1** (by hand on the Frame, with the two fixes above): `VR_Init` works, as
`Utility` (vrserver.txt: "New Connect message … (VRApplication_Utility)"; SteamVR did
**not** refuse Utility this time, unlike with the template). Then: "Manifest Previously
Installed", "Could not find working directory of already installed application:
VRApplicationError_PropertyNotSet".
- **Cause:** Steam's generated `steamapps.vrmanifest` lists every owned VR app, including
OVRAS (`steam.overlay.1009850`, launch via `steam://` URL), although the Steam version is
uninstalled. Not a leftover of the uninstallation; it can't be removed permanently.
- **Next:** design DESIGN.md section 2a (approved 2026-10-07).
- **Implementation (`setup.cpp`, `setup.h`):** as designed: key without working directory →
nothing removed, `AddApplicationManifest` directly (error name on refusal);
`logApplicationEntry()` after "Manifest reinstalled." logs source, launch type, working
directory, binary, URL and auto-launch for the key.
- **Result:** cross build and package succeed, `Qt_6_PRIVATE_API` without `QVulkan*`: 0.
- **Not tested yet:** on the Frame. The log decides: `AppKeyAlreadyExists` or Steam's entry
(`steam://` URL) → own app key needed (option 2); our working directory and binary →
enough.
**Result on the Frame (2026-10-07, deploy with `ec82b5f`):** SteamVR accepts our manifest
(no error) but keeps using Steam's entry for the key:
`source: "steam" launch type: "url" working dir: PropertyNotSet binary: PropertyNotSet
URL: "steam://launch/1009850/openvroverlay" auto-launch: true`.
- **Consequence:** the native build needs its own app key (option 2, DESIGN.md 2a,
follow-up). Design pending.
- **Side effect:** `--force-install-manifest` switched auto-launch on for Steam's key. At the
next SteamVR start, SteamVR may launch `steam://launch/1009850` (the uninstalled Steam
version). To be switched off again.
- OVRAS itself was started by `install.sh` and keeps running (not yet checked in the
dashboard, T2).
### Fix: debug output only with debugSymbolsAndLogs (2026-10-07)
- **Problem:** the log filled with `[D] updateAllPulseData done.` about every 11 ms (8399
lines in 6 s). `AudioTabController::eventLoopTick()` reads the microphone's mute state on
every tick, and every read calls `updateAllPulseData()`
(`AudioManagerPulse_internal.h:312`).
- **Cause (regression in `qt6`):** on `master` the message was easylogging's `LOG(DEBUG)`,
and the Debug level was disabled unless built with `debugSymbolsAndLogs`
(`ENABLE_DEBUG_LOGGING`). `qt6` switched to `qDebug()` and lost that; the constant
`debugLoggingEnabled` in `setup.cpp` was left unused, so all 46 `qDebug()` calls were
always active.
- **Fix (`setup.cpp`, `setUpLogging()`):** without `ENABLE_DEBUG_LOGGING`,
`QLoggingCategory::setFilterRules("*.debug=false")`. `QT_LOGGING_RULES="*.debug=true"`
takes precedence and turns debug output on again without a rebuild.
- **Result:** cross build and package succeed, `Qt_6_PRIVATE_API` without `QVulkan*`: 0.
- **Not tested yet:** on the Frame (log without `[D]` lines; with `QT_LOGGING_RULES` with
them).
- **Open for R6/T10:** each tick makes three synchronous PulseAudio queries (server info,
all sinks, all sources) for the mute state. Not changed now.
### Layout cut off on the Frame; diagnosis tools (2026-10-08)
- **Observed (headset photo):** the dashboard tab appears, navigation and laser work. The
fixed 1200×800 layout overflows: about the bottom quarter is cut off (left: Statistics,
Bindings, Settings; right: Microphone half, Push-to-Talk). Navigation icons overlap their
text. The right edge is complete.
- **Ruled out by experiment on the Frame:** `QT_FONT_DPI=96` (verified in the process
environment): no change. `QT_QUICK_CONTROLS_STYLE=Basic` (Qt 5's "Default" style; `qt6`
sets no style, so Qt 6 uses Fusion on Linux; only `mainwidget.qml` imports Material): no
visible change. `QT_QUICK_CONTROLS_MATERIAL_VARIANT=Dense`: slightly better, still cut
off.
- **Facts:** Frame X screen 1920×1080, `Xft.dpi: 144`; default font Noto Sans
(`fc-match sans-serif`). Template renders 1600×1000.
- **Also seen:** after removing the Steam license, Steam's `steamapps.vrmanifest` still
lists `steam.overlay.1009850` (not yet regenerated); the debug-output fix works (0 `[D]`
lines).
- **Diagnosis tools (DESIGN.md section 10):** font/screen metrics in the log, option
`--snapshot <file>`, `run.sh` passes further options.
- **Result:** cross build and package succeed, `Qt_6_PRIVATE_API` without `QVulkan*`: 0,
generated `run.sh` checked with `sh -n`. No new warnings in the changed lines.
- **Not tested yet:** on the Frame (log line "Font: …", PNG written).
### Fix: Quick Controls style Fusion (2026-10-08)
- **Measurements on the Frame** (snapshots of the root page, OpenGL):
- Log: `Font: "Noto Sans" 9 pt = 12 px (requested "Sans Serif" 9 pt); 20 pt = 27 px;
screen "gamescope" logical DPI 96 physical DPI 63.972 device pixel ratio 1`. Fonts, DPI
and scaling are as intended.
- The PNG itself is cut off: OVRAS renders the content too large; SteamVR doesn't crop.
- Navigation buttons: as `qt6` is, 65 px (pitch 75) = Material. With
`QT_QUICK_CONTROLS_STYLE=Basic` (verified in the process environment): identical, the
import overrides it. Test build without the Material import (not committed): 45 px
(pitch 55) = Fusion, **everything fits**. Same build with Basic: 49 px (pitch 59), the
last button is cut off.
- **Fix (DESIGN.md section 11):** `mainwidget.qml` imports `QtQuick.Controls` again;
`main.cpp` sets `QQuickStyle::setStyle("Fusion")`.
- **Result:** cross build and package succeed, `Qt_6_PRIVATE_API` without `QVulkan*`: 0.
- **Frame test of the committed build (`ff4ea8f`, 2026-10-08, OpenGL):** root page fits
completely (12 navigation buttons 45 px, pitch 55, Microphone, Push-to-Talk, version
visible), same as the test build; "Chaperone Settings" fits completely too. No `QT_`
variables set, so Fusion comes from the code. Log: no new errors.
- **Not tested yet:** the other pages, Vulkan.
- **Seen, not changed:** on "Chaperone" and "Space Fix" the icon still overlaps the text
slightly (`MyPushButton`: icon as wide as the button is high, text only indented by
spaces).
### Test checklist on the Frame (step 5)
Deploy: `build_scripts/steamframe/deploy-to-frame.sh <user>@<frame>` (SteamVR running).
| # | Check | Criterion | Result |
|---|---|---|---|
| T1 | `install.sh`: registration works (log: "Manifest reinstalled", via Background) | R2 | open |
| T2 | Tab appears in the dashboard, UI correct (OpenGL) | R2, R3 | open |
| T1 | `install.sh`: registration works (log: "Manifest reinstalled", via Background) | R2 | partly: "Manifest reinstalled" after three fixes (display, OpenVR library, Steam's entry), but SteamVR keeps Steam's entry for the key (see "Finding: app key …") |
| T2 | Tab appears in the dashboard, UI correct (OpenGL) | R2, R3 | partly: tab appears; root page and "Chaperone Settings" correct after the style fix; other pages not checked |
| T3 | `./run.sh vulkan`: UI correct | R3 | open |
| T4 | Laser right way up, click, scrolling on several pages (both APIs) | R4 | open |
| T4 | Laser right way up, click, scrolling on several pages (both APIs) | R4 | partly: laser and navigation work (OpenGL, user's observation); scrolling and Vulkan not checked |
| T5 | Text field (e.g. new profile name): SteamVR keyboard opens, input arrives | R4 | open |
| T6 | An OVRAS action arrives (e.g. push-to-talk, log/notification) | R4 | open |
| T7 | Play area before/after start unchanged; log: "CommitWorkingCopy skipped" | R7 | open |
@@ -186,3 +329,34 @@ Deploy: `build_scripts/steamframe/deploy-to-frame.sh <user>@<frame>` (SteamVR ru
| T9 | Auto-start after a SteamVR restart / reboot | R2 | open |
| T10 | `pidstat -p $(pgrep -f AdvancedSettings_app) 10 6`: dashboard closed / OVRAS visible / other tab visible | R6 | open |
| T11 | `uninstall.sh` unregisters it | R2 | open |
### Wrap-up (2026-10-07)
- **Remotes:** `origin` = `git@git.matzman.net:SteamFrame/OpenVR-AdvancedSettings-frame.git`
(branches `steamframe`, `qt6`, `master`); `upstream` = GitHub OVRAS.
- **Handover documentation:** `docs/steamframe/README.md` (entry point), `CLAUDE.md`
(working rules, conventions), `docs/steamframe/OVRAS-FEATURES.md` (feature analysis).
- **Still open in iteration 1:** phase 4 (tests on the Frame, T1–T11) and phase 5
(assessment).
### Wrap-up (2026-10-08)
- **First tests on the Frame (2026-10-07/08):** six fixes, each its own commit:
`537539a` manifest commands without a display, `317db68` OpenVR library
`bin/linuxarm64`, `ec82b5f` registration when Steam lists the key, `bbb3d1b` no debug
output by default, `d664f86` layout diagnosis (font log, `--snapshot`), `ff4ea8f` Fusion
style instead of Material.
- **State on the Frame:** package of `ff4ea8f` deployed, running with
`--snapshot /tmp/ovras.png` (OpenGL).
- **Open, in this order:**
1. Page-by-page layout check with `--snapshot` (SteamVR, Offsets, Motion, Rotation,
Space Fix, Audio, Video, Utilities, Statistics, Settings, Chaperone sub-pages). Known:
icon slightly overlaps the text on "Chaperone" and "Space Fix".
2. Rest of T1–T11, especially T3 (Vulkan), T5 (keyboard), T8 (quit), T9 (auto-start),
T10 (CPU), T11 (uninstall).
3. App key: Steam's entry still wins (see README "Known open points"); auto-launch is
switched on for Steam's key (`~/.config/openvr/config/vrappconfig/steam.overlay.1009850.vrappconfig`,
created by our `install.sh`). Decide on an own app key (option 2).
4. R6: three synchronous PulseAudio queries per tick for the microphone mute state.
- **Not in the repository:** the headset photo `frame-headset-2026-10-07T22-00-14-104Z.png`
in the working tree (untracked).
+190
View File
@@ -0,0 +1,190 @@
# OVRAS for the Steam Frame: start here
This fork ports **OpenVR Advanced Settings (OVRAS)** so that it runs **natively on the Steam
Frame** (SteamOS, aarch64, standalone) as a SteamVR dashboard tab. This file is the entry
point for continuing the work on any computer.
| Document | Content |
|---|---|
| [REQUIREMENTS.md](REQUIREMENTS.md) | Goal, decisions (with dates), requirements per iteration, feature classification |
| [DESIGN.md](DESIGN.md) | Design of iteration 1 (approved): what changes where, and why |
| [PROGRESS.md](PROGRESS.md) | What was done per step, test results, **test checklist for the Frame** |
| [OVRAS-FEATURES.md](OVRAS-FEATURES.md) | OVRAS's features and how they are implemented; issues found while reading |
| [`/CLAUDE.md`](../../CLAUDE.md) | Working rules and conventions (for people and for Claude Code) |
## Status (2026-10-08)
- **Iteration 1 ("native start on the Frame"): implemented; testing on the Frame has
started** (phase 4). OVRAS starts natively on the Frame, the dashboard tab appears, laser
and navigation work, the root page and "Chaperone Settings" fit (OpenGL). Five problems
found on the first deployment were fixed (details in [PROGRESS.md](PROGRESS.md)):
manifest commands without a display, wrong aarch64 OpenVR library, app key already
registered by Steam, debug log flood, layout cut off (Material style).
- Tested without a headset:
- Linux x86_64 build.
- aarch64 cross build: no private Qt API except `QVulkan*`, Qt 6.8, GLIBC ≤ 2.38.
- Render tests with OpenGL and Vulkan; Vulkan with validation layers and no messages.
- Release package (1.6 MB).
- **Next step:** continue on the Frame: page-by-page layout check with `--snapshot`, then the
rest of the test checklist T1–T11 ([PROGRESS.md](PROGRESS.md), "Wrap-up (2026-10-08)"
lists what is open), then assessment.
- **After that:** iteration 2. R5 hides the features that are native in SteamVR or don't
work on the Frame. Then the remaining features are checked one by one.
## Background in brief
- **Proton:** The Windows build of OVRAS runs on the Frame through Proton. Its laser y-axis is
mirrored there, which makes it unusable, and it is inefficient (x86 emulation + Wine).
- **Basis:** the fork is based on upstream's **`qt6` branch**, the maintainer's Qt 6.8 / CMake
port, still in progress. `master` is fully merged into `qt6`.
- **Goal: a minimal change set against `qt6`**, so changes can later go upstream as pull
requests.
- **Template:** the platform knowledge comes from the author's **vrdashboard template**,
verified on the Frame. Its baseline is commit `4a5b090` in
`git@git.matzman.net:SteamFrame/FrameAssistant.git`; its `docs/STEAM-FRAME.md` and
`CLAUDE.md` describe the Frame. From it come the render backends (`src/render/`) and the
build/package scripts (`build_scripts/steamframe/`).
- **Hard rule: only public Qt API.** The binary must run on the Frame's system Qt (6.8.0).
QRhi (private) was therefore replaced and QML is compiled to bytecode only.
## Repository, branches, remotes
| Branch | Meaning |
|---|---|
| `steamframe` | **The work branch.** Based on `qt6` (`2d4db7e`). |
| `qt6` | Mirror of upstream's `qt6` at `2d4db7e` (reference / diff base) |
| `master` | Mirror of upstream's `master` at `31df8b4` (5.8.17) |
Commits on `steamframe` (one per step; use for later pull requests):
```
aac3edf Steam Frame port: requirements and design of iteration 1
266a2f9 Fix the Linux build of the qt6 branch, compile QML to bytecode only
423ea57 Add a Steam Frame build option; no chaperone commit at start; timer-based tick
4826686 Render through public Qt API: OpenGL and Vulkan backends, --graphics-api
b22c35a Manifest for Linux; connect as Background app where Utility is refused
bde675f Steam Frame build, packaging and deployment scripts; render test
3ed686d Read the version from build_scripts/compile_version_string.txt
```
Then the handover documentation (this file, `CLAUDE.md`, `OVRAS-FEATURES.md`).
Fork-only files that don't belong in an upstream pull request: `docs/steamframe/`,
`CLAUDE.md`.
## Setting up on another computer
```sh
git clone -b steamframe git@git.matzman.net:SteamFrame/OpenVR-AdvancedSettings-frame.git
cd OpenVR-AdvancedSettings-frame
git remote add upstream https://github.com/OpenVR-Advanced-Settings/OpenVR-AdvancedSettings.git
git fetch upstream # to see whether upstream's qt6 has moved on
```
**Requirements:**
- **For the Frame build and the tests:** Docker or Podman (containers based on Debian
trixie; they bring Qt 6.8 and everything else).
- **For a local Linux build (optional):**
- CMake ≥ 3.20, Ninja, GCC/Clang (C++20).
- Qt ≥ 6.8 with Quick, QuickControls2, QuickDialogs2, Multimedia, WebSockets, Svg, DBus;
`Test` for the tests.
- Vulkan headers.
- libpulse, libX11, libXt, libXtst.
- Tested on Arch with Qt 6.11.
- **For deployment:** SSH access to the Frame, `rsync`.
## Commands
```sh
# Local build (x86_64), optionally with tests
cmake -S . -B build/x86 -G Ninja -DCMAKE_BUILD_TYPE=Debug -DOVRAS_BUILD_TESTS=ON
cmake --build build/x86
ctest --test-dir build/x86 # render tests: OpenGL and Vulkan
# -DOVRAS_STEAM_FRAME=ON: Frame capabilities/defaults (src/utils/platform_capabilities.h)
# Frame (aarch64) build in the container → build/steamframe-cross/Release/
build_scripts/steamframe/container-build.sh cross
build_scripts/steamframe/container-build.sh cross shell # shell in the container
# Tests in the container, Vulkan with validation layers (must print "No Vulkan validation messages")
build_scripts/steamframe/container-test.sh
VK_DRIVER=lvp build_scripts/steamframe/container-test.sh # software Vulkan
# Package → dist/AdvancedSettings-<version>-linux-arm64(.tar.gz)
build_scripts/steamframe/package-release.sh # builds first; --no-build packages only
# Copy to the Frame and install (SteamVR must be running there)
build_scripts/steamframe/deploy-to-frame.sh <user>@<frame>
```
**Check for private Qt API** (R1). Only `QVulkan*` may appear:
```sh
docker run --rm -v "$PWD:$PWD" -w "$PWD" ovras-steamframe-cross-arm64 \
aarch64-linux-gnu-objdump -T build/steamframe-cross/Release/AdvancedSettings_app \
| grep Qt_6_PRIVATE_API | grep -v QVulkan
```
**On the Frame** (in `~/AdvancedSettings/`):
| Command | Effect |
|---|---|
| `./install.sh` | Registers with SteamVR (auto-start) and starts |
| `./run.sh opengl\|vulkan [options]` | Restarts with that graphics API; further options go to the program |
| `./uninstall.sh` | Unregisters |
| `AdvancedSettings.log` | Log (in the working directory, i.e. this folder) |
Program options (new): `--graphics-api opengl|vulkan` (default `opengl`); `--snapshot <file>`
(diagnosis: writes the rendered overlay as PNG at most once per second while visible). Existing ones
include `--force-install-manifest` and `--force-remove-manifest`; see `--help`.
## Steam Frame facts the code relies on
Verified with the template on SteamOS holo 0.4.3; details in FrameAssistant's
`docs/STEAM-FRAME.md`.
- **Runtime:**
- The system Qt is 6.8.0, with Multimedia, WebSockets, DBus, Svg and Widgets.
- libpulse is present.
- GLIBC is 2.39.
- **Manifest and SteamVR:**
- The manifest needs `binary_path_linux_arm`; entries without it are skipped silently.
- SteamVR refuses `VRApplication_Utility`. Manifest commands therefore fall back to
`Background`, which needs a running SteamVR. (2026-10-07: with OVRAS, SteamVR accepted
`Utility`; the fallback stays.)
- Steam's generated `~/.local/share/Steam/config/steamapps.vrmanifest` lists every VR app
the user owns, installed or not, with a `steam://` URL. For a key listed there, SteamVR
keeps using Steam's entry even after our manifest is added (seen 2026-10-07).
- Use the OpenVR SDK's `bin/linuxarm64/libopenvr_api.so`; the SDK's `lib/linuxarm64` one
looks for the path registry in `/data/work` and never finds SteamVR.
- `VR_Shutdown` must run before the graphics device is destroyed (crash with Vulkan
otherwise).
- **Rendering and input:**
- OpenGL runs through zink (no native GL driver); Vulkan is about 1.9× faster per frame.
- SteamVR reports overlay mouse coordinates with y from the bottom.
- **Processes and display:**
- Over SSH there is no display: set `DISPLAY=:0` (the scripts do). Commands that draw
nothing run with `QT_QPA_PLATFORM=offscreen`.
- X screen 1920×1080 with `Xft.dpi: 144`, yet Qt reports logical DPI 96 and device pixel
ratio 1 for OVRAS; default font Noto Sans (20 pt = 27 px).
- Process names are cut to 15 characters: find instances by command line.
- An executable must not be named like a SteamVR process (e.g. `vrdashboard`).
- **Private package repositories:** the Frame's `/etc/pacman.conf` points to private Valve
repositories marked "DO_NOT_SHARE". Never put those URLs into the repository.
## Known open points and risks
- **SteamVR keyboard:** text fields may not get focus. The template had to send the offscreen
window an explicit `FocusIn`; `qt6` doesn't (T5).
- **App key taken by Steam's entry:** whoever owns OVRAS on Steam has
`steam.overlay.1009850` in Steam's `steamapps.vrmanifest`, and SteamVR keeps using that
entry (auto-start would launch the Steam version). On the author's Frame it is still
listed after removing the license from the account. Possible fix: an own app key for the
native build (option 2, DESIGN.md section 2a; needs its own design).
- **Unverified on the Frame:** QML modules Dialogs and Multimedia (Controls, Layouts and
the binary's libraries load, since OVRAS runs).
- **Remaining play-area commit:** "auto-apply chaperone" (opt-in, default off) still commits
at start-up; this belongs to R5.
- **Windows:** the Windows build is broken on `steamframe`. There is no D3D11 render path
anymore (decision D1: evaluated later, together with upstream).
- **Version:** OVRAS's version comes from `build_scripts/compile_version_string.txt`.
+11
View File
@@ -3,6 +3,7 @@
#include "openvr/ovr_settings_wrapper.h"
#include "render/RenderBackend.h"
#include <QQuickStyle>
#include <QtLogging>
#include <QtDebug>
#include <memory>
@@ -80,6 +81,15 @@ int main( int argc, char* argv[] )
}
QQuickWindow::setGraphicsApi( renderBackend->graphicsApi() );
// The QML layout has a fixed size (1200x800) and was made for Qt 5's
// default style. Without an explicit style, Qt 6 picks one per platform
// (Linux: Fusion, Windows: Windows), so the sizes would differ by
// platform. Fusion was measured on the Steam Frame: the root page fits
// (navigation buttons 45 px; Basic, Qt 5's style renamed: 49 px, the last
// button is cut off; Material: 65 px, a quarter is cut off). Must be set
// before any QML is loaded.
QQuickStyle::setStyle( "Fusion" );
try
{
QQmlEngine qmlEngine;
@@ -97,6 +107,7 @@ int main( int argc, char* argv[] )
commandLineArgs.forceNoSound,
qmlEngine,
std::move( renderBackend ) ) );
controller->setSnapshotPath( commandLineArgs.snapshotPath );
//const auto url
// = QUrl( ":/qt/qml/AdvancedSettings/src/res/qml/common/mainwidget.qml" );
QQmlComponent component( &qmlEngine, ":/qt/qml/AdvancedSettings/src/res/qml/common/mainwidget.qml" );
+34
View File
@@ -16,6 +16,9 @@
#include <QCursor>
#include <QProcess>
#include <QMessageBox>
#include <QFile>
#include <QFontInfo>
#include <QScreen>
#include <QtLogging>
#include <QtDebug>
#include <iostream>
@@ -456,6 +459,20 @@ void OverlayController::SetWidget( QQuickItem* quickItem,
static_cast<int>( quickItem->width() ),
static_cast<int>( quickItem->height() ) );
// Font and screen metrics that decide how big the fixed-size
// (1200x800) QML layout comes out, e.g. on the Steam Frame (Noto Sans,
// Xft.dpi 144) compared to Windows (Segoe UI, 96 DPI).
const QFont font = QGuiApplication::font();
const QFontInfo fontInfo( font );
qInfo() << "Font:" << fontInfo.family() << fontInfo.pointSizeF()
<< "pt =" << fontInfo.pixelSize() << "px (requested"
<< font.family() << font.pointSizeF() << "pt); 20 pt ="
<< QFontInfo( QFont( font.family(), 20 ) ).pixelSize()
<< "px; screen" << m_window.screen()->name() << "logical DPI"
<< m_window.screen()->logicalDotsPerInch() << "physical DPI"
<< m_window.screen()->physicalDotsPerInch()
<< "device pixel ratio" << m_window.effectiveDevicePixelRatio();
vr::HmdVector2_t vecWindowSize
= { static_cast<float>( quickItem->width() ),
static_cast<float>( quickItem->height() ) };
@@ -572,6 +589,23 @@ void OverlayController::renderOverlay()
m_renderControl.endFrame();
m_renderBackend->finishFrame();
// --snapshot: writes the rendered overlay to a PNG at most once per
// second while it is visible, so the layout can be checked without a
// headset photo. Written to a temporary file and renamed, so readers
// never see a partial file.
if ( !m_snapshotPath.isEmpty()
&& ( !m_snapshotTimer.isValid()
|| m_snapshotTimer.elapsed() >= 1000 ) )
{
m_snapshotTimer.start();
const QString tmp = m_snapshotPath + ".tmp";
if ( m_renderBackend->readback().save( tmp, "PNG" ) )
{
QFile::remove( m_snapshotPath );
QFile::rename( tmp, m_snapshotPath );
}
}
vr::VRTextureBounds_t bounds = m_renderBackend->vrTextureBounds();
vr::VROverlay()->SetOverlayTextureBounds( m_ulOverlayHandle, &bounds );
vr::Texture_t texture = m_renderBackend->vrTexture();
+10
View File
@@ -23,6 +23,7 @@
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QQuickRenderTarget>
#include <QElapsedTimer>
#include <memory>
#include <optional>
#include "render/RenderBackend.h"
@@ -117,6 +118,9 @@ private:
QQuickWindow m_window{ &m_renderControl };
std::optional<vr::ETextureType> m_cached_vr_texture_type;
// --snapshot (see renderOverlay()): target file and time of the last one.
QString m_snapshotPath;
QElapsedTimer m_snapshotTimer;
// Set by Shutdown(), which must run only once (see there).
bool m_shutdownDone = false;
@@ -210,6 +214,12 @@ public:
std::unique_ptr<RenderBackend> renderBackend );
virtual ~OverlayController();
// Diagnosis (--snapshot): see renderOverlay(). Empty: off.
void setSnapshotPath( const QString& path )
{
m_snapshotPath = path;
}
void Shutdown();
Q_INVOKABLE void exitApp();
Q_INVOKABLE void setAutoChapProfileName( int index );
+5 -1
View File
@@ -1,5 +1,9 @@
import QtQuick
import QtQuick.Controls.Material
// Not QtQuick.Controls.Material: importing a style module fixes that style
// for all controls and overrides the one set in main.cpp. Material's
// controls are too tall for the fixed-size layout (cut off on the Steam
// Frame).
import QtQuick.Controls
import QtQuick.Layouts
import ".."
import "../utilities_page"
+85 -7
View File
@@ -1,6 +1,7 @@
#include "setup.h"
#include <QtLogging>
#include <QtDebug>
#include <QLoggingCategory>
#ifdef ENABLE_DEBUG_LOGGING
constexpr auto debugLoggingEnabled = true;
#else
@@ -64,6 +65,10 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application )
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 );
@@ -93,13 +98,16 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application )
const QString graphicsApiValue = parser.value( graphicsApi );
qInfo() << "Graphics API:" << graphicsApiValue;
const CommandLineOptions commandLineArgs{ desktopModeEnabled,
forceNoSoundEnabled,
forceNoManifestEnabled,
forceInstallManifestEnabled,
forceRemoveManifestEnabled,
resetSettingsEnabled,
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.";
@@ -191,6 +199,29 @@ void reinstallApplicationManifest( const std::string manifestPath )
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(
@@ -251,6 +282,43 @@ void forceRemoveApplicationManifest()
}
}
// 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.
@@ -298,6 +366,7 @@ void forceRemoveApplicationManifest()
reinstallApplicationManifest( *manifestPath );
enableApplicationAutostart();
qInfo() << "Manifest reinstalled.";
logApplicationEntry();
}
else if ( removeManifest )
{
@@ -321,4 +390,13 @@ 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" );
}
}
+10
View File
@@ -30,6 +30,8 @@ struct CommandLineOptions
const bool resetSettings = false;
// Value of --graphics-api; checked in main() with parseGraphicsApi().
const QString graphicsApi = "opengl";
// Value of --snapshot; empty: no snapshots.
const QString snapshotPath;
};
// Manages the programs control flow and main settings.
@@ -98,6 +100,13 @@ constexpr auto k_graphicsApiDescription
= "Graphics API for rendering the overlay: opengl (default) or vulkan.";
constexpr auto k_graphicsApiValueName = "api";
constexpr auto k_graphicsApiDefault = "opengl";
// Diagnosis: writes the rendered overlay to a PNG while it is visible, to check
// the layout without a headset photo (OverlayController::renderOverlay()).
constexpr auto k_snapshot = "snapshot";
constexpr auto k_snapshotDescription
= "Writes the rendered overlay to <file> (PNG) at most once per second "
"while it is visible.";
constexpr auto k_snapshotValueName = "file";
constexpr auto k_resetSettings = "reset-steamvr-settings";
constexpr auto k_resetSettingsDescription
@@ -117,6 +126,7 @@ void installApplicationManifest( const std::string manifestPath );
void removeApplicationManifest( const std::string manifestPath );
void reinstallApplicationManifest( const std::string manifestPath );
void forceRemoveApplicationManifest();
void logApplicationEntry();
[[noreturn]] void handleManifests( const bool installManifest,
const bool removeManifest );
Binary file not shown.