Frame Home on a PC with SteamVR (Windows, Linux); thumbnails from the spawn

- src/home_platform.h: what Frame Home and the panel need from the system (paths, finding and stopping each
  other, running a tool, restarting, priorities) for SteamOS, Windows and Linux PCs. On a PC the environments are
  Frame Studio's My Environments; Frame Home's own files in %LOCALAPPDATA%\Frame Home (Windows).
- src/openvr_loader.cpp: a PC build finds SteamVR's own OpenVR library at run time (openvrpaths.vrpath), so it runs
  on any PC with SteamVR; Vulkan through volk. CMake: FRAME_HOME_PC (default on Windows and x86-64 Linux); the Steam
  Frame build is unchanged.
- Windows: Frame Home is SteamVR's home app; SteamVR's classic environment shows a 360-degree picture of the current
  home from its spawn (frame-home --panorama --spawn, made by the panel when the home changes) instead of Aurora
  while Frame Home starts. Linux PCs: the Steam Frame's compositor layer, as on the headset.
- frame/pc: install-frame-home-pc.ps1 (+ .bat): online through SteamVR's API or offline in its config files; the
  SteamVR settings it changes are saved and restored exactly on -Uninstall (tested in a sandbox both ways).
  frame-switcher --uninstall. Studio: Properties > Headset > This PC installs it from pc/ beside Studio.
- Packaging and CI: pc/ in the Windows x64 and Linux x64 Studio downloads; FrameHome-PC-windows-x64.zip.
- Thumbnails come from where Frame Home starts you (the first spawn, eyes 1.6 m up): Studio's saves and the
  panel's (frame-home --snapshot --spawn).
- docs/pc-steamvr.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 5.5 committed 2026-10-21 14:37:49 -05:00
1 parent b0609d3664
commit 3469b77ca3
21 files changed
+1296 -257

No files matched your search

