Initial FrameAssistant baseline from the vrdashboard template

Template stripped to the overlay platform (examples, demo UI and Frame
test package removed), renamed to FrameAssistant, and working rules for
the project recorded in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 00:06:58 +02:00
co-authored by Claude Opus 5.5
commit 4a5b09053e
63 changed files with 5193 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# vrdash_bundle_qt()
#
# Call once, after all dashboards are defined (vrdash_add_dashboard() registers them).
# Adds an install step that makes the install prefix self-contained for all of them
# together: the Qt libraries
# (and their non-system dependencies), required Qt plugins and the QML modules the UI
# imports are copied next to the executable, RPATHs are made relative and a qt.conf is
# written. Works for native AND cross builds (Qt's own qt_generate_deploy_*_app_script
# refuses cross-compiled Linux targets, and with a /usr Qt it would bundle glibc too).
#
# Libraries matched by VRDASH_BUNDLE_EXCLUDE are expected on the target system
# (SteamOS): libc, the C++ runtime and everything tied to the GPU driver / display
# server. Bundling those would break GPU access, so they always come from the host.
# Regexes are matched against DT_NEEDED names.
set(VRDASH_BUNDLE_EXCLUDE
"^ld-linux" "^libc\\.so" "^libm\\.so" "^libdl\\.so" "^libpthread\\.so" "^librt\\.so"
"^libresolv\\.so" "^libutil\\.so" "^libstdc\\+\\+\\.so" "^libgcc_s\\.so"
"^libGL" "^libEGL" "^libOpenGL" "^libGLX" "^libGLdispatch" "^libgbm" "^libdrm"
"^libvulkan" "^libwayland-" "^libX" "^libxcb" "^libxkbcommon" "^libxshmfence"
"^libfontconfig" "^libfreetype" "^libexpat" "^libz\\.so"
"^libdbus-1" "^libsystemd" "^libudev" "^libcap\\.so" "^libgcrypt" "^libgpg-error"
"^liblzma" "^liblz4" "^libzstd" "^libffi"
"^libopenvr_api")
# SteamVR's process environment decides the platform (Wayland under gamescope/Plasma,
# XWayland via DISPLAY, or offscreen); embedded platforms (eglfs, vnc, …) are left out.
set(VRDASH_BUNDLE_PLUGINS
platforms/libqwayland-*.so platforms/libqxcb.so platforms/libqoffscreen.so
wayland-shell-integration/*.so wayland-graphics-integration-client/*.so
wayland-decoration-client/*.so xcbglintegrations/*.so)
function(vrdash_bundle_qt)
get_property(targets GLOBAL PROPERTY VRDASH_DASHBOARD_TARGETS)
get_property(qml_dirs GLOBAL PROPERTY VRDASH_DASHBOARD_QML_DIRS)
set(executables "")
foreach(target IN LISTS targets)
list(APPEND executables "$<TARGET_FILE_NAME:${target}>")
endforeach()
find_program(VRDASH_PATCHELF patchelf REQUIRED)
if(CMAKE_CROSSCOMPILING)
# Inspect target ELF files with the target toolchain's objdump.
set(objdump ${CMAKE_OBJDUMP})
else()
find_program(objdump objdump REQUIRED)
endif()
set(qt_prefix ${QT6_INSTALL_PREFIX})
set(qt_libdir ${QT6_INSTALL_PREFIX}/${QT6_INSTALL_LIBS})
if(CMAKE_LIBRARY_ARCHITECTURE AND EXISTS ${qt_prefix}/lib/${CMAKE_LIBRARY_ARCHITECTURE})
set(qt_libdir ${qt_prefix}/lib/${CMAKE_LIBRARY_ARCHITECTURE}) # Debian multiarch
endif()
# The regexes end up inside a quoted string in the generated script: escape backslashes.
string(REPLACE "\\" "\\\\" bundle_exclude "${VRDASH_BUNDLE_EXCLUDE}")
set(script ${CMAKE_BINARY_DIR}/bundle-qt.cmake)
configure_file(${PROJECT_SOURCE_DIR}/cmake/bundle-install.cmake.in ${script}.in @ONLY)
file(GENERATE OUTPUT ${script} INPUT ${script}.in)
install(SCRIPT ${script})
endfunction()
+72
View File
@@ -0,0 +1,72 @@
# vrdash_add_dashboard(<target>
# OUTPUT_NAME <executable file name>
# QML_URI <QML module URI>
# SOURCES <main.cpp and other plain C++>
# [QML_SOURCES <C++ with QML_ELEMENT types>]
# QML_FILES <.qml files>)
#
# A dashboard executable on top of vrdashboard_core (DashboardApp), with everything
# all dashboards share: its QML module, bytecode-only QML for system-Qt builds,
# libopenvr_api.so next to it, installation, registration for vrdash_bundle_qt().
# SteamVR identifies processes by executable name: never use one of its own (an
# executable called "vrdashboard" made SteamVR treat us as its dashboard).
set(VRDASH_RESERVED_EXECUTABLE_NAMES
vrdashboard vrserver vrcompositor vrmonitor vrstartup vrwebhelper vrservice
vrpathreg vrcmd)
function(vrdash_add_dashboard target)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "OUTPUT_NAME;QML_URI"
"SOURCES;QML_SOURCES;QML_FILES")
if(arg_OUTPUT_NAME IN_LIST VRDASH_RESERVED_EXECUTABLE_NAMES)
message(FATAL_ERROR "Executable name '${arg_OUTPUT_NAME}' collides with a SteamVR process")
endif()
# The QML module's URI becomes DashboardApplication's default module, so it
# is written down only here (QML_URI).
set(uri_source ${CMAKE_CURRENT_BINARY_DIR}/${target}_qml_module.cpp)
file(GENERATE OUTPUT ${uri_source} CONTENT
"// Generated by vrdash_add_dashboard() (cmake/Dashboard.cmake).
#include \"app/DashboardApplication.h\"
namespace {
[[maybe_unused]] const bool registered =
(DashboardApplication::setDefaultQmlModule(\"${arg_QML_URI}\"), true);
} // namespace
")
qt_add_executable(${target} ${arg_SOURCES} ${uri_source})
set_target_properties(${target} PROPERTIES OUTPUT_NAME ${arg_OUTPUT_NAME})
target_link_libraries(${target} PRIVATE vrdashboard_core)
target_compile_options(${target} PRIVATE -Wall -Wextra)
if(VRDASH_QML_BYTECODE_ONLY)
# Must be set before qt_add_qml_module() reads it.
set_target_properties(${target} PROPERTIES QT_QMLCACHEGEN_ARGUMENTS --only-bytecode)
endif()
qt_add_qml_module(${target}
URI ${arg_QML_URI}
VERSION 1.0
SOURCES ${arg_QML_SOURCES}
QML_FILES ${arg_QML_FILES})
get_target_property(_qt_core_type Qt6::Core TYPE)
if(_qt_core_type STREQUAL "SHARED_LIBRARY")
# The import scan links QML plugins statically and is only meant for static Qt;
# with Debian's shared Qt it misfires (warnings). Shared Qt loads them at runtime.
set_target_properties(${target} PROPERTIES QT_QML_MODULE_NO_IMPORT_SCAN ON)
endif()
# libopenvr_api.so is not a system library: <prefix>/lib, found via RPATH.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${OPENVR_LIBRARY}
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/
VERBATIM)
install(TARGETS ${target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
# For vrdash_bundle_qt(), which bundles for all dashboards at once.
set_property(GLOBAL APPEND PROPERTY VRDASH_DASHBOARD_TARGETS ${target})
set_property(GLOBAL APPEND PROPERTY VRDASH_DASHBOARD_QML_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/qml)
endfunction()
+47
View File
@@ -0,0 +1,47 @@
# Provides the imported target OpenVR::OpenVR.
#
# Either point OPENVR_ROOT at an OpenVR SDK checkout (needs headers/ and bin/<platform>/),
# or leave it empty and the required files of OPENVR_VERSION are downloaded once into
# third_party/openvr-<version>/ (header + Linux libraries only, not the whole SDK).
set(OPENVR_ROOT "" CACHE PATH "Path to an OpenVR SDK (leave empty to auto-download)")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(_openvr_platform linuxarm64)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
set(_openvr_platform linux64)
else()
message(FATAL_ERROR "Unsupported target processor for OpenVR: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
if(NOT OPENVR_ROOT)
set(OPENVR_ROOT ${CMAKE_SOURCE_DIR}/third_party/openvr-${OPENVR_VERSION})
set(_base "https://raw.githubusercontent.com/ValveSoftware/openvr/v${OPENVR_VERSION}")
foreach(_file headers/openvr.h LICENSE bin/${_openvr_platform}/libopenvr_api.so)
if(NOT EXISTS ${OPENVR_ROOT}/${_file})
message(STATUS "Downloading OpenVR ${OPENVR_VERSION}: ${_file}")
file(DOWNLOAD ${_base}/${_file} ${OPENVR_ROOT}/${_file}.part STATUS _status TLS_VERIFY ON)
list(GET _status 0 _code)
if(NOT _code EQUAL 0)
file(REMOVE ${OPENVR_ROOT}/${_file}.part)
message(FATAL_ERROR "Download of ${_base}/${_file} failed: ${_status}\n"
"Set OPENVR_ROOT to a local OpenVR SDK to build offline.")
endif()
file(RENAME ${OPENVR_ROOT}/${_file}.part ${OPENVR_ROOT}/${_file})
endif()
endforeach()
endif()
set(_openvr_lib ${OPENVR_ROOT}/bin/${_openvr_platform}/libopenvr_api.so)
if(NOT EXISTS ${OPENVR_ROOT}/headers/openvr.h OR NOT EXISTS ${_openvr_lib})
message(FATAL_ERROR "OPENVR_ROOT=${OPENVR_ROOT} lacks headers/openvr.h or ${_openvr_lib}")
endif()
add_library(OpenVR::OpenVR SHARED IMPORTED GLOBAL)
set_target_properties(OpenVR::OpenVR PROPERTIES
IMPORTED_LOCATION ${_openvr_lib}
IMPORTED_NO_SONAME TRUE
INTERFACE_INCLUDE_DIRECTORIES ${OPENVR_ROOT}/headers)
set(OPENVR_LIBRARY ${_openvr_lib})
set(OPENVR_LICENSE ${OPENVR_ROOT}/LICENSE)
message(STATUS "OpenVR: ${OPENVR_ROOT} (${_openvr_platform})")
+166
View File
@@ -0,0 +1,166 @@
# Generated from cmake/bundle-install.cmake.in by vrdash_bundle_qt(). Runs at install time,
# once for all dashboard executables.
cmake_policy(VERSION 3.22)
get_filename_component(prefix "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}" ABSOLUTE)
set(executables "")
foreach(name @executables@)
list(APPEND executables "${prefix}/@CMAKE_INSTALL_BINDIR@/${name}")
endforeach()
set(libdir "${prefix}/@CMAKE_INSTALL_LIBDIR@")
set(plugindir "${prefix}/plugins")
set(qmldir "${prefix}/qml")
set(qt_plugins "@QT6_INSTALL_PREFIX@/@QT6_INSTALL_PLUGINS@")
set(qt_qml "@QT6_INSTALL_PREFIX@/@QT6_INSTALL_QML@")
set(patchelf "@VRDASH_PATCHELF@")
set(exclude "@bundle_exclude@")
message(STATUS "Bundling Qt into ${prefix}")
# --- QML modules the UI imports (transitively) -------------------------------------------
set(modules "")
foreach(root @qml_dirs@)
execute_process(
COMMAND "$<TARGET_FILE:Qt6::qmlimportscanner>" -rootPath "${root}" -importPath "${qt_qml}"
OUTPUT_VARIABLE imports_json
RESULT_VARIABLE result)
if(NOT result EQUAL 0)
message(FATAL_ERROR "qmlimportscanner failed on ${root} (${result})")
endif()
string(JSON count LENGTH "${imports_json}")
if(count EQUAL 0)
continue()
endif()
math(EXPR last "${count} - 1")
foreach(i RANGE ${last})
string(JSON type ERROR_VARIABLE err GET "${imports_json}" ${i} type)
string(JSON path ERROR_VARIABLE err GET "${imports_json}" ${i} path)
if(type STREQUAL "module" AND path AND path MATCHES "^${qt_qml}/")
list(APPEND modules "${path}")
endif()
endforeach()
endforeach()
list(REMOVE_DUPLICATES modules)
set(qml_plugins "")
foreach(module IN LISTS modules)
file(RELATIVE_PATH rel "${qt_qml}" "${module}")
# Only the module's own files; submodules are listed separately if they are needed.
file(GLOB files LIST_DIRECTORIES false "${module}/*")
file(INSTALL ${files} DESTINATION "${qmldir}/${rel}" FOLLOW_SYMLINK_CHAIN)
file(GLOB so_files "${qmldir}/${rel}/*.so")
list(APPEND qml_plugins ${so_files})
endforeach()
# --- Qt plugins --------------------------------------------------------------------------
set(qt_plugin_files "")
foreach(pattern @VRDASH_BUNDLE_PLUGINS@)
file(GLOB files "${qt_plugins}/${pattern}")
foreach(file IN LISTS files)
file(RELATIVE_PATH rel "${qt_plugins}" "${file}")
get_filename_component(dir "${rel}" DIRECTORY)
file(INSTALL "${file}" DESTINATION "${plugindir}/${dir}")
list(APPEND qt_plugin_files "${plugindir}/${rel}")
endforeach()
endforeach()
# --- Shared libraries needed by executable + plugins --------------------------------------
set(CMAKE_GET_RUNTIME_DEPENDENCIES_PLATFORM "linux+elf")
set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL "objdump")
set(CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND "@objdump@")
file(GET_RUNTIME_DEPENDENCIES
EXECUTABLES ${executables}
MODULES ${qt_plugin_files} ${qml_plugins}
DIRECTORIES "@qt_libdir@" "${libdir}"
PRE_EXCLUDE_REGEXES ${exclude} # matched against DT_NEEDED names
POST_EXCLUDE_REGEXES "^${libdir}/"
RESOLVED_DEPENDENCIES_VAR resolved
UNRESOLVED_DEPENDENCIES_VAR unresolved)
if(unresolved)
message(WARNING "Unresolved (expected on target system): ${unresolved}")
endif()
file(INSTALL ${resolved} DESTINATION "${libdir}" FOLLOW_SYMLINK_CHAIN)
# --- Relative RPATHs ------------------------------------------------------------------------
# DT_RUNPATH of the executable does not apply to transitive dependencies, so every bundled
# ELF file needs its own path back to lib/.
function(set_rpath file rpath)
execute_process(COMMAND "${patchelf}" --set-rpath "${rpath}" "${file}" RESULT_VARIABLE rc)
if(NOT rc EQUAL 0)
message(FATAL_ERROR "patchelf failed on ${file}")
endif()
endfunction()
file(GLOB bundled_libs "${libdir}/*.so*")
foreach(lib IN LISTS bundled_libs)
if(NOT IS_SYMLINK "${lib}")
set_rpath("${lib}" "\$ORIGIN")
endif()
endforeach()
foreach(file IN LISTS qt_plugin_files qml_plugins)
get_filename_component(dir "${file}" DIRECTORY)
file(RELATIVE_PATH rel "${dir}" "${libdir}")
set_rpath("${file}" "\$ORIGIN/${rel}")
endforeach()
# --- License files of everything bundled ----------------------------------------------
# Bundled libraries come from Debian packages: ship each package's copyright file and
# the common license texts they reference (LGPL/GPL/...), as their licenses require.
find_program(dpkg dpkg)
if(dpkg)
set(docdir "${prefix}/@CMAKE_INSTALL_DOCDIR@/third-party")
set(packages "")
foreach(file IN LISTS resolved qt_plugin_files qml_plugins)
# Ask dpkg about the original file: map installed plugins back, resolve
# symlinks and /lib -> /usr/lib (usrmerge: packages register /usr paths).
string(REPLACE "${plugindir}" "${qt_plugins}" original "${file}")
string(REPLACE "${qmldir}" "${qt_qml}" original "${original}")
file(REAL_PATH "${original}" original)
execute_process(COMMAND "${dpkg}" -S "${original}" OUTPUT_VARIABLE owner
ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REGEX REPLACE ":.*" "" owner "${owner}")
if(owner)
list(APPEND packages "${owner}")
endif()
endforeach()
list(REMOVE_DUPLICATES packages)
set(licenses "")
foreach(package IN LISTS packages)
set(copyright "/usr/share/doc/${package}/copyright")
if(EXISTS "${copyright}")
file(INSTALL "${copyright}" DESTINATION "${docdir}"
RENAME "${package}.copyright" FOLLOW_SYMLINK_CHAIN)
file(STRINGS "${copyright}" refs REGEX "/usr/share/common-licenses/[A-Za-z0-9.+-]+")
foreach(ref IN LISTS refs)
string(REGEX MATCHALL "/usr/share/common-licenses/[A-Za-z0-9.+-]+" paths "${ref}")
list(APPEND licenses ${paths})
endforeach()
endif()
endforeach()
list(REMOVE_DUPLICATES licenses)
foreach(license IN LISTS licenses)
string(REGEX REPLACE "[.]$" "" license "${license}")
if(EXISTS "${license}")
file(INSTALL "${license}" DESTINATION "${docdir}/common-licenses" FOLLOW_SYMLINK_CHAIN)
endif()
endforeach()
list(LENGTH packages n_packages)
message(STATUS "Collected copyright files of ${n_packages} Debian packages")
else()
message(WARNING "dpkg not found: license files of bundled libraries not collected")
endif()
file(WRITE "${prefix}/@CMAKE_INSTALL_BINDIR@/qt.conf"
"[Paths]
Prefix = ..
Libraries = @CMAKE_INSTALL_LIBDIR@
Plugins = plugins
QmlImports = qml
")
list(LENGTH resolved n_libs)
list(LENGTH modules n_modules)
list(LENGTH qt_plugin_files n_plugins)
message(STATUS "Bundled ${n_libs} libraries, ${n_plugins} Qt plugins, ${n_modules} QML modules")
+25
View File
@@ -0,0 +1,25 @@
# Cross-compile for aarch64 Linux (Steam Frame) from an x86_64 Debian multiarch environment.
# Used by the "cross-arm64" preset inside docker/cross-arm64.Dockerfile; see scripts/cross-build.sh.
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)
# Lets CMake/Qt deploy tooling inspect target binaries.
set(CMAKE_OBJDUMP aarch64-linux-gnu-objdump)