From aac3edf7e3cf89999c513696739ff113c96a374a Mon Sep 17 00:00:00 2001 From: Matthias Janetschek Date: Wed, 7 Oct 2026 13:29:53 +0200 Subject: [PATCH] Steam Frame port: requirements and design of iteration 1 Internal fork based on upstream's qt6 branch (2d4db7e). Goal: OVRAS runs natively on the Steam Frame (SteamOS, aarch64) with a minimal change set against qt6 and only public Qt API. Co-Authored-By: Claude Opus 5.5 --- docs/steamframe/DESIGN.md | 259 ++++++++++++++++++++++++++++++++ docs/steamframe/REQUIREMENTS.md | 77 ++++++++++ 2 files changed, 336 insertions(+) create mode 100644 docs/steamframe/DESIGN.md create mode 100644 docs/steamframe/REQUIREMENTS.md diff --git a/docs/steamframe/DESIGN.md b/docs/steamframe/DESIGN.md new file mode 100644 index 0000000..e992183 --- /dev/null +++ b/docs/steamframe/DESIGN.md @@ -0,0 +1,259 @@ +# Steam Frame port: design + +Design of iteration 1 (R1–R4, R6, R7 in [REQUIREMENTS.md](REQUIREMENTS.md)). +Status: **approved 2026-10-07**. + +Principle: **minimal change set against `qt6`**. Existing OVRAS code is changed only where +the Frame or the "public API only" rule requires it. New parts come in as self-contained +files. Every change gets comments that explain what it does and why. + +## Starting point + +`qt6` (`2d4db7e`) doesn't build on Linux. Checked in a scratch copy with Qt 6.11; with the +fixes from section 1 it builds: + +| Error | Place | +|---|---| +| `Qt::GuiPrivate` not found (Qt ≥ 6.10 requires it in `find_package`) | `CMakeLists.txt` | +| `d3d11.h` and `QRhi::D3D11` used on Linux too | `overlaycontroller.cpp` | +| 8 × easylogging `LOG(...)` left over | `overlaycontroller.cpp` (6), `ChaperoneTabController.cpp`, `MoveCenterTabController.cpp` | +| No link path to `libopenvr_api.so` on Linux | `CMakeLists.txt` | +| `libXtst` not linked (`XTestFakeKeyEvent`) | `CMakeLists.txt` | + +Private Qt symbols in the binary built that way (`objdump -T`, `Qt_6_PRIVATE_API`): +- `QQmlPrivate::AOTCompiledContext` (2), from compiling QML to C++. +- 7 × `QRhi…`, from the rendering in `OverlayController`. + +## Changes at a glance + +| # | Area | Files | Kind | +|---|---|---|---| +| 1 | Build fixes, bytecode-only QML, OpenVR per architecture | `CMakeLists.txt`, 3 × `.cpp` | Change | +| 2 | Manifest for Linux; installation tries `Utility`, then `Background` | `manifest.vrmanifest`, `setup.cpp` | Change | +| 3 | Rendering through the template's backends instead of QRhi | `src/render/*` (new), `overlaycontroller.*`, `main.cpp` | New + change | +| 4 | Command-line option `--graphics-api` | `setup.*`, `main.cpp` | Change | +| 5 | Mouse y the right way up | `overlaycontroller.cpp` | Change (−4 lines) | +| 6 | Tick: the timer runs at the tick interval instead of counting 1 ms ticks | `overlaycontroller.cpp` | Change | +| 7 | Platform option `OVRAS_STEAM_FRAME` with capability constants | `CMakeLists.txt`, `src/utils/platform_capabilities.h` (new) | New | +| 8 | No play-area commit at start (R7) | `MoveCenterTabController.cpp` | Change | +| 9 | Frame build, packaging, deployment | `build_scripts/steamframe/*` (new) | New | + +## 1. Build (R1) + +- **The five errors in the table above:** + - `GuiPrivate` is not needed after section 3, so it is removed rather than added. + - `d3d11.h` goes along with QRhi (section 3). + - `LOG(INFO|WARNING|ERROR)` becomes `qInfo()` / `qWarning()` / `qCritical()`. + - OpenVR link path and `Xtst` are added. +- **QML as bytecode only:** `qt_add_qml_module(... )` gets the bytecode-only flag + (`QT_QMLCACHEGEN_ARGUMENTS --only-bytecode`, as the template's + `VRDASH_QML_BYTECODE_ONLY`). This removes `QQmlPrivate::AOTCompiledContext`. Code compiled + from QML to C++ only fits the exact Qt version it was built with. +- **Qt modules:** + - `Core5Compat` is removed: it is unused, and it isn't on the Frame. + - `GuiPrivate` is removed (section 3). + - `Widgets`, `QuickDialogs2`, `Svg`, `Multimedia` and `WebSockets` stay. They are present + on the Frame (SteamOS 0.4.3, system Qt 6.8.0). +- **OpenVR:** the repository's `third-party/openvr`. The library comes from `lib/linux64` or + `lib/linuxarm64` depending on `CMAKE_SYSTEM_PROCESSOR`. It is copied next to the binary, + and the RPATH is `$ORIGIN`. +- **Check:** `objdump -T AdvancedSettings_app | grep Qt_6_PRIVATE_API` may only show `QVulkan*`. + Qt's Vulkan wrappers carry that tag but are generated from the Vulkan specification. + +## 2. Manifest (R2) + +- **`manifest.vrmanifest`:** gets `binary_path_linux` and `binary_path_linux_arm`, both set + to `AdvancedSettings_app`. + - On Linux, `qt6` names the binary that way (QTBUG-98568). + - The Frame only reads `binary_path_linux_arm`; entries without it are skipped. +- **`manifest::handleManifests` (`setup.cpp:255`):** + 1. First `VR_Init(Utility)`, as before. + 2. On `VRInitError_Init_NotAvailableToUtilityApps`, `VR_Init(Background)`. + - Same behavior on every platform. + - `Background` needs a running SteamVR and doesn't start it. `Utility` stays first, so + that Windows' installer still works without SteamVR running. + - It calls `VR_Init` directly, without `initializeOpenVR`'s retry loop. That loop waits + and retries on every error, including the one we expect here. + +## 3. Rendering through public API (R1, R3) + +**From the template** (`4a5b090`, verified on the Frame), adopted largely unchanged: +- `RenderBackend.h/.cpp`: the interface, `RenderInterop`, the factory +- `OpenGLRenderBackend.*` +- `VulkanRenderBackend.*` + +They depend only on `openvr.h` and public Qt API: +- textures via `QQuickRenderTarget::fromOpenGLTexture` / `fromVulkanImage` +- Qt's device via `QSGRendererInterface::getResource` + +The Vulkan backend already handles what `qt6`'s Vulkan path lacks: +- the instance/device extensions SteamVR requires +- the HMD's GPU (`GetOutputDevice`) +- the correct `VkFormat` +- the layout transition to `TRANSFER_SRC_OPTIMAL` after each frame + +**Changes in OVRAS:** +- **`main.cpp`**, in this order: + 1. parse the command line + 2. `initializeOpenVR(Overlay)` + 3. `createRenderBackend(api)` and `initialize(renderInterop)` + 4. `QQuickWindow::setGraphicsApi(backend->graphicsApi())` + 5. construct `OverlayController(…, backend)` + + `setGraphicsApi` must come before any `QQuickWindow` exists. `OverlayController::m_window` + is a member, so the API is fixed before the controller is constructed. +- **`openvr_init`:** gets `renderInterop()`. Its three functions query + `GetVulkanInstanceExtensionsRequired`, `GetVulkanDeviceExtensionsRequired` and + `GetOutputDevice`; the code is copied from the template's `VrRuntime::renderInterop()`. +- **`OverlayController::SetWidget`:** the QRhi block (`newTexture`, `newTextureRenderTarget`, + `QVulkanInstance` creation) is replaced by: + 1. `backend->configureWindow(&m_window)` + 2. `m_renderControl.initialize()` + 3. `backend->attachWindow(&m_window)` + 4. `m_window.setRenderTarget(backend->createRenderTarget(size))` +- **`OverlayController::renderOverlay`:** the frame is wrapped in + `backend->beginFrame()` / `finishFrame()`. Submission is + `SetOverlayTexture(backend->vrTexture())` plus + `SetOverlayTextureBounds(backend->vrTextureBounds())`, while `makeCurrent()` is active (the + OpenGL submission requires it). `SetOverlayFromQRhiTexture` and the QRhi members are + removed. +- **Frame-rate cap:** the render delay timer goes from 5 to 16 ms. A frame starts at most + 16 ms after a change request, which is about 60 fps. +- **Shutdown:** `exitApp()` ends with `exit()`, so destructors don't run. Before that it calls + `vr::VR_Shutdown()`. + - Why: OpenVR has to shut down while the graphics device still exists. Otherwise its client + frees objects on a dead `VkDevice`, which crashed on the Frame with Vulkan. + - Today OVRAS never calls `VR_Shutdown` in overlay mode. + +## 4. Command-line option `--graphics-api` (R3) + +- **Syntax:** `--graphics-api opengl|vulkan`; the default is `opengl`. + - It goes into `CommandLineOptions` and `returnCommandLineParser` (`setup.*`), next to the + existing options. +- **Unknown value, or Vulkan not compiled in:** error message, exit code `GENERAL_FAILURE`. +- **No automatic fallback.** OpenGL is the default anyway, and an explicit choice shouldn't + be changed silently (as in the template). +- **Vulkan in the build:** compiled in when the Vulkan headers and a Qt with Vulkan support + are present. The cross container has both. + +## 5. Mouse y (R4) + +- `getMousePositionForEvent` already flips `y` everywhere, then flips it back on Linux in + `#ifdef __linux__`. The Linux block goes, so `y` is flipped on every platform. +- Why: SteamVR delivers mouse coordinates in GL convention (y from the bottom), regardless of + platform and graphics API. This was verified with the template on the Frame for OpenGL and + Vulkan. +- The "`TODO temp mouse is negative??`" comment is replaced by this explanation. + +## 6. Tick (R6) + +- **Today:** `m_pumpEventsTimer` fires every 1 ms. + - With "vsync" on, it checks for a new compositor frame. + - With "Disable App Vsync", it counts 1 ms ticks up to `customTickRateMs`. +- **Change:** + - With "Disable App Vsync", the timer runs directly at `customTickRateMs`, as a + `Qt::PreciseTimer`. The 1 ms counting is dropped. Same behavior, about 1/11 of the + wake-ups at 11 ms. + - `setVsyncDisabled` / `setCustomTickRateMs` restart the timer with the new interval. + - The vsync mode stays unchanged. +- **Frame defaults** through the platform option (section 7): `vsyncDisabled = true` and + `customTickRateMs = 11`, about 90 Hz as in the template's event polling. Tick-counted + intervals in OVRAS (designed for ~90 fps) keep their meaning. + - Upstream defaults stay `false` / `20`. + - Both can still be changed in the OVRAS settings. +- **Measurement:** `pidstat -p 10 6` on the Frame, in three states: dashboard closed, + OVRAS tab visible, another tab visible. + +## 7. Platform option and capability constants + +- **CMake option `OVRAS_STEAM_FRAME`:** default `OFF`, `ON` in the Frame build. It sets the + compile definition `OVRAS_STEAM_FRAME=1`. +- **New `src/utils/platform_capabilities.h`:** the only place that evaluates the definition. + - Everything else asks for named constants and never for the platform. + - Each constant is commented with why it differs on the Frame. + - Iteration 1: + +```cpp +namespace platform +{ +// IVRChaperoneSetup::CommitWorkingCopy changes the live play area. On the +// Steam Frame (inside-out tracking) this shifts the boundary. +constexpr bool k_chaperoneCommitSupported = !OVRAS_STEAM_FRAME; +// Fixed-interval tick instead of 1 ms vsync polling (fewer wake-ups). +constexpr bool k_defaultVsyncDisabled = OVRAS_STEAM_FRAME; +constexpr int k_defaultCustomTickRateMs = OVRAS_STEAM_FRAME ? 11 : 20; +} // namespace platform +``` + +- The setting defaults in `settings_controller.h` use the two default constants. + +## 8. No play-area commit at start (R7) + +**Cause** (`MoveCenterTabController.cpp`): +- `zeroOffsets()` → `updateChaperoneResetData()` → `CommitWorkingCopy(Live)` (`:1185`). +- At init it creates an autosave profile (`:1109`), but only when + `GetCalibrationState() != 200`. +- Otherwise `m_pendingZeroOffsets` stays set, and the commit repeats on every tick. + +**Change:** +- With `!platform::k_chaperoneCommitSupported`: + - `updateChaperoneResetData()` still reads the working copy, but skips + `CommitWorkingCopy` / `RevertWorkingCopy`. + - The same applies to `applyChaperoneResetData()`. + - The init autosave profile is skipped. + - Each skip is logged once. +- What the playspace features do when the user operates them is R5 (iteration 2). + +## 9. Frame build, packaging, deployment (R1, R2) + +New files under `build_scripts/steamframe/`, adapted from the template. They change no +existing code: + +| File | Purpose | +|---|---| +| `cross-arm64.Dockerfile` | Debian container with Qt 6.8 (aarch64 multiarch), CMake, Vulkan headers, X11/Xtst, libpulse | +| `aarch64-linux-gnu.cmake` | Cross-compile toolchain | +| `container-build.sh` | Configures the build (`OVRAS_STEAM_FRAME=ON`) and builds it in the container | +| `package-release.sh` | Folder with the binary, `libopenvr_api.so`, `res/`, manifests, `install.sh` / `uninstall.sh` → `.tar.gz` | +| `deploy-to-frame.sh` | Copies the package to the Frame over SSH and runs `install.sh` | +| `install.sh` / `uninstall.sh` | `--force-install-manifest` / `--force-remove-manifest` (section 2) and start / stop | + +- Packages use the **system Qt**: no Qt is bundled, because of section 3 the binary uses no + private API. +- The template's Qt-bundling fallback is not adopted for now. + +## Tests + +- **x86_64 locally:** the build succeeds; `objdump` check (R1). +- **Vulkan:** `container-test.sh` with validation layers. It reports no messages, and the + render path is checked by a snapshot test (`tst_render` from the template, reduced to the + backends). +- **OVRAS:** the keyboard parser test `test/keyboard_input` is included in CMake if that is + easy; otherwise it is noted as not run. +- **On the Frame:** a checklist per acceptance criterion R1–R4, R6, R7, with both graphics + APIs. Results go to `docs/steamframe/PROGRESS.md`. + +## Implementation order + +1. Build fixes and bytecode-only QML. Linux x86_64 builds with QRhi (baseline). +2. Platform option, R7, tick. +3. Rendering via the template's backends, `--graphics-api`, mouse y, shutdown order. + `objdump` check. +4. Manifest and installation. +5. Frame build scripts, package, deployment, tests on the Frame (OpenGL, then Vulkan). + +## Decisions (formerly open) + +- **D1 – Windows render path: (c).** Windows stays open until the upstream evaluation. The + Windows build is broken on `steamframe` until then (2026-10-07). +- **D2 – Code style: OVRAS style**, including for the template files in `src/render/` + (2026-10-07). + - The repository's `.clang-format` dates from 2018-12-27 (one fix the day after). The whole + code base was reformatted once on 2018-12-28 (`51057c1`). The style hasn't changed since; + `qt6` uses the same `.clang-format`. + - The format check in CI is disabled (`.travis.yml`). Under clang-format 22, 12 of 86 files + in `src/` already deviate today (e.g. `overlaycontroller.cpp` with 138 places, `master` + likewise). + - **Hence: only lines we change get formatted (`git clang-format`), never whole existing + files.** Otherwise the diff would contain unrelated formatting changes. New files are + formatted completely. diff --git a/docs/steamframe/REQUIREMENTS.md b/docs/steamframe/REQUIREMENTS.md new file mode 100644 index 0000000..f99346d --- /dev/null +++ b/docs/steamframe/REQUIREMENTS.md @@ -0,0 +1,77 @@ +# Steam Frame port: requirements + +Goal: OVRAS runs natively on the Steam Frame (SteamOS, aarch64) as a SteamVR dashboard tab. + +- The work happens on the internal branch `steamframe`, based on upstream's `qt6` branch + (commit `2d4db7e`). +- Changes may later go back upstream as pull requests. That decision is made once + something runs. +- **Hence: the change set against `qt6` stays minimal.** + +## Background + +- **Proton:** The Windows build runs on the Frame through Proton. Its laser y-axis is + mirrored, which makes it unusable. Running x86 code under emulation plus Wine is also + inefficient. +- **State of `qt6`:** Upstream's `qt6` branch (Qt 6.8, CMake) doesn't build on Linux yet. + Its rendering uses QRhi, which is private Qt API (`Qt_6_PRIVATE_API`). +- **Template:** The author's vrdashboard template (FrameAssistant commit `4a5b090`) is + verified on the Frame. It serves as a source of knowledge and of individual, + self-contained parts: render backends, build container, packaging scripts. It does not + replace OVRAS's platform layer. + +## Decisions + +| Date | Decision | +|---|---| +| 2026-10-07 | Internal fork, Frame only. Windows and upstream pull requests are evaluated once something runs. | +| 2026-10-07 | Minimal change set against `qt6`, rather than rebuilding OVRAS on the template. | +| 2026-10-07 | Only public Qt API: rendering without QRhi, QML compiled to bytecode only. | +| 2026-10-07 | Manifest installation tries `Utility` first, then `Background`, on every platform. | +| 2026-10-07 | Vulkan is an additional render path, selected by a command-line option. OpenGL stays the default. | +| 2026-10-07 | Frame differences are switched by one build option. In the code they are capability constants, named by capability. | +| 2026-10-07 | OVRAS's code style everywhere, including template code that comes in. Only changed lines get formatted. | +| 2026-10-07 | Windows (render path, build) stays open until the upstream evaluation. | +| 2026-10-07 | Every code change gets comments that explain it fully, but directly and concisely. | +| 2026-10-07 | The app key stays `steam.overlay.1009850`; the Proton build gets uninstalled. | + +## Iteration 1: native start on the Frame + +| # | Requirement | Acceptance criterion | +|---|---|---| +| R1 | Linux-only build: cross-build for aarch64 against the Frame's system Qt 6.8, plus an x86_64 build for development. Only public Qt API. | The container build succeeds. The package contains no Qt. `objdump -T` shows no `Qt_6_PRIVATE_API` symbols except `QVulkan*`. | +| R2 | Runs natively on the Frame as a dashboard tab, with install, auto-start and uninstall. | After `deploy-to-frame.sh` the tab appears and starts by itself after a reboot. Quitting doesn't crash. | +| R3 | Rendering: OpenGL by default, Vulkan as an additional path via a command-line option. Render only on change, capped at 60 fps. | Both paths show every page correctly. With Vulkan, `container-test.sh` reports no validation messages. | +| R4 | Input: laser the right way up, scrolling, text fields via OVRAS's existing SteamVR keyboard handling. OVRAS's SteamVR Input actions work. | Every page can be operated. An OVRAS action (e.g. push-to-talk) arrives. | +| R6 | Throttled tick: a fixed 11 ms timer (as in the template) instead of 1 ms polling. | Idle CPU load is measured and documented. There is no comparison with the Proton build. | +| R7 | OVRAS doesn't change the play area at start-up. Today `MoveCenterTabController::zeroOffsets()` commits the working copy as the live chaperone (`CommitWorkingCopy(Live)` in `updateChaperoneResetData()`) and creates a chaperone autosave profile. On the Frame this shifts the play area at start. | After starting OVRAS on the Frame, the play area is where it was before. The log shows no `CommitWorkingCopy`. | + +R5 (hiding features) was moved to iteration 2; the number stays reserved. + +**Not in iteration 1:** +- hiding or disabling features +- checking features one by one +- replacements for the playspace features +- the template's D-pad navigation +- Windows +- upstream pull requests + +## Iteration 2 (planned) + +| # | Requirement | Acceptance criterion | +|---|---|---| +| R5 | Hide features per the feature classification below. Features that don't work make no `IVRChaperoneSetup` zero-pose calls. | The affected pages/groups are hidden; no chaperone-setup calls appear in the log. | + +Then check features one by one and re-enable them. + +## Feature classification (basis for R5) + +Native in SteamVR: according to SteamVR's release notes up to 2.17 / 2.18 beta and Valve's +Steam Frame guide (researched 2026-10-07). + +| Group | Features | Plan | +|---|---|---| +| Native in SteamVR | Wall height, fade distance, style, center marker, brightness, supersampling, motion smoothing, supersample filtering, audio devices and mirror, performance graph, do not disturb, pairing, tracker battery, recenter, native chaperone import; also Oculus SDK block and restart SteamVR (Windows only), multiple drivers | Hide | +| Only in OVRAS, useful on the Frame | Proximity warnings, dim when crouching, push-to-talk/-mute, mic mute when the HMD is taken off, audio and video profiles, color tint, media keys (MPRIS), alarm clock, statistics, per-game bindings | Keep; check one by one | +| Doesn't work, needs a replacement | Space drag/turn, gravity, height toggle, offsets, fix floor, chaperone profiles (geometry), all rotation features | Disable for now | +| Questionable | Keyboard input; cord untangling and rotation counter (the Frame is wireless) | Disable for now |