+39 -1
View File
@@ -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
+58 -9
View File
@@ -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()
+7
View File
@@ -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 |
+1
View File
@@ -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 |
+58
View File
@@ -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`.
+2
View File
@@ -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@<address>`.
See [headset-setup.md](headset-setup.md) and [install-frame-home.md](install-frame-home.md).
@@ -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
@@ -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
+128
View File
@@ -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
+38 -3
View File
@@ -52,7 +52,38 @@ build() {
"$CMAKE" --build "$dir" --target frame-studio --parallel
}
# dist/FrameStudio-<target>: 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/<target>-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-<target>: 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" ;;
+20
View File
@@ -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 <sys/mman.h>
#include <sys/socket.h>
#include <sys/un.h>
@@ -141,3 +160,4 @@ private:
uint32_t generation_ = 0;
std::chrono::steady_clock::time_point lastTry_;
};
#endif // _WIN32
+5
View File
@@ -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 <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <ctime>
@@ -65,9 +66,13 @@ struct State {
};
inline uint64_t nowNs() {
#ifdef _WIN32
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
#else
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + static_cast<uint64_t>(ts.tv_nsec);
#endif
}
} // namespace envshare
+299
View File
@@ -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 <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <io.h>
#include <psapi.h>
#include <shlobj.h>
#include <tlhelp32.h>
#else
#include <climits>
#include <csignal>
#include <fcntl.h>
#include <fstream>
#include <iterator>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>
#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<DWORD>(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<long>(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<Pid> runningCopies(const char* name) {
std::vector<Pid> 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<Pid> 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<std::string>& 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<std::string>& 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<int>(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<size_t>(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<Pid> runningCopies(const char* name) {
std::vector<Pid> 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<Pid>(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<char>(c)), std::istreambuf_iterator<char>());
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<Pid> runningWith(const char* name, const std::string& arg) {
std::vector<Pid> 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<Pid>(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<Pid> 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<std::string>& args, int nice, bool wait) {
std::vector<std::string> a = args;
std::vector<char*> 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<std::string> args;
for (std::string a; std::getline(cl, a, '\0');) args.push_back(a);
if (args.empty()) args.push_back(self);
std::vector<char*> 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
+1 -1
View File
@@ -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_);
+137 -77
View File
@@ -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 <openvr.h>
#include <signal.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
@@ -47,7 +45,6 @@
#include <string>
#include <thread>
#include <vector>
#include <sys/resource.h>
#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<std::string> currentHome() {
const fs::path envs = rootDir() / "envs";
const fs::path envs = homeos::envs();
std::vector<std::string> 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& apks, double seconds, uint32_t w, u
r.msaa = perf.msaa;
r.anisotropy = static_cast<float>(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<std::string>& apks, double seconds, uint32_t w, u
std::vector<const View*> 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<std::string>& 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<std::string>& 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
// <out>.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<std::string>& 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<std::string>& apks, co
v.yawDeg = cam[3];
std::vector<u8> 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<unsigned long long>(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<int>(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<std::string>& 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<std::string>& 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<std::string>& 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<std::string> args;
for (std::string a; std::getline(cl, a, '\0');) args.push_back(a);
std::vector<char*> 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 <root>/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<int>(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<int>(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<std::string> 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] <footprint.apk> [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
+21
View File
@@ -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 <windows.h>
#else
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include <atomic>
#include <chrono>
@@ -27,6 +37,16 @@ struct MenuSticks {
inline double menuNow() { return std::chrono::duration<double>(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<MenuSticks*>(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<MenuSticks*>(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) {
+162
View File
@@ -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": [ "<SteamVR's folder>", ... ]), in
// %LOCALAPPDATA%\openvr (Windows) or ~/.config/openvr (Linux); then a copy beside this program, if there is one.
#include <openvr.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#else
#include <dlfcn.h>
#endif
namespace {
namespace fs = std::filesystem;
// The runtime folders openvrpaths.vrpath lists, in order.
std::vector<fs::path> 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<char>(in)), std::istreambuf_iterator<char>());
std::vector<fs::path> 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<void*>(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 <typename F> F entry(const char* name) {
const Lib l = library();
return l ? reinterpret_cast<F>(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<F>("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<F>("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<F>("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<F>("VR_IsInterfaceVersionValid");
return f && f(pchInterfaceVersion);
}
uint32_t VR_CALLTYPE VR_GetInitToken() {
using F = uint32_t(VR_CALLTYPE*)();
static const F f = entry<F>("VR_GetInitToken");
return f ? f() : 0;
}
bool VR_CALLTYPE VR_IsHmdPresent() {
using F = bool(VR_CALLTYPE*)();
static const F f = entry<F>("VR_IsHmdPresent");
return f && f();
}
bool VR_CALLTYPE VR_IsRuntimeInstalled() {
using F = bool(VR_CALLTYPE*)();
static const F f = entry<F>("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<F>("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<F>("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<F>("VR_GetVRInitErrorAsEnglishDescription");
return f ? f(error) : "SteamVR isn't installed (its OpenVR library wasn't found)";
}
} // namespace vr
+16 -1
View File
@@ -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 <vulkan/vulkan.h>
#include <openvr.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <algorithm>
#include <cstdint>
@@ -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<PFN_vkGetInstanceProcAddr>(reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(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<PFN_vkGetInstanceProcAddr>(dlsym(lib_, "vkGetInstanceProcAddr"));
#endif
if (!gipa_) return fail(err, "no vkGetInstanceProcAddr");
#define OT_GLOBAL(n) n = reinterpret_cast<PFN_##n>(gipa_(VK_NULL_HANDLE, #n)); if (!n) return fail(err, "missing " #n)
OT_GLOBAL(vkCreateInstance);
-63
View File
@@ -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 <signal.h>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
// Processes named `name` running without arguments: the environment / the panel, not tools (--snapshot, --list).
inline std::vector<pid_t> runningCopies(const char* name) {
std::vector<pid_t> 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<pid_t>(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<char>(c)), std::istreambuf_iterator<char>());
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<pid_t> runningWith(const char* name, const std::string& arg) {
std::vector<pid_t> 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<pid_t>(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<pid_t> 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);
}
+178 -83
View File
@@ -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 <openvr.h>
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#include <fcntl.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>
#include <csignal>
#include <algorithm>
#include <atomic>
@@ -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<Env> scanEnvs() {
const fs::path envs = rootDir() / "envs";
const fs::path envs = homeos::envs();
std::vector<Env> out;
std::error_code ec;
std::vector<fs::path> 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<Env>& 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<Env>& 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<std::string> failed; // "<file>|<size>|<time>" of bundles that didn't unpack
std::error_code ec;
const fs::path envs = rootDir() / "envs";
const fs::path envs = homeos::envs();
for (const auto& e : fs::directory_iterator(envs, ec)) {
if (!e.is_regular_file(ec) || !isFenvBundlePath(e.path().string())) continue;
const std::string state = e.path().filename().string() + "|" + std::to_string(e.file_size(ec)) + "|" +
@@ -1454,12 +1449,12 @@ void unpackBundles() {
std::string err;
const auto t0 = std::chrono::steady_clock::now();
if (!fenvBundleExtract(e.path().string(), envs.string(), unpacked, err)) {
LOG("can't unpack the bundle %s: %s", e.path().filename().c_str(), err.c_str());
LOG("can't unpack the bundle %s: %s", e.path().filename().string().c_str(), err.c_str());
failed.insert(state);
continue;
}
fs::remove(e.path(), ec);
LOG("unpacked the bundle %s into %zu environments (%.1f s)", e.path().filename().c_str(), unpacked.size(),
LOG("unpacked the bundle %s into %zu environments (%.1f s)", e.path().filename().string().c_str(), unpacked.size(),
std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
}
}
@@ -1471,20 +1466,20 @@ void prepareEnvironments() {
again = true; // files added while a run is going get a run of their own
if (busy.exchange(true)) return;
std::thread([] {
setpriority(PRIO_PROCESS, 0, 15);
homeos::lowerPriority(15);
for (;;) {
while (again.exchange(false)) try {
unpackBundles();
std::error_code ec;
for (const auto& e : fs::directory_iterator(rootDir() / "envs", ec)) {
for (const auto& e : fs::directory_iterator(homeos::envs(), ec)) {
if (e.path().extension() != ".fenv") continue;
FenvManifest m;
std::vector<std::string> sources, models;
std::string edits, err;
const auto t0 = std::chrono::steady_clock::now();
if (!fenvExtract(e.path().string(), fenvCacheDir(), m, sources, models, edits, err)) { LOG("can't unpack %s: %s", e.path().filename().c_str(), err.c_str()); continue; }
if (!fenvExtract(e.path().string(), fenvCacheDir(), m, sources, models, edits, err)) { LOG("can't unpack %s: %s", e.path().filename().string().c_str(), err.c_str()); continue; }
const double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
if (sec > 0.5) LOG("unpacked %s ahead of time (%.1f s)", e.path().filename().c_str(), sec);
if (sec > 0.5) LOG("unpacked %s ahead of time (%.1f s)", e.path().filename().string().c_str(), sec);
}
} catch (const std::exception& ex) { LOG("unpacking ahead of time stopped: %s", ex.what()); } // a damaged file must not kill the panel
busy = false;
@@ -1521,8 +1516,8 @@ int runPanel() {
auto rescan = [&]() {
std::error_code ec;
std::string sig;
for (const auto& e : fs::directory_iterator(rootDir() / "envs", ec)) sig += e.path().filename().string() + std::to_string(static_cast<long long>(fs::last_write_time(e.path(), ec).time_since_epoch().count()));
for (const auto& e : fs::directory_iterator(rootDir() / "envs" / ".thumbs", ec)) sig += e.path().filename().string() + std::to_string(static_cast<long long>(fs::last_write_time(e.path(), ec).time_since_epoch().count()));
for (const auto& e : fs::directory_iterator(homeos::envs(), ec)) sig += e.path().filename().string() + std::to_string(static_cast<long long>(fs::last_write_time(e.path(), ec).time_since_epoch().count()));
for (const auto& e : fs::directory_iterator(homeos::thumbs(), ec)) sig += e.path().filename().string() + std::to_string(static_cast<long long>(fs::last_write_time(e.path(), ec).time_since_epoch().count()));
sig += readText(rootDir() / "pairs.txt") + "|" + currentKey();
if (sig == lastScan) return false;
lastScan = sig;
@@ -1543,11 +1538,30 @@ int runPanel() {
LOG("turned SteamVR's overlay input override (globalActionSetPriority) back off");
}
}
// Frame Home is the system environment: the compositor layer (layer/frame_home_layer.cpp) draws it in Aurora's
// place, and frame-home --system renders it. This panel starts with SteamVR, so it keeps that process running
// (restarting it if it stops) and makes sure the old way, Frame Home as SteamVR's home app, is off: no app, no
// dock entry, and Aurora's slot is where the home shows.
{
// Two ways Frame Home takes SteamVR's environment:
// - the system environment (the Steam Frame, and a Linux PC with the compositor layer installed: ~/frame-home/
// layer): the layer (layer/frame_home_layer.cpp) draws it in Aurora's place and frame-home --system renders it.
// This panel starts with SteamVR, so it keeps that process running and makes sure SteamVR's home app is off.
// - SteamVR's home app (a Windows PC, whose compositor isn't Vulkan): SteamVR runs Frame Home itself whenever no
// game is open. Here the panel makes sure that's on, and that the moments before Frame Home draws (SteamVR
// starting, a game quitting) show the home too: SteamVR's background is a 360-degree picture of it
// (keepBackground), with the classic environment instead of Aurora.
#ifdef _WIN32
const bool systemEnv = false;
#else
const bool systemEnv = fs::exists(rootDir() / "layer");
#endif
// (Frame Home uninstalled from a PC: its manifest is gone, and SteamVR's settings are left as the uninstall put them)
const bool homeInstalled = systemEnv || fs::exists(rootDir() / "frame-home.vrmanifest");
if (!systemEnv && homeInstalled) {
vr::EVRSettingsError se = vr::VRSettingsError_None;
if (!vr::VRSettings()->GetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", &se)) {
vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", true, &se);
LOG("Frame Home is SteamVR's home app: turned it on");
}
if (vr::VRSettings()->GetInt32(vr::k_pch_SteamVR_Section, "environmentMode", &se) != 0)
vr::VRSettings()->SetInt32(vr::k_pch_SteamVR_Section, "environmentMode", 0, &se); // the background, not Aurora
} else {
vr::EVRSettingsError se = vr::VRSettingsError_None;
if (vr::VRSettings()->GetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", &se)) {
vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, "enableHomeApp", false, &se);
@@ -1557,20 +1571,22 @@ int runPanel() {
vr::VRSettings()->SetInt32(vr::k_pch_SteamVR_Section, "environmentMode", 1, &se); // Aurora's pipelines are what the layer replaces
}
}
{ // its own SteamVR identity (hidden: no list, no dock), so its controller bindings are its own
if (systemEnv) { // its own SteamVR identity (hidden: no list, no dock), so its controller bindings are its own
const fs::path m = rootDir() / "frame-environment.vrmanifest";
std::ofstream(m) << "{\n \"source\": \"builtin\",\n \"applications\": [ {\n \"app_key\": \"framehome.environment\",\n"
" \"launch_type\": \"binary\",\n \"binary_path_linux\": \"" << (rootDir() / "bin" / "frame-home").string() << "\",\n"
" \"binary_path_linux_arm\": \"" << (rootDir() / "bin" / "frame-home").string() << "\",\n \"arguments\": \"--system\",\n"
" \"is_dashboard_overlay\": true,\n \"is_hidden\": true,\n"
" \"strings\": { \"en_us\": { \"name\": \"Frame Home\", \"description\": \"The environment\" } }\n } ]\n}\n";
const vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(fs::absolute(m).c_str(), false);
const vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(fs::absolute(m).u8string().c_str(), false);
if (ae != vr::VRApplicationError_None) LOG("registering the environment: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae));
}
pid_t systemPid = -1;
homeos::Pid systemPid = 0;
bool adopted = false; // systemPid isn't our child (a panel before this one started it)
auto lastSpawn = std::chrono::steady_clock::now() - std::chrono::minutes(1);
auto keepSystem = [&](std::chrono::steady_clock::time_point now) {
if (!systemEnv) return; // SteamVR runs Frame Home itself (its home app)
#ifndef _WIN32
if (systemPid > 0 && adopted) {
if (kill(systemPid, 0) == 0) return;
LOG("frame-home --system (pid %d) stopped: restarting it", static_cast<int>(systemPid));
@@ -1587,7 +1603,7 @@ int runPanel() {
if (now - lastSpawn < std::chrono::seconds(5)) return; // a crash loop doesn't spin
lastSpawn = now;
// Still running from a panel before this one (the panel restarted): keep it, never a second renderer.
if (const std::vector<pid_t> running = runningWith("frame-home", "--system"); !running.empty()) {
if (const std::vector<pid_t> running = homeos::runningWith("frame-home", "--system"); !running.empty()) {
systemPid = running.front();
adopted = true;
LOG("frame-home --system already running (pid %d): keeping it", static_cast<int>(systemPid));
@@ -1602,6 +1618,9 @@ int runPanel() {
_exit(127);
}
if (pid > 0) { systemPid = pid; LOG("started frame-home --system (pid %d)", static_cast<int>(pid)); }
#else
(void)now;
#endif
};
// The panel's pixels go to SteamVR in Vulkan images of our own (overlay_texture.h): SetOverlayRaw recreated the
// texture on every redraw and the panel flickered. SetOverlayRaw stays as the fallback.
@@ -1628,9 +1647,10 @@ int runPanel() {
panel.kFlingGain, panel.kFlingMax, panel.kFlingDecay);
// The header's button: Restart stops frame-home --system (keepSystem starts a new one as soon as it's gone);
// Start / Turn On clears the layer's off switch (the layer checks it every second) and starts it now.
pid_t restartingPid = -1;
homeos::Pid restartingPid = 0;
std::chrono::steady_clock::time_point restartAt{}, killAt{};
bool restarting = false;
auto homeRunning = [&] { return systemEnv ? systemPid > 0 && homeos::alive(systemPid) : !homeos::runningCopies("frame-home").empty(); };
auto homeTick = [&](std::chrono::steady_clock::time_point now) {
std::error_code ec;
if (panel.restartAsked) {
@@ -1638,34 +1658,101 @@ int runPanel() {
restarting = true;
restartAt = now;
restartingPid = systemPid;
if (fs::exists(rootDir() / "layer" / "off", ec)) {
fs::remove(rootDir() / "layer" / "off", ec);
fs::remove(rootDir() / "layer" / "crashes", ec);
LOG("Frame Home turned back on from the panel");
if (!systemEnv) { // SteamVR's home app: end it and have SteamVR start it again
LOG("restarting Frame Home (SteamVR's home app) from the panel");
homeos::stopCopies("frame-home");
const vr::EVRApplicationError le = vr::VRApplications()->LaunchApplication("openvr.tool.steamvr_environments");
if (le != vr::VRApplicationError_None) LOG("start: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(le));
lastSpawn = now;
} else {
if (fs::exists(rootDir() / "layer" / "off", ec)) {
fs::remove(rootDir() / "layer" / "off", ec);
fs::remove(rootDir() / "layer" / "crashes", ec);
LOG("Frame Home turned back on from the panel");
}
if (systemPid > 0 && homeos::alive(systemPid)) {
LOG("restarting frame-home --system (pid %d) from the panel", static_cast<int>(systemPid));
homeos::stop(systemPid, false);
killAt = now + std::chrono::seconds(4);
}
lastSpawn = now - std::chrono::minutes(1); // no 5 s wait: a new one as soon as the old one is gone
keepSystem(now);
}
if (systemPid > 0 && kill(systemPid, 0) == 0) {
LOG("restarting frame-home --system (pid %d) from the panel", static_cast<int>(systemPid));
kill(systemPid, SIGTERM);
killAt = now + std::chrono::seconds(4);
}
lastSpawn = now - std::chrono::minutes(1); // no 5 s wait: a new one as soon as the old one is gone
keepSystem(now);
}
if (restarting && restartingPid > 0 && systemPid == restartingPid && now > killAt && kill(restartingPid, 0) == 0) {
if (systemEnv && restarting && restartingPid > 0 && systemPid == restartingPid && now > killAt && homeos::alive(restartingPid)) {
LOG("frame-home --system (pid %d) didn't stop: killing it", static_cast<int>(restartingPid));
kill(restartingPid, SIGKILL);
homeos::stop(restartingPid, true);
killAt = now + std::chrono::seconds(10);
}
// Restarting until a new process has been up a moment (it loads the home and fades in), 20 s at most.
if (restarting && (now - restartAt > std::chrono::seconds(20) ||
(systemPid > 0 && systemPid != restartingPid && now - lastSpawn > std::chrono::seconds(2))))
(systemEnv ? systemPid > 0 && systemPid != restartingPid && now - lastSpawn > std::chrono::seconds(2)
: homeRunning() && now - lastSpawn > std::chrono::seconds(2))))
restarting = false;
const bool alive = systemPid > 0 && kill(systemPid, 0) == 0;
const Panel::Home h = restarting ? Panel::Home::Restarting : fs::exists(rootDir() / "layer" / "off", ec) ? Panel::Home::TurnedOff
const bool alive = homeRunning();
const Panel::Home h = restarting ? Panel::Home::Restarting : systemEnv && fs::exists(rootDir() / "layer" / "off", ec) ? Panel::Home::TurnedOff
: alive ? Panel::Home::Running : Panel::Home::Stopped;
if (h != panel.home) { panel.home = h; return true; }
return false;
};
// SteamVR's background, when Frame Home is its home app (a Windows PC): a 360-degree picture of the current home
// from its spawn, so the moments before Frame Home draws (SteamVR starting, a game quitting) show the home and
// not Aurora. Made by frame-home --panorama off the frame loop, at low priority, whenever the home changes; set
// here once it's written (a new file each time: SteamVR reloads a background when its path changes).
std::string backgroundFor; // the home key + file times the background shows
std::thread backgroundJob;
std::atomic<bool> backgroundDone{false};
std::string backgroundPng; // (the job's result, read once backgroundDone)
auto keepBackground = [&] {
if (systemEnv || !homeInstalled) return;
if (backgroundDone.exchange(false)) {
if (backgroundJob.joinable()) backgroundJob.join();
float eye = 1.6f, dome = 0;
const std::string j = readText(backgroundPng + ".json");
try { const tinyjson::Value d = tinyjson::parse(j); eye = d["eyeHeight"].asFloat(); dome = d["domeRadius"].asFloat(); } catch (...) {}
vr::EVRSettingsError se = vr::VRSettingsError_None;
vr::VRSettings()->SetString(vr::k_pch_SteamVR_Section, "background", fs::u8path(backgroundPng).u8string().c_str(), &se);
vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, "backgroundUseDomeProjection", true, &se);
vr::VRSettings()->SetFloat(vr::k_pch_SteamVR_Section, "backgroundCameraHeight", eye, &se);
vr::VRSettings()->SetFloat(vr::k_pch_SteamVR_Section, "backgroundDomeRadius", dome, &se);
LOG("SteamVR's background: %s (eyes %.2f m, dome %.1f m)%s", fs::u8path(backgroundPng).filename().string().c_str(), eye, dome,
se == vr::VRSettingsError_None ? "" : " (a setting was refused)");
std::error_code ec; // the earlier pictures go
std::vector<fs::path> old;
for (const auto& e : fs::directory_iterator(rootDir() / "background", ec))
if (e.path() != fs::path(backgroundPng) && e.path() != fs::path(backgroundPng + ".json")) old.push_back(e.path());
for (const fs::path& o : old) fs::remove(o, ec);
}
if (backgroundJob.joinable()) return; // one at a time
const std::vector<Env> envs = scanEnvs();
if (envs.empty()) return;
const std::string cur = currentKey();
const Env* e = &envs.front();
for (const Env& x : envs) if (x.key == cur) e = &x;
std::string want = e->key;
std::error_code ec;
for (const auto& f : e->files) want += std::to_string(static_cast<long long>(fs::last_write_time(homeos::envs() / f, ec).time_since_epoch().count()));
if (want == backgroundFor) return;
backgroundFor = want;
fs::create_directories(rootDir() / "background", ec);
static int n = 0;
backgroundPng = (rootDir() / "background" / ("home-" + std::to_string(static_cast<long long>(std::time(nullptr))) + "-" + std::to_string(++n) + ".png")).string();
std::vector<std::string> args = {(rootDir() / "bin" / homeos::exeName("frame-home")).string(), "--panorama", backgroundPng, "--size", "4096", "--spawn"};
for (const auto& f : e->files) args.push_back((homeos::envs() / f).string());
LOG("making SteamVR's background from %s", e->name.c_str());
backgroundJob = std::thread([&, args] {
const int rc = homeos::run(args, 15, true);
if (rc != 0) LOG("the background picture failed (%d)", rc);
else backgroundDone = true;
});
};
// Thumbnails for environments that have none of their own (frame-switcher --thumbs: one at a time, low priority,
// only while nobody is looking at Frame Home), once a run.
{
bool missing = false;
for (const Env& e : scanEnvs()) missing |= e.thumb.rgba.empty();
if (missing) homeos::run({homeos::selfPath().string(), "--thumbs"}, 19, false);
}
double lastSmooth = -10;
int scrollLogs = 0;
bool dirty = true, wasVisible = false;
@@ -1676,6 +1763,7 @@ int runPanel() {
lastHomeCheck = t;
if (!panel.restartAsked) keepSystem(t);
dirty |= homeTick(t);
keepBackground();
}
panel.now = Panel::clockNow();
vr::VREvent_t ev;
@@ -1748,42 +1836,60 @@ int runPanel() {
}
std::this_thread::sleep_for(std::chrono::milliseconds(!visible ? 16 : moving || dirty ? 2 : 8));
}
if (systemPid > 0) kill(systemPid, SIGTERM); // SteamVR is going: so is the environment
if (systemEnv && systemPid > 0) homeos::stop(systemPid, false); // SteamVR is going: so is the environment
if (backgroundJob.joinable()) backgroundJob.join();
vr::VR_Shutdown();
tex.destroy(); // after VR_Shutdown: vrclient holds the images until then
return 0;
}
int runInstall() {
char self[4096] = {};
const ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1);
if (n <= 0) return 1;
std::string self = homeos::selfPath().string();
if (self.empty()) return 1;
std::error_code ec; // the installed copy is what SteamVR runs (see frame-home --install)
const fs::path installed = rootDir() / "bin" / "frame-switcher";
if (fs::exists(installed, ec)) std::snprintf(self, sizeof self, "%s", installed.c_str());
const fs::path installed = rootDir() / "bin" / homeos::exeName("frame-switcher");
if (fs::exists(installed, ec)) self = installed.string();
for (size_t at = 0; (at = self.find('\\', at)) != std::string::npos; at += 2) self.insert(at, 1, '\\'); // (JSON)
const fs::path manifest = rootDir() / "frame-switcher.vrmanifest";
std::ofstream(manifest) << "{\n \"source\": \"builtin\",\n \"applications\": [ {\n \"app_key\": \"" << kAppKey
<< "\",\n \"launch_type\": \"binary\",\n \"binary_path_linux\": \"" << self
<< "\",\n \"binary_path_windows\": \"" << self
<< "\",\n \"binary_path_linux_arm\": \"" << self
<< "\",\n \"arguments\": \"\",\n \"is_dashboard_overlay\": true,\n"
" \"strings\": { \"en_us\": { \"name\": \"Environments\",\n"
" \"description\": \"Switch the Steam Frame's environment\" } }\n } ]\n}\n";
" \"description\": \"Switch SteamVR's environment (Frame Home)\" } }\n } ]\n}\n";
vr::EVRInitError ierr = vr::VRInitError_None;
vr::VR_Init(&ierr, vr::VRApplication_Utility);
// 3: SteamVR not running — the installer registers the manifest in SteamVR's config files instead.
if (ierr != vr::VRInitError_None) { LOG("VR_Init: %s (SteamVR not running)", vr::VR_GetVRInitErrorAsEnglishDescription(ierr)); return 3; }
vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(fs::absolute(manifest).c_str(), false);
vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(fs::absolute(manifest).u8string().c_str(), false);
LOG("AddApplicationManifest: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(ae));
vr::EVRApplicationError al = vr::VRApplications()->SetApplicationAutoLaunch(kAppKey, true);
LOG("auto-launch with SteamVR: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(al));
// A running (older) panel is replaced by this binary.
stopCopies("frame-switcher");
homeos::stopCopies("frame-switcher");
const vr::EVRApplicationError le = vr::VRApplications()->LaunchApplication(kAppKey);
LOG("start: %s", vr::VRApplications()->GetApplicationsErrorNameFromEnum(le));
vr::VR_Shutdown();
return ae == vr::VRApplicationError_None ? 0 : 1;
}
// The panel off SteamVR: no longer started with it, its manifest gone, a running one closed.
int runUninstall() {
const fs::path manifest = rootDir() / "frame-switcher.vrmanifest";
vr::EVRInitError ierr = vr::VRInitError_None;
vr::VR_Init(&ierr, vr::VRApplication_Utility);
if (ierr != vr::VRInitError_None) { LOG("VR_Init: %s (SteamVR not running)", vr::VR_GetVRInitErrorAsEnglishDescription(ierr)); return 3; }
vr::VRApplications()->SetApplicationAutoLaunch(kAppKey, false);
std::error_code ec;
if (fs::exists(manifest, ec)) vr::VRApplications()->RemoveApplicationManifest(fs::absolute(manifest).u8string().c_str());
fs::remove(manifest, ec);
homeos::stopCopies("frame-switcher");
vr::VR_Shutdown();
LOG("the Environments panel is off SteamVR");
return 0;
}
// Is somebody looking at Frame Home? It draws at the display's rate then, and a frame a second when nobody is (the
// headset is off, or a game runs). A stats.json older than 10 s: Frame Home isn't running.
bool frameHomeShowing() {
@@ -1803,8 +1909,8 @@ bool frameHomeShowing() {
// Frame Home, so they never stutter the home you're in (29 at once took 7 minutes of stutter, 2026-10-21).
int runThumbs() {
std::error_code ec;
fs::create_directories(rootDir() / "envs" / ".thumbs", ec);
const std::string bin = (rootDir() / "bin" / "frame-home").string();
fs::create_directories(homeos::thumbs(), ec);
const std::string bin = (rootDir() / "bin" / homeos::exeName("frame-home")).string();
int made = 0;
for (const Env& e : scanEnvs()) {
if (!e.thumb.rgba.empty()) continue;
@@ -1815,23 +1921,11 @@ int runThumbs() {
std::this_thread::sleep_for(std::chrono::seconds(10));
}
const fs::path out = thumbPath(e.key);
// (an argv, not a shell line: an environment's name can hold any character)
std::vector<std::string> args = {bin, "--snapshot", out.string(), "--size", "384", "--cam", "0,1.6,0,0,-5"};
for (const auto& f : e.files) args.push_back((rootDir() / "envs" / f).string());
std::vector<char*> argv;
for (std::string& a : args) argv.push_back(a.data());
argv.push_back(nullptr);
// From where Frame Home starts you: the first spawn, eyes 1.6 m up, facing its way, 5 degrees down.
std::vector<std::string> args = {bin, "--snapshot", out.string(), "--size", "384", "--spawn", "--cam", "0,1.6,0,0,-5"};
for (const auto& f : e.files) args.push_back((homeos::envs() / f).string());
LOG("thumbnail: %s", e.name.c_str());
const pid_t pid = fork();
if (pid == 0) {
setpriority(PRIO_PROCESS, 0, 19);
const int null = ::open("/dev/null", O_RDWR);
if (null >= 0) { dup2(null, 1); dup2(null, 2); }
execv(argv[0], argv.data());
_exit(127);
}
int st = 0;
if (pid > 0 && waitpid(pid, &st, 0) == pid && WIFEXITED(st) && WEXITSTATUS(st) == 0) ++made;
if (homeos::run(args, 19, true) == 0) ++made;
}
LOG("%d thumbnails made", made);
return 0;
@@ -1843,8 +1937,8 @@ int runTestDelete(const fs::path& dirIn) {
if (fs::exists(dirIn, ec)) { std::fprintf(stderr, "test-delete: %s exists; give a new folder\n", dirIn.c_str()); return 2; }
fs::create_directories(dirIn, ec);
const fs::path dir = fs::canonical(dirIn, ec);
setenv("HOME", dir.c_str(), 1);
setenv("FRAME_FENV_CACHE", (dir / "cache").c_str(), 1);
homeos::setEnv("HOME", dir.string().c_str());
homeos::setEnv("FRAME_FENV_CACHE", (dir / "cache").string().c_str());
const fs::path root = rootDir(), envs = root / "envs", th = envs / ".thumbs", cache = dir / "cache";
if (root != dir / "frame-home") return 2;
fs::create_directories(th, ec);
@@ -1936,6 +2030,7 @@ int main(int argc, char** argv) {
const std::string a = argc > 1 ? argv[1] : "";
if (a == "--install") return runInstall();
if (a == "--thumbs") return runThumbs();
if (a == "--uninstall") return runUninstall();
if (a == "--unpack-bundles") { unpackBundles(); return 0; }
if (a == "--test-delete" && argc > 2) return runTestDelete(argv[2]);
if (a == "--render" && argc > 2) { // the panel as it looks now, to a PPM (no SteamVR needed)
+116 -19
View File
@@ -1994,25 +1994,8 @@ struct Studio {
saveStatus = "Couldn't save: the object edits couldn't be written to " + narrow(tmpEdits.wstring());
return;
}
std::vector<u8> thumb = imageThumb(); // the picture chosen for it, else the current view
if (thumb.empty()) { // the current view, 512 px wide
std::vector<u8> rgba;
const uint32_t w = renderer.width(), h = renderer.height();
if (rendered && w > 8 && h > 8 && renderer.readback(0, rgba) && rgba.size() >= static_cast<size_t>(w) * h * 4) {
const uint32_t tw = 512, th = std::max<uint32_t>(1, h * tw / w);
std::vector<u8> small(static_cast<size_t>(tw) * th * 4);
for (uint32_t y = 0; y < th; ++y)
for (uint32_t x = 0; x < tw; ++x) {
const u8* src = &rgba[(static_cast<size_t>(y * h / th) * w + x * w / tw) * 4];
u8* dst = &small[(static_cast<size_t>(y) * tw + x) * 4];
dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = 255;
}
stbi_write_png_to_func([](void* ctx, void* data, int size) {
auto* v = static_cast<std::vector<u8>*>(ctx);
v->insert(v->end(), static_cast<u8*>(data), static_cast<u8*>(data) + size);
}, &thumb, tw, th, 4, small.data(), tw * 4);
}
}
std::vector<u8> thumb = imageThumb(); // the picture chosen for it, else the view from where you start
if (thumb.empty()) thumb = spawnThumb();
if (saver.joinable()) saver.join();
saving = true;
{ std::lock_guard<std::mutex> l(statusMutex); saveStatus = "Saving " + narrow(fs::path(widen(out)).filename().wstring()) + "\u2026"; }
@@ -2253,6 +2236,49 @@ struct Studio {
dirty = true;
updateTitle();
}
// A .fenv's thumbnail when it has no picture of its own: the home as you first see it in Frame Home, from the first
// spawn (standing on the floor under it, eyes 1.6 m up, facing its way, a little down). One frame drawn from there
// without the selection or the walkable area, read back at 512 px wide as PNG; the view is put back after. No
// spawn: the view as it is.
std::vector<u8> spawnThumb() {
std::vector<u8> png;
const Camera kept = cam;
if (scene && !scene->world.spawns.empty()) {
const WorldPoint& sp = scene->world.spawns[0];
float floor = sp.pos[1];
float f;
if (floorUnder(sp.pos[0], sp.pos[2], sp.pos[1] + 0.5f, f)) floor = f;
cam.yaw = sp.yaw;
cam.pitch = -5.0f * kPi / 180.0f;
cam.dist = 0.5f;
float fwd[3];
cam.forward(fwd);
const float eye[3] = {sp.pos[0], floor + 1.6f, sp.pos[2]};
for (int k = 0; k < 3; ++k) cam.pivot[k] = eye[k] + fwd[k] * cam.dist;
renderer.setSelection(-1);
if (walkGuideOn) { renderer.setStaticGuide({}); walkGuideOn = false; walkGuideDirty = true; } // (back next frame)
renderViewport();
}
std::vector<u8> rgba;
const uint32_t w = renderer.width(), h = renderer.height();
if (rendered && w > 8 && h > 8 && renderer.readback(0, rgba) && rgba.size() >= static_cast<size_t>(w) * h * 4) {
const uint32_t tw = 512, th = std::max<uint32_t>(1, h * tw / w);
std::vector<u8> small(static_cast<size_t>(tw) * th * 4);
for (uint32_t y = 0; y < th; ++y)
for (uint32_t x = 0; x < tw; ++x) {
const u8* src = &rgba[(static_cast<size_t>(y * h / th) * w + x * w / tw) * 4];
u8* dst = &small[(static_cast<size_t>(y) * tw + x) * 4];
dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = 255;
}
stbi_write_png_to_func([](void* ctx, void* data, int size) {
auto* v = static_cast<std::vector<u8>*>(ctx);
v->insert(v->end(), static_cast<u8*>(data), static_cast<u8*>(data) + size);
}, &png, tw, th, 4, small.data(), tw * 4);
}
cam = kept;
return png;
}
// The picture as the .fenv's thumbnail: 512 px wide, PNG (empty: none chosen, or it doesn't read).
std::vector<u8> imageThumb() const {
std::vector<u8> png;
@@ -5289,6 +5315,74 @@ struct Studio {
ImGui::PopTextWrapPos();
section("INSTALL FRAME HOME");
installPanel();
if (pcAvailable()) { section("THIS PC"); pcPanel(); }
}
// ── This PC: Frame Home as SteamVR's environment on the computer Studio runs on (the installer beside Studio,
// pc/: install-frame-home-pc.ps1 on Windows, install.sh on Linux). It shows My Environments, so what's saved here
// is in VR straight away; the Environments panel in the Steam menu switches them.
std::thread pcJob;
std::atomic<bool> pcBusy{false};
std::string pcStatus; // (statusMutex)
fs::path pcPayload() const { return exeDir() / L"pc"; }
bool pcAvailable() const {
#if defined(_WIN32)
return fs::exists(pcPayload() / L"install-frame-home-pc.ps1");
#elif defined(__APPLE__)
return false; // (no SteamVR for macOS)
#else
return fs::exists(pcPayload() / L"install.sh");
#endif
}
static fs::path pcRoot() {
#ifdef _WIN32
const char* l = std::getenv("LOCALAPPDATA");
return fs::u8path(l ? l : "") / L"Frame Home";
#else
const char* h = std::getenv("HOME");
return fs::u8path(h ? h : "") / "frame-home";
#endif
}
bool pcInstalled() const { std::error_code ec; return fs::exists(pcRoot() / L"frame-switcher.vrmanifest", ec); }
void pcInstall(bool uninstall) {
if (pcBusy) return;
if (pcJob.joinable()) pcJob.join();
pcBusy = true;
{ std::lock_guard<std::mutex> l(statusMutex); pcStatus = uninstall ? "Giving SteamVR its own environment back\u2026" : "Installing\u2026"; }
pcJob = std::thread([this, uninstall] {
#ifdef _WIN32
const std::string cmd = "powershell -NoProfile -ExecutionPolicy Bypass -File \"" + narrow((pcPayload() / L"install-frame-home-pc.ps1").wstring()) + "\"" +
(uninstall ? " -Uninstall" : "");
#else
const std::string cmd = "sh \"" + narrow((pcPayload() / L"install.sh").wstring()) + "\"" + (uninstall ? " uninstall" : "");
#endif
std::string out;
const int rc = platform::run(cmd, pcPayload(), out);
std::string msg = lastLine(out); // the installer's verdict: its last "frame-install:" line
if (const size_t at = out.rfind("frame-install: "); at != std::string::npos) msg = lastLine(out.substr(at + 15, out.find('\n', at) == std::string::npos ? std::string::npos : out.find('\n', at) - at - 15));
if (!msg.empty()) msg[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(msg[0])));
{ std::lock_guard<std::mutex> l(statusMutex); pcStatus = rc == 0 ? msg : "Failed: " + (msg.empty() ? std::to_string(rc) : msg); }
pcBusy = false;
});
}
void pcPanel() {
ImGui::PushTextWrapPos(0);
ImGui::TextWrapped("SteamVR on this computer: your environments (My Environments) become its environment, switched in the "
"Steam menu's Environments panel. SteamVR's own settings are put back exactly when you remove it.");
ImGui::Spacing();
const bool installed = pcInstalled();
ImGui::BeginDisabled(pcBusy);
if (!installed) pushPrimary();
if (ImGui::Button(pcBusy ? "Working\u2026###pcinst" : installed ? "Update Frame Home on This PC###pcinst" : "Install Frame Home on This PC###pcinst",
ImVec2(-1, ImGui::GetFrameHeight() * (installed ? 1.0f : 1.4f))))
pcInstall(false);
if (!installed) ImGui::PopStyleColor(3);
if (installed && ImGui::Button("Remove from This PC", ImVec2(-1, 0))) pcInstall(true);
ImGui::EndDisabled();
std::string st;
{ std::lock_guard<std::mutex> l(statusMutex); st = pcStatus; }
if (!st.empty()) { ImGui::Spacing(); ImGui::TextWrapped("%s", st.c_str()); }
ImGui::PopTextWrapPos();
}
@@ -9122,6 +9216,8 @@ bool allowClose() {
if (!g_app->confirmDiscard()) return false;
if (g_app->installing)
platform::message("Frame Studio", "Frame Home is still being installed on the headset. Frame Studio will close when the copy finishes.");
if (g_app->pcBusy)
platform::message("Frame Studio", "Frame Home is still being installed on this PC. Frame Studio will close when it's done.");
if (!g_app->confirmBundleDiscard()) return false;
if (g_app->bundleBusy)
platform::message("Frame Studio", "A bundle is still being written. Frame Studio will close when it's done.");
@@ -9443,6 +9539,7 @@ int runStudio(StudioWindow& window, const std::vector<std::string>& args) {
if (app->tuner.joinable()) app->tuner.join();
if (app->filesReader.joinable()) app->filesReader.join();
if (app->installer.joinable()) app->installer.join();
if (app->pcJob.joinable()) app->pcJob.join();
if (app->bundler.joinable()) app->bundler.join();
if (app->headsetThread.joinable()) app->headsetThread.join();
if (app->headsetDebugThread.joinable()) app->headsetDebugThread.join();