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:
ToastconcernandClaude Opus 4.8 committed 2026-10-21 19:39:36 -05:00
1 parent 283e19f87c
commit d22cc04cb3
14 files changed
+1156 -117

No files matched your search

+80
View File
@@ -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) {