Remove the pc-streaming folder

Deletes pc-streaming/ (the experimental Windows D3D11 compositor proxy that
tried to make a home SteamVR's environment on a tethered PC, with its .def,
installer .ps1/.bat and notes) and everything that pointed at its files:

- CMakeLists: the frame-home-d3d11 proxy target.
- build-studio.sh: the Windows pc/ staging and the d3d11.dll build step.
- CI: the pc-home-windows job and the Windows Studio job's download/staging
  of FrameHome-PC-windows-x64.
- Studio: the Windows branch of the "This PC" installer (it ran the deleted
  .ps1); the feature is now Linux-only, where it installs the Vulkan layer.
- README and docs: the Windows PC download row, the PC-streaming highlight,
  and the dead Frame-Home-on-a-PC / pc-steamvr.md links.

FRAME_HOME_PC and the runtime OpenVR/Vulkan loader stay, so Frame Home still
builds for a PC; only the pc-streaming folder and its wiring are gone. The
Linux PC layer (layer/) and its install path are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 4.8 committed 2026-10-21 23:59:40 -05:00
1 parent 64ad00fa1d
commit c1ff887477
14 files changed
+13 -1165

No files matched your search

+1 -39
View File
@@ -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
-10
View File
@@ -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)
-8
View File
@@ -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 |
-1
View File
@@ -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 |
+2 -2
View File
@@ -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@<address>`.
See [headset-setup.md](headset-setup.md) and [install-frame-home.md](install-frame-home.md).
@@ -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
-35
View File
@@ -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.
@@ -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
-57
View File
@@ -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
-786
View File
@@ -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 <windows.h>
#include <d3d11.h>
#include <d3d11_1.h>
#include <dxgi1_2.h>
#include <atomic>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#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<std::mutex> 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<std::mutex> 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<char> readFile(const fs::path& p) {
std::ifstream in(p, std::ios::binary);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}
uint64_t fnv(const void* data, size_t n) {
uint64_t h = 1469598103934665603ull;
const unsigned char* b = static_cast<const unsigned char*>(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<uint64_t, Kind> g_shaderKinds; // bytecode hash -> what to do
std::unordered_map<uint64_t, std::string> 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<char> 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<bool> g_off{false};
std::atomic<bool> g_connected{false};
std::atomic<uint64_t> g_replaced{0}, g_skipped{0}; // draws swapped for the home / left undrawn (a diagnostic)
std::atomic<uint64_t> g_drawsHere{0}, g_drawsOther{0}; // every draw on a hooked context (any shader), for diagnosis
std::atomic<int> 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<SharedTex> 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<ID3D11PixelShader*, std::string> g_psName; // every recognised shader's .fxo name (diagnostic)
std::unordered_map<ID3D11DeviceContext*, ID3D11PixelShader*> 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<int>(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<bool> 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<char> 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<float4> 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<Fn>(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<const char*>(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<int> g_ctxHookCount{0};
std::mutex g_hookMutex;
CtxHooks* hooksFor(void* ctx) {
void* vt = *reinterpret_cast<void**>(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<ID3D11PixelShader*, Kind> 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<std::mutex> 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<void*> devices; // how many distinct devices the compositor makes shaders on
if (devices.insert(dev).second) { g_deviceCount.store(static_cast<int>(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<int> 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<std::string> seen;
static std::mutex m;
std::lock_guard<std::mutex> 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<std::mutex> 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<std::string> g_seenHere, g_seenOther;
std::atomic<int> g_seenCount{0};
void noteDraw(ID3D11DeviceContext* ctx) {
static std::atomic<uint64_t> 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<std::mutex> 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<std::mutex> 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 <typename Draw>
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<std::mutex> lock(g_texMutex);
const int slot = frameSlot();
if (slot < 0 || !ensureResources() || slot >= static_cast<int>(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 <typename T>
void hookSlot(void* obj, int index, void* hook, T* real) {
void** vtable = *reinterpret_cast<void***>(obj);
DWORD old = 0;
if (VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &old)) {
*real = reinterpret_cast<T>(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<void**>(ctx);
std::lock_guard<std::mutex> 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<void**>(vt);
h.vtbl = vt;
h.psSetShader = reinterpret_cast<PFN_PSSetShader>(vtable[9]);
h.drawIndexed = reinterpret_cast<PFN_DrawIndexed>(vtable[12]);
h.draw = reinterpret_cast<PFN_Draw>(vtable[13]);
h.drawIndexedInstanced = reinterpret_cast<PFN_DrawIndexedInstanced>(vtable[20]);
h.drawInstanced = reinterpret_cast<PFN_DrawInstanced>(vtable[21]);
g_ctxHookCount.store(idx + 1, std::memory_order_release); // findable before the slots point at our hooks
patchSlot(vtable, 9, reinterpret_cast<void*>(PSSetShader_hook));
patchSlot(vtable, 12, reinterpret_cast<void*>(DrawIndexed_hook));
patchSlot(vtable, 13, reinterpret_cast<void*>(Draw_hook));
patchSlot(vtable, 20, reinterpret_cast<void*>(DrawIndexedInstanced_hook));
patchSlot(vtable, 21, reinterpret_cast<void*>(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<ID3D11Device*>(dev) == g_device ? "ours" : "another"); if (reinterpret_cast<ID3D11Device*>(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<void**>(&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<void**>(&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<std::mutex> 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<HANDLE>(static_cast<uintptr_t>(req.state));
g_state = static_cast<envshare::State*>(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<uint64_t>(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<std::mutex> 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<void*>(CreatePixelShader_hook), &g_realCreatePixelShader);
hookSlot(dev, 27, reinterpret_cast<void*>(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<void**>(&dev1))) && dev1) {
hookSlot(dev, 44, reinterpret_cast<void*>(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<PFN_D3D11CreateDevice>(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<void*>(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;
}
-122
View File
@@ -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
-72
View File
@@ -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.
+6 -12
View File
@@ -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/<target>-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" ;;
+4 -11
View File
@@ -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<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)
#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<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