Frame Home as the real SteamVR environment on a PC (Windows too), not a home app

On the Steam Frame a layer inside SteamVR's compositor draws the home where it would draw Aurora, so the home is the
environment itself: at boot, during game loads, behind the Steam menu, with the controllers and passthrough, and it
steps aside for a game the instant it opens. On a PC this was faked with SteamVR's home-app slot and a flat 360 photo
in the gaps, which launched and handed off like an app and showed a picture, not the live home. This does it properly
on any PC that runs SteamVR, the Frame's way:

- Windows: the compositor is Direct3D 11, so the layer is a proxy d3d11.dll beside the compositor (layer/
  frame_home_layer_d3d11.cpp, built from the .def that forwards every other export to the real d3d11.dll, kept as
  fh_d3d11real.dll). It wraps the compositor's device, finds Aurora's draws by their shader bytecode and draws the
  home over the sky, leaving the floor and stars off; everything else passes straight through. Pure pass-through in
  any process that isn't vrcompositor.
- The home's frames cross from Frame Home (Vulkan) to the compositor (D3D11): the layer owns kSlots shared textures
  and hands Frame Home their NT handles over a named pipe; Frame Home imports them (VK_KHR_external_memory_win32) and
  renders into them. No cross-API lock — a slot is published only once the GPU has finished it and the four rotate,
  as on Linux. The shared State stays Frame Home's.
- The Environments panel runs frame-home --system and keeps it alive (one at a time, a pid file across panel
  restarts), the same systemEnv path as the Frame. The installer places/removes the proxy and marks the layer with a
  root/layer folder, in place of the old home-app registration and background pictures.
- Safe like the Vulkan layer: no home, or one stale over half a second, and the compositor draws Aurora as usual;
  the layer turns itself off after two unhealthy compositor starts, and root/layer/off turns it off by hand.

Linux keeps its Vulkan layer (unchanged). Verified without a headset on the null driver: the proxy loads and wraps
the compositor without crashing, recognises Aurora's shaders, and the frame hand-over completes (Frame Home connects,
imports the textures, renders). The compositor only draws the environment while presenting to a headset, so the final
on-headset image is the one step the null driver can't exercise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 4.8 committed 2026-10-21 19:39:36 -05:00
1 parent 283e19f87c
commit d22cc04cb3
14 files changed
+1156 -117

No files matched your search

+66 -72
View File
@@ -1,128 +1,122 @@
# 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.
# Frame Home on a Windows PC running SteamVR: your environments become SteamVR's environment itself — drawn where
# Aurora would be, so they show at boot, during game loads and behind the Steam menu, with the controllers and
# passthrough. 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
# powershell -ExecutionPolicy Bypass -File install-frame-home-pc.ps1 -Uninstall SteamVR's own Aurora 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).
# How: a small Direct3D 11 layer (d3d11.dll beside SteamVR's compositor) draws the home in Aurora's place, fed by
# frame-home --system; the real d3d11.dll is kept beside it as fh_d3d11real.dll and every call it makes passes
# straight through. The Environments panel runs frame-home --system and switches homes. -Uninstall takes the layer
# off (deleting those two files) and SteamVR is exactly as before.
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'
$layer = Join-Path $root 'layer'
function Say($m) { Write-Host "frame-install: $m" }
# SteamVR's config folder, from OpenVR's own record of where things are.
# SteamVR, from OpenVR's own record of where things are: its config folder and where it's installed (for the compositor).
$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 }
$runtime = @($openvr.runtime)[0]
if (-not $cfg -or -not $runtime) { Say "SteamVR's folders aren't listed in $vrpath"; exit 1 }
$bin64 = Join-Path $runtime 'bin\win64'
$proxy = Join-Path $bin64 'd3d11.dll'
$real = Join-Path $bin64 'fh_d3d11real.dll'
$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 StopHome { Get-Process frame-home, frame-switcher -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 400 }
# JSON as SteamVR writes it: UTF-8 without a byte-order mark.
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))
[IO.File]::WriteAllText("$p.tmp", ($o | ConvertTo-Json -Depth 32), (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 }
}
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'))
# enableHomeApp off (no separate home app) and environmentMode 1 (the compositor draws Aurora, which the layer
# replaces). Saved once as they were, put back on -Uninstall. Also the panel keeps them so while SteamVR runs.
$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
function Settings($install) {
$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]@{
if (-not (Test-Path $before)) {
Save $before ([pscustomobject]@{
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
SetProp $sv 'enableHomeApp' $false
SetProp $sv 'environmentMode' 1
} 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
}
# The panel, registered with SteamVR (frame-switcher --install) or, when SteamVR isn't running, in its config files.
$switcherManifest = Join-Path $root 'frame-switcher.vrmanifest'
function PanelOffline($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)
if ($install -and -not $paths.Contains($switcherManifest)) { [void]$paths.Add($switcherManifest) }
if (-not $install -and $paths.Contains($switcherManifest)) { $paths.Remove($switcherManifest) }
$a.manifest_paths = @($paths)
Save $ac $a
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)"
StopHome
if ($running) { & (Join-Path $bin 'frame-switcher.exe') --uninstall } else { PanelOffline $false }
Settings $false
Remove-Item -Force -ErrorAction SilentlyContinue $proxy # the layer off: SteamVR loads the system d3d11 again
Remove-Item -Force -ErrorAction SilentlyContinue $real
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $layer
Say "Frame Home is off: SteamVR's own environment is back (restart SteamVR to see it). Your environments are kept in Documents\Frame Studio\Environments"
exit 0
}
foreach ($x in 'frame-home.exe', 'frame-switcher.exe') {
foreach ($x in 'frame-home.exe', 'frame-switcher.exe', 'd3d11.dll') {
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).
# Frame Home runs on this PC (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
StopHome
New-Item -ItemType Directory -Force -Path $bin, $layer, (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) }
# The compositor layer: keep the real d3d11.dll (from System32 — SteamVR's bin\win64 has none of its own) as
# fh_d3d11real.dll, then drop our proxy in as d3d11.dll. (We copy the real one only once, never our own proxy.)
if (-not (Test-Path $bin64)) { Say "SteamVR's compositor folder isn't where $vrpath says ($bin64)"; exit 1 }
if (-not (Test-Path $real)) { Copy-Item -Force (Join-Path $env:windir 'System32\d3d11.dll') $real }
Copy-Item -Force (Join-Path $here 'd3d11.dll') $proxy
Settings $true
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"
& (Join-Path $bin 'frame-switcher.exe') --install # registers the panel, auto-launches it, starts it now
Say "installed. SteamVR is running, so restart it (or your headset) once to load the environment layer; the Environments panel is in the Steam menu."
} 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"
& (Join-Path $bin 'frame-switcher.exe') --install | Out-Null
PanelOffline $true
Say "installed: your environment becomes SteamVR's when SteamVR next starts."
}
exit 0