diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb7efeb..bcdbd6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,8 +38,36 @@ jobs: name: FrameStudio-linux-${{ matrix.arch }} path: dist/FrameStudio-linux-${{ matrix.arch }}.tar.gz + # Frame Home for SteamVR on a Windows PC (frame-home.exe, frame-switcher.exe and the PC installer), cross-built with + # llvm-mingw as it's developed: the pc/ folder of the Windows x64 Studio, and a download of its own. + pc-home-windows: + name: Frame Home for Windows PCs + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: llvm-mingw + run: | + sudo apt-get update && sudo apt-get install -y ninja-build cmake + url=$(curl -s https://api.github.com/repos/mstorsjo/llvm-mingw/releases/latest | grep -o 'https://[^"]*ucrt-ubuntu-[0-9.]*-x86_64\.tar\.xz' | head -1) + curl -sL "$url" | tar -xJ -C "$HOME" && mv "$HOME"/llvm-mingw-* "$HOME/llvm-mingw" + - name: Build + run: | + export PATH="$HOME/llvm-mingw/bin:$PATH" + cmake -S . -B build/pc-win64 -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-mingw.cmake -DWINDOWS_ARCH=x86_64 -DLLVM_MINGW="$HOME/llvm-mingw" + cmake --build build/pc-win64 --target frame-home frame-switcher --parallel + mkdir -p "build/stage/FrameHome-PC-windows-x64" dist + cp build/pc-win64/frame-home.exe build/pc-win64/frame-switcher.exe frame/pc/install-frame-home-pc.ps1 \ + "frame/pc/Install Frame Home on this PC.bat" "frame/pc/Remove Frame Home from this PC.bat" "build/stage/FrameHome-PC-windows-x64/" + cp LICENSE build/stage/FrameHome-PC-windows-x64/LICENSE.txt + (cd build/stage && zip -qr ../../dist/FrameHome-PC-windows-x64.zip FrameHome-PC-windows-x64) + - uses: actions/upload-artifact@v4 + with: + name: FrameHome-PC-windows-x64 + path: dist/FrameHome-PC-windows-x64.zip + windows: name: Windows ${{ matrix.arch }} + needs: [pc-home-windows] strategy: fail-fast: false matrix: @@ -47,6 +75,11 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + if: matrix.arch == 'x64' + with: + name: FrameHome-PC-windows-x64 + path: pc - name: Build (MSVC) shell: cmd run: studio\build_windows.bat ${{ matrix.arch }} "%GITHUB_WORKSPACE%\build\windows-${{ matrix.arch }}" @@ -59,6 +92,11 @@ jobs: Copy-Item "build/windows-${{ matrix.arch }}/Frame Studio.exe" $stage Copy-Item -Recurse "build/windows-${{ matrix.arch }}/frame" "$stage/frame" Copy-Item LICENSE "$stage/LICENSE.txt" + if ("${{ matrix.arch }}" -eq "x64") { # Frame Home for this PC's SteamVR, beside Studio + Expand-Archive "pc/FrameHome-PC-windows-x64.zip" -DestinationPath build/pc + Copy-Item -Recurse "build/pc/FrameHome-PC-windows-x64" "$stage/pc" + Remove-Item "$stage/pc/LICENSE.txt" + } New-Item -ItemType Directory -Force dist | Out-Null Compress-Archive -Path $stage -DestinationPath "dist/$name.zip" -Force - uses: actions/upload-artifact@v4 @@ -102,7 +140,7 @@ jobs: name: Publish # (also when a platform failed: "latest" gets every build that worked; a version release needs them all) if: ${{ !cancelled() && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') }} - needs: [linux, windows, macos, frame-home] + needs: [linux, windows, macos, frame-home, pc-home-windows] runs-on: ubuntu-24.04 permissions: contents: write diff --git a/CMakeLists.txt b/CMakeLists.txt index dd1353a..090dd17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,13 +7,42 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() -# SteamVR ships the OpenVR client library; link against the runtime's own copy. -set(OPENVR_LIB_DIR "/opt/steamvr/bin/linuxarm64" CACHE PATH "Directory containing libopenvr_api.so") -find_library(OPENVR_LIB NAMES openvr_api PATHS "${OPENVR_LIB_DIR}" NO_DEFAULT_PATH REQUIRED) -find_package(Vulkan REQUIRED) - set(TP "${CMAKE_SOURCE_DIR}/third_party") +# The Steam Frame build (SteamOS, arm64) links SteamVR's own OpenVR library and the system's Vulkan. A PC build +# (Windows, or x86-64 Linux) has to run on any PC with SteamVR: it finds SteamVR's OpenVR library at run time +# (src/openvr_loader.cpp) and loads Vulkan the same way (volk), so it depends on neither at build time. +if(WIN32 OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + set(FRAME_HOME_PC_DEFAULT ON) +else() + set(FRAME_HOME_PC_DEFAULT OFF) +endif() +option(FRAME_HOME_PC "Build Frame Home for a PC running SteamVR (OpenVR and Vulkan found at run time)" ${FRAME_HOME_PC_DEFAULT}) +if(FRAME_HOME_PC) + add_library(volk STATIC "${TP}/volk/volk.c") + target_include_directories(volk PUBLIC "${TP}/volk" "${TP}/vulkan-headers/include") + if(NOT WIN32) + target_link_libraries(volk PUBLIC dl) + endif() + add_library(openvr-loader STATIC src/openvr_loader.cpp) + target_include_directories(openvr-loader PUBLIC "${TP}/openvr") + target_compile_definitions(openvr-loader PUBLIC OPENVR_BUILD_STATIC) + if(WIN32) + target_link_libraries(openvr-loader PUBLIC shell32 ole32) + else() + target_link_libraries(openvr-loader PUBLIC dl) + endif() + set(FH_VULKAN volk) + set(FH_OPENVR openvr-loader) +else() + # SteamVR ships the OpenVR client library; link against the runtime's own copy. + set(OPENVR_LIB_DIR "/opt/steamvr/bin/linuxarm64" CACHE PATH "Directory containing libopenvr_api.so") + find_library(OPENVR_LIB NAMES openvr_api PATHS "${OPENVR_LIB_DIR}" NO_DEFAULT_PATH REQUIRED) + find_package(Vulkan REQUIRED) + set(FH_VULKAN Vulkan::Vulkan) + set(FH_OPENVR "${OPENVR_LIB}") +endif() + # ── Editor loader stack (Quest Home Editor: ASMH/HSTF/RENDMESH/MATL/RENDTXTR/RENDSHAD) ── add_library(miniz STATIC "${TP}/miniz/miniz.c" "${TP}/miniz/miniz_tdef.c" "${TP}/miniz/miniz_tinfl.c" "${TP}/miniz/miniz_zip.c") target_include_directories(miniz PUBLIC "${TP}/miniz") @@ -96,8 +125,19 @@ add_executable(frame-home "${TP}/stb/stb_vorbis_impl.c" "${TP}/miniaudio/miniaudio_impl.c") target_include_directories(frame-home PRIVATE "${TP}/openvr" "${TP}/miniaudio") -target_link_libraries(frame-home PRIVATE frame-scene opusfile Vulkan::Vulkan "${OPENVR_LIB}" dl m) -set_target_properties(frame-home PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}") +target_link_libraries(frame-home PRIVATE frame-scene opusfile ${FH_VULKAN} ${FH_OPENVR}) +if(FRAME_HOME_PC) + target_compile_definitions(frame-home PRIVATE FH_USE_VOLK) +endif() +if(WIN32) + target_link_libraries(frame-home PRIVATE psapi ole32 shell32 winmm) + target_link_options(frame-home PRIVATE -static) +else() + target_link_libraries(frame-home PRIVATE dl m) +endif() +if(NOT FRAME_HOME_PC) + set_target_properties(frame-home PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}") +endif() # fenv-tool: convert homes and models into .fenv environments. add_executable(fenv-tool src/fenv_tool.cpp) @@ -106,5 +146,14 @@ target_link_libraries(fenv-tool PRIVATE frame-scene) # frame-switcher: the SteamVR dashboard panel that switches the environment. add_executable(frame-switcher src/switcher.cpp) target_include_directories(frame-switcher PRIVATE src "${TP}/stb" "${TP}/qhe" "${TP}/miniz" "${TP}/openvr") -target_link_libraries(frame-switcher PRIVATE frame-scene "${OPENVR_LIB}") -set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}") +if(FRAME_HOME_PC) + target_include_directories(frame-switcher PRIVATE "${TP}/vulkan-headers/include") +endif() +target_link_libraries(frame-switcher PRIVATE frame-scene ${FH_OPENVR}) +if(WIN32) + target_link_libraries(frame-switcher PRIVATE psapi ole32 shell32) + target_link_options(frame-switcher PRIVATE -static) +endif() +if(NOT FRAME_HOME_PC) + set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}") +endif() diff --git a/README.md b/README.md index b273e8e..8dfc52b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Artifacts). | Mac with Apple Silicon (M1 and later) | `FrameStudio-macos-arm64.zip` | | Mac with Intel | `FrameStudio-macos-x64.zip` | | Frame Home only, installed from any computer | `FrameHome-installer.zip` | +| Frame Home for SteamVR on a Windows PC, without Studio | `FrameHome-PC-windows-x64.zip` (Studio's Windows x64 and Linux x64 downloads include it) | Studio needs a GPU with Vulkan 1.2: any recent NVIDIA, AMD or Intel driver on Windows and Linux. On a Mac, Vulkan runs on Metal through MoltenVK, which comes inside the app. The 32-bit builds have at most 4 GB to work in, so the @@ -49,6 +50,11 @@ very largest homes may not fit. thermal governor that keeps the headset cool. - Safe: a compositor layer that steps aside if Frame Home stops, and a one-file off switch. +**On a PC with SteamVR** + +- The same Frame Home as SteamVR's environment on Windows and Linux, switched from the Steam menu, showing what you + save in Studio ([guide](docs/pc-steamvr.md)). + **Frame Studio (on your computer)** - Opens Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pairs rooms with vistas. @@ -72,6 +78,7 @@ very largest homes may not fit. | [Performance](docs/performance.md) | Reaching 120 fps, checking it, fixing stutter | | [Frame Studio guide](docs/studio-guide.md) | The Studio window, tools and panels | | [Bundles](docs/bundles.md) | Several environments in one `.fbundle` | +| [Frame Home on a PC](docs/pc-steamvr.md) | Your homes as SteamVR's environment on a Windows or Linux PC | | [Building from source](docs/building.md) | Building on each system, CI, the headset side | | [Troubleshooting](docs/troubleshooting.md) | Symptoms and fixes, logs, the self-test | diff --git a/docs/README.md b/docs/README.md index b16b3e2..50c7abd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,5 +10,6 @@ | [Performance](performance.md) | How Frame Home reaches 120 fps, checking it, fixing stutter, Studio's own pacing | | [Frame Studio guide](studio-guide.md) | The Studio window, tools and panels | | [Bundles](bundles.md) | Several environments in one `.fbundle` file | +| [Frame Home on a PC](pc-steamvr.md) | Your homes as SteamVR's environment on a Windows or Linux PC | | [Building from source](building.md) | Building on each system, the CI, the headset side, the repository layout | | [Troubleshooting](troubleshooting.md) | Symptoms and fixes, logs, the self-test | diff --git a/docs/pc-steamvr.md b/docs/pc-steamvr.md new file mode 100644 index 0000000..a398451 --- /dev/null +++ b/docs/pc-steamvr.md @@ -0,0 +1,58 @@ +# Frame Home on a PC (SteamVR) + +Frame Home also runs on a PC with SteamVR: the same renderer, homes and settings as on the Steam Frame, as SteamVR's +environment whenever no game is open. The **Environments** panel in the Steam menu switches homes, as on the headset. +It works with any headset SteamVR drives, including a Steam Frame streaming from the PC. + +On a PC, Frame Home shows the environments in Frame Studio's **My Environments** (`Documents\Frame Studio\Environments`): +save something in Studio and it's in VR straight away. Bundles (`.fbundle`) dropped there are unpacked by the panel. + +## Install + +**From Frame Studio:** **Properties > Headset**, section **This PC**: **Install Frame Home on This PC**. The same button +updates it after you update Studio; **Remove from This PC** takes it off. + +**Without Studio (Windows):** download `FrameHome-PC-windows-x64.zip` from the +[latest build](https://github.com/Toastconcern/FrameStudio/releases), unzip it, and double-click +**Install Frame Home on this PC.bat** (or **Remove Frame Home from this PC.bat**). From a terminal: + +```powershell +powershell -ExecutionPolicy Bypass -File install-frame-home-pc.ps1 # install or update +powershell -ExecutionPolicy Bypass -File install-frame-home-pc.ps1 -Uninstall # SteamVR's own environment back +``` + +SteamVR can be running or not: running, the change happens at once; not running, it's written to SteamVR's settings +and takes effect when SteamVR starts. + +## How it works + +**Windows.** Frame Home becomes SteamVR's *home app*: SteamVR starts it whenever no game is open, and brings it back +when a game quits. For the moments before it draws (SteamVR starting, a game closing) SteamVR shows its classic +environment instead of Aurora, with its background set to a 360-degree picture of your current home taken from its +spawn. So you see your home, not Aurora. The panel makes a new picture whenever you switch homes. + +**Linux.** Exactly the Steam Frame's way: a small Vulkan layer in SteamVR's compositor draws Frame Home in Aurora's +place, and `frame-home --system` renders it. (Linux PCs are supported but haven't been tested on a desktop yet.) + +The installer saves the SteamVR settings it changes (`enableHomeApp`, `environmentMode`, `background` and its +projection) and puts them back exactly when you remove Frame Home. + +## Files + +| Where | What | +|---|---| +| `%LOCALAPPDATA%\Frame Home\bin` | `frame-home.exe`, `frame-switcher.exe` | +| `%LOCALAPPDATA%\Frame Home\current` | the environment shown | +| `%LOCALAPPDATA%\Frame Home\settings.json` | Frame Home's settings (the panel's Settings page) | +| `%LOCALAPPDATA%\Frame Home\logs` | logs | +| `%LOCALAPPDATA%\Frame Home\background` | the picture SteamVR shows while Frame Home starts | +| `Documents\Frame Studio\Environments` | the environments | + +On Linux everything is in `~/frame-home`, as on the headset. + +## Troubleshooting + +- **Aurora still shows after installing:** SteamVR was running with its settings window open; restart SteamVR. +- **Nothing in the Steam menu:** the panel starts with SteamVR. In SteamVR's settings, **Startup / Shutdown > Choose + Startup Overlay Apps** should list **Environments** switched on. +- **Black world:** Frame Home needs a GPU with Vulkan 1.2. Its log is `%LOCALAPPDATA%\Frame Home\logs\frame-home.log`. diff --git a/docs/studio-guide.md b/docs/studio-guide.md index a125e02..2ad37e5 100644 --- a/docs/studio-guide.md +++ b/docs/studio-guide.md @@ -160,6 +160,8 @@ The **Headset** tab (or the **Headset** menu): - **Send to Frame** makes the open environment the headset's current one, saved or not. - **Install Frame Home** puts Frame Home itself on the headset, with the environments and bundles you list under **Environments to install**. **Restore SteamVR's Environment** takes it off again. +- **This PC** (Windows and Linux): **Install Frame Home on This PC** makes your environments SteamVR's environment on + the computer Studio runs on. See [pc-steamvr.md](pc-steamvr.md). - The **Headset** field is the headset's address: an SSH alias such as `frame`, or `steamos@
`. See [headset-setup.md](headset-setup.md) and [install-frame-home.md](install-frame-home.md). diff --git a/frame/pc/Install Frame Home on this PC.bat b/frame/pc/Install Frame Home on this PC.bat new file mode 100644 index 0000000..34f8147 --- /dev/null +++ b/frame/pc/Install Frame Home on this PC.bat @@ -0,0 +1,6 @@ +@echo off +rem Makes Frame Home SteamVR's environment on this PC (your homes from Frame Studio's My Environments, switched +rem in the Steam menu's Environments panel). Run it again after updating Frame Studio. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-frame-home-pc.ps1" +if errorlevel 1 (echo. ^& echo Frame Home was NOT installed: see the messages above.) else (echo. ^& echo Done.) +pause diff --git a/frame/pc/Remove Frame Home from this PC.bat b/frame/pc/Remove Frame Home from this PC.bat new file mode 100644 index 0000000..78f2ab5 --- /dev/null +++ b/frame/pc/Remove Frame Home from this PC.bat @@ -0,0 +1,4 @@ +@echo off +rem Gives SteamVR its own environment back on this PC (your environments are kept). +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-frame-home-pc.ps1" -Uninstall +pause diff --git a/frame/pc/install-frame-home-pc.ps1 b/frame/pc/install-frame-home-pc.ps1 new file mode 100644 index 0000000..b3c2198 --- /dev/null +++ b/frame/pc/install-frame-home-pc.ps1 @@ -0,0 +1,128 @@ +# Frame Home on a Windows PC running SteamVR: your homes become SteamVR's environment, and the Environments panel +# in the Steam menu switches them. It shows the environments in Frame Studio's My Environments +# (Documents\Frame Studio\Environments): save one in Studio and it's there. +# +# powershell -ExecutionPolicy Bypass -File install-frame-home-pc.ps1 install or update +# powershell -ExecutionPolicy Bypass -File install-frame-home-pc.ps1 -Uninstall SteamVR's own environment back +# +# How: Frame Home becomes SteamVR's home app (SteamVR runs it whenever no game is open), with SteamVR's classic +# environment in place of Aurora showing a 360-degree picture of the current home for the moments before Frame Home +# draws. The SteamVR settings this changes are saved first and put back exactly on -Uninstall. Works with SteamVR +# running (through its API) or not (its config files, read when it next starts). +param([switch]$Uninstall) +$ErrorActionPreference = 'Stop' + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$root = $(if ($env:FRAME_HOME_ROOT) { $env:FRAME_HOME_ROOT } else { Join-Path $env:LOCALAPPDATA 'Frame Home' }) # (FRAME_HOME_ROOT: tests) +$bin = Join-Path $root 'bin' +function Say($m) { Write-Host "frame-install: $m" } + +# SteamVR's config folder, from OpenVR's own record of where things are. +$vrpath = Join-Path $env:LOCALAPPDATA 'openvr\openvrpaths.vrpath' +if (-not (Test-Path $vrpath)) { Say "SteamVR isn't installed on this PC (no $vrpath). Install SteamVR from Steam first."; exit 1 } +$openvr = Get-Content $vrpath -Raw | ConvertFrom-Json +$cfg = @($openvr.config)[0] +if (-not $cfg) { Say "SteamVR's config folder isn't listed in $vrpath"; exit 1 } +$running = [bool](Get-Process vrserver -ErrorAction SilentlyContinue) + +# JSON as SteamVR writes it: UTF-8 without a byte-order mark, replaced in one step. +function Load($p) { + if (-not (Test-Path $p)) { return [pscustomobject]@{} } + try { return Get-Content $p -Raw | ConvertFrom-Json } catch { Say "$p does not read: left as it is"; exit 1 } +} +function Save($p, $o) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $p) | Out-Null + $json = $o | ConvertTo-Json -Depth 32 + [IO.File]::WriteAllText("$p.tmp", $json, (New-Object Text.UTF8Encoding $false)) + Move-Item -Force "$p.tmp" $p +} +function SetProp($o, $name, $value) { + if ($o.PSObject.Properties[$name]) { $o.$name = $value } else { $o | Add-Member -NotePropertyName $name -NotePropertyValue $value } +} + +$manifests = @((Join-Path $root 'frame-home.vrmanifest'), (Join-Path $root 'frame-switcher.vrmanifest')) +$before = Join-Path $root 'steamvr-before.json' + +# SteamVR not running: the same registration, in its config files. +function Offline($install) { + $ac = Join-Path $cfg 'appconfig.json' + $a = Load $ac + if (-not $a.PSObject.Properties['manifest_paths']) { SetProp $a 'manifest_paths' @() } + $paths = [System.Collections.ArrayList]@($a.manifest_paths) + foreach ($m in $manifests) { + if ($install -and -not $paths.Contains($m)) { [void]$paths.Add($m) } + if (-not $install -and $paths.Contains($m)) { $paths.Remove($m) } + } + $a.manifest_paths = @($paths) + Save $ac $a + $vs = Join-Path $cfg 'steamvr.vrsettings' + $s = Load $vs + if (-not $s.PSObject.Properties['steamvr']) { SetProp $s 'steamvr' ([pscustomobject]@{}) } + $sv = $s.steamvr + if ($install) { + if (-not (Test-Path $before)) { # what was there before Frame Home, saved once + $was = [ordered]@{ + enableHomeApp = [bool]$sv.enableHomeApp + environmentMode = $(if ($sv.PSObject.Properties['environmentMode']) { [int]$sv.environmentMode } else { 1 }) + background = [string]$sv.background + backgroundUseDomeProjection = [bool]$sv.backgroundUseDomeProjection + backgroundCameraHeight = $(if ($sv.PSObject.Properties['backgroundCameraHeight']) { [double]$sv.backgroundCameraHeight } else { 1.6 }) + backgroundDomeRadius = [double]$sv.backgroundDomeRadius + } + Save $before ([pscustomobject]$was) + } + SetProp $sv 'enableHomeApp' $true + SetProp $sv 'environmentMode' 0 + } else { + $was = $(if (Test-Path $before) { Get-Content $before -Raw | ConvertFrom-Json } else { $null }) + SetProp $sv 'enableHomeApp' $(if ($was) { [bool]$was.enableHomeApp } else { $false }) + SetProp $sv 'environmentMode' $(if ($was) { [int]$was.environmentMode } else { 1 }) + SetProp $sv 'background' $(if ($was) { [string]$was.background } else { '' }) + SetProp $sv 'backgroundUseDomeProjection' $(if ($was) { [bool]$was.backgroundUseDomeProjection } else { $false }) + if ($was) { SetProp $sv 'backgroundCameraHeight' ([double]$was.backgroundCameraHeight); SetProp $sv 'backgroundDomeRadius' ([double]$was.backgroundDomeRadius) } + Remove-Item -Force -ErrorAction SilentlyContinue $before + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $root 'background') + foreach ($m in $manifests) { Remove-Item -Force -ErrorAction SilentlyContinue $m } + } + Save $vs $s + Save (Join-Path $cfg 'vrappconfig\framehome.switcher.vrappconfig') ([pscustomobject]@{ autolaunch = [bool]$install; last_launch_time = '0' }) +} + +function StopAll { Get-Process frame-home, frame-switcher -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500 } + +if ($Uninstall) { + if ($running) { + & (Join-Path $bin 'frame-home.exe') --uninstall + & (Join-Path $bin 'frame-switcher.exe') --uninstall + } else { + Offline $false + } + StopAll + Say "SteamVR's own environment is back (your environments are kept in Documents\Frame Studio\Environments)" + exit 0 +} + +foreach ($x in 'frame-home.exe', 'frame-switcher.exe') { + if (-not (Test-Path (Join-Path $here $x))) { Say "this installer has no $x"; exit 1 } +} +# Runs here, before anything installed is touched (a missing Vulkan driver shows up now, not as a black world later). +& (Join-Path $here 'frame-home.exe') --version | Out-Null +if ($LASTEXITCODE -ne 0) { Say "Frame Home can't run on this PC (Vulkan driver?): nothing was changed"; exit 1 } + +StopAll +New-Item -ItemType Directory -Force -Path $bin, (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'Frame Studio\Environments') | Out-Null +foreach ($x in 'frame-home.exe', 'frame-switcher.exe') { Copy-Item -Force (Join-Path $here $x) (Join-Path $bin $x) } + +if ($running) { + & (Join-Path $here 'frame-switcher.exe') --install + & (Join-Path $here 'frame-home.exe') --install + if ($LASTEXITCODE -ne 0) { Say "SteamVR refused Frame Home ($LASTEXITCODE)"; exit 1 } + Say "installed: Frame Home is SteamVR's environment now" +} else { + # (with SteamVR not running these write their manifests and say so) + & (Join-Path $here 'frame-switcher.exe') --install | Out-Null + & (Join-Path $here 'frame-home.exe') --install | Out-Null + Offline $true + Say "installed: Frame Home becomes SteamVR's environment when SteamVR next starts" +} +exit 0 diff --git a/scripts/build-studio.sh b/scripts/build-studio.sh index 5552be7..4ab161d 100755 --- a/scripts/build-studio.sh +++ b/scripts/build-studio.sh @@ -52,7 +52,38 @@ build() { "$CMAKE" --build "$dir" --target frame-studio --parallel } -# dist/FrameStudio-: the program and, beside it, frame/ (the headset side Studio installs) +# Frame Home for SteamVR on the computer itself (Windows x64, Linux x64), built from the top-level project into +# $BUILD/-home. $1 target, then extra cmake arguments. +build_pc_home() { + target=$1; shift + dir="$BUILD/$target-home" + say "building Frame Home for $target's SteamVR" + "$CMAKE" -S "$ROOT" -B "$dir" -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON "$@" > "$dir.configure.log" 2>&1 || { + tail -20 "$dir.configure.log"; say "$target: Frame Home configure failed (full log: $dir.configure.log)"; exit 1; } + "$CMAKE" --build "$dir" --target frame-home frame-switcher --parallel +} + +# pc/ beside Studio: Frame Home for this computer's SteamVR and its installer (Studio's Headset tab runs it). +# Windows: SteamVR's home app (install-frame-home-pc.ps1). Linux: the Steam Frame's way, the compositor layer, with +# the headset's own install.sh. +stage_pc() { + target=$1 stage=$2 home="$BUILD/$1-home" + case "$target" in + windows-x64) mkdir -p "$stage/pc" + cp "$home/frame-home.exe" "$home/frame-switcher.exe" "$stage/pc/" + cp "$ROOT/frame/pc/install-frame-home-pc.ps1" "$ROOT/frame/pc/Install Frame Home on this PC.bat" \ + "$ROOT/frame/pc/Remove Frame Home from this PC.bat" "$stage/pc/" ;; + linux-x64) mkdir -p "$stage/pc/bin" "$stage/pc/layer" + cp "$home/frame-home" "$home/frame-switcher" "$stage/pc/bin/" + ${CXX:-g++} -O2 -shared -fPIC -std=c++17 -fvisibility=hidden -I"$ROOT/third_party/vulkan-headers/include" -I"$ROOT/src" \ + "$ROOT/layer/frame_home_layer.cpp" -o "$stage/pc/layer/libframe_home_layer.so" -lpthread + cp "$ROOT/frame/payload/layer/env_ps.spv" "$stage/pc/layer/" + cp "$ROOT/frame/payload/install.sh" "$stage/pc/" && chmod +x "$stage/pc/install.sh" "$stage"/pc/bin/* ;; + esac +} + +# dist/FrameStudio-: the program and, beside it, frame/ (the headset side Studio installs) and pc/ (Frame Home +# for this computer's SteamVR, where there is one) package() { target=$1 dir="$BUILD/$1" name="FrameStudio-$1" stage="$BUILD/stage/$name" @@ -60,8 +91,10 @@ package() { cp "$ROOT/LICENSE" "$stage/LICENSE.txt" case "$target" in windows-*) cp "$dir/Frame Studio.exe" "$stage/" && cp -R "$dir/frame" "$stage/frame" + stage_pc "$target" "$stage" (cd "$BUILD/stage" && rm -f "$DIST/$name.zip" && zip -qr "$DIST/$name.zip" "$name") ;; linux-*) cp "$dir/Frame Studio" "$stage/" && cp -R "$dir/frame" "$stage/frame" + stage_pc "$target" "$stage" tar -czf "$DIST/$name.tar.gz" -C "$BUILD/stage" "$name" ;; macos-*) cp -R "$dir/Frame Studio.app" "$stage/" bundle_moltenvk "$stage/Frame Studio.app" @@ -97,10 +130,12 @@ package_frame_home() { for t in $TARGETS; do case "$t" in - linux-x64|linux-arm64) build "$t" -G Ninja && package "$t" ;; + linux-x64) build "$t" -G Ninja && build_pc_home "$t" -G Ninja && package "$t" ;; + linux-arm64) build "$t" -G Ninja && package "$t" ;; linux-x86) build "$t" -G Ninja -DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_SYSTEM_PROCESSOR=i686 \ -DCMAKE_LIBRARY_ARCHITECTURE=i386-linux-gnu && package "$t" ;; - windows-x64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=x86_64 && package "$t" ;; + windows-x64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=x86_64 && + build_pc_home "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=x86_64 && package "$t" ;; windows-x86) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=i686 && package "$t" ;; windows-arm64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=aarch64 && package "$t" ;; macos-arm64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 && package "$t" ;; diff --git a/src/env_export.h b/src/env_export.h index 82622fc..9e9386c 100644 --- a/src/env_export.h +++ b/src/env_export.h @@ -4,6 +4,25 @@ #include "env_share.h" #include "hsl_renderer.h" +#ifdef _WIN32 +// A PC has no compositor layer: Frame Home is SteamVR's scene app there, so system mode never starts (init() fails). +class EnvExport { +public: + bool init() { return false; } + envshare::State* state() { return &state_; } + void release(HslRenderer&) {} + bool connected() const { return false; } + bool createImages(HslRenderer&) { return false; } + bool connect() { return false; } + void poll() {} + int beginFrame() { return 0; } + void publish(int, const float[2][16], const float[2][3]) {} + uint32_t refusedFlags() const { return 0; } + double sinceSampled() const { return 1e9; } +private: + envshare::State state_; +}; +#else #include #include #include @@ -141,3 +160,4 @@ private: uint32_t generation_ = 0; std::chrono::steady_clock::time_point lastTry_; }; +#endif // _WIN32 diff --git a/src/env_share.h b/src/env_share.h index 1f1b687..10df7c4 100644 --- a/src/env_share.h +++ b/src/env_share.h @@ -12,6 +12,7 @@ // with SCM_RIGHTS fds: kSlots image memories, then the shared State (a memfd). A new Hello (new size) replaces the // old images. #include +#include #include #include #include @@ -65,9 +66,13 @@ struct State { }; inline uint64_t nowNs() { +#ifdef _WIN32 + return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); +#else timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return static_cast(ts.tv_sec) * 1000000000ull + static_cast(ts.tv_nsec); +#endif } } // namespace envshare diff --git a/src/home_platform.h b/src/home_platform.h new file mode 100644 index 0000000..082a6c8 --- /dev/null +++ b/src/home_platform.h @@ -0,0 +1,299 @@ +#pragma once +// What Frame Home and its Environments panel need from the system, on the Steam Frame (SteamOS) and on a PC running +// SteamVR (Windows, or Linux): where their files are, finding and stopping each other, starting a tool and waiting +// for it, starting again, and a few process settings. Everything else in them is the same everywhere. +// +// Files: the Steam Frame keeps everything in ~/frame-home (environments in envs/). A PC keeps Frame Home's own files +// in %LOCALAPPDATA%\Frame Home (Windows) or ~/frame-home (Linux), and shows the environments saved in Frame Studio's +// My Environments (Documents\Frame Studio\Environments): what you save there is in VR straight away. +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace homeos { + +#ifdef _WIN32 +using Pid = DWORD; + +inline std::filesystem::path knownFolder(REFKNOWNFOLDERID id) { + PWSTR p = nullptr; + std::filesystem::path out; + if (SUCCEEDED(SHGetKnownFolderPath(id, 0, nullptr, &p))) out = p; + CoTaskMemFree(p); + return out; +} +inline std::filesystem::path root() { + if (const char* r = std::getenv("FRAME_HOME_ROOT")) return std::filesystem::u8path(r); // (tests, a portable copy) + return knownFolder(FOLDERID_LocalAppData) / L"Frame Home"; +} +inline std::filesystem::path envs() { + if (const char* e = std::getenv("FRAME_HOME_ENVS")) return std::filesystem::u8path(e); + return knownFolder(FOLDERID_Documents) / L"Frame Studio" / L"Environments"; +} +inline std::filesystem::path thumbs() { return root() / L"thumbs"; } // (not beside the environments: it'd show there) +inline std::string exeName(const char* base) { return std::string(base) + ".exe"; } + +inline std::filesystem::path selfPath() { + wchar_t buf[MAX_PATH * 4] = {}; + const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast(std::size(buf))); + return n ? std::filesystem::path(buf) : std::filesystem::path(); +} +inline bool stderrIsTerminal() { return _isatty(_fileno(stderr)) != 0; } +inline long peakRamMB() { + PROCESS_MEMORY_COUNTERS pmc = {}; + return GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc) ? static_cast(pmc.PeakWorkingSetSize / (1024 * 1024)) : 0; +} +inline void setEnv(const char* name, const char* value) { _putenv_s(name, value); } +// Background work (thumbnails, unpacking): 15 and up below normal, 19 idle. +inline void lowerPriority(int nice) { SetPriorityClass(GetCurrentProcess(), nice >= 19 ? IDLE_PRIORITY_CLASS : BELOW_NORMAL_PRIORITY_CLASS); } + +// Running copies of `name` other than this one. (Windows doesn't let one process read another's arguments cheaply, +// and the PC has no --system process to tell apart: a copy is a copy.) +inline std::vector runningCopies(const char* name) { + std::vector out; + const HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) return out; + PROCESSENTRY32W e = {}; + e.dwSize = sizeof e; + const std::wstring want = std::filesystem::path(exeName(name)).wstring(); + for (BOOL ok = Process32FirstW(snap, &e); ok; ok = Process32NextW(snap, &e)) + if (e.th32ProcessID != GetCurrentProcessId() && _wcsicmp(e.szExeFile, want.c_str()) == 0) out.push_back(e.th32ProcessID); + CloseHandle(snap); + return out; +} +inline std::vector runningWith(const char*, const std::string&) { return {}; } +inline bool alive(Pid p) { + const HANDLE h = OpenProcess(SYNCHRONIZE, FALSE, p); + if (!h) return false; + const bool running = WaitForSingleObject(h, 0) == WAIT_TIMEOUT; + CloseHandle(h); + return running; +} +// No polite quit for another process's message-less loop on Windows: it's ended (Frame Home keeps nothing unsaved). +inline void stop(Pid p, bool) { + if (const HANDLE h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, p)) { + TerminateProcess(h, 0); + WaitForSingleObject(h, 5000); + CloseHandle(h); + } +} +inline void stopCopies(const char* name) { for (Pid p : runningCopies(name)) stop(p, true); } + +inline std::wstring commandLine(const std::vector& args) { // each argument quoted as CreateProcess wants + std::wstring cl; + for (const std::string& a : args) { + const std::wstring w = std::filesystem::u8path(a).wstring(); + if (!cl.empty()) cl += L' '; + cl += L'"'; + size_t slashes = 0; + for (wchar_t c : w) { + if (c == L'\\') { ++slashes; continue; } + if (c == L'"') cl.append(slashes * 2 + 1, L'\\'); + else cl.append(slashes, L'\\'); + slashes = 0; + cl += c; + } + cl.append(slashes * 2, L'\\'); + cl += L'"'; + } + return cl; +} +// Starts args[0] with the rest (no window, output discarded); waits when asked. The exit code, or -1. +inline int run(const std::vector& args, int nice, bool wait) { + std::wstring cl = commandLine(args); + STARTUPINFOW si = {}; + si.cb = sizeof si; + PROCESS_INFORMATION pi = {}; + const DWORD flags = CREATE_NO_WINDOW | (nice >= 19 ? IDLE_PRIORITY_CLASS : nice > 0 ? BELOW_NORMAL_PRIORITY_CLASS : 0); + if (!CreateProcessW(nullptr, cl.data(), nullptr, nullptr, FALSE, flags, nullptr, nullptr, &si, &pi)) return -1; + DWORD code = 0; + if (wait) { WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &code); } + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return static_cast(code); +} +// This program again with the same arguments (settings that build the renderer differently, a lost GPU); this one +// ends. SteamVR knows its home app by its file, so the new one is still the environment. +[[noreturn]] inline void restartSelf() { + STARTUPINFOW si = {}; + si.cb = sizeof si; + PROCESS_INFORMATION pi = {}; + std::wstring cl = GetCommandLineW(); + if (CreateProcessW(nullptr, cl.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } + std::exit(0); +} +#else +using Pid = pid_t; + +inline std::filesystem::path root() { + if (const char* r = std::getenv("FRAME_HOME_ROOT")) return r; // (tests, a portable copy) + const char* home = std::getenv("HOME"); + return std::filesystem::path(home && *home ? home : "/home/steamos") / "frame-home"; +} +inline std::filesystem::path envs() { + if (const char* e = std::getenv("FRAME_HOME_ENVS")) return e; + return root() / "envs"; +} +inline std::filesystem::path thumbs() { return envs() / ".thumbs"; } +inline std::string exeName(const char* base) { return base; } + +inline std::filesystem::path selfPath() { + char buf[PATH_MAX] = {}; + const ssize_t n = readlink("/proc/self/exe", buf, sizeof buf - 1); + return n > 0 ? std::filesystem::path(std::string(buf, static_cast(n))) : std::filesystem::path(); +} +inline bool stderrIsTerminal() { return isatty(2) != 0; } +inline long peakRamMB() { rusage u = {}; getrusage(RUSAGE_SELF, &u); return u.ru_maxrss / 1024; } // (KB on Linux) +inline void setEnv(const char* name, const char* value) { setenv(name, value, 1); } +inline void lowerPriority(int nice) { setpriority(PRIO_PROCESS, 0, nice); } + +// Copies of `name` running without arguments (the environment, the panel: not tools such as --snapshot, --list). +// SteamVR's own bookkeeping (GetApplicationProcessId) loses track of an app once a utility process started from the +// same binary has connected, so /proc it is. +inline std::vector runningCopies(const char* name) { + std::vector out; + std::error_code ec; + for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) { + const std::string id = e.path().filename().string(); + if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue; + const Pid pid = static_cast(std::stol(id)); + if (pid == getpid()) continue; + std::string comm; + std::getline(std::ifstream(e.path() / "comm"), comm); + if (comm != name) continue; + std::ifstream c(e.path() / "cmdline", std::ios::binary); + const std::string cmd((std::istreambuf_iterator(c)), std::istreambuf_iterator()); + size_t args = 0; + for (char ch : cmd) args += ch == '\0'; + if (args <= 1) out.push_back(pid); + } + return out; +} +// Running copies of `name` (other than this process) started with `arg` among their arguments. +inline std::vector runningWith(const char* name, const std::string& arg) { + std::vector out; + std::error_code ec; + for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) { + const std::string id = e.path().filename().string(); + if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue; + const Pid pid = static_cast(std::stol(id)); + if (pid == getpid()) continue; + std::string comm; + std::getline(std::ifstream(e.path() / "comm"), comm); + if (comm != name) continue; + std::ifstream c(e.path() / "cmdline", std::ios::binary); + for (std::string a; std::getline(c, a, '\0');) if (a == arg) { out.push_back(pid); break; } + } + return out; +} +inline bool alive(Pid p) { return kill(p, 0) == 0; } +inline void stop(Pid p, bool force) { kill(p, force ? SIGKILL : SIGTERM); } +// Asks them to quit, then insists after 5 s. +inline void stopCopies(const char* name) { + const std::vector pids = runningCopies(name); + for (Pid p : pids) kill(p, SIGTERM); + for (int i = 0; i < 50; ++i) { + bool any = false; + for (Pid p : pids) any |= kill(p, 0) == 0; + if (!any) return; + usleep(100000); + } + for (Pid p : pids) kill(p, SIGKILL); +} + +// Starts args[0] with the rest (output discarded, at `nice`); waits when asked. The exit code, or -1. +// (an argv, not a shell line: an environment's name can hold any character) +inline int run(const std::vector& args, int nice, bool wait) { + std::vector a = args; + std::vector argv; + for (std::string& s : a) argv.push_back(s.data()); + argv.push_back(nullptr); + const pid_t pid = fork(); + if (pid == 0) { + if (nice > 0) setpriority(PRIO_PROCESS, 0, nice); + const int null = ::open("/dev/null", O_RDWR); + if (null >= 0) { dup2(null, 1); dup2(null, 2); } + execv(argv[0], argv.data()); + _exit(127); + } + if (pid < 0) return -1; + if (!wait) return 0; + int st = 0; + return waitpid(pid, &st, 0) == pid && WIFEXITED(st) ? WEXITSTATUS(st) : -1; +} +// This program again with the same arguments, in this process (SteamVR knows it by its path: still the environment). +[[noreturn]] inline void restartSelf() { + const std::string self = selfPath().string(); + std::ifstream cl("/proc/self/cmdline", std::ios::binary); + std::vector args; + for (std::string a; std::getline(cl, a, '\0');) args.push_back(a); + if (args.empty()) args.push_back(self); + std::vector argv; + for (auto& a : args) argv.push_back(a.data()); + argv.push_back(nullptr); + execv(self.c_str(), argv.data()); + std::exit(1); +} +#endif + +inline void sleepMs(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } + +// Files by path, whatever the characters in it (Windows: the wide API; a user name like "José" is no problem). +inline FILE* openFile(const std::filesystem::path& p, const char* mode) { +#ifdef _WIN32 + const std::wstring m(mode, mode + std::strlen(mode)); + return _wfopen(p.wstring().c_str(), m.c_str()); +#else + return std::fopen(p.c_str(), mode); +#endif +} +inline bool reopenStderr(const std::filesystem::path& p, const char* mode) { +#ifdef _WIN32 + const std::wstring m(mode, mode + std::strlen(mode)); + return _wfreopen(p.wstring().c_str(), m.c_str(), stderr) != nullptr; +#else + return std::freopen(p.c_str(), mode, stderr) != nullptr; +#endif +} + +// The memory this process uses now, MB. +inline double ramMB() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS pmc = {}; + return GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc) ? pmc.WorkingSetSize / 1048576.0 : 0.0; +#else + long pages = 0, rssPages = 0; + if (FILE* sm = std::fopen("/proc/self/statm", "r")) { if (std::fscanf(sm, "%ld %ld", &pages, &rssPages) != 2) rssPages = 0; std::fclose(sm); } + return rssPages * (sysconf(_SC_PAGESIZE) / 1048576.0); +#endif +} + +} // namespace homeos diff --git a/src/locomotion.h b/src/locomotion.h index 73ba32d..288aa8a 100644 --- a/src/locomotion.h +++ b/src/locomotion.h @@ -87,7 +87,7 @@ public: "/actions/main/in/left_trigger": "Sit: left hand", "/actions/main/in/right_trigger": "Sit: right hand", "/actions/main/in/gaze": "Where you look" } ] })"; vr::IVRInput* in = vr::VRInput(); - if (!in || in->SetActionManifestPath(fs::absolute(dir / "actions.json").c_str()) != vr::VRInputError_None) return false; + if (!in || in->SetActionManifestPath(fs::absolute(dir / "actions.json").u8string().c_str()) != vr::VRInputError_None) return false; in->GetActionSetHandle("/actions/main", &set_); in->GetActionHandle("/actions/main/in/left_stick", &leftStick_); in->GetActionHandle("/actions/main/in/right_stick", &rightStick_); diff --git a/src/main.cpp b/src/main.cpp index 7f4b165..2ea6ca5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,13 +23,11 @@ #include "perf_settings.h" #include "thermal.h" #include "fenv.h" -#include "proc_util.h" +#include "home_platform.h" #include "stb_image_write.h" #include -#include -#include #include #include @@ -47,7 +45,6 @@ #include #include #include -#include #define LOG(...) do { std::fprintf(stderr, "[frame-home] " __VA_ARGS__); std::fputc('\n', stderr); } while (0) @@ -117,10 +114,7 @@ namespace fs = std::filesystem; // SteamVR's home-app slot: the app it keeps running as the world whenever no game is open. constexpr const char* kHomeAppKey = "openvr.tool.steamvr_environments"; -fs::path rootDir() { - const char* home = std::getenv("HOME"); - return fs::path(home && *home ? home : "/home/steamos") / "frame-home"; -} +fs::path rootDir() { return homeos::root(); } // (home_platform.h: ~/frame-home, or a PC's) std::string readText(const fs::path& p) { std::ifstream f(p); @@ -132,7 +126,7 @@ std::string readText(const fs::path& p) { // The home environment mode shows: ~/frame-home/current lists its APKs in envs/, one per line (footprint, // then vista); else the first home there. std::vector currentHome() { - const fs::path envs = rootDir() / "envs"; + const fs::path envs = homeos::envs(); std::vector home; std::istringstream lines(readText(rootDir() / "current")); std::string name; @@ -437,20 +431,24 @@ void sleepUntilNs(uint64_t t) { // ── Headless snapshot (no SteamVR) ────────────────────────────────────────────────────────────── +// Where Frame Home starts you: the first spawn, eyes 1.6 m over its floor, facing its way (cam: x, y, z, yaw; the +// pitch is left as given). +void spawnView(const HslScene& scene, float cam[5]) { + Locomotion loco; + loco.setWorld(walkWorld(scene)); + float rig[16]; + loco.rig(rig); + cam[0] = rig[12]; cam[1] = rig[13] + 1.6f; cam[2] = rig[14]; + cam[3] = std::atan2(rig[8], rig[0]) * 180.0f / 3.14159265f; + LOG("spawn view: (%.2f %.2f %.2f) yaw %.0f", cam[0], cam[1], cam[2], cam[3]); +} + int runSnapshot(const std::string& out, const std::vector& apks, const float camIn[5], uint32_t size, bool fromSpawn) { HslScene scene; std::string err; if (!loadHslScene(apks, scene, err)) { LOG("%s", err.c_str()); return 1; } float cam[5] = {camIn[0], camIn[1], camIn[2], camIn[3], camIn[4]}; - if (fromSpawn) { // where Frame Home starts you: the spawn, eyes 1.6 m over its floor, facing its way - Locomotion loco; - loco.setWorld(walkWorld(scene)); - float rig[16]; - loco.rig(rig); - cam[0] = rig[12]; cam[1] = rig[13] + 1.6f; cam[2] = rig[14]; - cam[3] = std::atan2(rig[8], rig[0]) * 180.0f / 3.14159265f; - LOG("spawn view: (%.2f %.2f %.2f) yaw %.0f", cam[0], cam[1], cam[2], cam[3]); - } + if (fromSpawn) spawnView(scene, cam); HslRenderer r; // FH_FOVEATE=level (1-3) and FH_GAZE=x,y (0..1 of the image): render foveated, looking there (tests foveation). if (const char* fv = std::getenv("FH_FOVEATE")) r.foveation = std::atoi(fv) > 0; @@ -571,7 +569,7 @@ int runSnapshot(const std::string& out, const std::vector& apks, co // FH_BENCH_MOVES=1 (with FH_BENCH_WALK): every 8 s, after 4 s of walking, a snap turn, a teleport ahead (aimed with the // real arc, landing at once) and a fast head turn (180 degrees in half a second): what a visitor does. The JSON's // "moves" says, for each, how many frames ran late and the slowest; "spikes" the slowest frames (s, ms, move). -static long peakRamMB() { rusage u = {}; getrusage(RUSAGE_SELF, &u); return u.ru_maxrss / 1024; } // (KB on Linux) +static long peakRamMB() { return homeos::peakRamMB(); } int runBench(const std::vector& apks, double seconds, uint32_t w, uint32_t h, float hz, bool unpaced) { const fs::path settingsPath = rootDir() / "settings.json"; @@ -614,8 +612,12 @@ int runBench(const std::vector& apks, double seconds, uint32_t w, u r.msaa = perf.msaa; r.anisotropy = static_cast(perf.anisotropy); r.foveation = perf.foveation > 0; +#ifdef _WIN32 + r.exportDirect = false; // (a PC: as the scene app draws; there's no system mode) +#else r.deviceExtensions = {"VK_KHR_external_memory", "VK_KHR_external_memory_fd"}; r.exportDirect = !std::getenv("FH_BENCH_COPY"); // as system mode draws (FH_BENCH_COPY=1: copied in, the old way) +#endif r.exportSubsampled = !std::getenv("FH_EXPORT_PLAIN"); // likewise (FH_EXPORT_PLAIN=1: our smoothing pass) r.pipelineCachePath = (fs::temp_directory_path() / "frame-home-bench-pipelines.bin").string(); if (const char* pc = std::getenv("FH_BENCH_PIPELINES")) r.pipelineCachePath = pc; @@ -833,13 +835,13 @@ int runBench(const std::vector& apks, double seconds, uint32_t w, u std::vector picked; for (const View& v : views) { if (picked.size() >= 5) break; - bool near = false; // another picked view within 1.5 m looking within 40 degrees + bool nearOne = false; // another picked view within 1.5 m looking within 40 degrees for (const View* q : picked) { float dy = std::fabs(v.v[3] - q->v[3]); if (dy > 180) dy = 360 - dy; - near |= std::hypot(v.v[0] - q->v[0], v.v[2] - q->v[2]) < 1.5f && dy < 40; + nearOne |= std::hypot(v.v[0] - q->v[0], v.v[2] - q->v[2]) < 1.5f && dy < 40; } - if (near) continue; + if (nearOne) continue; picked.push_back(&v); char b[128]; std::snprintf(b, sizeof b, "%s[%.2f,%.2f,%.2f,%.0f,%.0f,%.2f]", worst.empty() ? "" : ",", v.v[0], v.v[1], v.v[2], v.v[3], v.v[4], v.gpuMs); @@ -896,10 +898,14 @@ int runBench(const std::vector& apks, double seconds, uint32_t w, u } // Renders a home into the Frame's native environment panorama (equirect PNG) and prints the dome radius. -int runPanorama(const std::string& out, const std::vector& apks, const float cam[5], uint32_t width) { +// A 360-degree picture of the home (equirectangular PNG, width x width/2) from `cam` (or the spawn), and beside it +// .json: the eye height and the dome radius SteamVR's background needs to project it (the panel on a PC sets it). +int runPanorama(const std::string& out, const std::vector& apks, const float camIn[5], uint32_t width, bool fromSpawn) { HslScene scene; std::string err; if (!loadHslScene(apks, scene, err)) { LOG("%s", err.c_str()); return 1; } + float cam[5] = {camIn[0], camIn[1], camIn[2], camIn[3], camIn[4]}; + if (fromSpawn) spawnView(scene, cam); HslRenderer r; if (!r.init(256, 256)) return 1; if (!r.load(scene)) { LOG("nothing renderable"); return 1; } @@ -910,8 +916,10 @@ int runPanorama(const std::string& out, const std::vector& apks, co v.yawDeg = cam[3]; std::vector rgb; if (!renderPanorama(r, v, width, rgb) || !writePng(out, rgb, width, width / 2)) { LOG("panorama failed"); return 1; } - std::printf("{\"png\": \"%s\", \"width\": %u, \"eyeHeight\": %.3f, \"domeRadius\": %.3f}\n", out.c_str(), width, cam[1], - panoramaDomeRadius(r, scene, v.eye)); + char j[512]; + std::snprintf(j, sizeof j, "{\"width\": %u, \"eyeHeight\": %.3f, \"domeRadius\": %.3f}\n", width, fromSpawn ? 1.6f : cam[1], panoramaDomeRadius(r, scene, v.eye)); + std::printf("%s", j); + if (FILE* f = homeos::openFile(out + ".json", "w")) { std::fputs(j, f); std::fclose(f); } r.shutdown(); return 0; } @@ -936,10 +944,10 @@ Mat4 eyeProjection(vr::EVREye eye, float widen = 1.0f) { // system: Frame Home as the system environment (frame-home --system): not a scene app; it renders for the // compositor layer, which shows it in Aurora's place (env_share.h, layer/frame_home_layer.cpp). int runVr(const std::vector& apks, bool system = false) { - if (!isatty(2)) { // started by SteamVR: messages go to ~/frame-home/logs/frame-home.log + if (!homeos::stderrIsTerminal()) { // started by SteamVR: messages go to ~/frame-home/logs/frame-home.log std::error_code ec; fs::create_directories(rootDir() / "logs", ec); - if (std::freopen((rootDir() / "logs" / "frame-home.log").c_str(), std::getenv("FH_RESTARTED") ? "a" : "w", stderr)) setvbuf(stderr, nullptr, _IOLBF, 0); + if (homeos::reopenStderr(rootDir() / "logs" / "frame-home.log", std::getenv("FH_RESTARTED") ? "a" : "w")) setvbuf(stderr, nullptr, _IOLBF, 0); } vr::EVRInitError ierr = vr::VRInitError_None; vr::VR_Init(&ierr, system ? vr::VRApplication_Overlay : vr::VRApplication_Scene); @@ -967,6 +975,9 @@ int runVr(const std::vector& apks, bool system = false) { ici.ppEnabledExtensionNames = ie.data(); VkInstance probe = VK_NULL_HANDLE; if (vkCreateInstance(&ici, nullptr, &probe) != VK_SUCCESS) { LOG("no Vulkan instance"); vr::VR_Shutdown(); return 1; } +#ifdef FH_USE_VOLK + volkLoadInstanceOnly(probe); // (the renderer loads its own instance's after) +#endif VkPhysicalDevice pd = pickVrDevice(probe); uint32_t dn = vr::VRCompositor()->GetVulkanDeviceExtensionsRequired(pd, nullptr, 0); std::string dext(dn, '\0'); @@ -1130,7 +1141,7 @@ int runVr(const std::vector& apks, bool system = false) { } }; checkHome(); - if (loadState == 0) LOG("no home to show yet: put one in %s", (rootDir() / "envs").c_str()); + if (loadState == 0) LOG("no home to show yet: put one in %s", homeos::envs().string().c_str()); auto lastCheck = std::chrono::steady_clock::now(); // System mode draws a little wider than the lens shows: the compositor turns the last frame with your head until the @@ -1315,13 +1326,13 @@ int runVr(const std::vector& apks, bool system = false) { { // what it's made of and what's wrong with it, for the Debug page (and logs/report.txt to read) pendingReport.loadSec += uploadSec; std::error_code ec; - if (FILE* f = std::fopen((rootDir() / "report.json.part").c_str(), "w")) { + if (FILE* f = homeos::openFile(rootDir() / "report.json.part", "w")) { const std::string j = pendingReport.json(); std::fwrite(j.data(), 1, j.size(), f); std::fclose(f); fs::rename(rootDir() / "report.json.part", rootDir() / "report.json", ec); } - if (FILE* f = std::fopen((rootDir() / "logs" / "report.txt").c_str(), "w")) { + if (FILE* f = homeos::openFile(rootDir() / "logs" / "report.txt", "w")) { const std::string t = pendingReport.text(); std::fwrite(t.data(), 1, t.size(), f); std::fclose(f); @@ -1493,8 +1504,6 @@ int runVr(const std::vector& apks, bool system = false) { std::snprintf(b, sizeof b, "\"gpuMs\":%.2f,", statGpu / statGpuN); j += b; } - long pages = 0, rssPages = 0; - if (FILE* sm = std::fopen("/proc/self/statm", "r")) { if (std::fscanf(sm, "%ld %ld", &pages, &rssPages) != 2) rssPages = 0; std::fclose(sm); } const HslRenderer::FrameStats& rs = r.lastStats(); std::snprintf(b, sizeof b, "\"budgetMs\":%.2f,\"scale\":%.2f,\"width\":%u,\"height\":%u,\"foveation\":%s,\"eyeTracking\":%s,\"msaa\":%d," @@ -1502,7 +1511,7 @@ int runVr(const std::vector& apks, bool system = false) { "\"ramMB\":%.0f,\"dashboard\":%s,\"debugView\":%d,\"loading\":%s,\"foveationLevel\":%d,\"tempC\":%.1f,\"margin\":%.3f,\"late\":%d,\"shownFps\":%.1f,\"home\":", budget, scale, r.width(), r.height(), r.foveationActive() ? "true" : "false", gazeOk ? "true" : "false", perf.msaa, statFrames ? statCpu / statFrames : 0.0, statFrames / secs, rs.draws, static_cast(rs.triangles), r.meshCount(), - r.drawnCount(), r.texturesPending(), rssPages * (sysconf(_SC_PAGESIZE) / 1048576.0), dashboard ? "true" : "false", r.debugView(), + r.drawnCount(), r.texturesPending(), homeos::ramMB(), dashboard ? "true" : "false", r.debugView(), loadState != 0 || uploading ? "true" : "false", r.foveationActive() ? static_cast(std::lround(r.foveationStrength())) : 0, thermal.available() ? thermal.hottest() : 0.0f, widenN ? widenSum / widenN : widen, statLate, statPublished / secs); statLate = statPublished = 0; @@ -1524,10 +1533,11 @@ int runVr(const std::vector& apks, bool system = false) { } j += "}\n"; statGpu = 0; statGpuN = 0; statCpu = 0; statFrames = 0; - if (FILE* f = std::fopen((statsPath.string() + ".part").c_str(), "w")) { + if (FILE* f = homeos::openFile(statsPath.string() + ".part", "w")) { std::fwrite(j.data(), 1, j.size(), f); std::fclose(f); - std::rename((statsPath.string() + ".part").c_str(), statsPath.c_str()); + std::error_code rec; + fs::rename(statsPath.string() + ".part", statsPath, rec); } } @@ -1538,7 +1548,7 @@ int runVr(const std::vector& apks, bool system = false) { const bool layer = env.connect(); if (env.refusedFlags()) { // an older layer, or a compositor that can't sample them: plain images instead LOG("the compositor can't read the frames with flags 0x%x: restarting with plain ones", env.refusedFlags()); - setenv("FH_EXPORT_PLAIN", "1", 1); + homeos::setEnv("FH_EXPORT_PLAIN", "1"); restart = true; running = false; } @@ -1799,22 +1809,11 @@ int runVr(const std::vector& apks, bool system = false) { r.shutdown(); if (restart) { // settings that build the renderer differently: start again as the same program (SteamVR // knows it by its path, so it's still the environment) - char self[4096] = {}; - const ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1); - if (n > 0) { - if (lostDevice) std::this_thread::sleep_for(std::chrono::seconds(2)); // (a GPU still resetting: not straight back into it) - LOG("restarting %s", lostDevice ? "with a new GPU device" : "with the new settings"); - std::fflush(stderr); - std::ifstream cl("/proc/self/cmdline", std::ios::binary); // the same arguments as this run - std::vector args; - for (std::string a; std::getline(cl, a, '\0');) args.push_back(a); - std::vector argv; - for (auto& a : args) argv.push_back(a.data()); - if (argv.empty()) argv.push_back(self); - argv.push_back(nullptr); - setenv("FH_RESTARTED", "1", 1); - execv(self, argv.data()); - } + if (lostDevice) std::this_thread::sleep_for(std::chrono::seconds(2)); // (a GPU still resetting: not straight back into it) + LOG("restarting %s", lostDevice ? "with a new GPU device" : "with the new settings"); + std::fflush(stderr); + homeos::setEnv("FH_RESTARTED", "1"); + homeos::restartSelf(); } return 0; } @@ -1823,11 +1822,12 @@ int runRegister(const char* manifest) { vr::EVRInitError ierr = vr::VRInitError_None; vr::VR_Init(&ierr, vr::VRApplication_Utility); if (ierr != vr::VRInitError_None) { LOG("VR_Init: %s", vr::VR_GetVRInitErrorAsEnglishDescription(ierr)); return 1; } - char full[4096]; int rc = 1; - if (realpath(manifest, full)) { - vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(full, false); - LOG("AddApplicationManifest(%s): %s", full, vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae)); + std::error_code ec; + if (fs::exists(manifest, ec)) { + const std::string full = fs::absolute(manifest, ec).u8string(); + vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(full.c_str(), false); + LOG("AddApplicationManifest(%s): %s", full.c_str(), vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae)); rc = ae == vr::VRApplicationError_None ? 0 : 1; } else { LOG("no such manifest: %s", manifest); @@ -1836,6 +1836,49 @@ int runRegister(const char* manifest) { return rc; } +// SteamVR's own settings that a PC install changes (steamvr section), kept in /steamvr-before.json the first +// time so an uninstall puts back exactly what was there. +struct SteamVrBefore { bool homeApp = false, dome = false; int envMode = 1; std::string background; float camHeight = 1.6f, domeRadius = 0; }; +void saveSteamVrBefore() { + const fs::path f = rootDir() / "steamvr-before.json"; + std::error_code ec; + if (fs::exists(f, ec)) return; // (already saved: what was there before Frame Home, not since) + vr::IVRSettings* s = vr::VRSettings(); + vr::EVRSettingsError se = vr::VRSettingsError_None; + char bg[2048] = {}; + s->GetString(vr::k_pch_SteamVR_Section, "background", bg, sizeof bg, &se); + std::string esc; + for (const char* c = bg; *c; ++c) { if (*c == '\\' || *c == '"') esc += '\\'; esc += *c; } + char j[4096]; + std::snprintf(j, sizeof j, "{\"enableHomeApp\": %s, \"environmentMode\": %d, \"background\": \"%s\", \"backgroundUseDomeProjection\": %s, " + "\"backgroundCameraHeight\": %.3f, \"backgroundDomeRadius\": %.3f}\n", + s->GetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", &se) ? "true" : "false", s->GetInt32(vr::k_pch_SteamVR_Section, "environmentMode", &se), + esc.c_str(), s->GetBool(vr::k_pch_SteamVR_Section, "backgroundUseDomeProjection", &se) ? "true" : "false", + s->GetFloat(vr::k_pch_SteamVR_Section, "backgroundCameraHeight", &se), s->GetFloat(vr::k_pch_SteamVR_Section, "backgroundDomeRadius", &se)); + fs::create_directories(rootDir(), ec); + if (FILE* o = homeos::openFile(f, "w")) { std::fputs(j, o); std::fclose(o); } +} +void restoreSteamVrBefore() { + const fs::path f = rootDir() / "steamvr-before.json"; + std::error_code ec; + SteamVrBefore b; + try { + const tinyjson::Value d = tinyjson::parse(readText(f)); + b.homeApp = d["enableHomeApp"].boolVal; b.envMode = static_cast(d["environmentMode"].asFloat()); b.background = d["background"].asString(); + b.dome = d["backgroundUseDomeProjection"].boolVal; b.camHeight = d["backgroundCameraHeight"].asFloat(); b.domeRadius = d["backgroundDomeRadius"].asFloat(); + } catch (...) {} // (none saved: SteamVR's defaults) + vr::IVRSettings* s = vr::VRSettings(); + vr::EVRSettingsError se = vr::VRSettingsError_None; + s->SetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", b.homeApp, &se); + s->SetInt32(vr::k_pch_SteamVR_Section, "environmentMode", b.envMode, &se); + s->SetString(vr::k_pch_SteamVR_Section, "background", b.background.c_str(), &se); + s->SetBool(vr::k_pch_SteamVR_Section, "backgroundUseDomeProjection", b.dome, &se); + s->SetFloat(vr::k_pch_SteamVR_Section, "backgroundCameraHeight", b.camHeight, &se); + s->SetFloat(vr::k_pch_SteamVR_Section, "backgroundDomeRadius", b.domeRadius, &se); + fs::remove(f, ec); + fs::remove_all(rootDir() / "background", ec); +} + // Makes Frame Home SteamVR's environment: registers it under the home-app key and turns the home app on, // so vrserver starts it whenever no game is running (and again after one quits). --uninstall reverts both. // Frame Home takes SteamVR's home-app slot (openvr.tool.steamvr_environments): SteamVR itself runs it as the @@ -1844,29 +1887,32 @@ int runRegister(const char* manifest) { // registers it in SteamVR's config files instead. int runInstall(bool install) { const fs::path manifest = rootDir() / "frame-home.vrmanifest"; - char self[4096] = {}; + std::string self; if (install) { - const ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1); - if (n <= 0) { LOG("cannot find my own path"); return 1; } + self = homeos::selfPath().string(); + if (self.empty()) { LOG("cannot find my own path"); return 1; } // The installed copy is what SteamVR runs. The installer runs this from its own folder on purpose: SteamVR // knows apps by binary path, and the installed path would make this process pass for the running home. std::error_code ec; - const fs::path installed = rootDir() / "bin" / "frame-home"; - if (fs::exists(installed, ec)) std::snprintf(self, sizeof self, "%s", installed.c_str()); - fs::create_directories(rootDir() / "envs", ec); + const fs::path installed = rootDir() / "bin" / homeos::exeName("frame-home"); + if (fs::exists(installed, ec)) self = installed.string(); + fs::create_directories(homeos::envs(), ec); + std::string json = self; // (JSON: a Windows path's backslashes doubled) + for (size_t at = 0; (at = json.find('\\', at)) != std::string::npos; at += 2) json.insert(at, 1, '\\'); std::ofstream m(manifest); m << "{\n \"source\": \"builtin\",\n \"applications\": [ {\n" << " \"app_key\": \"" << kHomeAppKey << "\",\n" << " \"launch_type\": \"binary\",\n" - << " \"binary_path_linux\": \"" << self << "\",\n" - << " \"binary_path_linux_arm\": \"" << self << "\",\n" + << " \"binary_path_linux\": \"" << json << "\",\n" + << " \"binary_path_linux_arm\": \"" << json << "\",\n" + << " \"binary_path_windows\": \"" << json << "\",\n" << " \"arguments\": \"\",\n" << " \"is_dashboard_overlay\": false,\n" // The environment, not an app: kept out of SteamVR's app lists and its dock. << " \"is_hidden\": true,\n" << " \"is_internal\": true,\n" << " \"strings\": { \"en_us\": { \"name\": \"Frame Home\",\n" - << " \"description\": \"Meta homes as the Steam Frame's environment\" } }\n" + << " \"description\": \"Meta homes as SteamVR's environment\" } }\n" << " } ]\n}\n"; } vr::EVRInitError ierr = vr::VRInitError_None; @@ -1881,31 +1927,42 @@ int runInstall(bool install) { vr::VRApplications()->GetApplicationPropertyString(kHomeAppKey, vr::VRApplicationProperty_BinaryPath_String, path, sizeof path, &pe); if (std::string(path) != self) LOG("replacing the existing environment app (%s)", path); } - vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(manifest.c_str(), false); - LOG("AddApplicationManifest(%s): %s", manifest.c_str(), vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae)); + vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(manifest.u8string().c_str(), false); + LOG("AddApplicationManifest(%s): %s", manifest.u8string().c_str(), vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae)); if (ae != vr::VRApplicationError_None) rc = 1; // An older Frame Home still running: replace it with the installed one. - const bool replaced = !runningCopies("frame-home").empty(); - stopCopies("frame-home"); + const bool replaced = !homeos::runningCopies("frame-home").empty(); + homeos::stopCopies("frame-home"); const bool wasEnabled = vr::VRSettings()->GetBool(vr::k_pch_SteamVR_Section, "enableHomeApp"); +#ifdef _WIN32 + // A Windows PC: SteamVR's classic environment (its background: the panel makes it a picture of the home) + // instead of Aurora, for the moments before Frame Home draws. + saveSteamVrBefore(); + vr::VRSettings()->SetInt32(vr::k_pch_SteamVR_Section, "environmentMode", 0, &se); +#endif vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", true, &se); // Turning the setting on makes SteamVR start the home app itself; when it was already on, start it now // unless a game is running (SteamVR brings the home back when the game quits). - usleep(1500000); - if ((wasEnabled || replaced) && runningCopies("frame-home").empty() && + homeos::sleepMs(1500); + if ((wasEnabled || replaced) && homeos::runningCopies("frame-home").empty() && vr::VRApplications()->GetSceneApplicationState() == vr::EVRSceneApplicationState_None) { const vr::EVRApplicationError le = vr::VRApplications()->LaunchApplication(kHomeAppKey); LOG("start: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(le)); } } else { +#ifdef _WIN32 + restoreSteamVrBefore(); // the settings as they were before Frame Home +#else vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", false, &se); +#endif std::error_code ec; if (fs::exists(manifest, ec)) { - vr::EVRApplicationError ae = vr::VRApplications()->RemoveApplicationManifest(manifest.c_str()); + vr::EVRApplicationError ae = vr::VRApplications()->RemoveApplicationManifest(manifest.u8string().c_str()); LOG("RemoveApplicationManifest: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae)); + fs::remove(manifest, ec); // (the panel takes SteamVR's settings in hand only while this is here) } // Close a running Frame Home so SteamVR's own environment comes back. - stopCopies("frame-home"); + homeos::stopCopies("frame-home"); } if (se != vr::VRSettingsError_None) { LOG("settings error %d", static_cast(se)); rc = 1; } LOG("%s", rc ? "failed" : install ? "Frame Home is now the environment" : "SteamVR's environment restored"); @@ -1916,9 +1973,12 @@ int runInstall(bool install) { } // namespace int main(int argc, char** argv) { +#ifdef FH_USE_VOLK + if (volkInitialize() != VK_SUCCESS) { std::fprintf(stderr, "frame-home: no Vulkan on this system\n"); return 1; } +#endif std::string snapshot, panorama; std::vector apks; // footprint [+ vista] - bool list = false, spawnView = false, bench = false, unpaced = false; + bool list = false, fromSpawn = false, bench = false, unpaced = false; float cam[5] = {0, 1.6f, 0, 0, 0}; uint32_t size = 1024, sizeH = 0; double benchSeconds = 20; @@ -1932,7 +1992,7 @@ int main(int argc, char** argv) { if (a == "--list") list = true; else if (a == "--panorama" && i + 1 < argc) panorama = argv[++i]; else if (a == "--snapshot" && i + 1 < argc) snapshot = argv[++i]; - else if (a == "--spawn") spawnView = true; + else if (a == "--spawn") fromSpawn = true; else if (a == "--cam" && i + 1 < argc) std::sscanf(argv[++i], "%f,%f,%f,%f,%f", &cam[0], &cam[1], &cam[2], &cam[3], &cam[4]); else if (a == "--size" && i + 1 < argc) { // N, or WxH (--bench) unsigned sw = 0, sh = 0; @@ -1975,13 +2035,13 @@ int main(int argc, char** argv) { const bool given = std::any_of(argv + 1, argv + argc, [](const char* x) { return std::strcmp(x, "--size") == 0; }); return runBench(apks, benchSeconds, given ? size : 0, given ? (sizeH ? sizeH : size) : 0, benchHz, unpaced); } - if (!panorama.empty() && !apks.empty()) return runPanorama(panorama, apks, cam, size >= 512 ? size : 8192); + if (!panorama.empty() && !apks.empty()) return runPanorama(panorama, apks, cam, size >= 512 ? size : 8192, fromSpawn); if (!snapshot.empty()) { if (apks.empty()) { std::fprintf(stderr, "usage: frame-home --snapshot out.ppm [--cam x,y,z,yaw,pitch] [--size N] [vista.apk]\n"); return 2; } - return runSnapshot(snapshot, apks, cam, size, spawnView); + return runSnapshot(snapshot, apks, cam, size, fromSpawn); } if (std::getenv("FH_SYSTEM") || (argc > 1 && std::string(argv[1]) == "--system")) return runVr({}, true); return runVr(apks); // no home given: environment mode diff --git a/src/menu_input.h b/src/menu_input.h index dcc3f88..36fbf2d 100644 --- a/src/menu_input.h +++ b/src/menu_input.h @@ -4,10 +4,20 @@ // always runs, takes the sticks through a global-priority action set (steamvr.globalActionSetPriority) whenever the // menu is up, Frame Home is the environment, and that hand isn't pointing at a panel, and passes them to Frame Home // through a shared page (/dev/shm/framehome-sticks). Pointing at the menu still drives the menu. +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else #include #include #include #include +#endif #include #include @@ -27,6 +37,16 @@ struct MenuSticks { inline double menuNow() { return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); } +#ifdef _WIN32 +// (Windows: a named mapping; kept open for the process's life, as the Linux one stays mapped) +inline MenuSticks* menuSticksMap(bool create) { + const HANDLE h = create ? CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(MenuSticks), L"Local\\FrameHomeSticks") + : OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\FrameHomeSticks"); + if (!h) return nullptr; + void* p = MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MenuSticks)); + return static_cast(p); +} +#else inline MenuSticks* menuSticksMap(bool create) { const int fd = open("/dev/shm/framehome-sticks", O_RDWR | (create ? O_CREAT : 0), 0600); if (fd < 0) return nullptr; @@ -37,6 +57,7 @@ inline MenuSticks* menuSticksMap(bool create) { close(fd); return p == MAP_FAILED ? nullptr : static_cast(p); } +#endif // A consistent copy (seqlock); false when there is nothing recent (within maxAge seconds). inline bool menuSticksRead(const MenuSticks* m, MenuSticks& out, double maxAge = 0.15) { diff --git a/src/openvr_loader.cpp b/src/openvr_loader.cpp new file mode 100644 index 0000000..e084c65 --- /dev/null +++ b/src/openvr_loader.cpp @@ -0,0 +1,162 @@ +// SteamVR's own OpenVR client library, loaded at run time from wherever SteamVR is installed on this PC: openvr.h's +// entry points, forwarded to it. A PC build of Frame Home and its panel then runs on any PC with SteamVR (Windows or +// Linux) without carrying a copy, the way the Steam Frame build links SteamVR's own (built with OPENVR_BUILD_STATIC, +// so openvr.h declares these as plain functions for this file to define). +// +// Where SteamVR is: OpenVR's own record, openvrpaths.vrpath ("runtime": [ "", ... ]), in +// %LOCALAPPDATA%\openvr (Windows) or ~/.config/openvr (Linux); then a copy beside this program, if there is one. +#include + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif + +namespace { + +namespace fs = std::filesystem; + +// The runtime folders openvrpaths.vrpath lists, in order. +std::vector runtimeFolders() { + fs::path file; +#ifdef _WIN32 + if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA"); l && *l) file = fs::path(l) / L"openvr" / L"openvrpaths.vrpath"; // (as set: a test can point it elsewhere) + else { + PWSTR p = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &p))) file = fs::path(p) / L"openvr" / L"openvrpaths.vrpath"; + CoTaskMemFree(p); + } +#else + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + const char* home = std::getenv("HOME"); + file = xdg && *xdg ? fs::path(xdg) / "openvr" / "openvrpaths.vrpath" : fs::path(home ? home : "") / ".config" / "openvr" / "openvrpaths.vrpath"; +#endif + std::ifstream in(file, std::ios::binary); + const std::string j((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + std::vector out; + size_t at = j.find("\"runtime\""); + if (at == std::string::npos) return out; + at = j.find('[', at); + const size_t end = at == std::string::npos ? at : j.find(']', at); + while (at != std::string::npos && at < end) { // each "..." in the array, JSON escapes undone + const size_t q = j.find('"', at + 1); + if (q == std::string::npos || q > end) break; + std::string s; + size_t k = q + 1; + for (; k < j.size() && j[k] != '"'; ++k) { + if (j[k] == '\\' && k + 1 < j.size()) ++k; + s += j[k]; + } + if (!s.empty()) out.push_back(fs::u8path(s)); + at = k + 1; + } + return out; +} + +#ifdef _WIN32 +using Lib = HMODULE; +Lib open(const fs::path& p) { return LoadLibraryW(p.wstring().c_str()); } +void* symbol(Lib l, const char* name) { return reinterpret_cast(GetProcAddress(l, name)); } +const char* kLibInRuntime = "bin/win64/openvr_api.dll"; +const char* kLibName = "openvr_api.dll"; +#else +using Lib = void*; +Lib open(const fs::path& p) { return dlopen(p.c_str(), RTLD_NOW | RTLD_LOCAL); } +void* symbol(Lib l, const char* name) { return dlsym(l, name); } +const char* kLibInRuntime = "bin/linux64/libopenvr_api.so"; +const char* kLibName = "libopenvr_api.so"; +#endif + +Lib library() { + static const Lib lib = [] { + for (const fs::path& r : runtimeFolders()) + if (const Lib l = open(r / fs::u8path(kLibInRuntime))) return l; + return open(fs::u8path(kLibName)); // (one beside the program, or on the search path) + }(); + return lib; +} + +template F entry(const char* name) { + const Lib l = library(); + return l ? reinterpret_cast(symbol(l, name)) : nullptr; +} + +} // namespace + +namespace vr { + +uint32_t VR_CALLTYPE VR_InitInternal2(EVRInitError* peError, EVRApplicationType eApplicationType, const char* pStartupInfo) { + using F = uint32_t(VR_CALLTYPE*)(EVRInitError*, EVRApplicationType, const char*); + static const F f = entry("VR_InitInternal2"); + if (!f) { if (peError) *peError = VRInitError_Init_InstallationNotFound; return 0; } + return f(peError, eApplicationType, pStartupInfo); +} + +void VR_CALLTYPE VR_ShutdownInternal() { + using F = void(VR_CALLTYPE*)(); + static const F f = entry("VR_ShutdownInternal"); + if (f) f(); +} + +void* VR_CALLTYPE VR_GetGenericInterface(const char* pchInterfaceVersion, EVRInitError* peError) { + using F = void*(VR_CALLTYPE*)(const char*, EVRInitError*); + static const F f = entry("VR_GetGenericInterface"); + if (!f) { if (peError) *peError = VRInitError_Init_InstallationNotFound; return nullptr; } + return f(pchInterfaceVersion, peError); +} + +bool VR_CALLTYPE VR_IsInterfaceVersionValid(const char* pchInterfaceVersion) { + using F = bool(VR_CALLTYPE*)(const char*); + static const F f = entry("VR_IsInterfaceVersionValid"); + return f && f(pchInterfaceVersion); +} + +uint32_t VR_CALLTYPE VR_GetInitToken() { + using F = uint32_t(VR_CALLTYPE*)(); + static const F f = entry("VR_GetInitToken"); + return f ? f() : 0; +} + +bool VR_CALLTYPE VR_IsHmdPresent() { + using F = bool(VR_CALLTYPE*)(); + static const F f = entry("VR_IsHmdPresent"); + return f && f(); +} + +bool VR_CALLTYPE VR_IsRuntimeInstalled() { + using F = bool(VR_CALLTYPE*)(); + static const F f = entry("VR_IsRuntimeInstalled"); + return f && f(); +} + +bool VR_GetRuntimePath(char* pchPathBuffer, uint32_t unBufferSize, uint32_t* punRequiredBufferSize) { + using F = bool (*)(char*, uint32_t, uint32_t*); + static const F f = entry("VR_GetRuntimePath"); + return f && f(pchPathBuffer, unBufferSize, punRequiredBufferSize); +} + +const char* VR_CALLTYPE VR_GetVRInitErrorAsSymbol(EVRInitError error) { + using F = const char*(VR_CALLTYPE*)(EVRInitError); + static const F f = entry("VR_GetVRInitErrorAsSymbol"); + return f ? f(error) : "VRInitError_Init_InstallationNotFound"; +} + +const char* VR_CALLTYPE VR_GetVRInitErrorAsEnglishDescription(EVRInitError error) { + using F = const char*(VR_CALLTYPE*)(EVRInitError); + static const F f = entry("VR_GetVRInitErrorAsEnglishDescription"); + return f ? f(error) : "SteamVR isn't installed (its OpenVR library wasn't found)"; +} + +} // namespace vr diff --git a/src/overlay_texture.h b/src/overlay_texture.h index 96e8278..312dced 100644 --- a/src/overlay_texture.h +++ b/src/overlay_texture.h @@ -5,7 +5,7 @@ // redrew (pointer moves, stats refreshes). Here the canvas goes into one of three images in turn (staging buffer, // copy, wait for the copy's fence), so the image SteamVR is showing is never the one being written. // -// Vulkan is loaded at run time (dlopen libvulkan.so.1): no link dependency, and if anything is missing init() +// Vulkan is loaded at run time (libvulkan.so.1, vulkan-1.dll): no link dependency, and if anything is missing init() // fails and the caller keeps using SetOverlayRaw. #ifndef VK_NO_PROTOTYPES #define VK_NO_PROTOTYPES @@ -13,7 +13,17 @@ #include #include +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else #include +#endif #include #include @@ -31,8 +41,13 @@ public: // VRSystem()->GetOutputDevice, the extensions from VRCompositor()). bool init(uint32_t w, uint32_t h, std::string& err) { w_ = w; h_ = h; +#ifdef _WIN32 + if (!(lib_ = LoadLibraryW(L"vulkan-1.dll"))) return fail(err, "no vulkan-1.dll"); + gipa_ = reinterpret_cast(reinterpret_cast(GetProcAddress(static_cast(lib_), "vkGetInstanceProcAddr"))); +#else if (!(lib_ = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL)) && !(lib_ = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL))) return fail(err, "no libvulkan"); gipa_ = reinterpret_cast(dlsym(lib_, "vkGetInstanceProcAddr")); +#endif if (!gipa_) return fail(err, "no vkGetInstanceProcAddr"); #define OT_GLOBAL(n) n = reinterpret_cast(gipa_(VK_NULL_HANDLE, #n)); if (!n) return fail(err, "missing " #n) OT_GLOBAL(vkCreateInstance); diff --git a/src/proc_util.h b/src/proc_util.h deleted file mode 100644 index 01e52c7..0000000 --- a/src/proc_util.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -// Running copies of a Frame program, for the installers. SteamVR's own bookkeeping (GetApplicationProcessId) loses -// track of an app once a utility process started from the same binary has connected, so look at /proc instead. -#include -#include - -#include -#include -#include -#include -#include - -// Processes named `name` running without arguments: the environment / the panel, not tools (--snapshot, --list). -inline std::vector runningCopies(const char* name) { - std::vector out; - std::error_code ec; - for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) { - const std::string id = e.path().filename().string(); - if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue; - const pid_t pid = static_cast(std::stol(id)); - if (pid == getpid()) continue; - std::string comm; - std::getline(std::ifstream(e.path() / "comm"), comm); - if (comm != name) continue; - std::ifstream c(e.path() / "cmdline", std::ios::binary); - const std::string cmd((std::istreambuf_iterator(c)), std::istreambuf_iterator()); - size_t args = 0; - for (char ch : cmd) args += ch == '\0'; - if (args <= 1) out.push_back(pid); - } - return out; -} - -// Asks them to quit, then insists after 5 s. -// Running copies of `name` (other than this process) started with `arg` among their arguments. -inline std::vector runningWith(const char* name, const std::string& arg) { - std::vector out; - std::error_code ec; - for (const auto& e : std::filesystem::directory_iterator("/proc", ec)) { - const std::string id = e.path().filename().string(); - if (id.empty() || id.find_first_not_of("0123456789") != std::string::npos) continue; - const pid_t pid = static_cast(std::stol(id)); - if (pid == getpid()) continue; - std::string comm; - std::getline(std::ifstream(e.path() / "comm"), comm); - if (comm != name) continue; - std::ifstream c(e.path() / "cmdline", std::ios::binary); - for (std::string a; std::getline(c, a, '\0');) if (a == arg) { out.push_back(pid); break; } - } - return out; -} - -inline void stopCopies(const char* name) { - const std::vector pids = runningCopies(name); - for (pid_t p : pids) kill(p, SIGTERM); - for (int i = 0; i < 50; ++i) { - bool alive = false; - for (pid_t p : pids) alive |= kill(p, 0) == 0; - if (!alive) return; - usleep(100000); - } - for (pid_t p : pids) kill(p, SIGKILL); -} diff --git a/src/switcher.cpp b/src/switcher.cpp index 7ccd385..3405248 100644 --- a/src/switcher.cpp +++ b/src/switcher.cpp @@ -1,13 +1,15 @@ -// frame-switcher — the Steam Frame's environment switcher: a SteamVR dashboard panel listing every environment -// in ~/frame-home/envs (Frame environments .fenv, home APKs, and footprint + vista pairs from pairs.txt) with -// thumbnails. Point and click one: ~/frame-home/current changes and Frame Home switches to it live. A bundle of -// environments (.fbundle) put there is unpacked into its .fenv files, which the panel then lists one by one. +// frame-switcher — Frame Home's environment switcher: a SteamVR dashboard panel listing every environment (on the +// Steam Frame ~/frame-home/envs; on a PC, Frame Studio's My Environments: home_platform.h) — Frame environments .fenv, +// home APKs, and footprint + vista pairs from pairs.txt — with thumbnails. Point and click one: `current` changes and +// Frame Home switches to it live. A bundle of environments (.fbundle) put there is unpacked into its .fenv files, +// which the panel then lists one by one. // // frame-switcher the dashboard panel (SteamVR starts it: see --install) // frame-switcher --install register with SteamVR and start it with SteamVR from now on +// frame-switcher --uninstall the other way round (a PC's uninstaller) // frame-switcher --list the environments, the current one marked with * // frame-switcher --set NAME switch (NAME: the file name, or a number from --list) -// frame-switcher --thumbs render missing thumbnails (frame-home --snapshot) into envs/.thumbs +// frame-switcher --thumbs render missing thumbnails (frame-home --snapshot, from the spawn) // frame-switcher --delete NAME delete an environment (like the panel's delete button; NAME as for --set) // frame-switcher --unpack-bundles unpack the bundles (.fbundle) in envs now (the panel does it by itself) // frame-switcher --test-delete DIR self-check of deleting, in a fake root made in DIR (must not exist yet) @@ -16,18 +18,14 @@ #include "overlay_texture.h" #include "overlays.h" #include "perf_settings.h" -#include "proc_util.h" +#include "home_platform.h" #include #define STB_TRUETYPE_IMPLEMENTATION #include "stb_truetype.h" -#include -#include -#include -#include -#include +#include #include #include @@ -57,10 +55,7 @@ namespace { constexpr const char* kAppKey = "framehome.switcher"; constexpr int W = 1280, H = 800; -fs::path rootDir() { - const char* home = std::getenv("HOME"); - return fs::path(home && *home ? home : "/home/steamos") / "frame-home"; -} +fs::path rootDir() { return homeos::root(); } // (home_platform.h: ~/frame-home, or a PC's) std::string readText(const fs::path& p) { std::ifstream f(p); @@ -142,11 +137,11 @@ std::string pretty(std::string stem) { fs::path thumbPath(const std::string& key) { std::string n = key; for (char& c : n) if (c == '\n' || c == '/') c = '+'; - return rootDir() / "envs" / ".thumbs" / (n + ".ppm"); + return homeos::thumbs() / (n + ".ppm"); } std::vector scanEnvs() { - const fs::path envs = rootDir() / "envs"; + const fs::path envs = homeos::envs(); std::vector out; std::error_code ec; std::vector files; @@ -284,7 +279,7 @@ struct DeleteResult { // folders go in a thread of their own (they can be big, and an unpack in progress is waited for). DeleteResult deleteEnv(const Env& e, const std::vector& all, bool cacheInBackground) { DeleteResult r; - const fs::path envs = rootDir() / "envs"; + const fs::path envs = homeos::envs(); std::error_code ec; if (e.files.empty()) { r.error = "nothing to delete"; return r; } for (const auto& f : e.files) { @@ -349,7 +344,7 @@ DeleteResult deleteEnv(const Env& e, const std::vector& all, bool cacheInBa keys.insert(keys.end(), pairKeys.begin(), pairKeys.end()); for (const auto& k : keys) { const fs::path t = thumbPath(k); - if (t.parent_path() == envs / ".thumbs" && fs::remove(t, ec)) r.removed.push_back("envs/.thumbs/" + t.filename().string()); + if (t.parent_path() == homeos::thumbs() && fs::remove(t, ec)) r.removed.push_back("thumbnail " + t.filename().string()); } // 5. The unpacked copies (they'd be cleaned up eventually anyway). @@ -1444,7 +1439,7 @@ volatile sig_atomic_t g_quit = 0; void unpackBundles() { static std::set failed; // "||