8 Commits
Author SHA1 Message Date
Matthias JanetschekandClaude Opus 5.5 7c5a9a7a1e Handover documentation for continuing on another computer
- docs/steamframe/README.md: entry point with status, branches/remotes, setup,
  commands, Steam Frame facts, open points.
- CLAUDE.md: working rules and code conventions (fork-only).
- docs/steamframe/OVRAS-FEATURES.md: OVRAS's features, how they are
  implemented, issues found while reading.
- PROGRESS.md: status and wrap-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:57:20 +02:00
Matthias JanetschekandClaude Opus 5.5 3ed686dec5 Read the version from build_scripts/compile_version_string.txt
The CMake build had the version hard-coded as 5.7.4, while the qmake build
read it from compile_version_string.txt (5.8.17-release). The stale number
also made the version check report an update. As in the qmake build, the
full string becomes APPLICATION_VERSION and its numeric part the project
version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:41:12 +02:00
Matthias JanetschekandClaude Opus 5.5 bde675f1b8 Steam Frame build, packaging and deployment scripts; render test
- 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>
2026-10-07 13:50:57 +02:00
Matthias JanetschekandClaude Opus 5.5 b22c35ae29 Manifest for Linux; connect as Background app where Utility is refused
- manifest.vrmanifest: binary_path_linux and binary_path_linux_arm. The
  Steam Frame's SteamVR reads only binary_path_linux_arm.
- handleManifests(): connect as Utility app first (works without a running
  SteamVR, e.g. from the Windows installer); if SteamVR refuses Utility apps,
  as on the Steam Frame, connect as Background app. Fail right away on other
  errors instead of going through initializeOpenVR()'s retry loop.
- main(): check --graphics-api before connecting to OpenVR, so a wrong value
  fails right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:42:16 +02:00
Matthias JanetschekandClaude Opus 5.5 4826686a4d Render through public Qt API: OpenGL and Vulkan backends, --graphics-api
Replace the QRhi rendering in OverlayController (private Qt API) with the
render backends of the vrdashboard template (src/render/), which use public
Qt API only (QQuickRenderTarget::fromOpenGLTexture / fromVulkanImage). The
binary then runs on another Qt 6 patch release, e.g. the Steam Frame's system
Qt.

The Vulkan backend also does what SteamVR requires and the QRhi path lacked:
the required instance/device extensions and the HMD's GPU (via
openvr_init::renderInterop()), VK_FORMAT_R8G8B8A8_UNORM, and the transition to
TRANSFER_SRC_OPTIMAL after each frame.

- New option --graphics-api opengl|vulkan (default opengl, no fallback).
- Shutdown() calls VR_Shutdown before the render target and Qt's device go
  (crash on the Steam Frame with Vulkan otherwise); it runs only once.
- Mouse y is flipped on every platform: SteamVR reports it in GL convention.
- Render delay 5 -> 16 ms (about 60 fps).
- GuiPrivate is no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:41:13 +02:00
Matthias JanetschekandClaude Opus 5.5 423ea57db7 Add a Steam Frame build option; no chaperone commit at start; timer-based tick
- CMake option OVRAS_STEAM_FRAME. It is evaluated only in
  src/utils/platform_capabilities.h, which turns it into named capability
  constants and platform defaults.
- Where the chaperone can't be committed (Steam Frame: it shifts the play
  area), MoveCenterTabController no longer commits the working copy at start-up
  or when applying reset data, and doesn't create the init autosave profile.
  Skips are logged once.
