mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-29 00:02:23 +01:00
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:
1 parent
b0609d3664
commit
3469b77ca3
21 files changed
+1296
-257
No files matched your search
@@ -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
@@ -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()
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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 |
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
@@ -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" ;;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user