diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 29af7d4..cb7efeb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -38,36 +38,8 @@ 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 d3d11.dll --parallel
- mkdir -p "build/stage/FrameHome-PC-windows-x64" dist
- cp build/pc-win64/frame-home.exe build/pc-win64/frame-switcher.exe build/pc-win64/d3d11.dll pc-streaming/install-frame-home-pc.ps1 \
- "pc-streaming/Install Frame Home on this PC.bat" "pc-streaming/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:
@@ -75,11 +47,6 @@ 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 }}"
@@ -92,11 +59,6 @@ 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
@@ -140,7 +102,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, pc-home-windows]
+ needs: [linux, windows, macos, frame-home]
runs-on: ubuntu-24.04
permissions:
contents: write
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0c44685..50df305 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -141,16 +141,6 @@ if(NOT FRAME_HOME_PC)
set_target_properties(frame-home PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}")
endif()
-# The Windows compositor layer: a proxy d3d11.dll that wraps vrcompositor's Direct3D 11 device and draws the home in
-# Aurora's place (pc-streaming/frame_home_layer_d3d11.cpp). The .def forwards every other d3d11 export to the real library.
-if(WIN32 AND FRAME_HOME_PC)
- add_library(frame-home-d3d11 MODULE pc-streaming/frame_home_layer_d3d11.cpp pc-streaming/d3d11_proxy.def)
- set_target_properties(frame-home-d3d11 PROPERTIES OUTPUT_NAME d3d11 PREFIX "")
- target_include_directories(frame-home-d3d11 PRIVATE src)
- target_link_libraries(frame-home-d3d11 PRIVATE dxguid uuid)
- target_link_options(frame-home-d3d11 PRIVATE -static -static-libgcc -static-libstdc++)
-endif()
-
# fenv-tool: convert homes and models into .fenv environments.
add_executable(fenv-tool src/fenv_tool.cpp)
target_link_libraries(fenv-tool PRIVATE frame-scene)
diff --git a/README.md b/README.md
index b464711..285d116 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,6 @@ 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
@@ -50,12 +49,6 @@ 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 (experimental, separate)**
-
-- A PC-side compositor layer that aims to make your homes SteamVR's environment on a desktop PC, kept apart in
- [`pc-streaming/`](pc-streaming/README.md). It is for a tethered PC headset; it does **not** work for the Steam
- Frame, which renders its home on the headset itself.
-
**Frame Studio (on your computer)**
- Opens Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pairs rooms with vistas.
@@ -81,7 +74,6 @@ 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](pc-streaming/README.md) | Experimental PC-side layer (separate; not for the Steam Frame) |
| [Building from source](docs/building.md) | Building on each system, CI, the headset side |
| [Troubleshooting](docs/troubleshooting.md) | Symptoms and fixes, logs, the self-test |
diff --git a/docs/README.md b/docs/README.md
index 50c7abd..b16b3e2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -10,6 +10,5 @@
| [Performance](performance.md) | How Frame Home reaches 120 fps, checking it, fixing stutter, Studio's own pacing |
| [Frame Studio guide](studio-guide.md) | The Studio window, tools and panels |
| [Bundles](bundles.md) | Several environments in one `.fbundle` file |
-| [Frame Home on a PC](pc-steamvr.md) | Your homes as SteamVR's environment on a Windows or Linux PC |
| [Building from source](building.md) | Building on each system, the CI, the headset side, the repository layout |
| [Troubleshooting](troubleshooting.md) | Symptoms and fixes, logs, the self-test |
diff --git a/docs/studio-guide.md b/docs/studio-guide.md
index d5e0e56..e4b6dbe 100644
--- a/docs/studio-guide.md
+++ b/docs/studio-guide.md
@@ -163,8 +163,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).
+- **This PC** (Linux): **Install Frame Home on This PC** makes your environments SteamVR's environment on the
+ computer Studio runs on.
- The **Headset** field is the headset's address: an SSH alias such as `frame`, or `steamos@
`.
See [headset-setup.md](headset-setup.md) and [install-frame-home.md](install-frame-home.md).
diff --git a/pc-streaming/Install Frame Home on this PC.bat b/pc-streaming/Install Frame Home on this PC.bat
deleted file mode 100644
index 34f8147..0000000
--- a/pc-streaming/Install Frame Home on this PC.bat
+++ /dev/null
@@ -1,6 +0,0 @@
-@echo off
-rem Makes Frame Home SteamVR's environment on this PC (your homes from Frame Studio's My Environments, switched
-rem in the Steam menu's Environments panel). Run it again after updating Frame Studio.
-powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-frame-home-pc.ps1"
-if errorlevel 1 (echo. ^& echo Frame Home was NOT installed: see the messages above.) else (echo. ^& echo Done.)
-pause
diff --git a/pc-streaming/README.md b/pc-streaming/README.md
deleted file mode 100644
index 82fe3f7..0000000
--- a/pc-streaming/README.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# PC streaming environments (experimental — not for the Steam Frame)
-
-This folder holds Frame Home's **PC-side** attempt to make your environments SteamVR's environment on a desktop PC,
-separate from the Steam Frame build. It is kept apart on purpose: it does **not** work for the Steam Frame, and the
-main project ships without it.
-
-## What's here
-
-| File | What |
-|---|---|
-| `frame_home_layer_d3d11.cpp` | A proxy `d3d11.dll` that wraps SteamVR's Windows compositor and draws the home where it draws Aurora — the Direct3D 11 counterpart of the Steam Frame's Vulkan layer (`../layer/frame_home_layer.cpp`). |
-| `d3d11_proxy.def` | The proxy's exports: `D3D11CreateDevice` is ours, every other export forwards to the real `d3d11.dll`. |
-| `install-frame-home-pc.ps1`, `*.bat` | The Windows installer that places the layer beside SteamVR's compositor. |
-| `pc-steamvr.md` | Notes on how it was meant to work. |
-
-The renderer's Windows support (importing the compositor's textures, the named-pipe hand-over, the `--system` path)
-lives in the shared sources under `src/` behind `#ifdef _WIN32` / the `FRAME_HOME_PC` CMake option, so it only
-compiles into a PC build and never affects the Steam Frame build.
-
-## Why it's set aside
-
-The compositor layer is the right design for a **tethered** PC headset (Index, Vive, Reverb), and likely for standard
-streaming (e.g. a Quest over Steam Link), where the **PC** renders the SteamVR environment. It was never confirmed on
-such hardware.
-
-It **cannot** work for the **Steam Frame**. Tracing every PC process showed the Steam Frame renders its home
-environment **on the headset itself** — `vrcompositor.exe` draws nothing and the `vrlink` streaming driver makes none
-of the environment's shaders; only game video is streamed. So there is nothing on the PC to replace. The home on the
-Steam Frame belongs to the **headset-side Frame Home** (the Vulkan layer in `../layer/`), which runs inside the Steam
-Frame's own compositor.
-
-## Building it
-
-With the main project configured for a PC (`FRAME_HOME_PC=ON`, the default on Windows/x86-64), the `frame-home-d3d11`
-target builds `d3d11.dll` from this folder. `install-frame-home-pc.ps1` installs it.
diff --git a/pc-streaming/Remove Frame Home from this PC.bat b/pc-streaming/Remove Frame Home from this PC.bat
deleted file mode 100644
index 78f2ab5..0000000
--- a/pc-streaming/Remove Frame Home from this PC.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-rem Gives SteamVR its own environment back on this PC (your environments are kept).
-powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-frame-home-pc.ps1" -Uninstall
-pause
diff --git a/pc-streaming/d3d11_proxy.def b/pc-streaming/d3d11_proxy.def
deleted file mode 100644
index 549125d..0000000
--- a/pc-streaming/d3d11_proxy.def
+++ /dev/null
@@ -1,57 +0,0 @@
-; Frame Home's d3d11.dll proxy (layer/frame_home_layer_d3d11.cpp): D3D11CreateDevice is ours, so we can wrap the
-; SteamVR compositor's device and draw the home where it would draw Aurora. Every other export forwards to the real
-; d3d11.dll, copied beside us as fh_d3d11real.dll by the installer, so the compositor behaves exactly as before in
-; every way we don't touch, and so does any other program that happens to load this proxy.
-LIBRARY d3d11
-EXPORTS
- D3D11CreateDevice
- CreateDirect3D11DeviceFromDXGIDevice = fh_d3d11real.CreateDirect3D11DeviceFromDXGIDevice
- CreateDirect3D11SurfaceFromDXGISurface = fh_d3d11real.CreateDirect3D11SurfaceFromDXGISurface
- D3D11CoreCreateDevice = fh_d3d11real.D3D11CoreCreateDevice
- D3D11CoreCreateLayeredDevice = fh_d3d11real.D3D11CoreCreateLayeredDevice
- D3D11CoreGetLayeredDeviceSize = fh_d3d11real.D3D11CoreGetLayeredDeviceSize
- D3D11CoreRegisterLayers = fh_d3d11real.D3D11CoreRegisterLayers
- D3D11CreateDeviceAndSwapChain = fh_d3d11real.D3D11CreateDeviceAndSwapChain
- D3D11CreateDeviceForD3D12 = fh_d3d11real.D3D11CreateDeviceForD3D12
- D3D11On12CreateDevice = fh_d3d11real.D3D11On12CreateDevice
- D3DKMTCloseAdapter = fh_d3d11real.D3DKMTCloseAdapter
- D3DKMTCreateAllocation = fh_d3d11real.D3DKMTCreateAllocation
- D3DKMTCreateContext = fh_d3d11real.D3DKMTCreateContext
- D3DKMTCreateDevice = fh_d3d11real.D3DKMTCreateDevice
- D3DKMTCreateSynchronizationObject = fh_d3d11real.D3DKMTCreateSynchronizationObject
- D3DKMTDestroyAllocation = fh_d3d11real.D3DKMTDestroyAllocation
- D3DKMTDestroyContext = fh_d3d11real.D3DKMTDestroyContext
- D3DKMTDestroyDevice = fh_d3d11real.D3DKMTDestroyDevice
- D3DKMTDestroySynchronizationObject = fh_d3d11real.D3DKMTDestroySynchronizationObject
- D3DKMTEscape = fh_d3d11real.D3DKMTEscape
- D3DKMTGetContextSchedulingPriority = fh_d3d11real.D3DKMTGetContextSchedulingPriority
- D3DKMTGetDeviceState = fh_d3d11real.D3DKMTGetDeviceState
- D3DKMTGetDisplayModeList = fh_d3d11real.D3DKMTGetDisplayModeList
- D3DKMTGetMultisampleMethodList = fh_d3d11real.D3DKMTGetMultisampleMethodList
- D3DKMTGetRuntimeData = fh_d3d11real.D3DKMTGetRuntimeData
- D3DKMTGetSharedPrimaryHandle = fh_d3d11real.D3DKMTGetSharedPrimaryHandle
- D3DKMTLock = fh_d3d11real.D3DKMTLock
- D3DKMTOpenAdapterFromHdc = fh_d3d11real.D3DKMTOpenAdapterFromHdc
- D3DKMTOpenResource = fh_d3d11real.D3DKMTOpenResource
- D3DKMTPresent = fh_d3d11real.D3DKMTPresent
- D3DKMTQueryAdapterInfo = fh_d3d11real.D3DKMTQueryAdapterInfo
- D3DKMTQueryAllocationResidency = fh_d3d11real.D3DKMTQueryAllocationResidency
- D3DKMTQueryResourceInfo = fh_d3d11real.D3DKMTQueryResourceInfo
- D3DKMTRender = fh_d3d11real.D3DKMTRender
- D3DKMTSetAllocationPriority = fh_d3d11real.D3DKMTSetAllocationPriority
- D3DKMTSetContextSchedulingPriority = fh_d3d11real.D3DKMTSetContextSchedulingPriority
- D3DKMTSetDisplayMode = fh_d3d11real.D3DKMTSetDisplayMode
- D3DKMTSetDisplayPrivateDriverFormat = fh_d3d11real.D3DKMTSetDisplayPrivateDriverFormat
- D3DKMTSetGammaRamp = fh_d3d11real.D3DKMTSetGammaRamp
- D3DKMTSetVidPnSourceOwner = fh_d3d11real.D3DKMTSetVidPnSourceOwner
- D3DKMTSignalSynchronizationObject = fh_d3d11real.D3DKMTSignalSynchronizationObject
- D3DKMTUnlock = fh_d3d11real.D3DKMTUnlock
- D3DKMTWaitForSynchronizationObject = fh_d3d11real.D3DKMTWaitForSynchronizationObject
- D3DKMTWaitForVerticalBlankEvent = fh_d3d11real.D3DKMTWaitForVerticalBlankEvent
- D3DPerformance_BeginEvent = fh_d3d11real.D3DPerformance_BeginEvent
- D3DPerformance_EndEvent = fh_d3d11real.D3DPerformance_EndEvent
- D3DPerformance_GetStatus = fh_d3d11real.D3DPerformance_GetStatus
- D3DPerformance_SetMarker = fh_d3d11real.D3DPerformance_SetMarker
- EnableFeatureLevelUpgrade = fh_d3d11real.EnableFeatureLevelUpgrade
- OpenAdapter10 = fh_d3d11real.OpenAdapter10
- OpenAdapter10_2 = fh_d3d11real.OpenAdapter10_2
diff --git a/pc-streaming/frame_home_layer_d3d11.cpp b/pc-streaming/frame_home_layer_d3d11.cpp
deleted file mode 100644
index b2609d2..0000000
--- a/pc-streaming/frame_home_layer_d3d11.cpp
+++ /dev/null
@@ -1,786 +0,0 @@
-// Frame Home's Direct3D 11 compositor layer (Windows): the same idea as the Vulkan layer on the Frame and Linux
-// (layer/frame_home_layer.cpp), but SteamVR's Windows compositor is Direct3D 11, so this lives inside it as a proxy
-// d3d11.dll. D3D11CreateDevice is ours (d3d11_proxy.def forwards every other export to the real d3d11.dll, copied
-// beside us as fh_d3d11real.dll), so when vrcompositor creates its device we wrap it and swap only the environment:
-// where the compositor draws Aurora's sky we draw the newest frame Frame Home (frame-home --system) rendered of your
-// home, looked up per pixel by direction (rotation reprojection), and Aurora's floor and stars we leave undrawn.
-// Everything else the compositor does is untouched, so the home shows at boot, during game loads, behind the Steam
-// menu, with the controllers and passthrough, exactly as Aurora would. Outside vrcompositor this is pure pass-through.
-//
-// Hand-over (env_share.h): Frame Home connects to our named pipe and sends its shared State's handle and the size it
-// wants; we make kSlots shared textures, hand it their NT handles, and it renders the home into them over Vulkan.
-//
-// Safety:
-// active only inside a process named vrcompositor.exe; everywhere else D3D11CreateDevice just calls the real one
-// no published frame, or one older than half a second (Frame Home stopped or crashed): Aurora as usual
-// %LOCALAPPDATA%\Frame Home\layer\off: pass-through (checked at start and each second)
-// twice the compositor started with us swapping and never got a healthy frame in: we write `off` ourselves
-// deleting fh_d3d11real.dll / this d3d11.dll from SteamVR's bin\win64 removes us entirely (the installer does)
-#ifndef WIN32_LEAN_AND_MEAN
-#define WIN32_LEAN_AND_MEAN
-#endif
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "../src/env_share.h"
-
-namespace fs = std::filesystem;
-
-namespace {
-
-// ── process / files ──────────────────────────────────────────────────────────────────────────────
-
-bool processIs(const wchar_t* exe) {
- wchar_t path[MAX_PATH] = {};
- GetModuleFileNameW(nullptr, path, MAX_PATH);
- const wchar_t* base = wcsrchr(path, L'\\');
- return _wcsicmp(base ? base + 1 : path, exe) == 0;
-}
-bool isCompositor() { static const int v = processIs(L"vrcompositor.exe"); return v; }
-bool isVrServer() { static const int v = processIs(L"vrserver.exe"); return v; }
-
-// %LOCALAPPDATA%\Frame Home (home_platform.h's root() on Windows), where the logs live.
-fs::path dir() {
- wchar_t* p = nullptr;
- size_t n = 0;
- _wdupenv_s(&p, &n, L"LOCALAPPDATA");
- fs::path d = p ? fs::path(p) / L"Frame Home" : fs::path(L"Frame Home");
- free(p);
- return d;
-}
-// The layer's own folder (root\layer): its off switch and crash guard, the same names and place the Environments
-// panel uses (rootDir()/layer/off, .../crashes, .../starting) and the installer makes to mark the layer present.
-fs::path layerDir() { return dir() / L"layer"; }
-bool fileExists(const fs::path& p) { std::error_code ec; return fs::exists(p, ec); }
-
-std::mutex g_logMutex;
-void logf(const char* fmt, ...) {
- std::lock_guard l(g_logMutex);
- std::error_code ec;
- fs::create_directories(dir() / "logs", ec);
- FILE* f = nullptr;
- if (_wfopen_s(&f, (dir() / "logs" / "frame-layer.log").wstring().c_str(), L"a") || !f) return;
- SYSTEMTIME t;
- GetLocalTime(&t);
- std::fprintf(f, "%02d:%02d:%02d ", t.wHour, t.wMinute, t.wSecond);
- va_list ap;
- va_start(ap, fmt);
- std::vfprintf(f, fmt, ap);
- va_end(ap);
- std::fputc('\n', f);
- std::fclose(f);
-}
-
-bool g_diagnostic = false; // this process (vrserver) only watches what the streaming driver draws; it replaces nothing
-void probef(const char* fmt, ...) {
- std::lock_guard l(g_logMutex);
- std::error_code ec;
- fs::create_directories(dir() / "logs", ec);
- FILE* f = nullptr;
- if (_wfopen_s(&f, (dir() / "logs" / "frame-d3d-probe.log").wstring().c_str(), L"a") || !f) return;
- SYSTEMTIME t;
- GetLocalTime(&t);
- std::fprintf(f, "%02d:%02d:%02d ", t.wHour, t.wMinute, t.wSecond);
- va_list ap;
- va_start(ap, fmt);
- std::vfprintf(f, fmt, ap);
- va_end(ap);
- std::fputc('\n', f);
- std::fclose(f);
-}
-
-std::vector readFile(const fs::path& p) {
- std::ifstream in(p, std::ios::binary);
- return {std::istreambuf_iterator(in), std::istreambuf_iterator()};
-}
-
-uint64_t fnv(const void* data, size_t n) {
- uint64_t h = 1469598103934665603ull;
- const unsigned char* b = static_cast(data);
- for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 1099511628211ull; }
- return h;
-}
-
-// ── Aurora shaders, by the bytecode the compositor hands CreatePixelShader ─────────────────────────
-// The compositor loads SteamVR's compiled pixel shaders (resources\shaders\d3d11\*.fxo) and creates them with the
-// file's bytes. We hash each file whose name marks it part of the environment and, at CreatePixelShader, tag the
-// ID3D11PixelShader it returns: the sky (and horizon) we replace with the home; the floor and stars we skip.
-enum class Kind { None, Replace, Skip };
-std::unordered_map g_shaderKinds; // bytecode hash -> what to do
-
-std::unordered_map g_shaderName; // bytecode hash -> .fxo name, every pixel shader (diagnostic)
-
-void scanAuroraShaders() {
- wchar_t path[MAX_PATH] = {};
- GetModuleFileNameW(nullptr, path, MAX_PATH); // ...\SteamVR\bin\win64\vrcompositor.exe
- fs::path shaders = fs::path(path).parent_path().parent_path().parent_path() / L"resources" / L"shaders" / L"d3d11";
- std::error_code ec;
- int replace = 0, skip = 0;
- for (auto& e : fs::directory_iterator(shaders, ec)) {
- if (e.path().extension() != ".fxo") continue;
- const std::string name = e.path().filename().string();
- if (name.find("_ps.fxo") == std::string::npos) continue;
- const std::vector b = readFile(e.path());
- if (b.empty()) continue;
- const uint64_t h = fnv(b.data(), b.size());
- g_shaderName[h] = name; // every pixel shader, so a draw can be named (diagnostic)
- if (name.find("aurora") == std::string::npos && name.find("motes") == std::string::npos) continue;
- // Aurora's sky and horizon (what you look out at) we draw the home into; its floor and its stars (the motes
- // "sky" that is really the floor's ground glow) we leave off, as the Vulkan layer does.
- Kind k = name.find("floor") != std::string::npos || name.find("motes") != std::string::npos ? Kind::Skip : Kind::Replace;
- g_shaderKinds[h] = k;
- (k == Kind::Replace ? replace : skip) += 1;
- }
- logf("aurora shaders: %d replaced, %d skipped, %zu pixel shaders named (from %s)", replace, skip, g_shaderName.size(), shaders.string().c_str());
-}
-
-// ── the home: shared state, the compositor's textures, and our draw resources ──────────────────────
-
-// Two locks, never held in the reverse order: g_shaderMutex guards the recognised shaders (written as the compositor
-// creates them, read at each draw); g_texMutex guards the shared textures (rebuilt by the pipe thread when Frame Home
-// connects or resizes, read while a draw is replaced). A replaced draw holds g_texMutex across the draw so the pipe
-// thread can't free a texture mid-draw — the race that crashed the compositor.
-std::mutex g_shaderMutex, g_texMutex;
-std::atomic g_off{false};
-std::atomic g_connected{false};
-std::atomic g_replaced{0}, g_skipped{0}; // draws swapped for the home / left undrawn (a diagnostic)
-std::atomic g_drawsHere{0}, g_drawsOther{0}; // every draw on a hooked context (any shader), for diagnosis
-std::atomic g_deviceCount{0}, g_deferredCount{0}; // distinct devices / deferred contexts the compositor made
-
-envshare::State* g_state = nullptr; // Frame Home's shared State (it owns it; we map its handle)
-HANDLE g_stateMap = nullptr;
-
-ID3D11Device* g_device = nullptr; // the compositor's device (not addref'd: it outlives us)
-ID3D11DeviceContext* g_context = nullptr; // its immediate context
-
-uint32_t g_generation = 0; // bumped each time Frame Home asks for new textures
-uint32_t g_texW = 0, g_texH = 0;
-struct SharedTex { ID3D11Texture2D* tex = nullptr; ID3D11ShaderResourceView* srv = nullptr; HANDLE handle = nullptr; };
-std::vector g_tex; // kSlots, on the compositor's device
-int g_lastGood = -1; // newest slot drawn (held while Frame Home makes new textures)
-
-// Our draw resources on the compositor's device.
-ID3D11PixelShader* g_homePs = nullptr;
-ID3D11Buffer* g_homeCb = nullptr; // HomeCb below (b13): reprojection + brightness
-ID3D11SamplerState* g_sampler = nullptr;
-bool g_resourcesTried = false, g_resourcesOk = false;
-
-constexpr UINT kSrvSlot = 13, kCbSlot = 13, kSampSlot = 13; // high slots the compositor's own shaders don't use
-
-struct HomeCb { // matches the cbuffer in the replacement shader (b13)
- float col[envshare::kSlots == 0 ? 1 : 2][4][4]; // per eye: the 4 columns of projection*viewRotation
- float eye[2][4]; // per eye position (xyz)
- float params[4]; // x: brightness (fade), y: 1 when there's something to show
-};
-
-// Per command buffer? D3D's immediate context is one object; the pixel shader bound on it, tracked by PSSetShader.
-ID3D11PixelShader* g_boundPs = nullptr;
-std::unordered_map g_psName; // every recognised shader's .fxo name (diagnostic)
-std::unordered_map g_boundByCtx; // the pixel shader bound on each context
-
-// Which slot to draw this compositor frame: picked at the first replaced draw and held briefly, so both eyes and
-// every aurora piece in the frame show the same home frame (frameSlot in the Vulkan layer).
-int g_latchSlot = -1;
-uint64_t g_latchNs = 0;
-bool g_latchStamped = false;
-constexpr uint64_t kLatchNs = 4000000ull;
-
-int liveSlot() { // the newest published slot, or -1 for Aurora (Frame Home gone or frozen 5 s)
- if (g_off || !g_state || g_tex.empty()) return -1;
- const uint64_t now = envshare::nowNs();
- if (now - g_state->publishNs.load() > 5000000000ull && now > g_state->busyUntilNs.load()) return -1;
- const uint32_t k = g_state->latest.load(std::memory_order_acquire);
- if (k < envshare::kSlots && g_state->slot[k].generation == g_generation) g_lastGood = static_cast(k);
- return g_lastGood;
-}
-int frameSlot() {
- const uint64_t now = envshare::nowNs();
- if (g_latchNs && now - g_latchNs < kLatchNs) return g_latchSlot;
- g_latchSlot = liveSlot();
- g_latchNs = now;
- g_latchStamped = false;
- return g_latchSlot;
-}
-
-// ── health: off switch and crash guard (mirrors the Vulkan layer) ──────────────────────────────────
-std::atomic g_armed{false};
-uint64_t g_healthyFrames = 0;
-
-void startHealth() {
- std::error_code ec;
- fs::create_directories(layerDir(), ec);
- if (fileExists(layerDir() / "off")) { g_off = true; logf("off: Aurora as usual (delete %s\\off to turn Frame Home back on)", dir().string().c_str()); return; }
- int crashes = 0;
- if (fileExists(layerDir() / "starting")) {
- const std::vector c = readFile(layerDir() / "crashes");
- crashes = c.empty() ? 1 : std::atoi(std::string(c.begin(), c.end()).c_str()) + 1;
- if (FILE* f = nullptr; !_wfopen_s(&f, (layerDir() / "crashes").wstring().c_str(), L"w") && f) { std::fprintf(f, "%d\n", crashes); std::fclose(f); }
- }
- if (crashes >= 2) {
- if (FILE* f = nullptr; !_wfopen_s(&f, (layerDir() / "off").wstring().c_str(), L"w") && f) std::fclose(f);
- DeleteFileW((layerDir() / "starting").wstring().c_str());
- g_off = true;
- logf("the compositor stopped twice before a healthy frame: turned myself off (Aurora as usual)");
- }
-}
-void arm() { // we are about to start swapping Aurora: a crash from here that never got healthy counts
- if (g_armed.exchange(true)) return;
- if (FILE* f = nullptr; !_wfopen_s(&f, (layerDir() / "starting").wstring().c_str(), L"w") && f) std::fclose(f);
-}
-void healthy() { // a frame drawn and shown: the compositor got this far, so a later crash isn't ours
- if (++g_healthyFrames == 30 && g_armed.exchange(false)) {
- DeleteFileW((layerDir() / "starting").wstring().c_str());
- DeleteFileW((layerDir() / "crashes").wstring().c_str());
- }
-}
-
-// ── the replacement pixel shader, compiled at startup ──────────────────────────────────────────────
-// Reads the compositor's own constant buffer (b0) for this eye's position, forms the direction from the eye to the
-// drawn point, looks it up in the home frame (projection * view rotation, column major, as Frame Home rendered it)
-// and samples that eye's layer. The same maths as layer/env_ps.frag.
-const char* kHlsl = R"(
-Texture2DArray home : register(t13);
-SamplerState samp : register(s13);
-cbuffer Aurora : register(b0) { float4 _pad0[28]; float4 g_vEyePositionWs; } // g_vEyePositionWs at offset 448
-cbuffer Home : register(b13) {
- float4 uCol[8]; // per eye (0: 0..3, 1: 4..7): the 4 columns of projection*viewRotation, tracking space
- float4 uEyePos[2]; // per eye position Frame Home rendered from, tracking space (xyz)
- float4 uParams; // x: brightness, y: 1 when there's something to show
-};
-struct PSIn { float4 pos : SV_Position; float3 worldPos : TEXCOORD1; };
-float4 main(PSIn i) : SV_Target {
- float3 eye = g_vEyePositionWs.xyz;
- float3 dir = normalize(i.worldPos - eye);
- int k = distance(eye, uEyePos[0].xyz) <= distance(eye, uEyePos[1].xyz) ? 0 : 1;
- int b = k * 4;
- float4 c = uCol[b + 0] * dir.x + uCol[b + 1] * dir.y + uCol[b + 2] * dir.z;
- float3 rgb = float3(0, 0, 0);
- if (c.w > 1e-5 && uParams.y > 0.5) {
- float2 uv = (c.xy / c.w) * 0.5 + 0.5;
- rgb = home.SampleLevel(samp, float3(uv, (float)k), 0.0).rgb;
- }
- return float4(rgb * uParams.x, 1.0);
-}
-)";
-
-ID3DBlob* compileHlsl() {
- HMODULE comp = LoadLibraryW(L"d3dcompiler_47.dll");
- if (!comp) { logf("no d3dcompiler_47.dll: Aurora stays"); return nullptr; }
- using Fn = HRESULT(WINAPI*)(LPCVOID, SIZE_T, LPCSTR, const D3D_SHADER_MACRO*, ID3DInclude*, LPCSTR, LPCSTR, UINT, UINT, ID3DBlob**, ID3DBlob**);
- auto d3dCompile = reinterpret_cast(GetProcAddress(comp, "D3DCompile"));
- if (!d3dCompile) return nullptr;
- ID3DBlob* code = nullptr;
- ID3DBlob* err = nullptr;
- const HRESULT hr = d3dCompile(kHlsl, std::strlen(kHlsl), "frame_home_env", nullptr, nullptr, "main", "ps_5_0", 0, 0, &code, &err);
- if (FAILED(hr)) { logf("replacement shader failed to compile (0x%08lx): %s", hr, err ? static_cast(err->GetBufferPointer()) : ""); if (err) err->Release(); return nullptr; }
- if (err) err->Release();
- return code;
-}
-
-bool ensureResources() {
- if (g_resourcesTried) return g_resourcesOk;
- g_resourcesTried = true;
- ID3DBlob* code = compileHlsl();
- if (!code) return false;
- if (FAILED(g_device->CreatePixelShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &g_homePs))) { code->Release(); logf("CreatePixelShader for the home failed"); return false; }
- code->Release();
- D3D11_BUFFER_DESC bd = {};
- bd.ByteWidth = sizeof(HomeCb);
- bd.Usage = D3D11_USAGE_DYNAMIC;
- bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
- bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
- if (FAILED(g_device->CreateBuffer(&bd, nullptr, &g_homeCb))) { logf("home cbuffer failed"); return false; }
- D3D11_SAMPLER_DESC sd = {};
- sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
- sd.AddressU = sd.AddressV = sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
- sd.MaxLOD = D3D11_FLOAT32_MAX;
- if (FAILED(g_device->CreateSamplerState(&sd, &g_sampler))) { logf("home sampler failed"); return false; }
- g_resourcesOk = true;
- logf("home draw resources ready");
- return true;
-}
-
-void fillCb(ID3D11DeviceContext* ctx, int slot) { // mapped on the recording context (the environment's deferred one)
- if (!g_homeCb) return;
- const envshare::Slot& s = g_state->slot[slot];
- HomeCb cb = {};
- for (int e = 0; e < 2; ++e) {
- std::memcpy(cb.col[e], s.viewProj[e], sizeof(float) * 16); // column major: 4 columns of 4 floats
- for (int c = 0; c < 4; ++c) cb.eye[e][c] = s.eyePos[e][c];
- }
- cb.params[0] = g_state->fade.load() / 1000.0f;
- cb.params[1] = g_state->show.load() ? 1.0f : 0.0f;
- D3D11_MAPPED_SUBRESOURCE m;
- if (SUCCEEDED(ctx->Map(g_homeCb, 0, D3D11_MAP_WRITE_DISCARD, 0, &m))) {
- std::memcpy(m.pData, &cb, sizeof cb);
- ctx->Unmap(g_homeCb, 0);
- }
-}
-
-// ── the hooks: CreatePixelShader + CreateDeferredContext on the device, PSSetShader + the draws on contexts ─────────
-//
-// The compositor records the environment on a *deferred* context, which in D3D11 is a different object with its own
-// vtable than the immediate context — hooking the immediate one alone saw nothing. So we hook every context's vtable
-// (the immediate one, and each deferred one as the device makes it), keeping that vtable's own originals. A few
-// vtables at most (immediate, deferred), found per call by the context's vtable pointer.
-
-using PFN_CreatePixelShader = HRESULT(STDMETHODCALLTYPE*)(ID3D11Device*, const void*, SIZE_T, ID3D11ClassLinkage*, ID3D11PixelShader**);
-using PFN_CreateDeferredContext = HRESULT(STDMETHODCALLTYPE*)(ID3D11Device*, UINT, ID3D11DeviceContext**);
-using PFN_CreateDeferredContext1 = HRESULT(STDMETHODCALLTYPE*)(ID3D11Device1*, UINT, ID3D11DeviceContext1**);
-using PFN_PSSetShader = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, ID3D11PixelShader*, ID3D11ClassInstance* const*, UINT);
-using PFN_Draw = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, UINT, UINT);
-using PFN_DrawIndexed = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, UINT, UINT, INT);
-using PFN_DrawInstanced = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, UINT, UINT, UINT, UINT);
-using PFN_DrawIndexedInstanced = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, UINT, UINT, UINT, INT, UINT);
-
-PFN_CreatePixelShader g_realCreatePixelShader = nullptr;
-PFN_CreatePixelShader g_serverRealCreatePixelShader = nullptr; // vrserver probe (diagnostic)
-PFN_CreateDeferredContext g_realCreateDeferredContext = nullptr;
-PFN_CreateDeferredContext1 g_realCreateDeferredContext1 = nullptr;
-
-// One per hooked context vtable, with that vtable's own original draw/PSSetShader functions.
-struct CtxHooks {
- void* vtbl = nullptr;
- PFN_PSSetShader psSetShader = nullptr;
- PFN_Draw draw = nullptr;
- PFN_DrawIndexed drawIndexed = nullptr;
- PFN_DrawInstanced drawInstanced = nullptr;
- PFN_DrawIndexedInstanced drawIndexedInstanced = nullptr;
-};
-CtxHooks g_ctxHooks[8];
-std::atomic g_ctxHookCount{0};
-std::mutex g_hookMutex;
-
-CtxHooks* hooksFor(void* ctx) {
- void* vt = *reinterpret_cast(ctx);
- const int n = g_ctxHookCount.load(std::memory_order_acquire);
- for (int i = 0; i < n; ++i) if (g_ctxHooks[i].vtbl == vt) return &g_ctxHooks[i];
- return nullptr;
-}
-
-std::unordered_map g_psKind; // shaders the compositor made that we recognised
-
-HRESULT STDMETHODCALLTYPE CreatePixelShader_hook(ID3D11Device* dev, const void* code, SIZE_T len, ID3D11ClassLinkage* link, ID3D11PixelShader** out) {
- const HRESULT hr = g_realCreatePixelShader(dev, code, len, link, out);
- if (SUCCEEDED(hr) && out && *out && code && len) {
- const uint64_t h = fnv(code, len);
- std::lock_guard l(g_shaderMutex);
- if (auto it = g_shaderKinds.find(h); it != g_shaderKinds.end()) g_psKind[*out] = it->second;
- if (auto it = g_shaderName.find(h); it != g_shaderName.end()) g_psName[*out] = it->second;
- static std::unordered_set devices; // how many distinct devices the compositor makes shaders on
- if (devices.insert(dev).second) { g_deviceCount.store(static_cast(devices.size())); logf("pixel shader created on device %p (%s)%s", (void*)dev, dev == g_device ? "ours" : "another", g_device ? "" : " [before wrap]"); }
- }
- return hr;
-}
-
-// vrserver probe: log whether the streaming driver creates the environment's own pixel shaders here.
-HRESULT STDMETHODCALLTYPE CreatePixelShaderProbe_hook(ID3D11Device* dev, const void* code, SIZE_T len, ID3D11ClassLinkage* link, ID3D11PixelShader** out) {
- const HRESULT hr = g_serverRealCreatePixelShader(dev, code, len, link, out);
- if (SUCCEEDED(hr) && code && len) {
- static std::atomic total{0};
- total.fetch_add(1);
- const uint64_t h = fnv(code, len);
- auto it = g_shaderName.find(h);
- if (it != g_shaderName.end()) {
- static std::unordered_set seen;
- static std::mutex m;
- std::lock_guard l(m);
- if (seen.insert(it->second).second) probef("vrserver creates %s%s", it->second.c_str(), g_shaderKinds.count(h) ? " <-- AN ENVIRONMENT SHADER (home could be drawn here)" : "");
- }
- if (total.load() == 1 || total.load() == 300) probef("vrserver has created %d pixel shaders", total.load());
- }
- return hr;
-}
-
-void STDMETHODCALLTYPE PSSetShader_hook(ID3D11DeviceContext* ctx, ID3D11PixelShader* ps, ID3D11ClassInstance* const* inst, UINT n) {
- if (ctx == g_context) g_boundPs = ps;
- { std::lock_guard l(g_shaderMutex); g_boundByCtx[ctx] = ps; } // per context (the environment uses a deferred one)
- if (CtxHooks* h = hooksFor(ctx)) h->psSetShader(ctx, ps, inst, n);
-}
-
-// Diagnostic: the first time each known pixel shader is drawn, note it and the context, so one headset session shows
-// exactly what the compositor draws the environment with (and whether it's our immediate context or another).
-std::unordered_set g_seenHere, g_seenOther;
-std::atomic g_seenCount{0};
-void noteDraw(ID3D11DeviceContext* ctx) {
- static std::atomic calls{0};
- if (g_off || calls.fetch_add(1, std::memory_order_relaxed) > 500000 || g_seenCount.load(std::memory_order_relaxed) >= 200) return; // a few seconds of draws, then nothing
- std::string name;
- {
- std::lock_guard l(g_shaderMutex);
- auto b = g_boundByCtx.find(ctx);
- if (b == g_boundByCtx.end() || !b->second) return;
- auto it = g_psName.find(b->second);
- if (it == g_psName.end()) return;
- auto& seen = ctx == g_context ? g_seenHere : g_seenOther;
- if (!seen.insert(it->second).second) return;
- name = it->second;
- ++g_seenCount;
- }
- logf("compositor draws %s%s", name.c_str(), ctx == g_context ? "" : " (on another context, not the one we track)");
-}
-
-// The pixel shader bound on this context, and what it means for us (any context the device made, not just immediate).
-Kind classify(ID3D11DeviceContext* ctx, ID3D11PixelShader*& bound) {
- bound = nullptr;
- if (g_off) return Kind::None;
- std::lock_guard l(g_shaderMutex);
- auto b = g_boundByCtx.find(ctx);
- if (b == g_boundByCtx.end() || !b->second) return Kind::None;
- bound = b->second;
- auto it = g_psKind.find(bound);
- return it == g_psKind.end() ? Kind::None : it->second;
-}
-
-// Binds the home over the compositor's state for one draw (on this context, through its own vtable), then restores.
-struct HomeBind {
- ID3D11DeviceContext* ctx;
- CtxHooks* h;
- ID3D11PixelShader* restore;
- ID3D11ShaderResourceView* srv;
- HomeBind(ID3D11DeviceContext* c, CtxHooks* hooks, int slot, ID3D11PixelShader* r) : ctx(c), h(hooks), restore(r), srv(g_tex[slot].srv) {
- h->psSetShader(ctx, g_homePs, nullptr, 0);
- ctx->PSSetShaderResources(kSrvSlot, 1, &srv);
- ctx->PSSetConstantBuffers(kCbSlot, 1, &g_homeCb);
- ctx->PSSetSamplers(kSampSlot, 1, &g_sampler);
- }
- ~HomeBind() {
- h->psSetShader(ctx, restore, nullptr, 0); // the compositor's aurora shader back, so our tracking holds
- healthy();
- }
-};
-
-// Runs the compositor's draw, swapping the home in for an Aurora sky draw or dropping an Aurora floor/stars draw.
-// The texture lock is held across a replaced draw so the pipe thread can't free the texture under it.
-template
-void replaceDraw(ID3D11DeviceContext* ctx, CtxHooks* h, Draw&& draw) {
- (ctx == g_context ? g_drawsHere : g_drawsOther).fetch_add(1, std::memory_order_relaxed); // every draw we see
- noteDraw(ctx); // diagnostic: what the compositor draws, and on which context
- ID3D11PixelShader* bound = nullptr;
- const Kind k = classify(ctx, bound);
- if (k == Kind::None) { draw(); return; }
- if (k == Kind::Skip) { g_skipped.fetch_add(1, std::memory_order_relaxed); return; }
- std::unique_lock lock(g_texMutex);
- const int slot = frameSlot();
- if (slot < 0 || !ensureResources() || slot >= static_cast(g_tex.size()) || !g_tex[slot].srv) { lock.unlock(); draw(); return; }
- fillCb(ctx, slot);
- if (!g_latchStamped) { g_latchStamped = true; g_state->sampledNs.store(g_latchNs); }
- g_replaced.fetch_add(1, std::memory_order_relaxed);
- arm(); // only now (we actually swap Aurora) does a compositor crash without a healthy frame count as ours
- HomeBind b(ctx, h, slot, bound);
- draw();
-}
-
-void STDMETHODCALLTYPE Draw_hook(ID3D11DeviceContext* ctx, UINT n, UINT start) {
- CtxHooks* h = hooksFor(ctx);
- if (!h) return;
- replaceDraw(ctx, h, [&] { h->draw(ctx, n, start); });
-}
-void STDMETHODCALLTYPE DrawIndexed_hook(ID3D11DeviceContext* ctx, UINT n, UINT start, INT base) {
- CtxHooks* h = hooksFor(ctx);
- if (!h) return;
- replaceDraw(ctx, h, [&] { h->drawIndexed(ctx, n, start, base); });
-}
-void STDMETHODCALLTYPE DrawInstanced_hook(ID3D11DeviceContext* ctx, UINT vpi, UINT inst, UINT sv, UINT si) {
- CtxHooks* h = hooksFor(ctx);
- if (!h) return;
- replaceDraw(ctx, h, [&] { h->drawInstanced(ctx, vpi, inst, sv, si); });
-}
-void STDMETHODCALLTYPE DrawIndexedInstanced_hook(ID3D11DeviceContext* ctx, UINT ipi, UINT inst, UINT si, INT bv, UINT sinst) {
- CtxHooks* h = hooksFor(ctx);
- if (!h) return;
- replaceDraw(ctx, h, [&] { h->drawIndexedInstanced(ctx, ipi, inst, si, bv, sinst); });
-}
-
-// Swaps one vtable entry for our hook, keeping the original. (All objects of a concrete class share one vtable, so
-// hooking the device's vtable, and each context vtable, covers every such object in the process.)
-template
-void hookSlot(void* obj, int index, void* hook, T* real) {
- void** vtable = *reinterpret_cast(obj);
- DWORD old = 0;
- if (VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &old)) {
- *real = reinterpret_cast(vtable[index]);
- vtable[index] = hook;
- VirtualProtect(&vtable[index], sizeof(void*), old, &old);
- }
-}
-void patchSlot(void** vtable, int index, void* hook) {
- DWORD old = 0;
- if (VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &old)) {
- vtable[index] = hook;
- VirtualProtect(&vtable[index], sizeof(void*), old, &old);
- }
-}
-
-// Hooks a context's vtable once (the immediate one, or a deferred one as the device makes it): remembers its own
-// originals, publishes the entry, then redirects PSSetShader and the four draws to our hooks.
-void hookContext(ID3D11DeviceContext* ctx) {
- void* vt = *reinterpret_cast(ctx);
- std::lock_guard l(g_hookMutex);
- for (int i = 0; i < g_ctxHookCount.load(); ++i) if (g_ctxHooks[i].vtbl == vt) return;
- const int idx = g_ctxHookCount.load();
- if (idx >= 8) return;
- CtxHooks& h = g_ctxHooks[idx];
- void** vtable = reinterpret_cast(vt);
- h.vtbl = vt;
- h.psSetShader = reinterpret_cast(vtable[9]);
- h.drawIndexed = reinterpret_cast(vtable[12]);
- h.draw = reinterpret_cast(vtable[13]);
- h.drawIndexedInstanced = reinterpret_cast(vtable[20]);
- h.drawInstanced = reinterpret_cast(vtable[21]);
- g_ctxHookCount.store(idx + 1, std::memory_order_release); // findable before the slots point at our hooks
- patchSlot(vtable, 9, reinterpret_cast(PSSetShader_hook));
- patchSlot(vtable, 12, reinterpret_cast(DrawIndexed_hook));
- patchSlot(vtable, 13, reinterpret_cast(Draw_hook));
- patchSlot(vtable, 20, reinterpret_cast(DrawIndexedInstanced_hook));
- patchSlot(vtable, 21, reinterpret_cast(DrawInstanced_hook));
- logf("hooked context vtable #%d (draws on it are seen now)", idx + 1);
-}
-
-HRESULT STDMETHODCALLTYPE CreateDeferredContext_hook(ID3D11Device* dev, UINT flags, ID3D11DeviceContext** out) {
- const HRESULT hr = g_realCreateDeferredContext(dev, flags, out);
- if (SUCCEEDED(hr) && out && *out) { g_deferredCount.fetch_add(1); logf("CreateDeferredContext: device %p (%s)", (void*)dev, dev == g_device ? "ours" : "another"); if (dev == g_device) hookContext(*out); }
- return hr;
-}
-HRESULT STDMETHODCALLTYPE CreateDeferredContext1_hook(ID3D11Device1* dev, UINT flags, ID3D11DeviceContext1** out) {
- const HRESULT hr = g_realCreateDeferredContext1(dev, flags, out);
- if (SUCCEEDED(hr) && out && *out) { g_deferredCount.fetch_add(1); logf("CreateDeferredContext1: device %p (%s)", (void*)dev, reinterpret_cast(dev) == g_device ? "ours" : "another"); if (reinterpret_cast(dev) == g_device) hookContext(*out); }
- return hr;
-}
-
-// ── the hand-over pipe: Frame Home asks for textures, we make them and share them ──────────────────
-
-void destroyTextures() {
- for (SharedTex& t : g_tex) {
- if (t.srv) t.srv->Release();
- if (t.tex) t.tex->Release();
- if (t.handle) CloseHandle(t.handle);
- }
- g_tex.clear();
- g_lastGood = -1;
-}
-
-bool makeTextures(uint32_t w, uint32_t h) {
- destroyTextures();
- g_tex.resize(envshare::kSlots);
- for (uint32_t i = 0; i < envshare::kSlots; ++i) {
- D3D11_TEXTURE2D_DESC td = {};
- td.Width = w;
- td.Height = h;
- td.MipLevels = 1;
- td.ArraySize = 2; // one layer per eye
- td.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
- td.SampleDesc.Count = 1;
- td.Usage = D3D11_USAGE_DEFAULT;
- td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
- td.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED;
- if (FAILED(g_device->CreateTexture2D(&td, nullptr, &g_tex[i].tex))) { logf("shared texture %u failed", i); destroyTextures(); return false; }
- D3D11_SHADER_RESOURCE_VIEW_DESC vd = {};
- vd.Format = td.Format;
- vd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
- vd.Texture2DArray.MipLevels = 1;
- vd.Texture2DArray.ArraySize = 2;
- if (FAILED(g_device->CreateShaderResourceView(g_tex[i].tex, &vd, &g_tex[i].srv))) { logf("shared srv %u failed", i); destroyTextures(); return false; }
- IDXGIResource1* res = nullptr;
- if (FAILED(g_tex[i].tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast(&res))) ||
- FAILED(res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, nullptr, &g_tex[i].handle))) {
- if (res) res->Release();
- logf("shared handle %u failed", i);
- destroyTextures();
- return false;
- }
- res->Release();
- }
- g_texW = w;
- g_texH = h;
- return true;
-}
-
-void adapterLuid(uint8_t out[8]) {
- std::memset(out, 0, 8);
- IDXGIDevice* dxgi = nullptr;
- IDXGIAdapter* adapter = nullptr;
- if (SUCCEEDED(g_device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast(&dxgi))) && SUCCEEDED(dxgi->GetAdapter(&adapter))) {
- DXGI_ADAPTER_DESC d;
- if (SUCCEEDED(adapter->GetDesc(&d))) std::memcpy(out, &d.AdapterLuid, 8);
- }
- if (adapter) adapter->Release();
- if (dxgi) dxgi->Release();
-}
-
-void handleRequest(HANDLE pipe, DWORD clientDefaultPid) {
- envshare::WinRequest req = {};
- DWORD got = 0;
- if (!ReadFile(pipe, &req, sizeof req, &got, nullptr) || got != sizeof req || req.magic != envshare::kMagic) return;
- envshare::WinReply reply = {};
- reply.magic = envshare::kMagic;
- reply.generation = req.generation;
-
- std::lock_guard l(g_texMutex); // the draw path reads g_state and g_tex under this
- // Frame Home's shared State: the handle it duplicated into us. Map it (a new mapping replaces an old one).
- if (req.state) {
- if (g_state) { UnmapViewOfFile(g_state); g_state = nullptr; }
- if (g_stateMap) { CloseHandle(g_stateMap); g_stateMap = nullptr; }
- g_stateMap = reinterpret_cast(static_cast(req.state));
- g_state = static_cast(MapViewOfFile(g_stateMap, FILE_MAP_ALL_ACCESS, 0, 0, req.stateSize ? req.stateSize : sizeof(envshare::State)));
- if (!g_state) { logf("could not map Frame Home's state"); }
- }
- if (g_device && g_state && makeTextures(req.width, req.height)) {
- g_generation = req.generation;
- g_lastGood = -1;
- DWORD pid = req.pid ? req.pid : clientDefaultPid;
- HANDLE client = OpenProcess(PROCESS_DUP_HANDLE, FALSE, pid);
- bool ok = client != nullptr;
- for (uint32_t i = 0; i < envshare::kSlots && ok; ++i) {
- HANDLE dup = nullptr;
- ok = DuplicateHandle(GetCurrentProcess(), g_tex[i].handle, client, &dup, 0, FALSE, DUPLICATE_SAME_ACCESS);
- reply.texture[i] = reinterpret_cast(dup);
- }
- if (client) CloseHandle(client);
- if (ok) {
- adapterLuid(reply.deviceLUID);
- reply.ok = 1;
- logf("Frame Home connected: %ux%u, generation %u", req.width, req.height, req.generation);
- } else {
- logf("could not share the textures with Frame Home (pid %lu)", pid);
- }
- }
- DWORD wrote = 0;
- WriteFile(pipe, &reply, sizeof reply, &wrote, nullptr);
- FlushFileBuffers(pipe);
-}
-
-void serve() {
- g_connected = false;
- for (;;) {
- if (fileExists(layerDir() / "off")) { g_off = true; return; }
- HANDLE pipe = CreateNamedPipeA(envshare::kPipeName, PIPE_ACCESS_DUPLEX,
- PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1,
- sizeof(envshare::WinReply) + 64, sizeof(envshare::WinRequest) + 64, 0, nullptr);
- if (pipe == INVALID_HANDLE_VALUE) { std::this_thread::sleep_for(std::chrono::seconds(1)); continue; }
- if (ConnectNamedPipe(pipe, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED) {
- ULONG pid = 0;
- GetNamedPipeClientProcessId(pipe, &pid);
- g_connected = true;
- handleRequest(pipe, pid);
- // Hold the connection: when it breaks, Frame Home has gone and we drop to Aurora.
- char b;
- DWORD n;
- while (ReadFile(pipe, &b, 0, &n, nullptr) || GetLastError() == ERROR_MORE_DATA) {
- if (fileExists(layerDir() / "off")) break;
- }
- g_connected = false;
- }
- DisconnectNamedPipe(pipe);
- CloseHandle(pipe);
- }
-}
-
-// Watches the off switch each second (turn Frame Home off/on without restarting SteamVR).
-void watchOff() {
- uint64_t sec = 0;
- for (;;) {
- std::this_thread::sleep_for(std::chrono::seconds(1));
- const bool off = fileExists(layerDir() / "off");
- if (off != g_off.load()) { g_off = off; logf(off ? "turned off: Aurora as usual" : "turned back on"); }
- // A heartbeat (every 5 s) of what the layer is doing, for testing without a headset to look through.
- if (++sec % 5 == 0) {
- const uint64_t r = g_replaced.exchange(0), s = g_skipped.exchange(0);
- const uint64_t dh = g_drawsHere.exchange(0), dohter = g_drawsOther.exchange(0);
- size_t here, other;
- { std::lock_guard l(g_shaderMutex); here = g_seenHere.size(); other = g_seenOther.size(); }
- logf("heartbeat: replaced %llu, skipped %llu; draws seen here=%llu other=%llu (5 s); named shaders here=%zu other=%zu; devices=%d deferred=%d hookedCtx=%d; connected=%d",
- (unsigned long long)r, (unsigned long long)s, (unsigned long long)dh, (unsigned long long)dohter, here, other,
- g_deviceCount.load(), g_deferredCount.load(), g_ctxHookCount.load(), g_connected.load() ? 1 : 0);
- }
- }
-}
-
-void wrap(ID3D11Device* dev, ID3D11DeviceContext* ctx) {
- static std::once_flag once;
- std::call_once(once, [&] {
- g_device = dev;
- g_context = ctx;
- g_device->AddRef(); // our own long-lived references (the context's is the one GetImmediateContext gave us),
- // so the vtables we hook stay valid however the compositor manages its own copies
- startHealth();
- if (g_off) { logf("vrcompositor device wrapped, but Frame Home is off: Aurora as usual"); return; }
- scanAuroraShaders();
- hookSlot(dev, 15, reinterpret_cast(CreatePixelShader_hook), &g_realCreatePixelShader);
- hookSlot(dev, 27, reinterpret_cast(CreateDeferredContext_hook), &g_realCreateDeferredContext); // the environment's context
- ID3D11Device1* dev1 = nullptr; // CreateDeferredContext1 (slot 44) only if the device is a Device1
- if (SUCCEEDED(dev->QueryInterface(__uuidof(ID3D11Device1), reinterpret_cast(&dev1))) && dev1) {
- hookSlot(dev, 44, reinterpret_cast(CreateDeferredContext1_hook), &g_realCreateDeferredContext1);
- dev1->Release();
- }
- hookContext(ctx); // the immediate context (deferred ones are hooked as they're made)
- std::thread(serve).detach();
- std::thread(watchOff).detach();
- logf("vrcompositor device wrapped: Frame Home is the environment");
- });
-}
-
-// ── the real d3d11.dll and our D3D11CreateDevice ───────────────────────────────────────────────────
-
-using PFN_D3D11CreateDevice = HRESULT(WINAPI*)(IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT, const D3D_FEATURE_LEVEL*, UINT, UINT,
- ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**);
-PFN_D3D11CreateDevice realCreateDevice() {
- static PFN_D3D11CreateDevice f = [] {
- HMODULE m = LoadLibraryW(L"fh_d3d11real.dll"); // the real d3d11.dll, copied beside us by the installer
- return m ? reinterpret_cast(GetProcAddress(m, "D3D11CreateDevice")) : nullptr;
- }();
- return f;
-}
-
-} // namespace
-
-extern "C" HRESULT WINAPI D3D11CreateDevice( // exported by layer/d3d11_proxy.def
- IDXGIAdapter* adapter, D3D_DRIVER_TYPE driverType, HMODULE software, UINT flags,
- const D3D_FEATURE_LEVEL* levels, UINT numLevels, UINT sdk,
- ID3D11Device** device, D3D_FEATURE_LEVEL* level, ID3D11DeviceContext** context) {
- PFN_D3D11CreateDevice real = realCreateDevice();
- if (!real) return E_FAIL;
- // Exactly as the caller asked (no tampering with its device/context or their lifetimes). If it's the compositor
- // and it got a device, we take our own reference to the device and its immediate context (wrap() AddRefs them)
- // and keep them for the process — so hooking their vtables is safe however the compositor manages its copies.
- const HRESULT hr = real(adapter, driverType, software, flags, levels, numLevels, sdk, device, level, context);
- if (SUCCEEDED(hr) && isCompositor() && device && *device) {
- ID3D11DeviceContext* immediate = nullptr;
- (*device)->GetImmediateContext(&immediate); // +1 ref, kept by wrap()
- if (immediate) wrap(*device, immediate);
- }
- // Probe: the Steam Frame streams, and its home may be rendered by the vrlink driver in vrserver, not the
- // compositor. In vrserver, hook CreatePixelShader to see whether the environment's own shaders are made there
- // (if so, that's where to draw the home for a streamed headset). Diagnostic only — nothing is replaced here.
- if (SUCCEEDED(hr) && isVrServer() && device && *device) {
- static std::once_flag once;
- std::call_once(once, [&] {
- g_diagnostic = true;
- scanAuroraShaders();
- hookSlot(*device, 15, reinterpret_cast(CreatePixelShaderProbe_hook), &g_serverRealCreatePixelShader);
- probef("vrserver: watching for the environment's shaders (%zu aurora shaders known)", g_shaderKinds.size());
- });
- }
- return hr;
-}
-
-BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) {
- if (reason == DLL_PROCESS_ATTACH && isCompositor()) realCreateDevice(); // load the real one early
- return TRUE;
-}
diff --git a/pc-streaming/install-frame-home-pc.ps1 b/pc-streaming/install-frame-home-pc.ps1
deleted file mode 100644
index 908e0f4..0000000
--- a/pc-streaming/install-frame-home-pc.ps1
+++ /dev/null
@@ -1,122 +0,0 @@
-# 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 Aurora back
-#
-# 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, 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]
-$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)
-
-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
- [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 } }
-
-# 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'
-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)) {
- Save $before ([pscustomobject]@{
- enableHomeApp = [bool]$sv.enableHomeApp
- environmentMode = $(if ($sv.PSObject.Properties['environmentMode']) { [int]$sv.environmentMode } else { 1 })
- })
- }
- 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 })
- Remove-Item -Force -ErrorAction SilentlyContinue $before
- }
- 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' })
-}
-
-if ($Uninstall) {
- 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', 'd3d11.dll') {
- if (-not (Test-Path (Join-Path $here $x))) { Say "this installer has no $x"; exit 1 }
-}
-# 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 }
-
-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 $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 {
- & (Join-Path $bin 'frame-switcher.exe') --install | Out-Null
- PanelOffline $true
- Say "installed: your environment becomes SteamVR's when SteamVR next starts."
-}
-exit 0
diff --git a/pc-streaming/pc-steamvr.md b/pc-streaming/pc-steamvr.md
deleted file mode 100644
index aaf2c9f..0000000
--- a/pc-streaming/pc-steamvr.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Frame Home on a PC (SteamVR)
-
-Frame Home also runs on a PC with SteamVR, exactly the way it runs on the Steam Frame: a small layer inside SteamVR's
-compositor draws your home where it would draw Aurora, so your home *is* the SteamVR environment — it shows at boot,
-during game loads and behind the Steam menu, with the controllers and passthrough, never a separate app that launches
-and hands off. 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
-
-Both platforms work the Steam Frame's way: a layer inside SteamVR's compositor draws your home in Aurora's place, and
-`frame-home --system` (which the Environments panel starts and keeps running) renders it. Because the swap happens
-inside the compositor, your home shows wherever Aurora would — at boot, during game loads, behind the Steam menu,
-with the controllers and passthrough — and steps aside for a game the instant it opens, coming back when it quits.
-
-**Windows.** SteamVR's compositor is Direct3D 11, so the layer is a proxy `d3d11.dll` placed beside the compositor
-(`SteamVR\bin\win64`). It wraps the compositor's device, finds Aurora's own draws by their shader bytecode and draws
-the home over them; every other Direct3D call passes straight through to the real `d3d11.dll`, kept beside it as
-`fh_d3d11real.dll`. Frame Home renders in Vulkan and shares each frame to the compositor as a Direct3D 11 texture.
-
-**Linux.** The same, as a Vulkan layer in the compositor (`layer/frame_home_layer.cpp`), since the Linux compositor
-is Vulkan. (Linux PCs are supported but haven't been tested on a desktop yet.)
-
-If the layer ever can't draw the home — Frame Home isn't running, or has stopped for more than half a second — the
-compositor draws Aurora as usual, so Frame Home stopping or crashing never blanks the headset. The layer turns itself
-off if the compositor starts twice without a healthy frame, and `%LOCALAPPDATA%\Frame Home\layer\off` turns it off by
-hand. The installer saves the SteamVR settings it changes (`enableHomeApp`, `environmentMode`) and puts them back,
-and removing the two files from `SteamVR\bin\win64` takes the layer off entirely.
-
-## 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 (`frame-home.log` the renderer, `frame-layer.log` the compositor layer) |
-| `%LOCALAPPDATA%\Frame Home\layer` | the layer's off switch and crash guard; its folder marks the layer installed |
-| `Documents\Frame Studio\Environments` | the environments |
-| `SteamVR\bin\win64\d3d11.dll`, `fh_d3d11real.dll` | the compositor layer and the real Direct3D 11 beside it |
-
-On Linux everything is in `~/frame-home`, as on the headset.
-
-## Troubleshooting
-
-- **Aurora still shows after installing:** the layer loads when the compositor starts, so restart SteamVR (or your
- headset) once after installing. If it still shows, see `frame-layer.log`.
-- **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`.
-- **Turn it off without uninstalling:** make a file `%LOCALAPPDATA%\Frame Home\layer\off` (the layer checks every
- second and draws Aurora); delete it to turn the home back on.
diff --git a/scripts/build-studio.sh b/scripts/build-studio.sh
index 2ff5bae..c30a970 100755
--- a/scripts/build-studio.sh
+++ b/scripts/build-studio.sh
@@ -52,7 +52,7 @@ build() {
"$CMAKE" --build "$dir" --target frame-studio --parallel
}
-# Frame Home for SteamVR on the computer itself (Windows x64, Linux x64), built from the top-level project into
+# Frame Home for SteamVR on the computer itself (Linux x64), built from the top-level project into
# $BUILD/-home. $1 target, then extra cmake arguments.
build_pc_home() {
target=$1; shift
@@ -60,20 +60,15 @@ build_pc_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 $([ "$target" = windows-x64 ] && echo d3d11.dll) --parallel
+ "$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). Both
-# platforms the Steam Frame's way now — the compositor layer draws the home in Aurora's place. Windows: the layer is
-# a proxy d3d11.dll (install-frame-home-pc.ps1 puts it beside SteamVR's compositor). Linux: the Vulkan layer, with
-# the headset's own install.sh.
+# pc/ beside Studio: Frame Home for this computer's SteamVR and its installer (Studio's Headset tab runs it), the Steam
+# Frame's way — the Vulkan compositor layer draws the home in Aurora's place, installed with the headset's own
+# install.sh (Linux only).
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" "$home/d3d11.dll" "$stage/pc/"
- cp "$ROOT/pc-streaming/install-frame-home-pc.ps1" "$ROOT/pc-streaming/Install Frame Home on this PC.bat" \
- "$ROOT/pc-streaming/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" \
@@ -135,8 +130,7 @@ for t in $TARGETS; do
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 &&
- build_pc_home "$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 && package "$t" ;;
windows-x86) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=i686 && package "$t" ;;
windows-arm64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=aarch64 && package "$t" ;;
macos-arm64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 && package "$t" ;;
diff --git a/studio/studio.cpp b/studio/studio.cpp
index 8631872..cf2d88b 100644
--- a/studio/studio.cpp
+++ b/studio/studio.cpp
@@ -5354,17 +5354,15 @@ struct Studio {
}
// ── 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.
+ // pc/install.sh, Linux only). 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 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)
+#if defined(_WIN32) || defined(__APPLE__)
+ return false; // (Linux only: no SteamVR on macOS; no PC compositor layer on Windows)
#else
return fs::exists(pcPayload() / L"install.sh");
#endif
@@ -5385,12 +5383,7 @@ struct Studio {
pcBusy = true;
{ std::lock_guard 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