mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-29 04:02:56 +01:00
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>
This commit is contained in:
1 parent
283e19f87c
commit
d22cc04cb3
14 files changed
+1156
-117
No files matched your search
+167
-13
@@ -5,22 +5,176 @@
|
||||
#include "hsl_renderer.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// A PC has no compositor layer: Frame Home is SteamVR's scene app there, so system mode never starts (init() fails).
|
||||
#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:
|
||||
bool init() { return false; }
|
||||
envshare::State* state() { return &state_; }
|
||||
void release(HslRenderer&) {}
|
||||
bool connected() const { return false; }
|
||||
bool createImages(HslRenderer&) { return false; }
|
||||
bool connect() { return false; }
|
||||
void poll() {}
|
||||
int beginFrame() { return 0; }
|
||||
void publish(int, const float[2][16], const float[2][3]) {}
|
||||
uint32_t refusedFlags() const { return 0; }
|
||||
double sinceSampled() const { return 1e9; }
|
||||
~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:
|
||||
envshare::State state_;
|
||||
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>
|
||||
|
||||
+38
-4
@@ -8,9 +8,17 @@
|
||||
// published frame (rotation reprojection from the pose that frame was rendered with). No frame, or a stale one,
|
||||
// and the layer leaves Aurora alone: Frame Home stopping or crashing never blanks the headset.
|
||||
//
|
||||
// Hand-over: Frame Home connects to the layer's unix socket (kSocketName in $XDG_RUNTIME_DIR) and sends one Hello
|
||||
// with SCM_RIGHTS fds: kSlots image memories, then the shared State (a memfd). A new Hello (new size) replaces the
|
||||
// old images.
|
||||
// Hand-over, Linux: Frame Home connects to the layer's unix socket (kSocketName in $XDG_RUNTIME_DIR) and sends one
|
||||
// Hello with SCM_RIGHTS fds: kSlots image memories, then the shared State (a memfd). A new Hello (new size) replaces
|
||||
// the old images. Frame Home owns the images (Vulkan external memory, opaque fds) and the layer imports them.
|
||||
//
|
||||
// Hand-over, Windows: the compositor is Direct3D 11, which cannot open Vulkan's exported memory (only the reverse),
|
||||
// so the images' ownership flips while the State stays Frame Home's (as on Linux). Frame Home connects to the layer's
|
||||
// named pipe (kPipeName) and sends a WinRequest: the size and format it wants, and the shared State's file-mapping
|
||||
// handle duplicated into the layer's process. The layer, inside vrcompositor, creates kSlots shared D3D11 textures,
|
||||
// duplicates their NT handles into Frame Home's process, and replies with a WinReply carrying the handle values and
|
||||
// the device LUID. Frame Home imports the textures into Vulkan (VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT) and
|
||||
// renders the home into them. A new WinRequest (new size) makes the layer build a new generation and reply fresh.
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
@@ -19,13 +27,39 @@
|
||||
|
||||
namespace envshare {
|
||||
|
||||
constexpr const char* kSocketName = "framehome-env.sock";
|
||||
constexpr const char* kSocketName = "framehome-env.sock"; // Linux: in $XDG_RUNTIME_DIR
|
||||
constexpr const char* kPipeName = "\\\\.\\pipe\\framehome-env"; // Windows: the layer's named pipe
|
||||
constexpr const char* kStateMapName = "Local\\framehome-env-state"; // Windows: the shared State mapping (+ a
|
||||
// generation suffix, so a new one is distinct)
|
||||
constexpr uint32_t kMagic = 0x46484556; // "FHEV"
|
||||
constexpr uint32_t kVersion = 5; // 4: Hello says the images' usage (Frame Home draws straight into them)
|
||||
// 5: ...and their create flags (subsampled: the compositor smooths foveation)
|
||||
// Four: the compositor reads the newest while Frame Home publishes the one before it finished and renders the next.
|
||||
constexpr uint32_t kSlots = 4;
|
||||
|
||||
// No cross-API GPU lock on the shared textures (as on Linux, where the images carry none): Frame Home publishes a
|
||||
// slot only once the GPU has finished writing it, and the four slots rotate, so the layer always reads a slot that
|
||||
// Frame Home finished and isn't touching. A torn read would need the layer to read the very slot being written four
|
||||
// frames on, which the rotation prevents.
|
||||
//
|
||||
// Windows hand-over messages on the pipe (little-endian, fixed size).
|
||||
struct WinRequest {
|
||||
uint32_t magic = kMagic, version = kVersion;
|
||||
uint32_t width = 0, height = 0, layers = 2;
|
||||
uint32_t format = 0; // DXGI_FORMAT the textures should be (87 = R8G8B8A8_UNORM_SRGB)
|
||||
uint32_t pid = 0; // Frame Home's process id (the layer duplicates the texture handles into it)
|
||||
uint32_t generation = 0; // which set Frame Home is asking to (re)create
|
||||
uint64_t state = 0; // the shared State's file-mapping handle, duplicated into the layer's process
|
||||
uint32_t stateSize = 0; // sizeof(State) as Frame Home built it (the layer maps that much)
|
||||
};
|
||||
struct WinReply {
|
||||
uint32_t magic = kMagic, version = kVersion;
|
||||
uint32_t ok = 0; // 0: the layer couldn't make them (Frame Home keeps waiting)
|
||||
uint32_t generation = 0;
|
||||
uint64_t texture[kSlots] = {}; // NT handles (duplicated into Frame Home), as integers
|
||||
uint8_t deviceLUID[8] = {}; // the compositor's adapter: Frame Home must render on the same one
|
||||
};
|
||||
|
||||
struct Hello {
|
||||
uint32_t magic = kMagic, version = kVersion;
|
||||
uint32_t width = 0, height = 0, layers = 2;
|
||||
|
||||
@@ -142,6 +142,23 @@ inline int run(const std::vector<std::string>& args, int nice, bool wait) {
|
||||
CloseHandle(pi.hProcess);
|
||||
return static_cast<int>(code);
|
||||
}
|
||||
// Starts args[0] detached (its own process, no window) and returns its pid, or 0. appKey, when given, is set as
|
||||
// STEAMVR_APPKEY for the child so SteamVR takes it as that application (the environment renderer is its own identity).
|
||||
inline Pid spawn(const std::vector<std::string>& args, const char* appKey = nullptr) {
|
||||
std::wstring cl = commandLine(args);
|
||||
if (appKey) SetEnvironmentVariableA("STEAMVR_APPKEY", appKey);
|
||||
STARTUPINFOW si = {};
|
||||
si.cb = sizeof si;
|
||||
PROCESS_INFORMATION pi = {};
|
||||
const BOOL ok = CreateProcessW(nullptr, cl.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW | DETACHED_PROCESS, nullptr, nullptr, &si, &pi);
|
||||
if (appKey) SetEnvironmentVariableA("STEAMVR_APPKEY", nullptr);
|
||||
if (!ok) return 0;
|
||||
const Pid pid = pi.dwProcessId;
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return pid;
|
||||
}
|
||||
|
||||
// This program again with the same arguments (settings that build the renderer differently, a lost GPU); this one
|
||||
// ends. SteamVR knows its home app by its file, so the new one is still the environment.
|
||||
[[noreturn]] inline void restartSelf() {
|
||||
|
||||
@@ -806,6 +806,12 @@ bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
|
||||
if (astcHdr_) dext.push_back(VK_EXT_TEXTURE_COMPRESSION_ASTC_HDR_EXTENSION_NAME);
|
||||
if (fdmOn_) dext.push_back(VK_EXT_FRAGMENT_DENSITY_MAP_EXTENSION_NAME);
|
||||
if (fdmOffset_) dext.push_back(VK_QCOM_FRAGMENT_DENSITY_MAP_OFFSET_EXTENSION_NAME);
|
||||
#ifdef _WIN32
|
||||
// Windows system mode: import the compositor's Direct3D 11 textures to render the home into them.
|
||||
if (importWin32)
|
||||
for (const char* e : {"VK_KHR_external_memory", "VK_KHR_external_memory_win32"})
|
||||
if (std::none_of(dext.begin(), dext.end(), [&](const char* d) { return !std::strcmp(d, e); })) dext.push_back(e);
|
||||
#endif
|
||||
// FH_PERF: the GPU's counters, for the bench (see perfCounterAverages).
|
||||
std::vector<uint32_t> perfIdx;
|
||||
VkPhysicalDevicePerformanceQueryFeaturesKHR perfFeat = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PERFORMANCE_QUERY_FEATURES_KHR};
|
||||
@@ -866,6 +872,15 @@ bool HslRenderer::init(uint32_t eyeWidth, uint32_t eyeHeight) {
|
||||
dci.ppEnabledExtensionNames = dext.data();
|
||||
if (!VK_OK(vkCreateDevice(physical_, &dci, nullptr, &device_))) return false;
|
||||
vkGetDeviceQueue(device_, queueFamily_, 0, &queue_);
|
||||
#ifdef _WIN32
|
||||
if (importWin32) { // the adapter this device is on, so the compositor can check it's the one it draws with
|
||||
VkPhysicalDeviceIDProperties idp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 pp = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
||||
pp.pNext = &idp;
|
||||
vkGetPhysicalDeviceProperties2(physical_, &pp);
|
||||
if (idp.deviceLUIDValid) std::memcpy(deviceLUID_, idp.deviceLUID, 8);
|
||||
}
|
||||
#endif
|
||||
if (!perfIdx.empty()) {
|
||||
VkQueryPoolPerformanceCreateInfoKHR pc = {VK_STRUCTURE_TYPE_QUERY_POOL_PERFORMANCE_CREATE_INFO_KHR};
|
||||
pc.queueFamilyIndex = queueFamily_;
|
||||
@@ -4187,6 +4202,65 @@ bool HslRenderer::createExportImages(uint32_t n, std::vector<ExportImage>& out,
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows system mode: the compositor made kSlots shared Direct3D 11 textures (R8G8B8A8_UNORM_SRGB, `n` layers) at
|
||||
// this render size and duplicated their NT handles into us. Each becomes a VkImage we render the home into; render()
|
||||
// copies the eye frame into exportImages_[exportSlot] through the ordinary (non-direct) copy path. No cross-API lock:
|
||||
// a slot is published only once its frame is retired and the four rotate, as on Linux.
|
||||
bool HslRenderer::importWin32Images(uint32_t n, const uint64_t handles[], uint32_t w, uint32_t h) {
|
||||
vkDeviceWaitIdle(device_); // an earlier generation may still be in flight
|
||||
for (VkImage img : exportImages_) vkDestroyImage(device_, img, nullptr);
|
||||
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
||||
exportImages_.clear();
|
||||
importMem_.clear();
|
||||
exportInited_.clear();
|
||||
exportSlot = -1;
|
||||
exportDirect = false; // copied in (the imported textures aren't our render targets)
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
||||
ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
|
||||
VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
ci.pNext = &ext;
|
||||
ci.imageType = VK_IMAGE_TYPE_2D;
|
||||
ci.format = VK_FORMAT_R8G8B8A8_SRGB; // == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
|
||||
ci.extent = {w, h, 1};
|
||||
ci.mipLevels = 1;
|
||||
ci.arrayLayers = viewCount; // 2 layers, one per eye
|
||||
ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
if (vkCreateImage(device_, &ci, nullptr, &image) != VK_SUCCESS) { LOG("system mode: import image %u failed", i); return false; }
|
||||
VkMemoryRequirements req;
|
||||
vkGetImageMemoryRequirements(device_, image, &req);
|
||||
VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
||||
ded.image = image;
|
||||
VkImportMemoryWin32HandleInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR};
|
||||
imp.pNext = &ded;
|
||||
imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
|
||||
imp.handle = reinterpret_cast<void*>(static_cast<uintptr_t>(handles[i]));
|
||||
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
ai.pNext = &imp;
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
VkDeviceMemory mem = VK_NULL_HANDLE;
|
||||
if (vkAllocateMemory(device_, &ai, nullptr, &mem) != VK_SUCCESS || vkBindImageMemory(device_, image, mem, 0) != VK_SUCCESS) {
|
||||
LOG("system mode: import memory %u failed (adapter mismatch, or the compositor's textures went away)", i);
|
||||
if (mem) vkFreeMemory(device_, mem, nullptr);
|
||||
vkDestroyImage(device_, image, nullptr);
|
||||
return false;
|
||||
}
|
||||
exportImages_.push_back(image);
|
||||
importMem_.push_back(mem);
|
||||
}
|
||||
exportInited_.assign(n, false);
|
||||
LOG("system mode: imported %u compositor textures (%ux%u, %u layers)", n, w, h, viewCount);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t HslRenderer::exportUsage() const {
|
||||
return VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
||||
(exportDirect ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0u);
|
||||
@@ -4300,6 +4374,12 @@ void HslRenderer::shutdown() {
|
||||
vkDestroyFramebuffer(device_, framebuffer_, nullptr);
|
||||
vkDestroyRenderPass(device_, renderPass_, nullptr);
|
||||
if (exportPass_) vkDestroyRenderPass(device_, exportPass_, nullptr);
|
||||
#ifdef _WIN32
|
||||
for (VkImage img : exportImages_) vkDestroyImage(device_, img, nullptr); // imported compositor textures
|
||||
for (VkDeviceMemory m : importMem_) vkFreeMemory(device_, m, nullptr);
|
||||
exportImages_.clear();
|
||||
importMem_.clear();
|
||||
#endif
|
||||
if (pipelineCache_) {
|
||||
size_t n = 0;
|
||||
if (!pipelineCachePath.empty() && vkGetPipelineCacheData(device_, pipelineCache_, &n, nullptr) == VK_SUCCESS && n) {
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#include "shader_bake.h"
|
||||
#include "spv_reflect.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define VK_USE_PLATFORM_WIN32_KHR // the Win32 external-memory structs (system mode imports D3D11 textures)
|
||||
#endif
|
||||
#ifdef FH_USE_VOLK
|
||||
#include <volk.h> // Windows: Vulkan entry points loaded at runtime (no SDK needed)
|
||||
#else
|
||||
@@ -154,6 +157,15 @@ public:
|
||||
uint32_t exportUsage() const; // the export images' usage flags (the layer imports them alike)
|
||||
uint32_t exportCreateFlags() const; // ...and their VkImageCreateFlags (likewise)
|
||||
void destroyExportImages(std::vector<ExportImage>& images);
|
||||
#ifdef _WIN32
|
||||
// Windows system mode: the compositor (Direct3D 11) owns the shared textures and hands Frame Home their NT
|
||||
// handles (env_share.h). Frame Home imports each as a VkImage, and render() copies the eye frame into
|
||||
// exportImages_[exportSlot] exactly as the Linux copy path does. Set importWin32 before init() so the device is
|
||||
// created with VK_KHR_external_memory_win32.
|
||||
bool importWin32 = false;
|
||||
bool importWin32Images(uint32_t n, const uint64_t handles[], uint32_t width, uint32_t height);
|
||||
const uint8_t* deviceLUID() const { return deviceLUID_; } // the adapter this device is on (valid after init)
|
||||
#endif
|
||||
int exportSlot = -1;
|
||||
void waitFrame();
|
||||
// Frames counted on the GPU (a timeline semaphore, when the device has them): render() signals frameSerial() once
|
||||
@@ -384,6 +396,10 @@ private:
|
||||
void densityTexels(float cx, float cy, std::vector<float>& dens) const; // two per texel: across, down
|
||||
void packDensity(const std::vector<float>& dens, uint8_t* dst) const; // in the map's format
|
||||
std::vector<bool> exportInited_;
|
||||
#ifdef _WIN32
|
||||
std::vector<VkDeviceMemory> importMem_; // importWin32Images: the imported memories, freed on shutdown
|
||||
uint8_t deviceLUID_[8] = {};
|
||||
#endif
|
||||
VkDescriptorPool descPool_ = VK_NULL_HANDLE;
|
||||
// Selection overlay (selection_spirv.h): edges as a line list + faces, solid colour from a 1x1 texture.
|
||||
struct Selection {
|
||||
|
||||
+26
-1
@@ -962,6 +962,26 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
||||
fs::create_directories(rootDir() / "logs", ec);
|
||||
if (homeos::reopenStderr(rootDir() / "logs" / "frame-home.log", std::getenv("FH_RESTARTED") ? "a" : "w")) setvbuf(stderr, nullptr, _IOLBF, 0);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// The environment renderer runs one at a time (a restarted panel, or a stray second launch, mustn't make a
|
||||
// second renderer fighting over the compositor's textures). A named mutex guards it; a pid file lets the panel
|
||||
// find the running one. Both go when this process ends.
|
||||
struct SystemGuard {
|
||||
HANDLE mutex = nullptr;
|
||||
bool held = false;
|
||||
fs::path pidFile;
|
||||
~SystemGuard() { if (held) { std::error_code ec; fs::remove(pidFile, ec); } if (mutex) CloseHandle(mutex); }
|
||||
} systemGuard;
|
||||
if (system) {
|
||||
systemGuard.mutex = CreateMutexW(nullptr, TRUE, L"Local\\FrameHomeSystemEnv");
|
||||
if (systemGuard.mutex && GetLastError() == ERROR_ALREADY_EXISTS) { LOG("another frame-home --system is running: this one steps aside"); return 0; }
|
||||
systemGuard.held = true;
|
||||
std::error_code ec;
|
||||
fs::create_directories(rootDir() / "layer", ec);
|
||||
systemGuard.pidFile = rootDir() / "layer" / "system.pid";
|
||||
if (FILE* f = homeos::openFile(systemGuard.pidFile, "w")) { std::fprintf(f, "%lu\n", static_cast<unsigned long>(GetCurrentProcessId())); std::fclose(f); }
|
||||
}
|
||||
#endif
|
||||
vr::EVRInitError ierr = vr::VRInitError_None;
|
||||
vr::VR_Init(&ierr, system ? vr::VRApplication_Overlay : vr::VRApplication_Scene);
|
||||
if (ierr != vr::VRInitError_None) { LOG("VR_Init: %s", vr::VR_GetVRInitErrorAsEnglishDescription(ierr)); return 1; }
|
||||
@@ -996,9 +1016,14 @@ int runVr(const std::vector<std::string>& apks, bool system = false) {
|
||||
std::string dext(dn, '\0');
|
||||
if (dn) vr::VRCompositor()->GetVulkanDeviceExtensionsRequired(pd, &dext[0], dn);
|
||||
r.deviceExtensions = split(dext.c_str());
|
||||
if (system) // the frames go to the compositor's device as external memory
|
||||
if (system) { // the frames go to the compositor's device as external memory
|
||||
#ifdef _WIN32
|
||||
r.importWin32 = true; // the renderer adds VK_KHR_external_memory_win32 (the compositor owns the textures)
|
||||
#else
|
||||
for (const char* e : {"VK_KHR_external_memory", "VK_KHR_external_memory_fd"})
|
||||
if (std::find(r.deviceExtensions.begin(), r.deviceExtensions.end(), e) == r.deviceExtensions.end()) r.deviceExtensions.push_back(e);
|
||||
#endif
|
||||
}
|
||||
vkDestroyInstance(probe, nullptr);
|
||||
}
|
||||
r.pipelineCachePath = (rootDir() / "pipelines.bin").string();
|
||||
|
||||
+21
-7
@@ -1551,11 +1551,9 @@ int runPanel() {
|
||||
// game is open. Here the panel makes sure that's on, and that the moments before Frame Home draws (SteamVR
|
||||
// starting, a game quitting) show the home too: SteamVR's background is a 360-degree picture of it
|
||||
// (keepBackground), with the classic environment instead of Aurora.
|
||||
#ifdef _WIN32
|
||||
const bool systemEnv = false;
|
||||
#else
|
||||
// The compositor layer is installed (the Frame always; a PC once Frame Home is installed there): its folder
|
||||
// (rootDir()/layer) is the marker. The layer draws the home in Aurora's place and frame-home --system renders it.
|
||||
const bool systemEnv = fs::exists(rootDir() / "layer");
|
||||
#endif
|
||||
// (Frame Home uninstalled from a PC: its manifest is gone, and SteamVR's settings are left as the uninstall put them)
|
||||
const bool homeInstalled = systemEnv || fs::exists(rootDir() / "frame-home.vrmanifest");
|
||||
if (!systemEnv && homeInstalled) {
|
||||
@@ -1578,9 +1576,12 @@ int runPanel() {
|
||||
}
|
||||
if (systemEnv) { // its own SteamVR identity (hidden: no list, no dock), so its controller bindings are its own
|
||||
const fs::path m = rootDir() / "frame-environment.vrmanifest";
|
||||
const std::string lin = (rootDir() / "bin" / "frame-home").string();
|
||||
std::string win = (rootDir() / "bin" / "frame-home.exe").string();
|
||||
for (size_t i = 0; (i = win.find('\\', i)) != std::string::npos; i += 2) win.replace(i, 1, "\\\\"); // JSON-escape the backslashes
|
||||
std::ofstream(m) << "{\n \"source\": \"builtin\",\n \"applications\": [ {\n \"app_key\": \"framehome.environment\",\n"
|
||||
" \"launch_type\": \"binary\",\n \"binary_path_linux\": \"" << (rootDir() / "bin" / "frame-home").string() << "\",\n"
|
||||
" \"binary_path_linux_arm\": \"" << (rootDir() / "bin" / "frame-home").string() << "\",\n \"arguments\": \"--system\",\n"
|
||||
" \"launch_type\": \"binary\",\n \"binary_path_linux\": \"" << lin << "\",\n"
|
||||
" \"binary_path_linux_arm\": \"" << lin << "\",\n \"binary_path_windows\": \"" << win << "\",\n \"arguments\": \"--system\",\n"
|
||||
" \"is_dashboard_overlay\": true,\n \"is_hidden\": true,\n"
|
||||
" \"strings\": { \"en_us\": { \"name\": \"Frame Home\", \"description\": \"The environment\" } }\n } ]\n}\n";
|
||||
const vr::EVRApplicationError ae = vr::VRApplications()->AddApplicationManifest(fs::absolute(m).u8string().c_str(), false);
|
||||
@@ -1632,7 +1633,20 @@ int runPanel() {
|
||||
}
|
||||
if (pid > 0) { systemPid = pid; LOG("started frame-home --system (pid %d)", static_cast<int>(pid)); }
|
||||
#else
|
||||
(void)now;
|
||||
// Windows: the panel starts frame-home --system itself and keeps it alive. A pid file (written by the
|
||||
// renderer, one at a time behind a named mutex) lets a restarted panel find the one already running.
|
||||
std::error_code ec;
|
||||
if (systemPid > 0 && homeos::alive(systemPid)) return;
|
||||
if (fs::exists(rootDir() / "layer" / "off", ec)) return;
|
||||
{
|
||||
std::ifstream in(rootDir() / "layer" / "system.pid");
|
||||
long p = 0;
|
||||
if (in >> p && p > 0 && homeos::alive(static_cast<homeos::Pid>(p))) { systemPid = static_cast<homeos::Pid>(p); adopted = true; return; }
|
||||
}
|
||||
if (now - lastSpawn < std::chrono::seconds(5)) return; // a crash loop doesn't spin
|
||||
lastSpawn = now;
|
||||
systemPid = homeos::spawn({(rootDir() / "bin" / "frame-home.exe").string(), "--system"}, "framehome.environment");
|
||||
if (systemPid > 0) LOG("started frame-home --system (pid %lu)", static_cast<unsigned long>(systemPid));
|
||||
#endif
|
||||
};
|
||||
// The panel's pixels go to SteamVR in Vulkan images of our own (overlay_texture.h): SetOverlayRaw recreated the
|
||||
|
||||
Reference in new issue
Block a user