Files
Toastconcern--FrameStudio/src/env_export.h
T
ToastconcernandClaude Opus 4.8 d22cc04cb3 Frame Home as the real SteamVR environment on a PC (Windows too), not a home app
On the Steam Frame a layer inside SteamVR's compositor draws the home where it would draw Aurora, so the home is the
environment itself: at boot, during game loads, behind the Steam menu, with the controllers and passthrough, and it
steps aside for a game the instant it opens. On a PC this was faked with SteamVR's home-app slot and a flat 360 photo
in the gaps, which launched and handed off like an app and showed a picture, not the live home. This does it properly
on any PC that runs SteamVR, the Frame's way:

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-21 19:39:36 -05:00

318 lines
14 KiB
C++

#pragma once
// Frame Home's side of the system environment (env_share.h): the shared state, the exported frame images and the
// connection to the compositor layer. frame-home --system renders into slot(), then publish() once the GPU is done.
#include "env_share.h"
#include "hsl_renderer.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <chrono>
#include <cstdio>
#include <cstring>
// Windows system mode: Frame Home renders the home into the compositor's own Direct3D 11 textures (see env_share.h).
// Frame Home owns the shared State (a file mapping) and hands the layer its handle; the layer owns the textures and
// hands Frame Home theirs. createImages() does that hand-over over the layer's named pipe and imports the textures
// into the renderer; after that it mirrors the Linux EnvExport.
class EnvExport {
public:
~EnvExport() {
closeTextures();
if (pipe_ != INVALID_HANDLE_VALUE) ::CloseHandle(pipe_);
if (state_) ::UnmapViewOfFile(state_);
if (stateMap_) ::CloseHandle(stateMap_);
}
bool init() {
stateMap_ = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(envshare::State), nullptr);
if (!stateMap_) return false;
void* p = ::MapViewOfFile(stateMap_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(envshare::State));
if (!p) return false;
state_ = new (p) envshare::State();
state_->show = 0;
return true;
}
envshare::State* state() { return state_; }
// (Re)negotiates the compositor's textures at the renderer's current size and imports them. Called at startup
// (where it waits a little for the layer to come up) and on every resize. True once the home can be drawn into
// the compositor's textures.
bool createImages(HslRenderer& r) {
r_ = &r;
const bool first = !everImported_;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(first ? 3 : 0);
do {
if (negotiate(r)) return true;
if (first) ::Sleep(200);
} while (first && std::chrono::steady_clock::now() < deadline);
return false;
}
bool connected() const { return pipe_ != INVALID_HANDLE_VALUE && imported_; }
// Keeps the hand-over alive: reconnects and re-imports after the compositor restarts (as the Linux connect()
// re-sends its Hello). Cheap when already connected.
bool connect() {
if (connected()) return true;
if (!r_) return false;
return negotiate(*r_);
}
// Is the layer still there? (the pipe breaks when the compositor goes away)
void poll() {
if (pipe_ == INVALID_HANDLE_VALUE) return;
DWORD avail = 0;
if (!::PeekNamedPipe(pipe_, nullptr, 0, nullptr, &avail, nullptr)) {
const DWORD e = ::GetLastError();
if (e == ERROR_BROKEN_PIPE || e == ERROR_PIPE_NOT_CONNECTED || e == ERROR_INVALID_HANDLE) {
std::fprintf(stderr, "[frame-home] the compositor went away: reconnecting\n");
dropPipe();
}
}
}
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
envshare::Slot& s = state_->slot[k];
s.frame = ++frame_;
s.generation = generation_;
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; }
state_->published.store(frame_);
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
state_->publishNs.store(envshare::nowNs());
}
uint32_t refusedFlags() const { return 0; } // (Windows frames carry no Vulkan create flags for the layer to refuse)
double sinceSampled() const {
const uint64_t s = state_->sampledNs.load();
return s ? (envshare::nowNs() - s) / 1e9 : 1e9;
}
void release(HslRenderer&) { dropPipe(); }
private:
void closeTextures() { for (HANDLE& h : texture_) if (h) { ::CloseHandle(h); h = nullptr; } }
void dropPipe() {
if (pipe_ != INVALID_HANDLE_VALUE) { ::CloseHandle(pipe_); pipe_ = INVALID_HANDLE_VALUE; }
imported_ = false;
}
// Opens the pipe if need be, asks the layer for textures at this size, imports them. Throttled to once a second
// while it can't connect. On success the renderer holds the imported images and connected() is true.
bool negotiate(HslRenderer& r) {
const auto now = std::chrono::steady_clock::now();
if (pipe_ == INVALID_HANDLE_VALUE) {
if (now - lastTry_ < std::chrono::seconds(1)) return false;
lastTry_ = now;
pipe_ = ::CreateFileA(envshare::kPipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe_ == INVALID_HANDLE_VALUE) {
static bool said = false;
if (!said) { said = true; std::fprintf(stderr, "[frame-home] waiting for the compositor layer (no pipe yet, error %lu)\n", ::GetLastError()); }
return false;
}
DWORD mode = PIPE_READMODE_MESSAGE;
::SetNamedPipeHandleState(pipe_, &mode, nullptr, nullptr);
}
// The State mapping, duplicated into the layer's (pipe server's) process so it can map it.
DWORD serverPid = 0;
HANDLE serverProc = nullptr, dupState = nullptr;
if (!::GetNamedPipeServerProcessId(pipe_, &serverPid) ||
!(serverProc = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE, serverPid)) ||
!::DuplicateHandle(::GetCurrentProcess(), stateMap_, serverProc, &dupState, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
if (serverProc) ::CloseHandle(serverProc);
dropPipe();
return false;
}
::CloseHandle(serverProc);
envshare::WinRequest req;
req.width = r.width();
req.height = r.height();
req.format = 87; // DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
req.pid = ::GetCurrentProcessId();
req.generation = generation_ + 1;
req.state = reinterpret_cast<uint64_t>(dupState);
req.stateSize = static_cast<uint32_t>(sizeof(envshare::State));
DWORD wrote = 0;
envshare::WinReply reply = {};
DWORD got = 0;
if (!::WriteFile(pipe_, &req, sizeof req, &wrote, nullptr) || wrote != sizeof req ||
!::ReadFile(pipe_, &reply, sizeof reply, &got, nullptr) || got != sizeof reply ||
reply.magic != envshare::kMagic || !reply.ok || reply.generation != req.generation) {
std::fprintf(stderr, "[frame-home] the layer didn't hand over textures (wrote %lu read %lu ok %u gen %u/%u)\n",
wrote, got, reply.ok, reply.generation, req.generation);
dropPipe();
return false; // (the layer now owns dupState: it closes it, or drops it when the pipe breaks)
}
// The compositor's adapter: Frame Home must be on it, or the import silently reads another GPU's memory.
if (const uint8_t* luid = r.deviceLUID(); std::memcmp(luid, reply.deviceLUID, 8) != 0) {
static bool said = false;
if (!said) { said = true; std::fprintf(stderr, "[frame-home] the compositor draws on a different GPU than Frame Home: the home can't be shared with it\n"); }
dropPipe();
return false;
}
closeTextures();
for (uint32_t i = 0; i < envshare::kSlots; ++i) texture_[i] = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(reply.texture[i]));
if (!r.importWin32Images(envshare::kSlots, reply.texture, req.width, req.height)) { dropPipe(); return false; }
generation_ = req.generation;
imported_ = everImported_ = true;
std::fprintf(stderr, "[frame-home] the compositor's textures are ours now: Frame Home is the environment (%ux%u per eye)\n", req.width, req.height);
return true;
}
envshare::State* state_ = nullptr;
HANDLE stateMap_ = nullptr;
HANDLE pipe_ = INVALID_HANDLE_VALUE;
HANDLE texture_[envshare::kSlots] = {};
HslRenderer* r_ = nullptr;
uint32_t next_ = 0, generation_ = 0;
uint64_t frame_ = 0;
bool imported_ = false, everImported_ = false;
std::chrono::steady_clock::time_point lastTry_;
};
#else
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <new>
#include <string>
#include <vector>
class EnvExport {
public:
~EnvExport() { disconnect(); if (state_) ::munmap(state_, sizeof(envshare::State)); if (stateFd_ >= 0) ::close(stateFd_); }
bool init() {
stateFd_ = memfd_create("framehome-env", MFD_CLOEXEC);
if (stateFd_ < 0 || ftruncate(stateFd_, sizeof(envshare::State)) != 0) return false;
void* p = ::mmap(nullptr, sizeof(envshare::State), PROT_READ | PROT_WRITE, MAP_SHARED, stateFd_, 0);
if (p == MAP_FAILED) return false;
state_ = new (p) envshare::State();
state_->show = 0;
return true;
}
envshare::State* state() { return state_; }
// The images go before the renderer's device does (after VR_Shutdown; exit and restart alike).
void release(HslRenderer& r) { disconnect(); r.destroyExportImages(images_); }
bool connected() const { return sock_ >= 0 && sent_; }
// (Re)creates the images at the renderer's size; they go to the layer on the next connect().
bool createImages(HslRenderer& r) {
r.exportSlot = -1;
if (!r.createExportImages(envshare::kSlots, images_, memoryType_, uuid_)) return false;
usage_ = r.exportUsage();
flags_ = r.exportCreateFlags();
w_ = r.width();
h_ = r.height();
++generation_; // the layer keeps showing the last frame of the old images until it has these
if (sent_) sent_ = false, lastTry_ = {};
return true;
}
// Connects to the layer (once a second at most) and sends the images; false while there's no layer.
bool connect() {
if (connected()) return true;
const auto now = std::chrono::steady_clock::now();
if (now - lastTry_ < std::chrono::seconds(1) || images_.empty()) return false;
lastTry_ = now;
if (sock_ < 0) {
const char* rt = std::getenv("XDG_RUNTIME_DIR");
const std::string path = std::string(rt && *rt ? rt : "/run/user/1000") + "/" + envshare::kSocketName;
sock_ = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
sockaddr_un a = {};
a.sun_family = AF_UNIX;
std::snprintf(a.sun_path, sizeof a.sun_path, "%s", path.c_str());
if (sock_ < 0 || ::connect(sock_, reinterpret_cast<sockaddr*>(&a), sizeof a) != 0) { disconnect(); return false; }
}
envshare::Hello h;
h.width = w_;
h.height = h_;
h.layers = 2;
h.format = 43; // VK_FORMAT_R8G8B8A8_SRGB
h.memoryType = memoryType_;
h.generation = generation_;
h.usage = usage_;
h.flags = flags_;
std::memcpy(h.deviceUUID, uuid_, 16);
int fds[envshare::kSlots + 1];
for (uint32_t i = 0; i < envshare::kSlots; ++i) { h.memorySize[i] = images_[i].size; fds[i] = images_[i].fd; }
fds[envshare::kSlots] = stateFd_;
iovec iov = {&h, sizeof h};
char ctrl[CMSG_SPACE(sizeof fds)] = {};
msghdr m = {};
m.msg_iov = &iov;
m.msg_iovlen = 1;
m.msg_control = ctrl;
m.msg_controllen = sizeof ctrl;
cmsghdr* cm = CMSG_FIRSTHDR(&m);
cm->cmsg_level = SOL_SOCKET;
cm->cmsg_type = SCM_RIGHTS;
cm->cmsg_len = CMSG_LEN(sizeof fds);
std::memcpy(CMSG_DATA(cm), fds, sizeof fds);
if (::sendmsg(sock_, &m, MSG_NOSIGNAL) != static_cast<ssize_t>(sizeof h)) { disconnect(); return false; }
sent_ = true;
std::fprintf(stderr, "[frame-home] connected to the compositor: Frame Home is the environment (%ux%u per eye)\n", w_, h_);
return true;
}
// Is the layer still there? (it closes the socket when the compositor goes away)
void poll() {
if (sock_ < 0) return;
char b;
const ssize_t n = ::recv(sock_, &b, 1, MSG_DONTWAIT);
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
std::fprintf(stderr, "[frame-home] the compositor went away: reconnecting\n");
disconnect();
}
}
// The slot to render the next frame into.
int beginFrame() { const int k = static_cast<int>(next_); next_ = (next_ + 1) % envshare::kSlots; return k; }
// The frame in slot k is done on the GPU: hand it to the layer with the eye poses it was rendered from.
void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) {
envshare::Slot& s = state_->slot[k];
s.frame = ++frame_;
s.generation = generation_;
std::memcpy(s.viewProj, viewProj, sizeof s.viewProj);
for (int e = 0; e < 2; ++e) { for (int k = 0; k < 3; ++k) s.eyePos[e][k] = eyePos[e][k]; s.eyePos[e][3] = 1; }
state_->published.store(frame_);
state_->latest.store(static_cast<uint32_t>(k), std::memory_order_release);
state_->publishNs.store(envshare::nowNs());
}
// The layer couldn't import the images with these create flags (0: none refused).
uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; }
// Seconds since the layer last drew the home (large while a game runs, or before the layer is there).
double sinceSampled() const {
const uint64_t s = state_->sampledNs.load();
return s ? (envshare::nowNs() - s) / 1e9 : 1e9;
}
private:
void disconnect() { if (sock_ >= 0) ::close(sock_); sock_ = -1; sent_ = false; }
envshare::State* state_ = nullptr;
int stateFd_ = -1, sock_ = -1;
bool sent_ = false;
std::vector<HslRenderer::ExportImage> images_;
uint32_t memoryType_ = 0, w_ = 0, h_ = 0, next_ = 0, usage_ = 0, flags_ = 0;
uint8_t uuid_[16] = {};
uint64_t frame_ = 0;
uint32_t generation_ = 0;
std::chrono::steady_clock::time_point lastTry_;
};
#endif // _WIN32