- build_scripts/steamframe/: Debian trixie (Qt 6.8) containers for the aarch64 cross build, native build and tests; toolchain; scripts to build, test (Vulkan validation layers), package (system Qt, ~3 MB) and deploy over SSH. Adapted from the vrdashboard template. - test/render/: render test for the OpenGL and Vulkan backends, driven like OverlayController (option OVRAS_BUILD_TESTS). - RPATH only $ORIGIN, without the build machine's link directories. - Ignore dist/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
272 lines
14 KiB
Markdown
272 lines
14 KiB
Markdown
# 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 <pid> 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 trixie (Qt 6.8) with arm64 multiarch libraries, cross compiler |
|
||
| `native.Dockerfile` | The same for the host's own architecture (on the Frame: native build); base of the test container |
|
||
| `test.Dockerfile` | Mesa (incl. lavapipe), Vulkan validation layers, Xvfb |
|
||
| `aarch64-linux-gnu.cmake` | Cross-compile toolchain |
|
||
| `container-build.sh cross\|native` | Builds with `OVRAS_STEAM_FRAME=ON` → `build/steamframe-<mode>/Release/` |
|
||
| `container-test.sh` | Render tests with both APIs, Vulkan with validation layers |
|
||
| `package-release.sh` | Build output + `install.sh` / `uninstall.sh` / `run.sh` / `README.txt` → `dist/AdvancedSettings-<version>-linux-arm64(.tar.gz)` |
|
||
| `deploy-to-frame.sh` | Copies the package to the Frame over SSH (`rsync`) and runs `install.sh` there |
|
||
|
||
- **Scripts in the package:**
|
||
- `install.sh` stops a running instance, registers it (`--force-install-manifest`, auto-start)
|
||
and starts it with `DISPLAY=:0`. Over SSH there is no display, and Qt needs one.
|
||
- `run.sh [opengl|vulkan]` restarts it with a given graphics API.
|
||
- The scripts start the program in the package folder, because OVRAS writes
|
||
`AdvancedSettings.log` into the working directory.
|
||
- Packages use the **system Qt**: no Qt is bundled, because of section 3 the binary uses no
|
||
private API.
|
||
- RPATH is only `$ORIGIN` (`BUILD_WITH_INSTALL_RPATH`): the build tree gets packaged.
|
||
|
||
## Tests
|
||
|
||
- **x86_64 locally:** the build succeeds; `objdump` check (R1).
|
||
- **Render test `test/render/tst_render`** (`OVRAS_BUILD_TESTS=ON`):
|
||
- Drives the backends and `QQuickRenderControl` in the same order as `OverlayController`.
|
||
- Checks the texture description for SteamVR (type, Vulkan format) and the content and
|
||
orientation via readback.
|
||
- One CTest entry per API.
|
||
- `container-test.sh` runs it with the Vulkan validation layers. It fails on validation
|
||
messages and on skipped tests.
|
||
- **OVRAS's keyboard parser test** (`test/keyboard_input`, qmake) is not included in CMake
|
||
in iteration 1 (not run).
|
||
- **On the Frame:** the checklist in `PROGRESS.md` per acceptance criterion R1–R4, R6, R7,
|
||
with both graphics APIs.
|
||
|
||
## 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.
|