Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a10cdf7ca | ||
|
|
7f14551a6a | ||
|
|
ff4ea8f455 | ||
|
|
d664f86945 | ||
|
|
bbb3d1ba0e | ||
|
|
ec82b5fead | ||
|
|
317db6831d | ||
|
|
537539ab5e | ||
|
|
7c5a9a7a1e |
@@ -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
@@ -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 ()
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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`.
|
||||
@@ -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" );
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
@@ -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" );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user