Frame Home runs on SteamVR on a Windows PC (tested on SteamVR's virtual headset)

- vr_compat.h: GetProjectionMatrix / GetEyeToHeadTransform called with Microsoft's member ABI on MinGW builds
  (SteamVR is built with MSVC; the hidden return pointer goes after this): the first frame crashed there.
- When SteamVR asks it to quit (a game, room setup), Frame Home leaves at once instead of waiting for a load (SteamVR
  killed it after 5 s).
- The panel keeps SteamVR's home app running on a PC: SteamVR doesn't start it again after it stopped on its own.
- Logs on Windows: the environment and the panel always write logs/frame-home.log and logs/frame-switcher.log (SteamVR
  gives them a hidden console); the panel logs to a file on the headset too now. A crash writes the exception and the
  stack's return addresses there.
- Tested on SteamVR's null driver: Frame Home started by SteamVR as its home app, the panel with it, SteamVR's
  background made from the home, a crash recovered in seconds, 0.79 ms GPU at 1976x1976 per eye with 4x MSAA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 5.5 committed 2026-10-21 15:45:28 -05:00
1 parent 1863e495d2
commit 4d058a9e7a
4 files changed
+89 -7

No files matched your search

+30 -1
View File
@@ -66,7 +66,9 @@ inline std::filesystem::path selfPath() {
const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast<DWORD>(std::size(buf)));
return n ? std::filesystem::path(buf) : std::filesystem::path();
}
inline bool stderrIsTerminal() { return _isatty(_fileno(stderr)) != 0; }
// Is somebody reading stderr? On Windows a program SteamVR starts gets a hidden console (or NUL), indistinguishable
// from a terminal: the modes SteamVR runs (the environment, the panel) always log to their files there.
inline bool stderrIsTerminal() { return false; }
inline long peakRamMB() {
PROCESS_MEMORY_COUNTERS pmc = {};
return GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc) ? static_cast<long>(pmc.PeakWorkingSetSize / (1024 * 1024)) : 0;
@@ -266,6 +268,33 @@ inline int run(const std::vector<std::string>& args, int nice, bool wait) {
inline void sleepMs(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
// A crash, said in the log (stderr): the exception, where, and the return addresses on the stack (each an address in
// this program: tools/symbolize or `nm` maps them to functions). Windows only: Linux has its own (coredumps, the
// headset's journal).
#ifdef _WIN32
inline LONG WINAPI logCrash(EXCEPTION_POINTERS* ep) {
const HMODULE self = GetModuleHandleW(nullptr);
const uintptr_t base = reinterpret_cast<uintptr_t>(self);
std::fprintf(stderr, "[crash] exception 0x%08lx at 0x%llx (this program loads at 0x%llx)\n", ep->ExceptionRecord->ExceptionCode,
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(ep->ExceptionRecord->ExceptionAddress)), static_cast<unsigned long long>(base));
const uintptr_t* sp = reinterpret_cast<const uintptr_t*>(ep->ContextRecord->Rsp);
MODULEINFO mi = {};
GetModuleInformation(GetCurrentProcess(), self, &mi, sizeof mi);
const uintptr_t end = base + mi.SizeOfImage;
int shown = 0;
for (int k = 0; k < 2048 && shown < 24; ++k) { // words on the stack that point into this program's code
uintptr_t v = 0;
if (!ReadProcessMemory(GetCurrentProcess(), sp + k, &v, sizeof v, nullptr)) break;
if (v > base && v < end) { std::fprintf(stderr, "[crash] stack+%d: +0x%llx\n", k * 8, static_cast<unsigned long long>(v - base)); ++shown; }
}
std::fflush(stderr);
return EXCEPTION_CONTINUE_SEARCH;
}
inline void logCrashes() { SetUnhandledExceptionFilter(logCrash); }
#else
inline void logCrashes() {}
#endif
// Is SteamVR running (its server)? Asked before connecting as a utility: on Windows, connecting starts SteamVR.
inline bool steamVrRunning() {
#ifdef _WIN32
+15 -5
View File
@@ -24,6 +24,7 @@
#include "thermal.h"
#include "fenv.h"
#include "home_platform.h"
#include "vr_compat.h"
#include "stb_image_write.h"
#include <openvr.h>
@@ -935,7 +936,7 @@ VkPhysicalDevice pickVrDevice(VkInstance instance) {
// widen > 1: a wider view than the lens shows (system mode: margin for the compositor's rotation reprojection).
Mat4 eyeProjection(vr::EVREye eye, float widen = 1.0f) {
// Tangents from OpenVR's own projection (GL style: row 3 = [0 0 -1 0], y up).
vr::HmdMatrix44_t p = vr::VRSystem()->GetProjectionMatrix(eye, kNear, 100.0f);
vr::HmdMatrix44_t p = vrcompat::projectionMatrix(eye, kNear, 100.0f);
const float l = (p.m[0][2] - 1) / p.m[0][0], r = (p.m[0][2] + 1) / p.m[0][0];
const float b = (p.m[1][2] - 1) / p.m[1][1], t = (p.m[1][2] + 1) / p.m[1][1];
return projection(l * widen, r * widen, b * widen, t * widen);
@@ -1154,8 +1155,8 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
float lastHeadR[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
double widenSum = 0;
uint64_t widenN = 0;
const Mat4 eyeToHead[2] = {fromHmd34(vr::VRSystem()->GetEyeToHeadTransform(vr::Eye_Left)),
fromHmd34(vr::VRSystem()->GetEyeToHeadTransform(vr::Eye_Right))};
const Mat4 eyeToHead[2] = {fromHmd34(vrcompat::eyeToHead(vr::Eye_Left)),
fromHmd34(vrcompat::eyeToHead(vr::Eye_Right))};
auto start = std::chrono::steady_clock::now(); // the shader clock (Animations off holds it)
const auto loopEpoch = start;
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
@@ -1180,7 +1181,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
float gazeSmooth[2][2] = {{-1, -1}, {-1, -1}}; // pixels: restarted when the resolution changes
uint32_t gazeW = 0;
uint64_t gpuSeen = 0;
bool restart = false, lostDevice = false;
bool restart = false, lostDevice = false, quitAsked = false;
float animTime = 0, animLength = 60;
auto animTick = std::chrono::steady_clock::now();
SystemPacer pacer; // system mode: frames timed to the compositor's
@@ -1250,7 +1251,7 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
while (running) {
vr::VREvent_t ev;
while (vr::VRSystem()->PollNextEvent(&ev, sizeof(ev)))
if (ev.eventType == vr::VREvent_Quit) { vr::VRSystem()->AcknowledgeQuit_Exiting(); running = false; }
if (ev.eventType == vr::VREvent_Quit) { vr::VRSystem()->AcknowledgeQuit_Exiting(); running = false; quitAsked = true; }
if (!running) break;
// A parsed home goes to the GPU a few milliseconds a frame (system mode: once faded to black), so frames keep
@@ -1798,6 +1799,14 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
if (++frames == 1) LOG("first frame submitted");
if (frames % 5400 == 0) LOG("%llu frames", static_cast<unsigned long long>(frames)); // alive (about once a minute)
}
if (quitAsked && !restart) { // SteamVR wants the scene for a game (or room setup) now, and kills a home that takes
// more than 5 s: off SteamVR at once, no waiting for a load under way; the system frees
// the rest.
vr::VR_Shutdown();
LOG("quit: SteamVR asked for the scene");
std::fflush(stderr);
std::_Exit(0);
}
if (worker.joinable()) worker.join();
if (soundJob.joinable()) soundJob.join();
publisher.finish();
@@ -1974,6 +1983,7 @@ int runInstall(bool install) {
} // namespace
int main(int argc, char** argv) {
homeos::logCrashes();
#ifdef FH_USE_VOLK
if (volkInitialize() != VK_SUCCESS) { std::fprintf(stderr, "frame-home: no Vulkan on this system\n"); return 1; }
#endif
+14 -1
View File
@@ -1489,6 +1489,11 @@ void prepareEnvironments() {
}
int runPanel() {
if (!homeos::stderrIsTerminal()) { // started by SteamVR: messages go to logs/frame-switcher.log
std::error_code ec;
fs::create_directories(rootDir() / "logs", ec);
if (homeos::reopenStderr(rootDir() / "logs" / "frame-switcher.log", "w")) setvbuf(stderr, nullptr, _IOLBF, 0);
}
signal(SIGINT, [](int) { g_quit = 1; });
signal(SIGTERM, [](int) { g_quit = 1; });
vr::EVRInitError ierr = vr::VRInitError_None;
@@ -1585,7 +1590,15 @@ int runPanel() {
bool adopted = false; // systemPid isn't our child (a panel before this one started it)
auto lastSpawn = std::chrono::steady_clock::now() - std::chrono::minutes(1);
auto keepSystem = [&](std::chrono::steady_clock::time_point now) {
if (!systemEnv) return; // SteamVR runs Frame Home itself (its home app)
if (!systemEnv) { // SteamVR's home app: SteamVR starts it, but not again after it stopped on its own (a crash):
// when nothing else is in VR, have it started (at most every 5 s)
if (!homeInstalled || now - lastSpawn < std::chrono::seconds(5) || !homeos::runningCopies("frame-home").empty()) return;
if (vr::VRApplications()->GetSceneApplicationState() != vr::EVRSceneApplicationState_None) return; // a game, or one starting / quitting
lastSpawn = now;
const vr::EVRApplicationError le = vr::VRApplications()->LaunchApplication("openvr.tool.steamvr_environments");
LOG("Frame Home wasn't running: started it (%s)", vr::VRApplications()->GetApplicationsErrorNameFromEnum(le));
return;
}
#ifndef _WIN32
if (systemPid > 0 && adopted) {
if (kill(systemPid, 0) == 0) return;
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// The OpenVR calls that return a struct, made the way SteamVR expects when Frame Home is built for Windows with
// MinGW (GCC or Clang). SteamVR is built with Microsoft's compiler, whose C++ ABI passes a member function's hidden
// return pointer after `this`; MinGW's (the Itanium ABI) passes it before. Called through the C++ interface, these
// jumped to garbage (a crash in the first frame). Here they go through the interface's vtable with Microsoft's order.
// Everywhere else (Linux, the Steam Frame, an MSVC build): the plain calls.
#include <openvr.h>
namespace vrcompat {
#if defined(_WIN32) && !defined(_MSC_VER)
// slot: the method's place in the interface's vtable, as openvr.h declares it for the version asked for (IVRSystem_026).
template <typename R, typename... A> R msvcMember(void* object, int slot, A... args) {
R result;
using Fn = R* (*)(void*, R*, A...); // x64: this, the return pointer, then the arguments, as Microsoft's members take them
(*reinterpret_cast<Fn* const*>(object))[slot](object, &result, args...);
return result;
}
inline vr::HmdMatrix44_t projectionMatrix(vr::EVREye eye, float nearZ, float farZ) {
return msvcMember<vr::HmdMatrix44_t>(vr::VRSystem(), 1, eye, nearZ, farZ); // IVRSystem::GetProjectionMatrix
}
inline vr::HmdMatrix34_t eyeToHead(vr::EVREye eye) {
return msvcMember<vr::HmdMatrix34_t>(vr::VRSystem(), 5, eye); // IVRSystem::GetEyeToHeadTransform
}
#else
inline vr::HmdMatrix44_t projectionMatrix(vr::EVREye eye, float nearZ, float farZ) { return vr::VRSystem()->GetProjectionMatrix(eye, nearZ, farZ); }
inline vr::HmdMatrix34_t eyeToHead(vr::EVREye eye) { return vr::VRSystem()->GetEyeToHeadTransform(eye); }
#endif
} // namespace vrcompat