diff --git a/.gitignore b/.gitignore index 7b3b410..f087087 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 20f03d5..b28e4a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,8 +283,10 @@ 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_RPATH "$ORIGIN" + BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN" ) endif() @@ -594,3 +596,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 () diff --git a/build_scripts/steamframe/aarch64-linux-gnu.cmake b/build_scripts/steamframe/aarch64-linux-gnu.cmake new file mode 100644 index 0000000..0065b82 --- /dev/null +++ b/build_scripts/steamframe/aarch64-linux-gnu.cmake @@ -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) diff --git a/build_scripts/steamframe/container-build.sh b/build_scripts/steamframe/container-build.sh new file mode 100755 index 0000000..5279a15 --- /dev/null +++ b/build_scripts/steamframe/container-build.sh @@ -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-/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 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" diff --git a/build_scripts/steamframe/container-test.sh b/build_scripts/steamframe/container-test.sh new file mode 100755 index 0000000..6504a10 --- /dev/null +++ b/build_scripts/steamframe/container-test.sh @@ -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." +' diff --git a/build_scripts/steamframe/cross-arm64.Dockerfile b/build_scripts/steamframe/cross-arm64.Dockerfile new file mode 100644 index 0000000..9fc5f08 --- /dev/null +++ b/build_scripts/steamframe/cross-arm64.Dockerfile @@ -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/* diff --git a/build_scripts/steamframe/deploy-to-frame.sh b/build_scripts/steamframe/deploy-to-frame.sh new file mode 100755 index 0000000..639c33c --- /dev/null +++ b/build_scripts/steamframe/deploy-to-frame.sh @@ -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 [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 [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" diff --git a/build_scripts/steamframe/native.Dockerfile b/build_scripts/steamframe/native.Dockerfile new file mode 100644 index 0000000..20fb681 --- /dev/null +++ b/build_scripts/steamframe/native.Dockerfile @@ -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/* diff --git a/build_scripts/steamframe/package-release.sh b/build_scripts/steamframe/package-release.sh new file mode 100755 index 0000000..1daa90f --- /dev/null +++ b/build_scripts/steamframe/package-release.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Builds OVRAS for the Steam Frame (aarch64) and packages it: +# dist/AdvancedSettings--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" </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" </dev/null +exec ./$exe --force-remove-manifest +EOF + +cat > "$out/run.sh" </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" </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--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. -- The template's Qt-bundling fallback is not adopted for now. +- RPATH is only `$ORIGIN` (`BUILD_WITH_INSTALL_RPATH`): the build tree gets packaged. ## Tests - **x86_64 locally:** the build succeeds; `objdump` check (R1). -- **Vulkan:** `container-test.sh` with validation layers. It reports no messages, and the - render path is checked by a snapshot test (`tst_render` from the template, reduced to the - backends). -- **OVRAS:** the keyboard parser test `test/keyboard_input` is included in CMake if that is - easy; otherwise it is noted as not run. -- **On the Frame:** a checklist per acceptance criterion R1–R4, R6, R7, with both graphics - APIs. Results go to `docs/steamframe/PROGRESS.md`. +- **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 diff --git a/docs/steamframe/PROGRESS.md b/docs/steamframe/PROGRESS.md index 10485c9..50c59dc 100644 --- a/docs/steamframe/PROGRESS.md +++ b/docs/steamframe/PROGRESS.md @@ -122,3 +122,55 @@ - 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). + 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? + +### Test checklist on the Frame (step 5) + +Deploy: `build_scripts/steamframe/deploy-to-frame.sh @` (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 | diff --git a/test/render/CMakeLists.txt b/test/render/CMakeLists.txt new file mode 100644 index 0000000..5951eec --- /dev/null +++ b/test/render/CMakeLists.txt @@ -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") diff --git a/test/render/TwoTone.qml b/test/render/TwoTone.qml new file mode 100644 index 0000000..2ee069e --- /dev/null +++ b/test/render/TwoTone.qml @@ -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" } +} diff --git a/test/render/tst_render.cpp b/test/render/tst_render.cpp new file mode 100644 index 0000000..4a89a68 --- /dev/null +++ b/test/render/tst_render.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#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( + texture.handle ); + QVERIFY( vk->m_nImage != 0 && vk->m_pDevice && vk->m_pQueue + && vk->m_pInstance ); + QCOMPARE( vk->m_nWidth, static_cast( 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( &renderControl ); + window->setGeometry( 0, 0, size.width(), size.height() ); + QQmlComponent component( &engine ); + component.loadFromModule( "RenderTest", "TwoTone" ); + root.reset( qobject_cast( 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 window; + std::unique_ptr root; + QString error; + }; + + std::unique_ptr m_backend; +}; + +QTEST_MAIN( RenderTest ) +#include "tst_render.moc"