- In "vsync disabled" mode the pump timer now runs directly at the custom tick
  rate instead of firing every 1 ms and counting. The Frame defaults to this
  mode at 11 ms (~90 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:35:46 +02:00
Matthias JanetschekandClaude Opus 5.5 266a2f9ce9 Fix the Linux build of the qt6 branch, compile QML to bytecode only
- Remove Core5Compat (unused, not part of the Steam Frame's system Qt).
- Request GuiPrivate explicitly (required since Qt 6.10 for QRhi); temporary
  until rendering moves to public API.
- Compile QML to bytecode only: C++ generated from QML uses private Qt API
  that only matches the exact Qt version it was built with.
- Pick the OpenVR library per architecture (linux64, linuxarm64, win64) for
  linking and for the copy next to the binary; RPATH $ORIGIN.
- Link libXtst, needed by input_sender_X11.cpp.
- Include d3d11.h and handle QRhi::D3D11 only on Windows.
- Replace leftover easylogging LOG(INFO) calls with qInfo().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:29:53 +02:00
Matthias JanetschekandClaude Opus 5.5 aac3edf7e3 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 <noreply@anthropic.com>
2026-10-07 13:29:53 +02:00
37 changed files with 2929 additions and 166 deletions
+2
View File
@@ -17,3 +17,5 @@ build_scripts/win/current_build.bat
AdvancedSettings_resource.rc
.qtc_clangd/
out/
# Steam Frame release packages (build_scripts/steamframe/package-release.sh)
dist/
+96
View File
@@ -0,0 +1,96 @@
# CLAUDE.md
Guidance for Claude Code (and humans) working in this fork. Fork-only file: not part of
upstream pull requests.
**Start with [docs/steamframe/README.md](docs/steamframe/README.md):** goal, status, setup,
commands, Steam Frame facts. Then [REQUIREMENTS.md](docs/steamframe/REQUIREMENTS.md)
(decisions), [DESIGN.md](docs/steamframe/DESIGN.md) and
[PROGRESS.md](docs/steamframe/PROGRESS.md) (status, Frame test checklist).
## Working rules (agreed with the user, always apply)
- **Proper software engineering:** follow established procedures and processes, not ad-hoc
hacking.
- **Nothing without explicit permission.**
- Propose first, then act only on the user's go-ahead.
- Permission covers only what was asked. No extra changes, renames, rewrites or "while I'm
at it" fixes: suggest those instead.
- **Ask, don't assume.** When a requirement, decision or detail is unclear, ask. Give a
recommendation with the question.
- **Iterations in phases.** Each phase ends with the user's OK (informal is enough):
1. requirements
2. design
3. implementation
4. tests
5. assessment
- Keep iterations small.
- **Document everything** in `docs/steamframe/`: requirements, decisions (with date), design,
progress, test results. Report results faithfully, including what was *not* tested. Keep
the docs in sync with every change.
- **Communication:** the user writes German or English; answer in the user's language. Docs
and code comments are in English.
## Goal and constraints (details: REQUIREMENTS.md)
- OVRAS runs natively on the Steam Frame (SteamOS, aarch64). Windows is out of scope for now.
- **Minimal change set against upstream's `qt6` branch**, because changes are meant to go
upstream later.
- Change existing code only where needed.
- Bring new parts in as self-contained files.
- **Only public Qt API.** No `GuiPrivate`/QRhi, and QML is compiled to bytecode only.
`objdump -T … | grep Qt_6_PRIVATE_API` may only show `QVulkan*`.
- **Frame differences:**
- Switched by the CMake option `OVRAS_STEAM_FRAME`.
- It is evaluated **only** in `src/utils/platform_capabilities.h`, which turns it into
capability constants named by capability (e.g. `k_chaperoneCommitSupported`).
- The code asks for capabilities, never for the platform.
## Code conventions
- **Comments:** every change gets comments that explain it fully (what and why, e.g. which
Frame or SteamVR fact). Keep them direct and concise. Trivial mechanical changes such as
`LOG(INFO)` → `qInfo()` are explained in the commit message instead.
- **Style:**
- OVRAS style (`.clang-format`, since 2018) everywhere, including code taken over from the
template.
- **Format only changed lines:** `git add …; git clang-format; git add …`. Never
reformat whole existing files, because a dozen files already deviate.
- New files are formatted completely (`clang-format -i`).
- Separate new comments from existing commented-out code with a blank line. Otherwise
clang-format reflows the old block and the diff grows.
- **Commits:**
- One commit per step / topic, with a message that explains the why.
- End every commit message with `Co-Authored-By: Claude …`.
- Push to `origin` (the fork); `upstream` is the GitHub OVRAS repository.
- **Template code** (`src/render/`, `build_scripts/steamframe/`) carries a note about its
origin (vrdashboard template, FrameAssistant `4a5b090`).
## Architecture notes (what is easy to break)
- **`main.cpp` order:**
1. parse the command line
2. choose the render backend (`--graphics-api`)
3. `initializeOpenVR(Overlay)`
4. `backend->initialize(openvr_init::renderInterop())`
5. `QQuickWindow::setGraphicsApi()`
6. `OverlayController`
`setGraphicsApi` must come before any `QQuickWindow` exists, and `OverlayController` owns
one.
- **`OverlayController`:**
- Owns `m_renderBackend`, declared *before* `m_renderControl` / `m_window`: Qt's device
comes from the backend.
- `SetWidget()` and `renderOverlay()` follow the call order in `src/render/RenderBackend.h`.
- **Shutdown:**
- OVRAS ends with `exit()`.
- `Shutdown()` (once only) calls `vr::VR_Shutdown()` *before* `releaseRenderTarget()` /
`detachWindow()`.
- **Tick:**
- `m_pumpEventsTimer`: 1 ms polling in "vsync" mode; in "vsync disabled" mode it runs
directly at `customTickRateMs` (`applyTickInterval()`).
- Frame defaults: vsync disabled, 11 ms.
- **Mouse:** SteamVR reports y from the bottom, so it is flipped on every platform.
- **Logging:** OVRAS writes `AdvancedSettings.log` into the working directory.
- **SteamVR Input:** `action_manifest.json` and `default_action_manifests/` (incl.
`…_default_frame.json` for `frame_controller`) lie next to the binary.
+83 -11
View File
@@ -1,5 +1,13 @@
cmake_minimum_required(VERSION 3.20)
project(AdvancedSettings VERSION 5.7.4 LANGUAGES CXX)
# The version comes from build_scripts/compile_version_string.txt, as in the
# qmake build (e.g. "5.8.17-release"): the full string becomes
# APPLICATION_VERSION (shown in the UI, compared by the version check), the
# numeric part before "-" the project version.
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/build_scripts/compile_version_string.txt"
OVRAS_VERSION_STRING LIMIT_COUNT 1)
string(REGEX REPLACE "-.*$" "" OVRAS_VERSION_NUMBER "${OVRAS_VERSION_STRING}")
project(AdvancedSettings VERSION ${OVRAS_VERSION_NUMBER} LANGUAGES CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -12,7 +20,10 @@ set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
#find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core)
#find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg)
#find_package(Qt${QT_VERSION_MAJOR} OPTIONAL_COMPONENTS DBus)
find_package(Qt6 REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg Qml Core5Compat)
# Core5Compat removed: nothing uses it, and the Steam Frame's system Qt
# doesn't ship it. No private Qt modules: the binary must run on another Qt 6
# patch release, e.g. the Frame's system Qt (docs/steamframe/DESIGN.md).
find_package(Qt6 REQUIRED COMPONENTS Gui Multimedia Qml Quick WebSockets Widgets QuickControls2 QuickDialogs2 Svg Qml)
find_package(Vulkan REQUIRED)
qt_standard_project_setup(REQUIRES 6.8)
@@ -64,6 +75,8 @@ set(SRCS
src/utils/paths.cpp
src/utils/setup.cpp
src/utils/update_rate.cpp
src/render/RenderBackend.cpp
src/render/OpenGLRenderBackend.cpp
)
set(HEADERS
@@ -106,6 +119,9 @@ set(HEADERS
src/utils/paths.h
src/utils/setup.h
src/utils/update_rate.h
src/utils/platform_capabilities.h
src/render/RenderBackend.h
src/render/OpenGLRenderBackend.h
src/utils/Matrix.h
src/quaternion/quaternion.h
)
@@ -246,6 +262,15 @@ qt_add_executable(AdvancedSettings
)
#src/res/resources.qrc
# Compile QML to bytecode only, not to C++. C++ generated from QML calls Qt's
# private API (QQmlPrivate::AOTCompiledContext), which only matches the exact
# Qt version it was built with. Bytecode keeps the binary on public API, so it
# runs on another Qt 6 patch release, e.g. the Steam Frame's system Qt.
# Must be set before qt_add_qml_module() reads it.
set_target_properties(AdvancedSettings PROPERTIES
QT_QMLCACHEGEN_ARGUMENTS --only-bytecode
)
qt_add_qml_module(AdvancedSettings
URI AdvancedSettings
QML_FILES
@@ -264,12 +289,43 @@ target_include_directories(AdvancedSettings PUBLIC
# Avoid collision between executable name and QML module directory (QTBUG-98568).
if(NOT WIN32 AND NOT APPLE)
set_property(TARGET AdvancedSettings PROPERTY OUTPUT_NAME AdvancedSettings_app)
# libopenvr_api.so is copied next to the executable (see "openvrApiCopy");
# $ORIGIN makes the loader look there, in the build tree and in packages.
# BUILD_WITH_INSTALL_RPATH: only $ORIGIN, without the build machine's
# link directories (the build tree is what gets packaged).
set_target_properties(AdvancedSettings PROPERTIES
BUILD_WITH_INSTALL_RPATH ON
INSTALL_RPATH "$ORIGIN"
)
endif()
target_compile_definitions(AdvancedSettings PRIVATE
APPLICATION_VERSION=\"${PROJECT_VERSION}\"
APPLICATION_VERSION=\"${OVRAS_VERSION_STRING}\"
)
# Vulkan render backend (src/render/VulkanRenderBackend.*), selected at run
# time with --graphics-api vulkan. Needs a Qt built with Vulkan support; the
# Vulkan headers come from find_package(Vulkan) above. Nothing links against
# libvulkan: Qt loads it at run time.
if (QT_FEATURE_vulkan)
target_sources(AdvancedSettings PRIVATE
src/render/VulkanRenderBackend.cpp
src/render/VulkanRenderBackend.h
)
target_compile_definitions(AdvancedSettings PRIVATE OVRAS_HAS_VULKAN=1)
else ()
target_compile_definitions(AdvancedSettings PRIVATE OVRAS_HAS_VULKAN=0)
endif ()
message(STATUS "Vulkan render backend: ${QT_FEATURE_vulkan}")
# Build for the Steam Frame (SteamOS, aarch64). Switches off what doesn't work
# there and changes some defaults; the details are named capability constants
# in src/utils/platform_capabilities.h, the only place reading this definition.
option(OVRAS_STEAM_FRAME "Build for the Steam Frame" OFF)
if (OVRAS_STEAM_FRAME)
target_compile_definitions(AdvancedSettings PRIVATE OVRAS_STEAM_FRAME=1)
endif ()
#find_library(OPENVR_LIBRARIES
# NAMES
# openvr_api
@@ -282,17 +338,19 @@ target_compile_definitions(AdvancedSettings PRIVATE
# NO_DEFAULT_PATH
# NO_CMAKE_FIND_ROOT_PATH)
# Directory of the OpenVR library for the target platform. Linux distinguishes
# x86_64 (linux64) from aarch64 (linuxarm64, e.g. the Steam Frame).
if (WIN32)
target_link_directories(AdvancedSettings PRIVATE
${CMAKE_SOURCE_DIR}/third-party/openvr/lib/win64
)
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)
else ()
set(OPENVR_LIB_DIR ${CMAKE_SOURCE_DIR}/third-party/openvr/lib/linux64)
endif ()
target_link_directories(AdvancedSettings PRIVATE ${OPENVR_LIB_DIR})
target_link_libraries(AdvancedSettings PRIVATE
Qt::Gui
Qt6::Core5Compat
# For QRhi
Qt::GuiPrivate
Qt::Multimedia
Qt::Qml
Qt::Quick
@@ -377,7 +435,7 @@ if (WIN32)
else ()
add_custom_command(TARGET AdvancedSettings POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/third-party/openvr/lib/linux64/libopenvr_api.so" "${COPY_DEST_DIR}/libopenvr_api.so"
"${OPENVR_LIB_DIR}/libopenvr_api.so" "${COPY_DEST_DIR}/libopenvr_api.so"
)
endif ()
@@ -430,7 +488,12 @@ elseif (UNIX)
src/keyboard_input/input_sender_X11.cpp
)
find_package(X11 REQUIRED)
target_link_libraries(AdvancedSettings PRIVATE ${X11_LIBRARIES})
# input_sender_X11.cpp uses the XTest extension (XTestFakeKeyEvent,
# XTestGrabControl), which lives in its own library.
if (NOT X11_XTest_FOUND)
message(FATAL_ERROR "libXtst (X11 XTest extension) not found")
endif ()
target_link_libraries(AdvancedSettings PRIVATE ${X11_LIBRARIES} ${X11_XTest_LIB})
elseif (NOT APPLE AND noX11)
target_sources(AdvancedSettings PRIVATE
src/keyboard_input/input_sender_dummy.cpp
@@ -541,3 +604,12 @@ install(SCRIPT ${deploy_script})
# "${COPY_DEST_DIR}/AdvancedSettings.exe"
#)
endif ()
# Tests (off by default). test/render checks the render backends
# (docs/steamframe/DESIGN.md, "Tests"); build_scripts/steamframe/
# container-test.sh runs them with the Vulkan validation layers.
option(OVRAS_BUILD_TESTS "Build the tests" OFF)
if (OVRAS_BUILD_TESTS)
enable_testing()
add_subdirectory(test/render)
endif ()
@@ -0,0 +1,27 @@
# CMake toolchain: cross-compile for aarch64 Linux (Steam Frame) inside
# cross-arm64.Dockerfile (Debian multiarch). Used by container-build.sh.
# Adapted from the vrdashboard template.
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Debian multiarch: target libraries live in /usr/lib/aarch64-linux-gnu;
# headers are shared.
set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
set(ENV{PKG_CONFIG_LIBDIR} /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig)
# Qt 6 cross builds need a host Qt for its code generators (moc, rcc,
# qmlcachegen, ...).
set(QT_HOST_PATH /usr CACHE PATH "Host Qt prefix")
set(QT_HOST_PATH_CMAKE_DIR /usr/lib/x86_64-linux-gnu/cmake CACHE PATH "Host Qt CMake dir")
set(CMAKE_PREFIX_PATH /usr/lib/aarch64-linux-gnu/cmake)
set(CMAKE_OBJDUMP aarch64-linux-gnu-objdump)
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Builds OVRAS for the Steam Frame inside a Debian container (Qt 6.8, the
# series of the Frame's system Qt). Result: build/steamframe-<mode>/Release/,
# packaged by package-release.sh. Adapted from the vrdashboard template.
#
# build_scripts/steamframe/container-build.sh cross x86_64 host -> aarch64
# build_scripts/steamframe/container-build.sh native for the host's own
# architecture (on the Frame: native arm64)
# build_scripts/steamframe/container-build.sh <mode> shell
# interactive shell in that container
#
# Uses podman if available, else docker (override with CONTAINER_ENGINE).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/../.." && pwd)"
mode="${1:-}"
case "$mode" in
cross) dockerfile=cross-arm64.Dockerfile
toolchain="-DCMAKE_TOOLCHAIN_FILE=build_scripts/steamframe/aarch64-linux-gnu.cmake" ;;
native) dockerfile=native.Dockerfile; toolchain="" ;;
*) echo "usage: $0 cross|native [shell]" >&2; exit 2 ;;
esac
image="ovras-steamframe-${dockerfile%.Dockerfile}"
engine="${CONTAINER_ENGINE:-$(command -v podman || command -v docker)}"
build="build/steamframe-$mode"
"$engine" build -q -t "$image" -f "$here/$dockerfile" "$here" >/dev/null
# Run as the calling user, so the build tree isn't owned by root.
run=("$engine" run --rm -v "$root:$root:z" -w "$root")
if [[ "$(basename "$engine")" == docker ]]; then
run+=(--user "$(id -u):$(id -g)")
else
run+=(--userns=keep-id)
fi
[[ -t 0 ]] && run+=(-it)
if [[ "${2:-}" == shell ]]; then
exec "${run[@]}" "$image" bash
fi
# OVRAS_STEAM_FRAME: Frame capabilities and defaults
# (src/utils/platform_capabilities.h).
"${run[@]}" "$image" bash -c "
set -e
cmake -S . -B $build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DOVRAS_STEAM_FRAME=ON $toolchain
cmake --build $build
"
echo "Built: $root/$build/Release"
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Builds and runs the tests (OVRAS_BUILD_TESTS) in a Debian container with
# both render backends, Vulkan with the Khronos validation layers. Fails on any
# validation message. Adapted from the vrdashboard template.
#
# build_scripts/steamframe/container-test.sh GPU via /dev/dri if
# available, else software
# VK_DRIVER=lvp build_scripts/steamframe/container-test.sh
# force lavapipe (software)
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/../.." && pwd)"
engine="${CONTAINER_ENGINE:-$(command -v podman || command -v docker)}"
"$engine" build -q -t ovras-steamframe-native -f "$here/native.Dockerfile" "$here" >/dev/null
"$engine" build -q -t ovras-steamframe-test -f "$here/test.Dockerfile" "$here" >/dev/null
run=("$engine" run --rm -v "$root:$root:z" -w "$root" -e HOME=/tmp -e "VK_DRIVER=${VK_DRIVER:-}")
if [[ "$(basename "$engine")" == docker ]]; then
run+=(--user "$(id -u):$(id -g)")
else
run+=(--userns=keep-id)
fi
[[ -e /dev/dri/renderD128 ]] && run+=(--device /dev/dri/renderD128)
"${run[@]}" ovras-steamframe-test bash -c '
set -e
build=build/steamframe-test
cmake -S . -B $build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DOVRAS_BUILD_TESTS=ON >/dev/null
cmake --build $build --target tst_render
if [[ -n "$VK_DRIVER" ]]; then export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/${VK_DRIVER}_icd.json; fi
vulkaninfo --summary 2>/dev/null | grep -m1 deviceName || true
Xvfb :99 >/dev/null 2>&1 &
sleep 1
export DISPLAY=:99 OVRAS_VULKAN_VALIDATION=1
ctest --test-dir $build --output-on-failure -V > $build/ctest.log 2>&1 \
|| { tail -40 $build/ctest.log; exit 1; }
grep -E "tests passed|Test +#|Totals" $build/ctest.log
# A skipped test (no GPU context) must not count as passed.
if grep -q "SKIP" $build/ctest.log; then
echo "Tests were skipped (see $build/ctest.log)"; exit 1
fi
messages=$(grep -oE "(VUID|UNASSIGNED)-[A-Za-z0-9_.-]+" $build/ctest.log | sort | uniq -c || true)
if [[ -n "$messages" ]]; then
echo "Vulkan validation messages (see $build/ctest.log):"
echo "$messages"
exit 1
fi
echo "No Vulkan validation messages."
'
@@ -0,0 +1,20 @@
# Cross-compilation environment for the Steam Frame (aarch64), from an x86_64
# host via Debian multiarch: the compiler and Qt's code generators (moc, rcc,
# qmlcachegen) run natively as amd64 builds, the target libraries are the
# arm64 builds. Debian trixie ships Qt 6.8, the series of the Frame's system
# Qt (SteamOS holo 0.4.3: 6.8.0). Adapted from the vrdashboard template.
FROM debian:trixie
ENV LANG=C.UTF-8
RUN dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates cmake ninja-build pkgconf file \
g++-aarch64-linux-gnu binutils-aarch64-linux-gnu \
qt6-base-dev qt6-base-dev-tools qt6-declarative-dev qt6-declarative-dev-tools \
qt6-base-dev:arm64 qt6-declarative-dev:arm64 qt6-multimedia-dev:arm64 \
qt6-websockets-dev:arm64 qt6-svg-dev:arm64 \
libgl-dev:arm64 libegl-dev:arm64 libvulkan-dev:arm64 libxkbcommon-dev:arm64 \
libx11-dev:arm64 libxt-dev:arm64 libxtst-dev:arm64 libpulse-dev:arm64 \
&& rm -rf /var/lib/apt/lists/*
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Copies the newest package (package-release.sh) to the Steam Frame over SSH
# and runs its install.sh there (registers with SteamVR and starts it; an
# existing installation in the same place is updated). SteamVR must be running
# on the Frame. Adapted from the vrdashboard template.
#
# build_scripts/steamframe/deploy-to-frame.sh <ssh-target> [package-dir] [remote-dir]
# e.g. build_scripts/steamframe/deploy-to-frame.sh steamos@frame
#
# remote-dir defaults to "AdvancedSettings" in the remote home directory.
set -euo pipefail
target="${1:?usage: $0 <ssh-target> [package-dir] [remote-dir]}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
package="${2:-$(ls -dt "$root"/dist/AdvancedSettings-*-linux-arm64 2>/dev/null | head -1)}"
[[ -n "$package" && -x "$package/install.sh" ]] || {
echo "No package found (run build_scripts/steamframe/package-release.sh first)" >&2; exit 1; }
# Without the version: updates go to the same place and replace the old one.
remote="${3:-AdvancedSettings}"
# Keep the log of earlier runs on the Frame.
rsync -a --delete --exclude AdvancedSettings.log "$package/" "$target:$remote/"
ssh "$target" "\$HOME/$remote/install.sh"
echo "Installed $(basename "$package") to $target:~/$remote"
@@ -0,0 +1,19 @@
# Native build environment (any architecture). On x86_64 it is the base of the
# test container (test.Dockerfile); on the Steam Frame itself (aarch64, e.g.
# with podman) it builds the same package as the cross build. Debian trixie
# ships Qt 6.8, the series of the Frame's system Qt. Adapted from the
# vrdashboard template.
FROM debian:trixie
ENV LANG=C.UTF-8
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates cmake ninja-build pkgconf file g++ \
qt6-base-dev qt6-base-dev-tools qt6-declarative-dev qt6-declarative-dev-tools \
qt6-multimedia-dev qt6-websockets-dev qt6-svg-dev \
libgl-dev libegl-dev libvulkan-dev libxkbcommon-dev \
libx11-dev libxt-dev libxtst-dev libpulse-dev \
qml6-module-qtquick qml6-module-qtqml-workerscript qml6-module-qtquick-window \
qt6-qpa-plugins \
&& rm -rf /var/lib/apt/lists/*
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# Builds OVRAS for the Steam Frame (aarch64) and packages it:
# dist/AdvancedSettings-<version>-linux-arm64/ (+ .tar.gz)
# The package uses SteamOS's own Qt (no Qt bundled). It contains the program
# folder as OVRAS expects it (binary, libopenvr_api.so, manifests, res/) plus
# install.sh, uninstall.sh, run.sh and README.txt. Adapted from the
# vrdashboard template.
#
# build_scripts/steamframe/package-release.sh [--no-build]
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/../.." && pwd)"
build=1
for arg in "$@"; do
case "$arg" in
--no-build) build=0 ;;
*) echo "usage: $0 [--no-build]" >&2; exit 2 ;;
esac
done
# On the Frame itself build natively, elsewhere cross-compile.
if [[ "$(uname -m)" == aarch64 ]]; then mode=native; else mode=cross; fi
(( build )) && "$here/container-build.sh" "$mode"
exe=AdvancedSettings_app
bin="$root/build/steamframe-$mode/Release"
version="$(sed -n 's/^CMAKE_PROJECT_VERSION:[A-Z]*=//p' "$root/build/steamframe-$mode/CMakeCache.txt")"
[[ "$(file -b "$bin/$exe")" == *aarch64* ]] || { echo "$bin/$exe is not an aarch64 build" >&2; exit 1; }
package="AdvancedSettings-$version-linux-arm64"
out="$root/dist/$package"
rm -rf "$out" "$out.tar.gz"
mkdir -p "$out"
cp -a "$bin/." "$out/"
# The scripts start the program from the package folder: OVRAS writes its log
# (AdvancedSettings.log) to the working directory. Process names are cut to
# 15 characters, so running instances are found by command line.
cat > "$out/install.sh" <<EOF
#!/bin/sh
# Installs or updates OVR Advanced Settings: registers it with SteamVR (starts
# automatically with SteamVR from now on) and starts it now. SteamVR must be
# running. Works over SSH.
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
# 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 &
echo "Installed and started."
EOF
cat > "$out/uninstall.sh" <<EOF
#!/bin/sh
# Unregisters OVR Advanced Settings from SteamVR and stops it. The folder can
# be deleted afterwards.
cd "\$(dirname "\$0")" || exit 1
pkill -f "/$exe( |\\\$)" 2>/dev/null
exec ./$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)
cd "\$(dirname "\$0")" || exit 1
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"
EOF
chmod +x "$out/install.sh" "$out/uninstall.sh" "$out/run.sh"
cat > "$out/README.txt" <<EOF
OVR Advanced Settings $version for the Steam Frame (SteamOS, aarch64)
Uses the system's Qt (SteamOS: Qt 6.8 or newer).
Install / update: ./install.sh (SteamVR must be running)
SteamVR remembers this folder's location: after moving it,
run ./install.sh again.
Uninstall: ./uninstall.sh
Test a graphics API: ./run.sh opengl | ./run.sh vulkan
Log: AdvancedSettings.log in the working directory (this folder
when started by the scripts)
License: GPL-3.0 (LICENSE); OpenVR: LICENSE-VALVE.
EOF
tar -C "$root/dist" -czf "$out.tar.gz" "$package"
echo "Release folder: $out"
echo "Archive: $out.tar.gz ($(du -h "$out.tar.gz" | cut -f1))"
+10
View File
@@ -0,0 +1,10 @@
# Test environment on top of native.Dockerfile: Mesa Vulkan/GL drivers (incl.
# the lavapipe software rasterizer), the Khronos validation layers and Xvfb.
# See container-test.sh. Adapted from the vrdashboard template.
FROM ovras-steamframe-native
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
mesa-vulkan-drivers vulkan-validationlayers vulkan-tools libgl1-mesa-dri \
xvfb xauth \
&& rm -rf /var/lib/apt/lists/*
+271
View File
@@ -0,0 +1,271 @@
# 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.
+197
View File
@@ -0,0 +1,197 @@
# OVRAS: features and how they are implemented
Analysis of 2026-10-07, based on upstream `master` (`31df8b4`, 5.8.17). Line numbers refer to
that state. `qt6` has the same features, so the numbers are close. Paths are relative to
`src/`. Which of these features SteamVR now offers itself is in
[REQUIREMENTS.md](REQUIREMENTS.md), "Feature classification".
## Architecture
- **Dashboard overlay:**
- `VR_Init(VRApplication_Overlay)`, app key `steam.overlay.1009850`.
- QML UI (1200×800) rendered off-screen via `QQuickRenderControl`.
- The texture goes to `SetOverlayTexture`. On `steamframe` the render backends in `render/`
do this; before, it was an OpenGL FBO, and on `qt6` QRhi.
- **Input:** SteamVR mouse/scroll events are turned into Qt events (`overlaycontroller.cpp`,
`mainEventLoop()`).
- **Main loop (`OnTimeoutPumpEvents` → `mainEventLoop()`):**
- Timer; "vsync" mode once per compositor frame (`GetTimeSinceLastVsync`).
- Per tick:
1. input actions
2. VR events
3. poses
4. `eventLoopTick` of the tab controllers (always)
5. `dashboardLoopTick` (only while the dashboard is visible)
- **SteamVR Input:** `package_files/action_manifest.json`, 5 action sets:
- `motion`: space drag/turn, gravity, snap/smooth turn, height toggle, offsets, auto-turn
- `music`: media keys
- `misc`: axis locks, chaperone toggle, keyboard shortcuts
- `system`: push-to-talk, exclusive input, haptic clicks
- `haptic`: vibration, proximity sensor
Default bindings exist for Vive, Index, WMR, HP, Touch, generic HMD and Frame controllers.
- **Persistence:**
- App settings in a `QSettings` INI file (`settings/`). Profiles are stored as arrays
(`ISettingsObject`).
- SteamVR's own settings go directly through `vr::VRSettings()` (`openvr/ovr_settings_wrapper`).
- **Platform:**
- Windows: WASAPI / `IPolicyConfig`, `SendInput`.
- Linux: PulseAudio, X11 XTest, D-Bus MPRIS.
- Elsewhere: dummies.
## Features
### Playspace movement (`tabcontrollers/MoveCenterTabController.cpp`)
Almost everything shifts or rotates the **working standing zero pose** through
`IVRChaperoneSetup` and shows it live with `ShowWorkingSetPreview()`. Nothing is committed
per frame. This mechanism does **not work on the Frame** (inside-out tracking).
- **Core, `updateSpace()` (:2497–2721):**
- Offsets X/Y/Z and the rotation (centidegrees) are multiplied with the stored basis.
- The result goes to `SetWorkingStandingZeroPoseToRawTrackingPose`, in seated mode
additionally to the seated pose.
- Safety limit: about 40 km.
- **Space drag:**
- The controller's position delta, multiplied by `dragMult`, is added to the offsets.
- Axis locks; the speed is kept for fling.
- Comfort mode skips frames.
- **Space turn:** the change of the yaw angle via quaternions
(`q·conj(q_last)`, `quaternion/quaternion.h`). Rotation pivots on the HMD by default.
- **Snap/smooth turn:** fixed angle per press resp. per tick, so smooth turn depends on the
frame rate.
- **Gravity/momentum:** Euler integration with friction, terminal velocity 50 m/s, landing on
the floor, reversible.
- **Height toggle:** Y offset plus a new gravity floor.
- **Offset profiles, apply/reset:**
- "Apply" commits with `CommitWorkingCopy(Live)` (limit 990 m).
- `zeroOffsets()` / `updateChaperoneResetData()` also commit at start-up. On the Frame
this shifts the play area, hence R7.
### Rotation (`tabcontrollers/RotationTabController.cpp`, via `setRotation`)
- **Auto-turn:**
- Rotates the space when the user gets near a wall, either as a snap or smoothly.
- Direction: from the corner shared with the last wall, otherwise cord untangling (HMD
total rotation count).
- **Vestibular motion:** rotation as if walking on a circle of radius r
(`2·asin(d/2r)`).
- **View ratchetting:** amplifies head turns away from the wall.
### Fix floor / recenter (`tabcontrollers/FixFloorTabController.cpp`)
- **Fix floor:**
- Measures the lower controller over 25 frames.
- Subtracts the controller thickness per model and orientation (Vive / Knuckles).
- Commits the result as a Y offset; undo is possible.
- **Recenter:** `VRChaperone()->ResetZeroPose(...)`.
### Chaperone (`tabcontrollers/ChaperoneTabController.cpp`)
- **Settings:**
- Visibility, color, fade distance, wall height, center marker, floor bounds and style.
- These are keys in `k_pch_CollisionBounds_Section`.
- "Disable" sets the fade distance to 0 and remembers the old value.
- **Own center marker overlay:** shows the rotation count (cord).
- **Dim when crouching:** below a height, the fade distance goes to 0 over 10 s.
- **Proximity warnings:**
- Distance of HMD and controllers to the walls (`utils/ChaperoneUtils.cpp`: projection onto
each wall segment).
- Reactions: switch to the beginner style, vibration (own thread,
`TriggerHapticVibrationAction`), alarm sound with distance-dependent volume, open the
dashboard.
- **Chaperone profiles:**
- Save the geometry (`GetLiveCollisionBoundsInfo`, zero pose, size) and the settings.
- Apply them with `SetWorking…` + `CommitWorkingCopy`.
- Autosave profile.
### Video (`tabcontrollers/VideoTabController.cpp`)
- **Brightness:** a black overlay right in front of the HMD. Its alpha `(1−x)^(1/3)`
compensates for perception.
- **Color tint:**
- either a colored overlay (`SetOverlayColor`)
- or SteamVR's `HmdDisplayColorGainR/G/B`
- **SteamVR keys:** supersampling (incl. manual override), motion smoothing, supersample
filtering.
- **Video profiles.**
### Audio (`tabcontrollers/AudioTabController.cpp`, `audiomanager/`)
- **Interface:** `AudioManager` with a Windows, a PulseAudio and a dummy implementation.
- **Windows:**
- WASAPI/MMDevice, `IAudioEndpointVolume`.
- The default device is set via the undocumented `IPolicyConfig`.
- External changes come in via `IMMNotificationClient`.
- **Linux:**
- PulseAudio: `pa_context_set_default_sink/source`; existing source outputs are moved to
the new source.
- The original state is restored at exit.
- **No mirror device.**
- **Mirror device and device override:** these are SteamVR keys (`k_pch_audio_*`), not
OVRAS's own routing.
- **Push-to-talk / -mute:** SteamVR Input action → mic mute + notification overlay on the
HMD.
- **Mute the mic when the HMD is taken off:** proximity sensor action.
- **Audio profiles.**
### SteamVR tab (`tabcontrollers/SteamVRTabController.cpp`)
- **Toggles:** each is a SteamVR setting.
- performance graph, multiple drivers, do not disturb, no-HMD mode
- camera options
- system button to all apps, don't fade to grid, controller power auto-launch
- **Restart SteamVR:** Windows only, via `.bat`.
- **Per-game controller bindings:**
- Use SteamVR's web server `localhost:27062`, with a spoofed `Referer`.
- Saved as JSON and applied when the scene app changes.
- **Receiver/tracker pairing:** WebSocket to the lighthouse web interface (`start_pairing`).
- **`openvr/lh_console_util.cpp`** (`lighthouse_console`) is compiled but **not used**.
- There is no base-station power management.
### Utilities (`tabcontrollers/UtilitiesTabController.cpp`, `keyboard_input/`, `media_keys/`, `alarm_clock/`)
- **Keyboard:**
- Buttons (Enter, Alt-Tab, Ctrl-C/V, …) and 3 freely defined shortcuts.
- A parser for the syntax `^`=Ctrl, `*`=Alt, `>`=Shift, `#`=Super.
- Sent via Windows `SendInput` or X11 XTest. On the Frame (gamescope) the X11 path probably
doesn't reach games.
- **Media keys:**
- Windows: `VK_MEDIA_*`.
- Linux: D-Bus to every `org.mpris.MediaPlayer2.*`.
- **Tracker battery:** icon overlay on each tracker (`Prop_DeviceBatteryPercentage_Float`),
only while the dashboard is visible.
- **Alarm clock:** HMD overlay at a set time, shown for 3 s.
### Statistics (`tabcontrollers/StatisticsTabController.cpp`)
- **Frame counters:** `VRCompositor()->GetCumulativeStats()` gives presented, dropped,
reprojected and timed-out frames. "Reset" only stores an offset.
- **Distance traveled:** horizontal HMD movement, every 11th tick, steps ≥ 1 cm.
- **Rotation count, maximum controller speed.**
### Settings tab (`tabcontrollers/SettingsTabController.cpp`)
Autostart (`SetApplicationAutoLaunch`), native chaperone import, block Oculus SDK, desktop
mode, tick rate (vsync / custom tick rate).
## Issues found while reading (not verified)
| Place (master) | Issue |
|---|---|
| `MoveCenterTabController.cpp:1198` | `updateChaperoneResetData()` allocates `new vr::HmdQuad_t[]` on every call and never frees the old array (memory leak) |
| `MoveCenterTabController.cpp:2512` | `if ( m_chaperoneHasCommit ) { m_chaperoneHasCommit = true; … }` looks like a slip |
| `MoveCenterTabController.cpp:1332` | `leftHandSpaceDrag`: the final `else` overwrites the swap/override choice |
| `MoveCenterTabController` | Rotation is wrapped inconsistently (snap/smooth/auto-turn 0–36000 vs. ±18000 elsewhere) |
| `VideoTabController.cpp:114` | Sets the sort order of the brightness overlay instead of the color overlay |
| `ChaperoneTabController.cpp:903` | Floor bounds are read as Bool but written as Int32 |
| `MoveCenterTabController::zeroOffsets` | If `GetCalibrationState() == 200`, init never completes and the commit repeats every tick (with commits enabled) |
| `utils/FrameRateUtils.cpp`, `openvr/lh_console_util.cpp` | Compiled but unused |
Found on the `qt6` branch and fixed on `steamframe` (see PROGRESS.md):
- the build fails on Linux
- QRhi Vulkan path: wrong `m_nFormat` (Qt enum instead of `VkFormat`); no layout transition,
no SteamVR extensions, no choice of the HMD's GPU
- mouse y not flipped on Linux
- no `VR_Shutdown`
- hard-coded version 5.7.4
+200
View File
@@ -0,0 +1,200 @@
# 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)
**Changes:**
- **`CMakeLists.txt`:**
- `Core5Compat` removed (unused, not on the Frame).
- `GuiPrivate` requested explicitly. This is temporary, until step 3 replaces QRhi.
- `QT_QMLCACHEGEN_ARGUMENTS --only-bytecode`.
- OpenVR library directory per architecture (`linux64` / `linuxarm64` / `win64`), for
linking and for the copy next to the binary.
- `RPATH $ORIGIN`.
- `libXtst` linked.
- **`overlaycontroller.cpp`:** `d3d11.h` and `case QRhi::D3D11` only under `_WIN32`.
- **Logging:** 8 active easylogging `LOG(INFO)` calls became `qInfo()`
(`overlaycontroller.cpp` 6, `ChaperoneTabController.cpp` 1,
`MoveCenterTabController.cpp` 1). Commented-out `LOG` lines are left unchanged.
- Only changed lines are formatted (`git clang-format`).
**Results (x86_64, Arch, Qt 6.11.1, GCC):**
- The build succeeds, with 1463 compiler warnings, as in `qt6`.
- `objdump -T`: 7 `Qt_6_PRIVATE_API` symbols, all `QRhi…` (step 3). The
`QQmlPrivate::AOTCompiledContext` symbols are gone (bytecode-only QML).
- `libopenvr_api.so` is resolved from the binary's directory (`ldd`).
- Not started: without a running SteamVR the start hangs in OpenVR init (expected; tested
on the Frame in step 5).
### Step 2: platform option, R7, tick (2026-10-07)
**Changes:**
- **Platform option:**
- CMake option `OVRAS_STEAM_FRAME` (default OFF) sets the compile definition
`OVRAS_STEAM_FRAME=1`.
- New `src/utils/platform_capabilities.h` evaluates it as the only place. It holds
`k_chaperoneCommitSupported`, `k_defaultVsyncDisabled` and `k_defaultCustomTickRateMs`.
- **Tick (R6):**
- In "vsync disabled" mode, `m_pumpEventsTimer` runs directly at `customTickRateMs`
(`applyTickInterval()`, `Qt::PreciseTimer`); the 1 ms counting is gone.
- Changing the setting restarts the timer.
- Setting defaults in `settings_controller.h` come from the constants: Frame `true`/11 ms,
otherwise unchanged `false`/20 ms.
- **R7:** with `!k_chaperoneCommitSupported`:
- `updateChaperoneResetData()` and `applyChaperoneResetData()` skip
`CommitWorkingCopy`/`RevertWorkingCopy`. Each logs once per function.
- The init autosave profile is skipped.
**Results:**
- Both variants (default and `-DOVRAS_STEAM_FRAME=ON`) build on x86_64. The definition is
only set in the Frame variant (`compile_commands.json`).
- Not tested at runtime yet (Frame, step 5).
**Notes:**
- Another commit at start-up remains, but only on opt-in. `SetWidget()` applies a chaperone
profile and commits when "auto-apply chaperone" (`autoApplyChaperoneToggle`, default
`false`) is on. That's a chaperone-profile feature, so it belongs to R5 (iteration 2).
- clang-format reflows comment blocks next to new comments. Separate new comments from
existing commented-out code with a blank line, otherwise the diff grows.
### Step 3: rendering through public API, `--graphics-api`, mouse y, shutdown order (2026-10-07)
**Changes:**
- **`src/render/`** (new): `RenderBackend`, `OpenGLRenderBackend`, `VulkanRenderBackend` from
the template (`4a5b090`).
- Changes to the template code: `VRDASH_*` → `OVRAS_*` names, origin note in the file
header, references to `OverlayController`.
- Formatted completely in OVRAS style.
- One comment in `VulkanRenderBackend.h` was wrapped by hand, because the automatic
wrapping had broken its column layout.
- **`OverlayController`:**
- QRhi block replaced by `configureWindow` → `renderControl.initialize` →
`attachWindow` → `createRenderTarget`.
- `renderOverlay()` wraps the frame in `beginFrame`/`finishFrame`. It submits
`vrTextureBounds` + `vrTexture` while the context is current.
- `SetOverlayFromQRhiTexture`, the QRhi members and `QVulkanInstance` are removed.
- Render delay 5 → 16 ms (about 60 fps).
- **Shutdown:** `Shutdown()` runs only once. It calls `VR_Shutdown()`, then frees the
backend's render target (`releaseRenderTarget`, `detachWindow`).
- **Mouse y:** always flipped; the Linux un-flip and its TODO are removed.
- **`openvr_init::renderInterop()`:** Vulkan extensions and HMD GPU from SteamVR.
- **`main.cpp`:** parses `--graphics-api`, then `createRenderBackend` → `initialize` →
`QQuickWindow::setGraphicsApi` → `OverlayController`.
- **`setup.*`:** `--graphics-api opengl|vulkan` (default `opengl`). Unknown or not built in
→ error, exit code `GENERAL_FAILURE`.
- **CMake:**
- `GuiPrivate` is gone.
- The Vulkan backend is built when `QT_FEATURE_vulkan` is set (`OVRAS_HAS_VULKAN`).
**Results (x86_64, Qt 6.11.1):**
- The build succeeds. The Vulkan backend is built in.
- `objdump -T`: only `QVulkanDeviceFunctions::*` / `QVulkanFunctions::*` remain as
`Qt_6_PRIVATE_API`. Acceptance criterion R1 (symbols) is met.
- Warnings 1463 → 1630. The new code itself has none; the additional ones come from
`openvr.h` in the three new translation units.
- **Not tested yet:**
- running both backends, Vulkan validation, the snapshot test (step 5, with the
containers)
- mouse direction (Frame)
**Risk:**
- `MyTextField` opens the SteamVR keyboard on `activeFocus`. The template had to send the
offscreen window an explicit `FocusIn`, otherwise `activeFocusItem` stayed null. The `qt6`
branch doesn't send it. If text fields don't respond on the Frame, that is the cause (R4).
### Step 4: manifest and installation (2026-10-07)
**Changes:**
- **`manifest.vrmanifest`:** `binary_path_linux` and `binary_path_linux_arm` →
`AdvancedSettings_app`.
- **`manifest::handleManifests`:**
- `VR_Init(Utility)`; on `VRInitError_Init_NotAvailableToUtilityApps`,
`VR_Init(Background)`.
- Other errors: message and exit code `OPENVR_INIT_ERROR`, instead of
`initializeOpenVR`'s retry loop.
- **`main.cpp`:** `--graphics-api` is checked before connecting to OpenVR. A wrong value
used to fail only after `initializeOpenVR`'s retry loop.
**Results (x86_64, without SteamVR):**
- `--force-install-manifest` exits right away with `OPENVR_INIT_ERROR` (−2).
- `--graphics-api foo` exits right away with `GENERAL_FAILURE` (−1).
- The build tree contains the binary, `libopenvr_api.so`, both manifests,
`default_action_manifests/` and `res/`.
- Not tested yet: `Background` on the Frame (step 5).
### Step 5: Frame build, package, tests (2026-10-07)
**Changes:**
- **`build_scripts/steamframe/`** (new, see DESIGN.md section 9): containers, toolchain, build,
test, package and deploy scripts.
- **`test/render/`** (new): render test for both backends (`OVRAS_BUILD_TESTS=ON`).
- **`CMakeLists.txt`:**
- option `OVRAS_BUILD_TESTS`
- RPATH only `$ORIGIN` (`BUILD_WITH_INSTALL_RPATH`). Before, the cross binary also had the
host's absolute link directory in its RUNPATH.
- **`.gitignore`:** `dist/`.
**Results:**
- **Render tests:**
- locally (Arch, Qt 6.11, Intel Iris Xe): OpenGL and Vulkan, 4/4 passed each
- in the container (Debian trixie, Qt 6.8, same GPU via `/dev/dri`): 2/2 passed,
**no Vulkan validation messages**
- Proof that the layer was active: `VK_LOADER_DEBUG=layer` shows
"Inserted device layer VK_LAYER_KHRONOS_validation".
- **Cross build (aarch64, Qt 6.8 Debian):** succeeds once the container has `libxt-dev`
(`input_sender_X11.cpp` includes `X11/Intrinsic.h`).
- `Qt_6_PRIVATE_API` without `QVulkan*`: 0 → **R1 symbols met**.
- Qt version tag: `Qt_6.8` (fits the Frame's 6.8.0).
- GLIBC ≤ 2.38 (the Frame has 2.39); GLIBCXX ≤ 3.4.32 (not checked against the Frame).
- NEEDED: Qt6 Core/Gui/Qml/Quick/Widgets/Network/DBus/Multimedia/WebSockets, libX11,
libXtst, libpulse, libopenvr_api, libstdc++/libc.
- **Package** `dist/AdvancedSettings-5.7.4-linux-arm64.tar.gz`: 3.0 MB (9.1 MB unpacked); since the version fix below `…-5.8.17-…`.
The version 5.7.4 comes from `qt6`'s `project()` (master is at 5.8.17).
**Open (needs the Frame):**
- Are the QML modules OVRAS imports present in the Frame's system Qt? QtQuick.Controls
(Material), Layouts, Dialogs, Multimedia.
- Do libXt/libXtst exist on SteamOS? Is libstdc++ new enough?
### Version from `compile_version_string.txt` (2026-10-07)
- **Problem:** `qt6`'s `CMakeLists.txt` had the version hard-coded as 5.7.4. The qmake build
read it from `build_scripts/compile_version_string.txt` (`5.8.17-release`). `master` is
fully merged into `qt6`, so only the number was stale. Consequence: the version check
(comparison with `ver/versioncheck.json`, 5.8.17) would have reported an update.
- **Fix:** CMake reads the file.
- `APPLICATION_VERSION` = full string, as in qmake.
- `project(VERSION)` = numeric part.
- **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).
### 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 |
| T3 | `./run.sh vulkan`: UI correct | R3 | open |
| T4 | Laser right way up, click, scrolling on several pages (both APIs) | R4 | open |
| 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 |
| T8 | Quit app (OVRAS "Exit" and Quit in the SteamVR dashboard): no crash (both APIs) | R2 | open |
| 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).
+170
View File
@@ -0,0 +1,170 @@
# 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-07)
- **Iteration 1 ("native start on the Frame"): implemented, not yet tested on the Frame.**
- 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 (3.0 MB).
- **Next step:** test checklist T1–T11 on the Frame ([PROGRESS.md](PROGRESS.md)), record
the results there, 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` | Restarts with that graphics API |
| `./uninstall.sh` | Unregisters |
| `AdvancedSettings.log` | Log (in the working directory, i.e. this folder) |
Program options (new): `--graphics-api opengl|vulkan` (default `opengl`). 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.
- `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).
- 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).
- **Unverified on the Frame:**
- QML modules: Controls/Material, Layouts, Dialogs, Multimedia.
- libXt/libXtst present? libstdc++ ≥ GLIBCXX_3.4.32?
- **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`.
+77
View File
@@ -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 |
+36 -1
View File
@@ -1,6 +1,7 @@
#include "utils/setup.h"
#include "settings/settings.h"
#include "openvr/ovr_settings_wrapper.h"
#include "render/RenderBackend.h"
#include <QtLogging>
#include <QtDebug>
@@ -44,8 +45,41 @@ int main( int argc, char* argv[] )
commandLineArgs.forceRemoveManifest );
}
// Render backend (render/RenderBackend.h). Chosen before connecting to
// OpenVR, so a wrong --graphics-api fails right away.
GraphicsApi graphicsApi = GraphicsApi::OpenGL;
if ( !parseGraphicsApi( commandLineArgs.graphicsApi, &graphicsApi ) )
{
qCritical() << "Unknown graphics API:" << commandLineArgs.graphicsApi
<< "(expected opengl or vulkan)";
return ReturnErrorCode::GENERAL_FAILURE;
}
std::unique_ptr<RenderBackend> renderBackend
= createRenderBackend( graphicsApi );
if ( !renderBackend )
{
qCritical() << "Graphics API" << commandLineArgs.graphicsApi
<< "is not compiled into this build";
return ReturnErrorCode::GENERAL_FAILURE;
}
openvr_init::initializeOpenVR(
openvr_init::OpenVrInitializationType::Overlay );
// Initialized after OpenVR, because the Vulkan backend needs SteamVR's
// device requirements (openvr_init::renderInterop()), and before
// OverlayController, because QQuickWindow::setGraphicsApi() must run
// before any QQuickWindow exists (OverlayController owns one).
QString renderBackendError;
if ( !renderBackend->initialize( openvr_init::renderInterop(),
&renderBackendError ) )
{
qCritical() << renderBackend->name()
<< "initialization failed:" << renderBackendError;
return ReturnErrorCode::GENERAL_FAILURE;
}
QQuickWindow::setGraphicsApi( renderBackend->graphicsApi() );
try
{
QQmlEngine qmlEngine;
@@ -61,7 +95,8 @@ int main( int argc, char* argv[] )
std::unique_ptr<advsettings::OverlayController> controller(
new advsettings::OverlayController( commandLineArgs.desktopMode,
commandLineArgs.forceNoSound,
qmlEngine ) );
qmlEngine,
std::move( renderBackend ) ) );
//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" );
+71
View File
@@ -6,6 +6,7 @@
#include <QtLogging>
#include <QtDebug>
#include "../openvr/ivrinput_manifest.h"
#include <QByteArray>
namespace openvr_init
{
@@ -136,4 +137,74 @@ void initializeOpenVR( const OpenVrInitializationType initType, int count )
}
}
// Calls an OpenVR "Get...ExtensionsRequired" function and splits its
// space-separated result. `query( buffer, size )` returns the size needed,
// including the terminating zero.
template <typename Query>
static QByteArrayList queryExtensionList( Query query )
{
const uint32_t size = query( nullptr, 0 );
if ( size == 0 )
{
return {};
}
QByteArray buffer( static_cast<int>( size ), '\0' );
query( buffer.data(), size );
QByteArrayList result;
for ( const QByteArray& ext :
buffer.left( buffer.indexOf( '\0' ) ).split( ' ' ) )
{
if ( !ext.isEmpty() )
{
result.append( ext );
}
}
return result;
}
// Same as the template's VrRuntime::renderInterop(). The functions are called
// by the Vulkan backend while it creates its instance and device.
RenderInterop renderInterop()
{
RenderInterop interop;
interop.vulkanInstanceExtensions = []
{
vr::IVRCompositor* compositor = vr::VRCompositor();
if ( !compositor )
{
qWarning() << "IVRCompositor unavailable: cannot query required "
"Vulkan instance extensions";
return QByteArrayList();
}
return queryExtensionList(
[&]( char* buffer, uint32_t size )
{
return compositor->GetVulkanInstanceExtensionsRequired( buffer,
size );
} );
};
interop.vulkanDeviceExtensions = []( VkPhysicalDevice_T* device )
{
vr::IVRCompositor* compositor = vr::VRCompositor();
if ( !compositor )
{
return QByteArrayList();
}
return queryExtensionList(
[&]( char* buffer, uint32_t size )
{
return compositor->GetVulkanDeviceExtensionsRequired(
device, buffer, size );
} );
};
interop.vulkanOutputDevice = []( VkInstance_T* instance )
{
uint64_t device = 0;
vr::VRSystem()->GetOutputDevice(
&device, vr::TextureType_Vulkan, instance );
return reinterpret_cast<VkPhysicalDevice_T*>( device );
};
return interop;
}
} // namespace openvr_init
+7
View File
@@ -1,5 +1,7 @@
#pragma once
#include "../render/RenderBackend.h"
namespace openvr_init
{
enum class OpenVrInitializationType
@@ -10,4 +12,9 @@ enum class OpenVrInitializationType
void initializeOpenVR( const OpenVrInitializationType initType, int count = 0 );
// What SteamVR requires from the render backend's graphics device: the
// Vulkan instance/device extensions and the GPU driving the HMD (Vulkan only;
// OpenGL ignores it). Requires an initialized OpenVR connection.
RenderInterop renderInterop();
} // namespace openvr_init
+96 -127
View File
@@ -28,9 +28,7 @@
#include <qapplication.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <rhi/qrhi.h>
#include <stdexcept>
#include <d3d11.h>
// application namespace
namespace advsettings
@@ -49,10 +47,13 @@ int verifyCustomTickRate( const int tickRate )
return tickRate;
}
OverlayController::OverlayController( bool desktopMode,
bool noSound,
QQmlEngine& qmlEngine )
: QObject(), m_desktopMode( desktopMode ), m_noSound( noSound ),
OverlayController::OverlayController(
bool desktopMode,
bool noSound,
QQmlEngine& qmlEngine,
std::unique_ptr<RenderBackend> renderBackend )
: QObject(), m_renderBackend( std::move( renderBackend ) ),
m_desktopMode( desktopMode ), m_noSound( noSound ),
m_verifiedCustomTickRateMs( verifyCustomTickRate( settings::getSetting(
settings::IntSetting::APPLICATION_customTickRateMs ) ) ),
m_actions(), m_alarm()
@@ -328,8 +329,16 @@ void OverlayController::exitApp()
// Does not fallthrough
}
// Stops ticking and rendering, disconnects from SteamVR and frees the render
// target. Called by exitApp() and by the destructor; runs only once.
void OverlayController::Shutdown()
{
if ( m_shutdownDone )
{
return;
}
m_shutdownDone = true;
disconnect( &m_pumpEventsTimer,
SIGNAL( timeout() ),
this,
@@ -353,9 +362,21 @@ void OverlayController::Shutdown()
m_pRenderTimer->stop();
m_pRenderTimer.reset();
}
m_render_pass_descriptor.reset();
m_render_target.reset();
m_pFBTexture.reset();
// Order: OpenVR shuts down while the graphics device still exists, then
// the render target goes. OpenVR's client holds objects on the device of
// the submitted textures; destroying the device first crashed on the
// Steam Frame with Vulkan (openvr.h: call VR_Shutdown before destroying
// these resources). OVRAS ends with exit(), so this is the only place
// where VR_Shutdown runs in overlay mode.
vr::VR_Shutdown();
if ( m_renderBackend && m_renderBackend->makeCurrent() )
{
// Frees what the backend created on Qt's device/context, before Qt
// destroys it together with m_renderControl.
m_renderBackend->releaseRenderTarget();
m_renderBackend->detachWindow();
}
}
void OverlayController::SetWidget( QQuickItem* quickItem,
@@ -404,52 +425,31 @@ void OverlayController::SetWidget( QQuickItem* quickItem,
m_pRenderTimer.reset( new QTimer() );
m_pRenderTimer->moveToThread( qApp->thread() );
m_pRenderTimer->setSingleShot( true );
m_pRenderTimer->setInterval( 5 );
// 16 ms: a frame starts at most 16 ms after a change, about 60 fps.
m_pRenderTimer->setInterval( 16 );
connect( m_pRenderTimer.get(),
SIGNAL( timeout() ),
this,
SLOT( renderOverlay() ) );
#if QT_CONFIG( vulkan )
if ( m_window.graphicsApi() == QSGRendererInterface::Vulkan )
{
m_vulkanInstance.reset( new QVulkanInstance );
m_vulkanInstance->setExtensions(
QQuickGraphicsConfiguration::preferredInstanceExtensions() );
if ( !m_vulkanInstance->create() )
throw std::runtime_error( "Cannot create vulkan instance" );
m_window.setVulkanInstance( m_vulkanInstance.get() );
}
#endif
// Render target through the render backend (public Qt API only).
// Call order: see render/RenderBackend.h.
m_renderBackend->configureWindow( &m_window );
if ( !m_renderBackend->makeCurrent() )
throw std::runtime_error( "render backend: makeCurrent failed" );
if ( !m_renderControl.initialize() )
throw std::runtime_error( "could not initialize m_renderControl" );
QRhi* rhi = this->rhi();
QString renderError;
if ( !m_renderBackend->attachWindow( &m_window, &renderError ) )
throw std::runtime_error( renderError.toStdString() );
qInfo() << "Rendering with" << m_renderBackend->name() << "on"
<< m_renderBackend->deviceName();
qInfo() << "Started with" << rhi->backendName();
m_pFBTexture.reset( rhi->newTexture(
QRhiTexture::RGBA8,
quickItem->size().toSize(),
1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource ) );
if ( !m_pFBTexture->create() )
throw std::runtime_error( "RhiTexture not created" );
m_render_target.reset(
rhi->newTextureRenderTarget( { m_pFBTexture.get() } ) );
m_render_pass_descriptor.reset(
m_render_target->newCompatibleRenderPassDescriptor() );
m_render_target->setRenderPassDescriptor(
m_render_pass_descriptor.get() );
if ( !m_render_target->create() )
throw std::runtime_error( "failed to create render target" );
auto qqrt
= QQuickRenderTarget::fromRhiRenderTarget( m_render_target.get() );
m_window.setRenderTarget( qqrt );
const QQuickRenderTarget renderTarget
= m_renderBackend->createRenderTarget( quickItem->size().toSize() );
if ( renderTarget.isNull() )
throw std::runtime_error( "could not create render target" );
m_window.setRenderTarget( renderTarget );
quickItem->setParentItem( m_window.contentItem() );
m_window.setGeometry( 0,
0,
@@ -477,9 +477,7 @@ void OverlayController::SetWidget( QQuickItem* quickItem,
this,
SLOT( OnTimeoutPumpEvents() ) );
// Every 1ms we check if the current frame has advanced (for vsync)
m_pumpEventsTimer.setInterval( 1 );
applyTickInterval();
m_pumpEventsTimer.start();
m_steamVRTabController.initStage2( this );
@@ -521,21 +519,25 @@ void OverlayController::SetWidget( QQuickItem* quickItem,
auto ctrlRole = info.second;
if ( vr::TrackedDeviceClass_HMD == ctrlRole )
{
LOG(INFO) << "HMD is: " << ovr_system_wrapper::getDeviceName(device);
qInfo() << "HMD is: "
<< ovr_system_wrapper::getDeviceName( device );
continue;
}
if ( vr::TrackedControllerRole_LeftHand == ctrlRole )
{
LOG(INFO) << "Controller (L) is: " << ovr_system_wrapper::getDeviceName(device);
qInfo() << "Controller (L) is: "
<< ovr_system_wrapper::getDeviceName( device );
continue;
}
if ( vr::TrackedControllerRole_RightHand == ctrlRole )
{
LOG(INFO) << "Controller (R) is: " << ovr_system_wrapper::getDeviceName(device);
qInfo() << "Controller (R) is: "
<< ovr_system_wrapper::getDeviceName( device );
continue;
}
}
LOG(INFO)<< "Initilizaion is complete--------------------------------------------------";
qInfo() << "Initilizaion is "
"complete--------------------------------------------------";
}
void OverlayController::OnRenderRequest()
@@ -546,59 +548,6 @@ void OverlayController::OnRenderRequest()
}
}
void OverlayController::SetOverlayFromQRhiTexture( QRhiTexture& tex )
{
vr::Texture_t vrTex = {};
QRhi* rhi = this->rhi();
switch ( rhi->backend() )
{
case QRhi::OpenGLES2:
{
vrTex.handle = reinterpret_cast<void*>( tex.nativeTexture().object );
vrTex.eType = vr::TextureType_OpenGL;
vrTex.eColorSpace = vr::ColorSpace_Auto;
}
break;
case QRhi::D3D11:
{
vrTex.handle = reinterpret_cast<ID3D11Texture2D *>( tex.nativeTexture().object );
vrTex.eType = vr::TextureType_DirectX;
vrTex.eColorSpace = vr::ColorSpace_Auto;
}
break;
#if QT_CONFIG( vulkan )
case QRhi::Vulkan:
{
const QRhiVulkanNativeHandles* vulkan_handles
= reinterpret_cast<const QRhiVulkanNativeHandles*>(
rhi->nativeHandles() );
vr::VRVulkanTextureData_t vulkan = {
.m_nImage = tex.nativeTexture().object,
.m_pDevice = vulkan_handles->dev,
.m_pPhysicalDevice = vulkan_handles->physDev,
.m_pInstance = vulkan_handles->inst->vkInstance(),
.m_pQueue = vulkan_handles->gfxQueue,
.m_nQueueFamilyIndex = vulkan_handles->gfxQueueIdx,
.m_nWidth = static_cast<uint32_t>( tex.pixelSize().width() ),
.m_nHeight = static_cast<uint32_t>( tex.pixelSize().height() ),
.m_nFormat = static_cast<uint32_t>(tex.format()),
.m_nSampleCount = static_cast<uint32_t>( tex.sampleCount() ),
};
vrTex.handle = &vulkan;
vrTex.eType = vr::TextureType_Vulkan;
vrTex.eColorSpace = vr::ColorSpace_Auto;
}
break;
#endif
default:
qFatal() << "unimplemented backend:" << rhi->backend();
throw std::runtime_error( "unimplemented backend" );
}
vr::VROverlay()->SetOverlayTexture( m_ulOverlayHandle, &vrTex );
}
void OverlayController::renderOverlay()
{
if ( !m_desktopMode )
@@ -609,13 +558,24 @@ void OverlayController::renderOverlay()
&& !vr::VROverlay()->IsOverlayVisible(
m_ulOverlayThumbnailHandle ) ) )
return;
// OpenGL: the context must be current while rendering and while
// handing the texture to SteamVR.
if ( !m_renderBackend->makeCurrent() )
return;
// The backends put the texture into the state Qt expects
// (beginFrame) and the one SteamVR expects (finishFrame).
m_renderBackend->beginFrame();
m_renderControl.polishItems();
m_renderControl.beginFrame();
m_renderControl.sync();
m_renderControl.render();
m_renderControl.endFrame();
m_renderBackend->finishFrame();
SetOverlayFromQRhiTexture( *m_pFBTexture );
vr::VRTextureBounds_t bounds = m_renderBackend->vrTextureBounds();
vr::VROverlay()->SetOverlayTextureBounds( m_ulOverlayHandle, &bounds );
vr::Texture_t texture = m_renderBackend->vrTexture();
vr::VROverlay()->SetOverlayTexture( m_ulOverlayHandle, &texture );
}
}
@@ -635,12 +595,10 @@ bool OverlayController::pollNextEvent( vr::VROverlayHandle_t ulOverlayHandle,
QPointF OverlayController::getMousePositionForEvent( vr::VREvent_Mouse_t mouse )
{
//TODO temp mouse is negative??
float y = m_window.height() - mouse.y;
#ifdef __linux__
float h = static_cast<float>( m_window.height() );
y = h - y;
#endif
// SteamVR reports mouse coordinates in GL convention (y counted from the
// bottom) on every platform and for every graphics API (verified on the
// Steam Frame with OpenGL and Vulkan); Qt counts y from the top.
float y = static_cast<float>( m_window.height() ) - mouse.y;
return { mouse.x, y };
}
@@ -956,6 +914,8 @@ void OverlayController::setVsyncDisabled( bool value, bool notify )
{
settings::setSetting( settings::BoolSetting::APPLICATION_vsyncDisabled,
value );
// The timer interval depends on the mode, see applyTickInterval().
applyTickInterval();
if ( notify )
{
emit vsyncDisabledChanged( value );
@@ -1066,6 +1026,8 @@ void OverlayController::setCustomTickRateMs( int value, bool notify )
settings::setSetting( settings::IntSetting::APPLICATION_customTickRateMs,
verifiedTickRate );
m_verifiedCustomTickRateMs = verifiedTickRate;
// In "vsync disabled" mode the timer runs at this interval.
applyTickInterval();
if ( notify )
{
@@ -1073,6 +1035,19 @@ void OverlayController::setCustomTickRateMs( int value, bool notify )
}
}
// Sets the interval of m_pumpEventsTimer for the current tick mode:
// - vsync: every 1 ms, OnTimeoutPumpEvents() checks whether the compositor
// started a new frame.
// - vsync disabled: the timer fires directly at the custom tick rate, so the
// process wakes up once per tick instead of every 1 ms.
// QTimer::setInterval() also restarts a running timer with the new interval.
void OverlayController::applyTickInterval()
{
m_pumpEventsTimer.setTimerType( Qt::PreciseTimer );
m_pumpEventsTimer.setInterval( vsyncDisabled() ? customTickRateMs() : 1 );
}
// Called by m_pumpEventsTimer, see applyTickInterval().
// vsync implementation:
// this function triggers every 1ms
// this function should remain lightweight and only check if it's time to
@@ -1081,17 +1056,10 @@ void OverlayController::OnTimeoutPumpEvents()
{
if ( vsyncDisabled() )
{
// check if it's time for a custom tick rate tick
if ( m_customTickRateCounter > customTickRateMs() )
{
mainEventLoop();
m_customTickRateCounter = 0;
updateRate.incrementCounter();
}
else
{
m_customTickRateCounter++;
}
// The timer already runs at the custom tick rate: every call is a
// tick.
mainEventLoop();
updateRate.incrementCounter();
}
// vsync is enabled
@@ -1425,7 +1393,7 @@ void OverlayController::RotateUniverseCenter(
// }
vr::HmdMatrix34_t curPos;
auto calState = vr::VRChaperone()->GetCalibrationState();
LOG( INFO ) << "Calibration State on Rotate is: " << calState;
qInfo() << "Calibration State on Rotate is: " << calState;
if ( universe == vr::TrackingUniverseStanding )
{
vr::VRChaperoneSetup()->GetWorkingStandingZeroPoseToRawTrackingPose(
@@ -1440,7 +1408,8 @@ void OverlayController::RotateUniverseCenter(
{
//TODO 8/3/26, will see if this causes issues
//should exclusively happen with openxr
LOG(INFO) << "rotating on undefined tracking universe probably openxr";
qInfo()
<< "rotating on undefined tracking universe probably openxr";
vr::VRChaperoneSetup()->GetLiveSeatedZeroPoseToRawTrackingPose(&curPos);
}
+15 -14
View File
@@ -25,7 +25,7 @@
#include <QQuickRenderTarget>
#include <memory>
#include <optional>
#include <rhi/qrhi.h>
#include "render/RenderBackend.h"
#include "openvr/openvr_init.h"
@@ -108,16 +108,17 @@ private:
vr::VROverlayHandle_t m_ulOverlayThumbnailHandle
= vr::k_ulOverlayHandleInvalid;
#if QT_CONFIG( vulkan )
std::unique_ptr<QVulkanInstance> m_vulkanInstance = nullptr;
#endif
// Renders the QML scene into a texture for SteamVR (OpenGL or Vulkan),
// using public Qt API only. Declared before m_renderControl and m_window:
// Qt's device/context is created from the backend's context/instance, so
// the backend must be destroyed after them.
std::unique_ptr<RenderBackend> m_renderBackend;
QQuickRenderControl m_renderControl;
QQuickWindow m_window{ &m_renderControl };
std::optional<vr::ETextureType> m_cached_vr_texture_type;
std::unique_ptr<QRhiTexture> m_pFBTexture;
std::unique_ptr<QRhiTextureRenderTarget> m_render_target;
std::unique_ptr<QRhiRenderPassDescriptor> m_render_pass_descriptor;
// Set by Shutdown(), which must run only once (see there).
bool m_shutdownDone = false;
/*
std::unique_ptr<QOpenGLFramebufferObject> m_pFbo;
@@ -158,7 +159,6 @@ private:
uint64_t m_currentFrame = 0;
uint64_t m_lastFrame = 0;
int m_vsyncTooLateCounter = 0;
int m_customTickRateCounter = 0;
int m_verifiedCustomTickRateMs = 0;
input::SteamIVRInput m_actions;
@@ -195,18 +195,19 @@ private:
void processRotationBindings();
void processExclusiveInputBinding();
inline QRhi* rhi()
{
return m_renderControl.rhi();
}
void applyTickInterval();
bool m_exclusiveState = false;
bool m_keyPressOneState = false;
bool m_keyPressTwoState = false;
public:
void SetOverlayFromQRhiTexture( QRhiTexture& tex );
OverlayController( bool desktopMode, bool noSound, QQmlEngine& qmlEngine );
// renderBackend: initialized, and its graphics API already set with
// QQuickWindow::setGraphicsApi() (must happen before m_window exists).
OverlayController( bool desktopMode,
bool noSound,
QQmlEngine& qmlEngine,
std::unique_ptr<RenderBackend> renderBackend );
virtual ~OverlayController();
void Shutdown();
+2
View File
@@ -4,6 +4,8 @@
"app_key": "steam.overlay.1009850",
"launch_type": "binary",
"binary_path_windows": "AdvancedSettings.exe",
"binary_path_linux": "AdvancedSettings_app",
"binary_path_linux_arm": "AdvancedSettings_app",
"is_dashboard_overlay": true,
"strings": {
+180
View File
@@ -0,0 +1,180 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#include "OpenGLRenderBackend.h"
#include <QOpenGLFunctions>
#include <QQuickGraphicsDevice>
#include <QQuickWindow>
#include <QSurfaceFormat>
#include <cstdint>
OpenGLRenderBackend::~OpenGLRenderBackend()
{
releaseRenderTarget();
}
bool OpenGLRenderBackend::initialize( const RenderInterop&, QString* error )
{
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
format.setDepthBufferSize( 24 );
format.setStencilBufferSize( 8 );
m_context = std::make_unique<QOpenGLContext>();
m_context->setFormat( format );
if ( !m_context->create() )
{
if ( error )
*error = QStringLiteral( "Failed to create OpenGL context" );
return false;
}
m_surface = std::make_unique<QOffscreenSurface>();
m_surface->setFormat( m_context->format() );
m_surface->create();
if ( !m_surface->isValid() )
{
if ( error )
*error = QStringLiteral( "Failed to create offscreen surface" );
return false;
}
return true;
}
void OpenGLRenderBackend::configureWindow( QQuickWindow* window )
{
window->setGraphicsDevice(
QQuickGraphicsDevice::fromOpenGLContext( m_context.get() ) );
}
bool OpenGLRenderBackend::attachWindow( QQuickWindow*, QString* error )
{
if ( !makeCurrent() )
{
*error = QStringLiteral( "makeCurrent failed" );
return false;
}
QOpenGLFunctions* f = m_context->functions();
m_deviceName = QStringLiteral( "%1 (%2)" )
.arg( QString::fromLatin1( reinterpret_cast<const char*>(
f->glGetString( GL_RENDERER ) ) ),
QString::fromLatin1( reinterpret_cast<const char*>(
f->glGetString( GL_VERSION ) ) ) );
return true;
}
bool OpenGLRenderBackend::makeCurrent()
{
return m_context && m_context->makeCurrent( m_surface.get() );
}
void OpenGLRenderBackend::doneCurrent()
{
if ( m_context )
m_context->doneCurrent();
}
QQuickRenderTarget OpenGLRenderBackend::createRenderTarget( QSize size )
{
releaseRenderTarget();
if ( !makeCurrent() )
return {};
QOpenGLFunctions* f = m_context->functions();
GLuint texture = 0;
f->glGenTextures( 1, &texture );
f->glBindTexture( GL_TEXTURE_2D, texture );
f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
f->glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
f->glTexImage2D( GL_TEXTURE_2D,
0,
GL_RGBA8,
size.width(),
size.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
nullptr );
f->glBindTexture( GL_TEXTURE_2D, 0 );
m_texture = texture;
m_size = size;
return QQuickRenderTarget::fromOpenGLTexture( m_texture, size );
}
void OpenGLRenderBackend::releaseRenderTarget()
{
if ( m_texture && makeCurrent() )
{
GLuint texture = m_texture;
m_context->functions()->glDeleteTextures( 1, &texture );
}
m_texture = 0;
m_size = {};
}
void OpenGLRenderBackend::finishFrame()
{
// SteamVR reads the texture from another process; make sure all
// rendering commands have been submitted before the handle is passed
// on.
m_context->functions()->glFlush();
}
void OpenGLRenderBackend::waitForGpu()
{
if ( makeCurrent() )
m_context->functions()->glFinish();
}
vr::Texture_t OpenGLRenderBackend::vrTexture()
{
// For TextureType_OpenGL the handle *is* the GL texture name, cast to a
// pointer.
return vr::Texture_t{ reinterpret_cast<void*>(
static_cast<uintptr_t>( m_texture ) ),
vr::TextureType_OpenGL,
vr::ColorSpace_Auto };
}
vr::VRTextureBounds_t OpenGLRenderBackend::vrTextureBounds() const
{
// Qt Quick renders into GL textures bottom-up (GL convention) and SteamVR
// treats TextureType_OpenGL accordingly: no flip needed (verified on the
// Steam Frame).
return vr::VRTextureBounds_t{ 0.f, 0.f, 1.f, 1.f };
}
QImage OpenGLRenderBackend::readback()
{
if ( !m_texture || !makeCurrent() )
return {};
QOpenGLFunctions* f = m_context->functions();
GLuint fbo = 0;
f->glGenFramebuffers( 1, &fbo );
f->glBindFramebuffer( GL_FRAMEBUFFER, fbo );
f->glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0 );
QImage image( m_size, QImage::Format_RGBA8888_Premultiplied );
f->glReadPixels( 0,
0,
m_size.width(),
m_size.height(),
GL_RGBA,
GL_UNSIGNED_BYTE,
image.bits() );
f->glBindFramebuffer( GL_FRAMEBUFFER,
m_context->defaultFramebufferObject() );
f->glDeleteFramebuffers( 1, &fbo );
// GL rows are bottom-up.
#if QT_VERSION >= QT_VERSION_CHECK( 6, 9, 0 )
return image.flipped( Qt::Vertical );
#else
return image.mirrored( false, true );
#endif
}
+57
View File
@@ -0,0 +1,57 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#pragma once
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include "RenderBackend.h"
class OpenGLRenderBackend final : public RenderBackend
{
public:
OpenGLRenderBackend() = default;
~OpenGLRenderBackend() override;
QString name() const override
{
return QStringLiteral( "OpenGL" );
}
QSGRendererInterface::GraphicsApi graphicsApi() const override
{
return QSGRendererInterface::OpenGL;
}
bool initialize( const RenderInterop& interop, QString* error ) override;
void configureWindow( QQuickWindow* window ) override;
bool attachWindow( QQuickWindow* window, QString* error ) override;
void detachWindow() override
{
releaseRenderTarget();
}
QString deviceName() const override
{
return m_deviceName;
}
bool makeCurrent() override;
void doneCurrent() override;
QQuickRenderTarget createRenderTarget( QSize size ) override;
void releaseRenderTarget() override;
void beginFrame() override {}
void finishFrame() override;
void waitForGpu() override;
vr::Texture_t vrTexture() override;
vr::VRTextureBounds_t vrTextureBounds() const override;
QImage readback() override;
private:
std::unique_ptr<QOpenGLContext> m_context;
std::unique_ptr<QOffscreenSurface> m_surface;
unsigned int m_texture = 0; // GLuint
QSize m_size;
QString m_deviceName;
};
+38
View File
@@ -0,0 +1,38 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#include "RenderBackend.h"
#include "OpenGLRenderBackend.h"
#if OVRAS_HAS_VULKAN
# include "VulkanRenderBackend.h"
#endif
std::unique_ptr<RenderBackend> createRenderBackend( GraphicsApi api )
{
switch ( api )
{
case GraphicsApi::OpenGL:
return std::make_unique<OpenGLRenderBackend>();
case GraphicsApi::Vulkan:
#if OVRAS_HAS_VULKAN
return std::make_unique<VulkanRenderBackend>();
#else
return nullptr; // built without Vulkan support
#endif
}
return nullptr;
}
bool parseGraphicsApi( const QString& name, GraphicsApi* api )
{
const QString lower = name.toLower();
if ( lower == QLatin1String( "opengl" ) || lower == QLatin1String( "gl" ) )
*api = GraphicsApi::OpenGL;
else if ( lower == QLatin1String( "vulkan" )
|| lower == QLatin1String( "vk" ) )
*api = GraphicsApi::Vulkan;
else
return false;
return true;
}
+116
View File
@@ -0,0 +1,116 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#pragma once
#include <openvr.h>
#include <QByteArrayList>
#include <QImage>
#include <QQuickRenderTarget>
#include <QSGRendererInterface>
#include <QSize>
#include <QString>
#include <functional>
#include <memory>
class QQuickWindow;
// Requirements the VR runtime imposes on the graphics device. All optional:
// without them (snapshot mode, tests) backends pick defaults.
struct RenderInterop
{
std::function<QByteArrayList()> vulkanInstanceExtensions;
std::function<QByteArrayList( VkPhysicalDevice_T* )> vulkanDeviceExtensions;
// The physical device driving the HMD, for a given VkInstance.
std::function<VkPhysicalDevice_T*( VkInstance_T* )> vulkanOutputDevice;
};
// Abstraction over the graphics API used to render the Qt Quick scene into a
// texture that SteamVR can consume. Everything API-specific (context/device
// setup, texture allocation, layout transitions, handing the texture to OpenVR,
// CPU readback) lives behind this interface; OverlayController and the
// VR/input code stay API-agnostic.
//
// Call order (driven by OverlayController):
// initialize → configureWindow → [QQuickRenderControl::initialize] →
// attachWindow → createRenderTarget → per frame: beginFrame … [Qt renders] …
// finishFrame → vrTexture → releaseRenderTarget → detachWindow → [Qt destroys
// its device/context]
class RenderBackend
{
public:
virtual ~RenderBackend() = default;
virtual QString name() const = 0;
// Must be applied via QQuickWindow::setGraphicsApi() before any
// QQuickWindow exists.
virtual QSGRendererInterface::GraphicsApi graphicsApi() const = 0;
// Creates API-level objects (context / instance). Requires a
// QGuiApplication.
virtual bool initialize( const RenderInterop& interop, QString* error ) = 0;
// Tells Qt Quick which device/context/instance to use. Before
// QQuickRenderControl::initialize().
virtual void configureWindow( QQuickWindow* window ) = 0;
// Picks up objects Qt created during QQuickRenderControl::initialize()
// (e.g. VkDevice).
virtual bool attachWindow( QQuickWindow* window, QString* error ) = 0;
// Releases everything created on Qt's device; called before Qt destroys
// it.
virtual void detachWindow() = 0;
// Human-readable GPU/driver description (valid after attachWindow).
virtual QString deviceName() const = 0;
// Bind the API context to the current thread (no-op for APIs without
// one).
virtual bool makeCurrent() = 0;
virtual void doneCurrent() = 0;
// (Re)allocates the shared color texture; returns the target for
// QQuickWindow::setRenderTarget.
virtual QQuickRenderTarget createRenderTarget( QSize size ) = 0;
// Frees the texture. Called before Qt's device/context goes away.
virtual void releaseRenderTarget() = 0;
// Before Qt renders a frame / after QQuickRenderControl::endFrame().
// Backends put the texture into the state Qt resp. SteamVR expects
// (layouts, flushes).
virtual void beginFrame() = 0;
virtual void finishFrame() = 0;
// Blocks until the GPU has finished all submitted work (benchmarks:
// makes the asynchronous OpenGL path comparable to the synchronous
// Vulkan one).
virtual void waitForGpu() = 0;
// Texture description for IVROverlay::SetOverlayTexture. Only valid
// while the render target exists; must be submitted with the context
// current (see makeCurrent()).
virtual vr::Texture_t vrTexture() = 0;
// UV bounds for IVROverlay::SetOverlayTextureBounds (compensates
// API-specific origin).
virtual vr::VRTextureBounds_t vrTextureBounds() const = 0;
// Reads the last finished frame back to the CPU (snapshots, tests).
// Slow.
virtual QImage readback() = 0;
};
enum class GraphicsApi
{
OpenGL,
Vulkan
};
// Returns nullptr if the API was not compiled in (Vulkan is optional, see
// OVRAS_HAS_VULKAN).
std::unique_ptr<RenderBackend> createRenderBackend( GraphicsApi api );
// "opengl" / "vulkan" (case-insensitive). Returns false for unknown names.
bool parseGraphicsApi( const QString& name, GraphicsApi* api );
+461
View File
@@ -0,0 +1,461 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#include "VulkanRenderBackend.h"
#include <QQuickGraphicsConfiguration>
#include <QQuickGraphicsDevice>
#include <QQuickWindow>
#include <QVersionNumber>
#include <QVulkanDeviceFunctions>
#include <QVulkanFunctions>
#include <cstring>
namespace
{
constexpr VkFormat kFormat = VK_FORMAT_R8G8B8A8_UNORM;
}
VulkanRenderBackend::~VulkanRenderBackend()
{
// The VkDevice belongs to Qt Quick; everything we created on it must
// already be gone.
Q_ASSERT( m_commandPool == VK_NULL_HANDLE && m_image == VK_NULL_HANDLE );
}
void VulkanRenderBackend::detachWindow()
{
releaseRenderTarget(); // waits for the device to be idle
if ( m_df )
{
for ( Submission* submission : { &m_restore, &m_handoff } )
{
if ( submission->fence != VK_NULL_HANDLE )
m_df->vkDestroyFence( m_device, submission->fence, nullptr );
*submission = {};
}
// Destroying the pool also frees its command buffers.
if ( m_commandPool != VK_NULL_HANDLE )
m_df->vkDestroyCommandPool( m_device, m_commandPool, nullptr );
}
m_commandPool = VK_NULL_HANDLE;
m_df = nullptr;
m_device = VK_NULL_HANDLE;
m_queue = VK_NULL_HANDLE;
}
bool VulkanRenderBackend::initialize( const RenderInterop& interop,
QString* error )
{
QByteArrayList instanceExtensions
= QQuickGraphicsConfiguration::preferredInstanceExtensions();
if ( interop.vulkanInstanceExtensions )
{
for ( const QByteArray& ext : interop.vulkanInstanceExtensions() )
{
if ( !instanceExtensions.contains( ext ) )
instanceExtensions.append( ext );
}
}
m_instance.setExtensions( instanceExtensions );
// QVulkanInstance defaults to Vulkan 1.0; request what the loader
// supports (capped at 1.3) so Qt can rely on core 1.1+ features instead
// of half-enabled extensions.
const QVersionNumber supported = m_instance.supportedApiVersion();
m_instance.setApiVersion( qMin( supported, QVersionNumber( 1, 3 ) ) );
if ( qEnvironmentVariableIntValue( "OVRAS_VULKAN_VALIDATION" ) )
m_instance.setLayers( { "VK_LAYER_KHRONOS_validation" } );
if ( !m_instance.create() )
{
*error
= QStringLiteral( "Failed to create Vulkan instance (VkResult %1)" )
.arg( m_instance.errorCode() );
return false;
}
// Use the GPU SteamVR renders the HMD with; otherwise Qt picks one.
if ( interop.vulkanOutputDevice )
m_physicalDevice
= interop.vulkanOutputDevice( m_instance.vkInstance() );
if ( m_physicalDevice != VK_NULL_HANDLE && interop.vulkanDeviceExtensions )
m_deviceExtensions = interop.vulkanDeviceExtensions( m_physicalDevice );
return true;
}
void VulkanRenderBackend::configureWindow( QQuickWindow* window )
{
window->setVulkanInstance( &m_instance );
if ( m_physicalDevice != VK_NULL_HANDLE )
window->setGraphicsDevice(
QQuickGraphicsDevice::fromPhysicalDevice( m_physicalDevice ) );
QQuickGraphicsConfiguration config = window->graphicsConfiguration();
config.setDeviceExtensions( m_deviceExtensions );
window->setGraphicsConfiguration( config );
}
bool VulkanRenderBackend::attachWindow( QQuickWindow* window, QString* error )
{
QSGRendererInterface* rif = window->rendererInterface();
auto resource = [&]( QSGRendererInterface::Resource r )
{ return rif->getResource( window, r ); };
auto* device = static_cast<VkDevice*>(
resource( QSGRendererInterface::DeviceResource ) );
auto* physicalDevice = static_cast<VkPhysicalDevice*>(
resource( QSGRendererInterface::PhysicalDeviceResource ) );
auto* queue = static_cast<VkQueue*>(
resource( QSGRendererInterface::CommandQueueResource ) );
auto* family = static_cast<uint32_t*>(
resource( QSGRendererInterface::GraphicsQueueFamilyIndexResource ) );
if ( !device || !physicalDevice || !queue || !family )
{
*error = QStringLiteral(
"Qt Quick did not expose its Vulkan device objects" );
return false;
}
m_device = *device;
m_physicalDevice = *physicalDevice;
m_queue = *queue;
m_queueFamilyIndex = *family;
m_df = m_instance.deviceFunctions( m_device );
VkPhysicalDeviceProperties props{};
m_instance.functions()->vkGetPhysicalDeviceProperties( m_physicalDevice,
&props );
m_deviceName = QStringLiteral( "%1 (Vulkan %2.%3.%4)" )
.arg( QString::fromUtf8( props.deviceName ) )
.arg( VK_API_VERSION_MAJOR( props.apiVersion ) )
.arg( VK_API_VERSION_MINOR( props.apiVersion ) )
.arg( VK_API_VERSION_PATCH( props.apiVersion ) );
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = m_queueFamilyIndex;
if ( m_df->vkCreateCommandPool(
m_device, &poolInfo, nullptr, &m_commandPool )
!= VK_SUCCESS )
{
*error = QStringLiteral( "vkCreateCommandPool failed" );
return false;
}
for ( Submission* submission : { &m_restore, &m_handoff } )
{
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
if ( m_df->vkAllocateCommandBuffers(
m_device, &allocInfo, &submission->commandBuffer )
!= VK_SUCCESS
|| m_df->vkCreateFence(
m_device, &fenceInfo, nullptr, &submission->fence )
!= VK_SUCCESS )
{
*error
= QStringLiteral( "Allocating command buffers/fences failed" );
return false;
}
}
return true;
}
bool VulkanRenderBackend::findMemoryType( uint32_t typeBits,
VkMemoryPropertyFlags properties,
uint32_t* index ) const
{
VkPhysicalDeviceMemoryProperties memProps{};
m_instance.functions()->vkGetPhysicalDeviceMemoryProperties(
m_physicalDevice, &memProps );
for ( uint32_t i = 0; i < memProps.memoryTypeCount; ++i )
{
if ( ( typeBits & ( 1u << i ) )
&& ( memProps.memoryTypes[i].propertyFlags & properties )
== properties )
{
*index = i;
return true;
}
}
return false;
}
QQuickRenderTarget VulkanRenderBackend::createRenderTarget( QSize size )
{
releaseRenderTarget();
if ( !m_df )
return {};
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = kFormat;
imageInfo.extent
= { uint32_t( size.width() ), uint32_t( size.height() ), 1 };
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
// COLOR_ATTACHMENT for Qt; TRANSFER_SRC | SAMPLED are required by
// OpenVR.
imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
| VK_IMAGE_USAGE_TRANSFER_SRC_BIT
| VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if ( m_df->vkCreateImage( m_device, &imageInfo, nullptr, &m_image )
!= VK_SUCCESS )
return {};
VkMemoryRequirements req{};
m_df->vkGetImageMemoryRequirements( m_device, m_image, &req );
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = req.size;
if ( !findMemoryType( req.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
&allocInfo.memoryTypeIndex )
|| m_df->vkAllocateMemory(
m_device, &allocInfo, nullptr, &m_imageMemory )
!= VK_SUCCESS
|| m_df->vkBindImageMemory( m_device, m_image, m_imageMemory, 0 )
!= VK_SUCCESS )
{
releaseRenderTarget();
return {};
}
m_size = size;
m_layout = VK_IMAGE_LAYOUT_UNDEFINED;
// Qt transitions UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL in the first
// frame.
return QQuickRenderTarget::fromVulkanImage(
m_image, VK_IMAGE_LAYOUT_UNDEFINED, kFormat, size );
}
void VulkanRenderBackend::releaseRenderTarget()
{
if ( !m_df )
return;
m_df->vkDeviceWaitIdle( m_device );
if ( m_image != VK_NULL_HANDLE )
m_df->vkDestroyImage( m_device, m_image, nullptr );
if ( m_imageMemory != VK_NULL_HANDLE )
m_df->vkFreeMemory( m_device, m_imageMemory, nullptr );
m_image = VK_NULL_HANDLE;
m_imageMemory = VK_NULL_HANDLE;
m_layout = VK_IMAGE_LAYOUT_UNDEFINED;
m_size = {};
}
bool VulkanRenderBackend::waitFor( Submission& submission )
{
if ( !submission.pending )
return true;
submission.pending = false;
const bool ok = m_df->vkWaitForFences(
m_device, 1, &submission.fence, VK_TRUE, UINT64_MAX )
== VK_SUCCESS;
m_df->vkResetFences( m_device, 1, &submission.fence );
return ok;
}
bool VulkanRenderBackend::submit(
Submission& submission,
const std::function<void( VkCommandBuffer )>& record,
bool wait )
{
if ( !waitFor( submission ) ) // the command buffer may still be in use
return false;
m_df->vkResetCommandBuffer( submission.commandBuffer, 0 );
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
m_df->vkBeginCommandBuffer( submission.commandBuffer, &beginInfo );
record( submission.commandBuffer );
m_df->vkEndCommandBuffer( submission.commandBuffer );
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &submission.commandBuffer;
if ( m_df->vkQueueSubmit( m_queue, 1, &submitInfo, submission.fence )
!= VK_SUCCESS )
return false;
submission.pending = true;
return wait ? waitFor( submission ) : true;
}
void VulkanRenderBackend::transitionImage( Submission& submission,
VkImageLayout from,
VkImageLayout to,
bool wait )
{
// Only two transitions exist: rendered -> hand-off and back.
const bool toTransfer = to == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
const VkAccessFlags colorWrite = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
const VkAccessFlags transferRead = VK_ACCESS_TRANSFER_READ_BIT;
const VkPipelineStageFlags colorStage
= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
const VkPipelineStageFlags transferStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = toTransfer ? colorWrite : transferRead;
barrier.dstAccessMask = toTransfer ? transferRead : colorWrite;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = m_image;
barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
const VkPipelineStageFlags srcStage
= toTransfer ? colorStage : transferStage;
const VkPipelineStageFlags dstStage
= toTransfer ? transferStage : colorStage;
submit(
submission,
[&]( VkCommandBuffer cb )
{
m_df->vkCmdPipelineBarrier( cb,
srcStage,
dstStage,
0,
0,
nullptr,
0,
nullptr,
1,
&barrier );
},
wait );
m_layout = to;
}
void VulkanRenderBackend::beginFrame()
{
// Hand the image back to Qt in the layout its tracking assumes (after
// the first frame). No CPU wait needed: Qt's frame is submitted to the
// same queue afterwards.
if ( m_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL )
transitionImage( m_restore,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
false );
}
void VulkanRenderBackend::finishFrame()
{
// Qt leaves the render target in COLOR_ATTACHMENT_OPTIMAL; OpenVR wants
// TRANSFER_SRC_OPTIMAL. Wait, so the texture is complete and in the
// right layout when SteamVR gets it.
transitionImage( m_handoff,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
true );
}
void VulkanRenderBackend::waitForGpu()
{
if ( m_df )
m_df->vkQueueWaitIdle( m_queue );
}
vr::Texture_t VulkanRenderBackend::vrTexture()
{
m_vrTextureData.m_nImage = reinterpret_cast<uint64_t>( m_image );
m_vrTextureData.m_pDevice = m_device;
m_vrTextureData.m_pPhysicalDevice = m_physicalDevice;
m_vrTextureData.m_pInstance = m_instance.vkInstance();
m_vrTextureData.m_pQueue = m_queue;
m_vrTextureData.m_nQueueFamilyIndex = m_queueFamilyIndex;
m_vrTextureData.m_nWidth = uint32_t( m_size.width() );
m_vrTextureData.m_nHeight = uint32_t( m_size.height() );
m_vrTextureData.m_nFormat = uint32_t( kFormat );
m_vrTextureData.m_nSampleCount = 1;
// For TextureType_Vulkan the handle points to a VRVulkanTextureData_t.
return vr::Texture_t{ &m_vrTextureData,
vr::TextureType_Vulkan,
vr::ColorSpace_Auto };
}
vr::VRTextureBounds_t VulkanRenderBackend::vrTextureBounds() const
{
// Vulkan images are top-down, like D3D — SteamVR's native convention
// (verified on the Steam Frame).
return vr::VRTextureBounds_t{ 0.f, 0.f, 1.f, 1.f };
}
QImage VulkanRenderBackend::readback()
{
if ( m_image == VK_NULL_HANDLE
|| m_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL )
return {}; // no finished frame yet
const VkDeviceSize byteSize
= VkDeviceSize( m_size.width() ) * m_size.height() * 4;
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = byteSize;
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
if ( m_df->vkCreateBuffer( m_device, &bufferInfo, nullptr, &buffer )
!= VK_SUCCESS )
return {};
QImage image;
VkMemoryRequirements req{};
m_df->vkGetBufferMemoryRequirements( m_device, buffer, &req );
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = req.size;
if ( findMemoryType( req.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
| VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&allocInfo.memoryTypeIndex )
&& m_df->vkAllocateMemory( m_device, &allocInfo, nullptr, &memory )
== VK_SUCCESS
&& m_df->vkBindBufferMemory( m_device, buffer, memory, 0 )
== VK_SUCCESS )
{
const bool copied = submit(
m_handoff,
[&]( VkCommandBuffer cb )
{
VkBufferImageCopy region{};
region.imageSubresource
= { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
region.imageExtent = { uint32_t( m_size.width() ),
uint32_t( m_size.height() ),
1 };
m_df->vkCmdCopyImageToBuffer(
cb,
m_image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
buffer,
1,
&region );
},
true );
void* data = nullptr;
if ( copied
&& m_df->vkMapMemory( m_device, memory, 0, byteSize, 0, &data )
== VK_SUCCESS )
{
image = QImage( m_size, QImage::Format_RGBA8888_Premultiplied );
std::memcpy( image.bits(), data, size_t( byteSize ) );
m_df->vkUnmapMemory( m_device, memory );
}
}
m_df->vkDestroyBuffer( m_device, buffer, nullptr );
if ( memory != VK_NULL_HANDLE )
m_df->vkFreeMemory( m_device, memory, nullptr );
return image;
}
+109
View File
@@ -0,0 +1,109 @@
// Adapted from the vrdashboard template (FrameAssistant commit 4a5b090),
// verified on the Steam Frame. See docs/steamframe/DESIGN.md, section 3.
#pragma once
#include <QVulkanInstance>
#include "RenderBackend.h"
// Vulkan implementation. Qt Quick creates the VkDevice (on the physical device
// SteamVR reports for the HMD, with the device extensions SteamVR requires);
// this backend owns the color image Qt renders into and handles the layout
// dance SteamVR needs:
//
// Qt renders -> image in COLOR_ATTACHMENT_OPTIMAL
// finishFrame() -> transition to TRANSFER_SRC_OPTIMAL (required by
// OpenVR); the CPU waits for it, the texture is
// handed over right after
// SetOverlayTexture -> SteamVR copies from the image using our queue
// beginFrame() (next) -> transition back to COLOR_ATTACHMENT_OPTIMAL, the
// layout Qt's resource tracking expects; no CPU
// wait: queue order puts it before Qt's frame
//
// Everything runs on the GUI thread; QQuickRenderControl::endFrame() waits for
// the GPU (offscreen frames are synchronous).
class VulkanRenderBackend final : public RenderBackend
{
public:
VulkanRenderBackend() = default;
~VulkanRenderBackend() override;
QString name() const override
{
return QStringLiteral( "Vulkan" );
}
QSGRendererInterface::GraphicsApi graphicsApi() const override
{
return QSGRendererInterface::Vulkan;
}
bool initialize( const RenderInterop& interop, QString* error ) override;
void configureWindow( QQuickWindow* window ) override;
bool attachWindow( QQuickWindow* window, QString* error ) override;
void detachWindow() override;
QString deviceName() const override
{
return m_deviceName;
}
bool makeCurrent() override
{
return true;
}
void doneCurrent() override {}
QQuickRenderTarget createRenderTarget( QSize size ) override;
void releaseRenderTarget() override;
void beginFrame() override;
void finishFrame() override;
void waitForGpu() override;
vr::Texture_t vrTexture() override;
vr::VRTextureBounds_t vrTextureBounds() const override;
QImage readback() override;
private:
bool findMemoryType( uint32_t typeBits,
VkMemoryPropertyFlags properties,
uint32_t* index ) const;
// A command buffer with its fence, for small submissions of our own.
struct Submission
{
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
bool pending = false;
};
// Records `record` into the submission's command buffer (after its
// previous use has finished) and submits it; with `wait`, blocks until
// the GPU has executed it.
bool submit( Submission& submission,
const std::function<void( VkCommandBuffer )>& record,
bool wait );
bool waitFor( Submission& submission );
void transitionImage( Submission& submission,
VkImageLayout from,
VkImageLayout to,
bool wait );
QVulkanInstance m_instance;
QByteArrayList m_deviceExtensions;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkDevice m_device = VK_NULL_HANDLE;
VkQueue m_queue = VK_NULL_HANDLE;
uint32_t m_queueFamilyIndex = 0;
QVulkanDeviceFunctions* m_df = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
Submission m_restore; // beginFrame(): async
Submission m_handoff; // finishFrame() and readback(): synchronous
VkImage m_image = VK_NULL_HANDLE;
VkDeviceMemory m_imageMemory = VK_NULL_HANDLE;
// Actual layout outside of Qt's frame
VkImageLayout m_layout = VK_IMAGE_LAYOUT_UNDEFINED;
QSize m_size;
vr::VRVulkanTextureData_t m_vrTextureData{};
QString m_deviceName;
};
+5 -2
View File
@@ -9,6 +9,7 @@
#include "../../utils/setup.h"
#include "specific_setting_value.h"
#include "../../tabcontrollers/MoveCenterTabController.h"
#include "../../utils/platform_capabilities.h"
namespace settings
{
@@ -303,10 +304,11 @@ private:
SettingCategory::Application,
QtInfo{ "previousShutdownSafe" },
true },
// Default depends on the platform (fixed-interval tick on the Frame).
BoolSettingValue{ BoolSetting::APPLICATION_vsyncDisabled,
SettingCategory::Application,
QtInfo{ "vsyncDisabled" },
false },
platform::k_defaultVsyncDisabled },
BoolSettingValue{ BoolSetting::APPLICATION_crashRecoveryDisabled,
SettingCategory::Application,
QtInfo{ "crashRecoveryDisabled" },
@@ -610,10 +612,11 @@ private:
SettingCategory::Application,
QtInfo{ "debugState" },
0 },
// Default depends on the platform (about 90 Hz on the Frame).
IntSettingValue{ IntSetting::APPLICATION_customTickRateMs,
SettingCategory::Application,
QtInfo{ "customTickRateMs" },
20 },
platform::k_defaultCustomTickRateMs },
IntSettingValue{ IntSetting::UTILITY_alarmHour,
SettingCategory::Utility,
@@ -1500,7 +1500,7 @@ void ChaperoneTabController::createNewAutosaveProfile()
true,
true,
true );
LOG(INFO) <<"Chaperone Automatic Chaperone Profile Saved";
qInfo() << "Chaperone Automatic Chaperone Profile Saved";
}
void ChaperoneTabController::applyAutosavedProfile()
+46 -3
View File
@@ -1,10 +1,13 @@
#include "MoveCenterTabController.h"
#include <QQuickWindow>
#include <set>
#include <string>
#include <QtLogging>
#include <QtDebug>
#include "../overlaycontroller.h"
#include "../quaternion/quaternion.h"
#include "../settings/settings.h"
#include "../utils/platform_capabilities.h"
void rotateCoordinates( double coordinates[3], double angle )
{
@@ -1106,10 +1109,17 @@ void MoveCenterTabController::zeroOffsets()
if ( calState != 200 )
{
// all init complete, safe to autosave chaperone profile
parent->m_chaperoneTabController.createNewAutosaveProfile();
// Not where the chaperone can't be committed: the autosave
// exists to restore a committed play area later.
if ( platform::k_chaperoneCommitSupported )
{
parent->m_chaperoneTabController
.createNewAutosaveProfile();
}
m_initComplete = true;
m_chaperoneBasisAcquired = true;
LOG( INFO ) << "Chaperone Initilization Complete, Space Drag Now Enabled";
qInfo() << "Chaperone Initilization Complete, Space Drag "
"Now Enabled";
}
//}
// else
@@ -1171,6 +1181,19 @@ void MoveCenterTabController::clampVelocity( double* velocity )
}
}
// Logs once per caller that a chaperone commit was skipped, so the log shows
// it without repeating it every tick.
static void logSkippedChaperoneCommit( const char* caller )
{
static std::set<std::string> logged;
if ( logged.insert( caller ).second )
{
qInfo() << caller
<< ": CommitWorkingCopy skipped, not supported on this "
"platform (logged once)";
}
}
void MoveCenterTabController::updateChaperoneResetData()
{
//auto cstate = vr::VRChaperone()->GetCalibrationState();
@@ -1182,9 +1205,20 @@ void MoveCenterTabController::updateChaperoneResetData()
// }
// else
//{
// Committing writes the working copy as the live play area. Where that
// isn't supported (it shifts the play area on the Steam Frame), only the
// working copy is read below.
if ( platform::k_chaperoneCommitSupported )
{
vr::VRChaperoneSetup()->CommitWorkingCopy(
vr::EChaperoneConfigFile_Live );
vr::VRChaperoneSetup()->RevertWorkingCopy();
}
else
{
logSkippedChaperoneCommit( "updateChaperoneResetData" );
}
//}
unsigned currentQuadCount = 0;
vr::VRChaperoneSetup()->GetWorkingCollisionBoundsInfo( nullptr,
@@ -1227,7 +1261,16 @@ void MoveCenterTabController::applyChaperoneResetData()
// vr::VRChaperoneSetup()->SetWorkingSeatedZeroPoseToRawTrackingPose(
// &m_seatedCenterForReset );
vr::VRChaperoneSetup()->CommitWorkingCopy( vr::EChaperoneConfigFile_Live );
// See updateChaperoneResetData(): no commit where it isn't supported.
if ( platform::k_chaperoneCommitSupported )
{
vr::VRChaperoneSetup()->CommitWorkingCopy(
vr::EChaperoneConfigFile_Live );
}
else
{
logSkippedChaperoneCommit( "applyChaperoneResetData" );
}
unsigned checkQuadCount = 0;
vr::VRChaperoneSetup()->GetWorkingCollisionBoundsInfo( nullptr,
+28
View File
@@ -0,0 +1,28 @@
#pragma once
// What the target platform supports, and platform-specific defaults.
//
// This is the only place that evaluates the build option OVRAS_STEAM_FRAME
// (CMake option of the same name). All other code asks for the named constants
// below, never for the platform, so it stays clear *why* something differs.
// See docs/steamframe/DESIGN.md, section 7.
#ifndef OVRAS_STEAM_FRAME
# define OVRAS_STEAM_FRAME 0
#endif
namespace platform
{
// IVRChaperoneSetup::CommitWorkingCopy() writes the working copy as the live
// chaperone. OVRAS does this at start-up and when applying offsets. On the
// Steam Frame (inside-out tracking) it shifts the play area, so it must not
// happen there.
constexpr bool k_chaperoneCommitSupported = !OVRAS_STEAM_FRAME;
// Default tick: by default OVRAS polls every 1 ms for a new compositor frame
// ("vsync"). The Frame defaults to a fixed-interval tick instead, which wakes
// the process about 11 times less often. 11 ms is about 90 Hz, the frame rate
// OVRAS's tick-counted intervals were designed for.
constexpr bool k_defaultVsyncDisabled = OVRAS_STEAM_FRAME;
constexpr int k_defaultCustomTickRateMs = OVRAS_STEAM_FRAME ? 11 : 20;
} // namespace platform
+36 -7
View File
@@ -58,6 +58,12 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application )
k_resetSettingsDescription );
parser.addOption( resetSettings );
QCommandLineOption graphicsApi( k_graphicsApi,
k_graphicsApiDescription,
k_graphicsApiValueName,
k_graphicsApiDefault );
parser.addOption( graphicsApi );
parser.process( application );
const bool desktopModeEnabled = parser.isSet( desktopMode );
@@ -84,11 +90,16 @@ CommandLineOptions returnCommandLineParser( const MyQApplication& application )
if ( resetSettingsEnabled )
qInfo() << "Reset SteamVR Settings.";
const CommandLineOptions commandLineArgs{
desktopModeEnabled, forceNoSoundEnabled,
forceNoManifestEnabled, forceInstallManifestEnabled,
forceRemoveManifestEnabled, resetSettingsEnabled
};
const QString graphicsApiValue = parser.value( graphicsApi );
qInfo() << "Graphics API:" << graphicsApiValue;
const CommandLineOptions commandLineArgs{ desktopModeEnabled,
forceNoSoundEnabled,
forceNoManifestEnabled,
forceInstallManifestEnabled,
forceRemoveManifestEnabled,
resetSettingsEnabled,
graphicsApiValue };
qInfo() << "Command line arguments processed.";
@@ -251,8 +262,26 @@ void forceRemoveApplicationManifest()
[[noreturn]] void handleManifests( const bool installManifest,
const bool removeManifest )
{
openvr_init::initializeOpenVR(
openvr_init::OpenVrInitializationType::Utility );
// Connect as Utility app first: it works without a running SteamVR (e.g.
// from the Windows installer). The Steam Frame's SteamVR refuses Utility
// apps (VRInitError_Init_NotAvailableToUtilityApps); there, connect as
// Background app, which needs a running SteamVR and never starts it.
// VR_Init is called directly: initializeOpenVR() retries every error with
// waits, including the one expected here.
vr::EVRInitError initError = vr::VRInitError_None;
vr::VR_Init( &initError, vr::VRApplication_Utility );
if ( initError == vr::VRInitError_Init_NotAvailableToUtilityApps )
{
qInfo() << "SteamVR refuses Utility apps, connecting as Background "
"app.";
vr::VR_Init( &initError, vr::VRApplication_Background );
}
if ( initError != vr::VRInitError_None )
{
qCritical() << "Failed to connect to SteamVR for manifest handling:"
<< vr::VR_GetVRInitErrorAsEnglishDescription( initError );
exit( ReturnErrorCode::OPENVR_INIT_ERROR );
}
int exit_code = ReturnErrorCode::SUCCESS;
try
+10
View File
@@ -28,6 +28,8 @@ struct CommandLineOptions
const bool forceInstallManifest = false;
const bool forceRemoveManifest = false;
const bool resetSettings = false;
// Value of --graphics-api; checked in main() with parseGraphicsApi().
const QString graphicsApi = "opengl";
};
// Manages the programs control flow and main settings.
@@ -89,6 +91,14 @@ constexpr auto k_forceInstallManifestDescription
constexpr auto k_forceRemoveManifest = "force-remove-manifest";
constexpr auto k_forceRemoveManifestDescription
= "Forces removing the applications manifest. Application will exit early.";
// Selects the render backend (render/RenderBackend.h). No automatic fallback:
// an explicit choice is never changed silently.
constexpr auto k_graphicsApi = "graphics-api";
constexpr auto k_graphicsApiDescription
= "Graphics API for rendering the overlay: opengl (default) or vulkan.";
constexpr auto k_graphicsApiValueName = "api";
constexpr auto k_graphicsApiDefault = "opengl";
constexpr auto k_resetSettings = "reset-steamvr-settings";
constexpr auto k_resetSettingsDescription
= "Resets all SteamVR values that can be modified in OVRAS to defaults.";
+35
View File
@@ -0,0 +1,35 @@
# Render test for src/render/ (see tst_render.cpp). Runs without SteamVR or a
# headset; skips itself without a GPU context. One CTest entry per graphics
# API, because the API is process-wide.
find_package(Qt6 REQUIRED COMPONENTS Test)
qt_add_executable(tst_render
tst_render.cpp
${CMAKE_SOURCE_DIR}/src/render/RenderBackend.cpp
${CMAKE_SOURCE_DIR}/src/render/OpenGLRenderBackend.cpp
)
target_include_directories(tst_render PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/third-party/openvr/headers
)
# Only openvr.h's types are used: nothing links against libopenvr_api.
target_link_libraries(tst_render PRIVATE Qt::Gui Qt::Quick Qt::Qml Qt6::Test)
if (QT_FEATURE_vulkan)
target_sources(tst_render PRIVATE
${CMAKE_SOURCE_DIR}/src/render/VulkanRenderBackend.cpp
)
target_compile_definitions(tst_render PRIVATE OVRAS_HAS_VULKAN=1)
else ()
target_compile_definitions(tst_render PRIVATE OVRAS_HAS_VULKAN=0)
endif ()
qt_add_qml_module(tst_render
URI RenderTest
VERSION 1.0
QML_FILES TwoTone.qml
)
add_test(NAME render_opengl COMMAND tst_render)
set_tests_properties(render_opengl PROPERTIES ENVIRONMENT "OVRAS_TEST_API=opengl")
add_test(NAME render_vulkan COMMAND tst_render)
set_tests_properties(render_vulkan PROPERTIES ENVIRONMENT "OVRAS_TEST_API=vulkan")
+7
View File
@@ -0,0 +1,7 @@
import QtQuick
// Top half red, bottom half blue: lets the render test check content and orientation.
Item {
Rectangle { width: parent.width; height: parent.height / 2; color: "#ff0000" }
Rectangle { y: parent.height / 2; width: parent.width; height: parent.height / 2; color: "#0000ff" }
}
+183
View File
@@ -0,0 +1,183 @@
// Render test for the render backends (src/render/), adapted from the
// vrdashboard template's tst_render. It drives a backend and
// QQuickRenderControl in the same order as OverlayController::SetWidget() and
// renderOverlay(), then checks the texture handed to SteamVR and its content
// (via readback).
//
// The backend comes from OVRAS_TEST_API (opengl / vulkan): the graphics API is
// process-wide, so CTest runs one process per API. Without a GPU context the
// test skips itself. With OVRAS_VULKAN_VALIDATION=1 the Vulkan validation
// layers are enabled (build_scripts/steamframe/container-test.sh).
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickRenderControl>
#include <QQuickWindow>
#include <QtTest>
#include <memory>
#include "render/RenderBackend.h"
class RenderTest : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
GraphicsApi api = GraphicsApi::OpenGL;
const QString apiName
= qEnvironmentVariable( "OVRAS_TEST_API", "opengl" );
QVERIFY2( parseGraphicsApi( apiName, &api ), qPrintable( apiName ) );
m_backend = createRenderBackend( api );
if ( !m_backend )
{
QSKIP( qPrintable( apiName + " not compiled in" ) );
}
// Before any QQuickWindow exists, as in main().
QQuickWindow::setGraphicsApi( m_backend->graphicsApi() );
QString error;
// No SteamVR here: empty interop, the backend picks defaults.
if ( !m_backend->initialize( {}, &error ) )
{
QSKIP( qPrintable( QString( "No %1 context: %2" )
.arg( m_backend->name(), error ) ) );
}
}
// One frame of a known scene (top red, bottom blue): texture type and
// description for SteamVR, content and orientation.
void rendersSceneUpright()
{
const QSize size( 64, 32 );
Scene scene( *m_backend, size );
QVERIFY2( scene.error.isEmpty(), qPrintable( scene.error ) );
const vr::Texture_t texture = scene.renderFrame();
QVERIFY( texture.handle != nullptr );
const auto expectedType
= m_backend->graphicsApi() == QSGRendererInterface::Vulkan
? vr::TextureType_Vulkan
: vr::TextureType_OpenGL;
QCOMPARE( texture.eType, expectedType );
if ( texture.eType == vr::TextureType_Vulkan )
{
const auto* vk = static_cast<const vr::VRVulkanTextureData_t*>(
texture.handle );
QVERIFY( vk->m_nImage != 0 && vk->m_pDevice && vk->m_pQueue
&& vk->m_pInstance );
QCOMPARE( vk->m_nWidth, static_cast<uint32_t>( size.width() ) );
// VK_FORMAT_R8G8B8A8_UNORM, the format SteamVR gets told.
QCOMPARE( vk->m_nFormat, 37u );
}
QVERIFY( !m_backend->deviceName().isEmpty() );
const QImage image = m_backend->readback();
QCOMPARE( image.size(), size );
QCOMPARE( image.pixelColor( size.width() / 2, 4 ),
QColor( 255, 0, 0 ) );
QCOMPARE( image.pixelColor( size.width() / 2, size.height() - 4 ),
QColor( 0, 0, 255 ) );
}
// Several frames in a row exercise the per-frame state handling (e.g.
// the Vulkan layout transitions between Qt's frames and the SteamVR
// hand-off).
void rendersRepeatedly()
{
Scene scene( *m_backend, { 32, 32 } );
QVERIFY2( scene.error.isEmpty(), qPrintable( scene.error ) );
for ( int i = 0; i < 5; ++i )
{
scene.renderFrame();
}
QCOMPARE( m_backend->readback().pixelColor( 16, 2 ),
QColor( 255, 0, 0 ) );
}
void cleanupTestCase()
{
m_backend.reset();
}
private:
// The TwoTone scene in an offscreen window, set up like
// OverlayController::SetWidget(). Tears down like
// OverlayController::Shutdown() (minus VR_Shutdown).
struct Scene
{
Scene( RenderBackend& backend, QSize size ) : backend( backend )
{
window = std::make_unique<QQuickWindow>( &renderControl );
window->setGeometry( 0, 0, size.width(), size.height() );
QQmlComponent component( &engine );
component.loadFromModule( "RenderTest", "TwoTone" );
root.reset( qobject_cast<QQuickItem*>( component.create() ) );
if ( !root )
{
error = component.errorString();
return;
}
root->setSize( size );
root->setParentItem( window->contentItem() );
backend.configureWindow( window.get() );
if ( !backend.makeCurrent() || !renderControl.initialize() )
{
error = "QQuickRenderControl::initialize failed";
return;
}
if ( !backend.attachWindow( window.get(), &error ) )
{
return;
}
const QQuickRenderTarget target
= backend.createRenderTarget( size );
if ( target.isNull() )
{
error = "createRenderTarget failed";
return;
}
window->setRenderTarget( target );
}
~Scene()
{
backend.makeCurrent();
root.reset();
backend.releaseRenderTarget();
backend.detachWindow();
window.reset();
}
// One frame as in OverlayController::renderOverlay(); returns what
// would be handed to IVROverlay::SetOverlayTexture.
vr::Texture_t renderFrame()
{
backend.makeCurrent();
backend.beginFrame();
renderControl.polishItems();
renderControl.beginFrame();
renderControl.sync();
renderControl.render();
renderControl.endFrame();
backend.finishFrame();
return backend.vrTexture();
}
RenderBackend& backend;
QQmlEngine engine;
// Destroyed after the window (declared before it): the window
// belongs to this render control.
QQuickRenderControl renderControl;
std::unique_ptr<QQuickWindow> window;
std::unique_ptr<QQuickItem> root;
QString error;
};
std::unique_ptr<RenderBackend> m_backend;
};
QTEST_MAIN( RenderTest )
#include "tst_render.moc"