From 4d058a9e7ad4a502e5cb3254016d2bd4277b89a5 Mon Sep 17 00:00:00 2001 From: Toastconcern Date: Wed, 21 Oct 2026 15:45:28 -0500 Subject: [PATCH] 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 --- src/home_platform.h | 31 ++++++++++++++++++++++++++++++- src/main.cpp | 20 +++++++++++++++----- src/switcher.cpp | 15 ++++++++++++++- src/vr_compat.h | 30 ++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/vr_compat.h diff --git a/src/home_platform.h b/src/home_platform.h index bd76ef6..38bc77b 100644 --- a/src/home_platform.h +++ b/src/home_platform.h @@ -66,7 +66,9 @@ inline std::filesystem::path selfPath() { const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast(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(pmc.PeakWorkingSetSize / (1024 * 1024)) : 0; @@ -266,6 +268,33 @@ inline int run(const std::vector& 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(self); + std::fprintf(stderr, "[crash] exception 0x%08lx at 0x%llx (this program loads at 0x%llx)\n", ep->ExceptionRecord->ExceptionCode, + static_cast(reinterpret_cast(ep->ExceptionRecord->ExceptionAddress)), static_cast(base)); + const uintptr_t* sp = reinterpret_cast(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(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 diff --git a/src/main.cpp b/src/main.cpp index c702bf6..8556b76 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,6 +24,7 @@ #include "thermal.h" #include "fenv.h" #include "home_platform.h" +#include "vr_compat.h" #include "stb_image_write.h" #include @@ -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& 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& 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& 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& apks, bool system = false) { if (++frames == 1) LOG("first frame submitted"); if (frames % 5400 == 0) LOG("%llu frames", static_cast(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 diff --git a/src/switcher.cpp b/src/switcher.cpp index 84ff428..9d66c09 100644 --- a/src/switcher.cpp +++ b/src/switcher.cpp @@ -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; diff --git a/src/vr_compat.h b/src/vr_compat.h new file mode 100644 index 0000000..bbd6cb6 --- /dev/null +++ b/src/vr_compat.h @@ -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 + +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 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(object))[slot](object, &result, args...); + return result; +} +inline vr::HmdMatrix44_t projectionMatrix(vr::EVREye eye, float nearZ, float farZ) { + return msvcMember(vr::VRSystem(), 1, eye, nearZ, farZ); // IVRSystem::GetProjectionMatrix +} +inline vr::HmdMatrix34_t eyeToHead(vr::EVREye eye) { + return msvcMember(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