fix: use SteamVR compositor on Quest

This commit is contained in:
XxJSONDeruloxX committed 2026-10-15 18:21:39 -04:00
1 parent 59f752c70d
commit 92f2696a0c
16 files changed
+793 -26

No files matched your search

+1
View File
@@ -555,6 +555,7 @@ private:
}
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("Frame texture upload failed");
sourceHeader_ = frame.header;
receiver_.Recycle(std::move(frame.pixels));
uploadedFrames_++;
if (verifyPixels_ || uploadedFrames_ % 72 == 1) health();
}
+1 -1
View File
@@ -129,7 +129,7 @@ tools/droiddeckctl wake
FOV and dimensions. It works over wired or wireless authorized ADB.
Newer builds also include `source.png`, the incoming stereo texture before the
Android eye draw, and sRGB attachment/write state in the metadata for color debugging.
Source PNGs are tagged as linear and eye PNGs as sRGB. Transfers use Android's
Source PNGs use the incoming frame's color space and eye PNGs use sRGB. Transfers use Android's
gzip utility to avoid sending full uncompressed images over wireless ADB.
A unique request ID prevents stale images from passing as a fresh capture; no valid XR
layer produces an explicit error. Captures exclude Meta system overlays and
+30 -4
View File
@@ -70,7 +70,7 @@ Host allocation and preload-chain tests do not establish headset operation.
In a running Steam session, open the session menu's SteamVR tab, install SteamVR
once, then choose Enter SteamVR. Cardboard has its own tab and can remain off. On supported Quest devices this opens the standard app's
OpenXR activity. The guest driver receives headset poses, Touch controller input
and display timing, and returns owned stereo images with their render poses.
and display timing, and returns owned stereo images tagged with the latest eye poses.
SteamVR haptics return through the tracking bridge. No root or separate XR APK
is required. Other devices retain their existing SteamVR start behavior.
@@ -86,6 +86,23 @@ invalid seated poses, causing Thumper to remain behind its tracking-loss fade.
On Quest, supplying both transforms made all three origins valid; seated height
started within a millimetre of zero while standing and raw height stayed unchanged.
Quest uses SteamVR's normal Linux compositor through its display component.
The Vulkan layer captures only the HMD display swapchain, retains its present
waits, and chains a GPU copy before presentation. Three bounded capture buffers
allow rendering and transfer to overlap. Color conversion happens on the GPU;
the readback allocation travels as a DMA-BUF descriptor through `SCM_RIGHTS`.
Android brackets its CPU read with `DMA_BUF_IOCTL_SYNC`, copies to owned pixels,
then acknowledges that buffer. Missing acknowledgements quarantine the transfer
instead of allowing a producer to overwrite a possible reader. This still
includes a CPU read and GLES upload; it is not GPU image import into OpenXR.
The helper enables SteamVR's Linux Vulkan asynchronous queue and disables motion
smoothing. It excludes the pinned Steam Frame package's optional CVP backend,
whose library constructor exits on Quest's unsupported SoC. The original library
is retained with a `.droiddeck-disabled` suffix. SteamVR binaries are not patched.
Cardboard retains its existing direct-mode path; `questframes` remains an explicit
diagnostic override for the earlier driver presenter.
The panel compositor detaches before OpenXR starts displaying guest frames.
Session suspension accounts for the immersive activity while the panel is hidden.
Closing the immersive activity stops SteamVR. The panel remains available for
@@ -105,10 +122,19 @@ On Quest 3, a 45-second hardware run maintained focused tracked 6DoF, both
controllers active, and advancing stereo images. A headset tester confirmed
controller models and trigger animation. That run delivered about 8.5 new stereo
images per second despite a 90 Hz XR session.
This branch retains the safe owned-frame transfer path; reducing transfer cost,
validating game input and testing actual VR games remain necessary.
That earlier measurement used the driver-direct presenter. With SteamVR's normal
compositor, a native Vulkan scene passed its first submission and continued
submitting stereo frames. Thumper and The Thrill of the Fight produced clean
immersive scenes. SUPERHOT's Windows OpenXR build still showed shared-image
corruption. A subsequent 60-second shared-buffer dashboard run delivered about
43 new stereo images per second. Reusing Android pixel buffers raised the next
60-second run to about 68 received and 65 uploaded images per second, with valid
focused tracking and no rejected frames. These dashboard results do not establish refresh-rate gameplay: game input,
sustained game performance, and exact association with SteamVR's final compositor
pose still require validation.
The frame channel carries linear RGBA8. Android samples it as linear and writes
The frame channel supports linear and sRGB RGBA8. SteamVR's final display image
uses sRGB display values; Android decodes those during texture sampling and writes
to an sRGB eye swapchain, encoding the display values once. A Quest color-chart
capture verified that 64, 128, 192 and 224 grey patches retain those values;
the previous sRGB source upload darkened them to approximately 13, 55, 134 and
@@ -78,7 +78,33 @@ JSON
export VK_INSTANCE_LAYERS="VK_LAYER_DROIDDECK_vr${VK_INSTANCE_LAYERS:+:$VK_INSTANCE_LAYERS}"
export VK_LOADER_LAYERS_DISABLE="${VK_LOADER_LAYERS_DISABLE:-~implicit~}"
if [ -S "${XDG_RUNTIME_DIR:-/run/droiddeck}/droiddeck-xr-frames" ] && [ -z "${DROIDDECK_VR_PRESENT:-}" ]; then
export DROIDDECK_VR_PRESENT=questframes
export DROIDDECK_VR_PRESENT=questwsi
fi
if [ "${DROIDDECK_VR_PRESENT:-}" = questwsi ]; then
python3 - "$HOME/.local/share/Steam/config/steamvr.vrsettings" <<'PY' || exit 1
import json, os, sys
path = sys.argv[1]
try:
with open(path) as source:
settings = json.load(source)
except FileNotFoundError:
settings = {}
steamvr = settings.setdefault('steamvr', {})
steamvr['enableLinuxVulkanAsync'] = True
steamvr['motionSmoothing'] = False
temporary = path + '.droiddeck.tmp'
with open(temporary, 'w') as destination:
json.dump(settings, destination, indent=3)
destination.write('\n')
os.replace(temporary, path)
PY
# The pinned Frame package's CVP constructor exits on Quest's unsupported SoC.
cvp=$steamvr/drivers/cv/bin/linuxarm64/libArcturusPerception.so
if [ -f "$cvp" ]; then
[ ! -e "$cvp.droiddeck-disabled" ] || { echo "CVP backup already exists: $cvp" >&2; exit 1; }
mv "$cvp" "$cvp.droiddeck-disabled" || exit 1
echo "Disabled Frame-only CVP backend for Quest; SteamVR uses its normal compositor"
fi
fi
if [ "${DROIDDECK_VR_PRESENT:-}" = questframes ]; then
export DROIDDECK_VR_PRODUCER_LEASE=${DROIDDECK_VR_PRODUCER_LEASE:-1}
+31 -2
View File
@@ -1,4 +1,5 @@
import os
import json
import shutil
import socket
import subprocess
@@ -83,11 +84,39 @@ class SteamVrHelperTest(unittest.TestCase):
'"${DROIDDECK_VR_PRESENT:-unset}" "${DROIDDECK_VR_PRODUCER_LEASE:-unset}"')
return self.start().read_text().strip()
def test_quest_receiver_automatically_enables_retained_producer_ownership(self):
def test_quest_receiver_uses_normal_compositor_and_excludes_unsupported_frame_backend(self):
self.runtime.mkdir()
cvp = self.steamvr / 'drivers/cv/bin/linuxarm64/libArcturusPerception.so'
cvp.parent.mkdir(parents=True)
cvp.write_bytes(b'frame-only backend')
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as receiver:
receiver.bind(str(self.runtime / 'droiddeck-xr-frames'))
self.assertIn('present=questframes lease=1', self.lease_log())
self.assertIn('present=questwsi lease=unset', self.lease_log())
settings = json.loads((self.home / '.local/share/Steam/config/steamvr.vrsettings').read_text())
self.assertEqual(settings['steamvr'], {'enableLinuxVulkanAsync': True, 'motionSmoothing': False})
self.assertFalse(cvp.exists())
self.assertEqual(Path(str(cvp) + '.droiddeck-disabled').read_bytes(), b'frame-only backend')
def test_quest_settings_preserve_other_preferences_and_start_is_idempotent(self):
self.environment['DROIDDECK_VR_PRESENT'] = 'questwsi'
path = self.home / '.local/share/Steam/config/steamvr.vrsettings'
path.parent.mkdir(parents=True)
path.write_text(json.dumps({'steamvr': {'somePreference': 42}, 'dashboard': {'somePreference': True}}))
self.start()
self.start()
settings = json.loads(path.read_text())
self.assertEqual(settings['steamvr']['somePreference'], 42)
self.assertEqual(settings['dashboard'], {'somePreference': True})
def test_quest_refuses_to_overwrite_malformed_settings(self):
self.environment['DROIDDECK_VR_PRESENT'] = 'questwsi'
path = self.home / '.local/share/Steam/config/steamvr.vrsettings'
path.parent.mkdir(parents=True)
path.write_text('invalid json')
result = subprocess.run(['sh', str(self.helper), 'start'], env=self.environment,
capture_output=True, text=True, timeout=5)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(path.read_text(), 'invalid json')
def test_explicit_quest_presenter_enables_the_lease(self):
self.environment['DROIDDECK_VR_PRESENT'] = 'questframes'
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
@unittest.skipUnless(shutil.which('g++'), 'needs a Linux C++ compiler')
class VrWsiTest(unittest.TestCase):
def test_compositor_handoff_and_frame_transport(self):
repository = Path(__file__).resolve().parents[2]
headers = next((p for p in (Path('/usr/include'), repository / 'build/vr/include')
if (p / 'vulkan/vulkan.h').is_file()), None)
if headers is None:
self.skipTest('needs Vulkan headers')
with tempfile.TemporaryDirectory() as directory:
for name in ('test_wsi_capture', 'test_xr_shared', 'test_xr_frames'):
with self.subTest(name=name):
executable = Path(directory) / name
subprocess.run(['g++', '-std=c++17', '-O2', '-pthread', '-Wall', '-Wextra', '-Werror',
'-Wno-missing-field-initializers', '-I' + str(headers),
str(repository / 'tools/vr/tests' / (name + '.cpp')), '-o', str(executable)], check=True)
subprocess.run([str(executable)], check=True, timeout=20)
+5 -2
View File
@@ -40,8 +40,11 @@ compile() {
-Itools/vr/third_party/wayland -Iapp/src/main/cpp/waylandcomp/generated \
-o build/vr/out/droiddeck/bin/linuxarm64/driver_droiddeck.so tools/vr/src/driver_droiddeck.cpp tools/vr/src/presenter.cpp \
build/vr/xdg-shell-protocol.o -Lbuild/vr/stub -lwayland-client -ldl
aarch64-linux-gnu-gcc -std=c11 -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
-Wl,-z,defs -Wl,-z,nodelete -Ibuild/vr/include -o build/vr/out/layer/libVkLayer_droiddeck_vr.so tools/vr/src/vk_layer_droiddeck_vr.c -ldl
aarch64-linux-gnu-gcc -std=c11 -c -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
-DDROIDDECK_WSI_CAPTURE -Ibuild/vr/include -o build/vr/vk_layer.o tools/vr/src/vk_layer_droiddeck_vr.c
aarch64-linux-gnu-g++ -std=c++17 -shared -fPIC -O2 -Wall -Wextra -Wno-missing-field-initializers -fvisibility=hidden -pthread \
-static-libstdc++ -static-libgcc -Wl,-z,defs -Wl,-z,nodelete -Ibuild/vr/include \
-o build/vr/out/layer/libVkLayer_droiddeck_vr.so build/vr/vk_layer.o tools/vr/src/wsi_capture.cpp -ldl
aarch64-linux-gnu-strip --strip-unneeded build/vr/out/droiddeck/bin/linuxarm64/driver_droiddeck.so \
build/vr/out/layer/libVkLayer_droiddeck_vr.so')
if [ "$native" = 1 ]; then
+2
View File
@@ -569,6 +569,8 @@ public:
VRProperties()->SetBoolProperty(c, Prop_IsOnDesktop_Bool, false);
VRProperties()->SetBoolProperty(c, Prop_DisplayDebugMode_Bool, false);
directMode_ = VRSettings()->GetBool(kSettingsSection, "directMode");
const char *presentation = std::getenv("DROIDDECK_VR_PRESENT");
if (xrMode_ && presentation && !std::strcmp(presentation, "questwsi")) directMode_ = false;
VRProperties()->SetBoolProperty(c, Prop_HasDriverDirectModeComponent_Bool, directMode_);
if (directMode_) {
VRProperties()->SetBoolProperty(c, Prop_DriverDirectModeSendsVsyncEvents_Bool, true);
+44 -3
View File
@@ -13,6 +13,9 @@
#include <errno.h>
#define DROIDDECK_PRODUCER_SERVER
#include "producer_lease.h"
#ifdef DROIDDECK_WSI_CAPTURE
#include "wsi_capture.h"
#endif
#define LAYER_NAME "VK_LAYER_DROIDDECK_vr"
#define EXPORT __attribute__((visibility("default")))
@@ -258,6 +261,9 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat
}
}
pthread_mutex_unlock(&g_lock);
#ifdef DROIDDECK_WSI_CAPTURE
if (in) droiddeck_wsi_device(*out, pd, in->handle, in->gipa, next_gdpa, ci);
#endif
return VK_SUCCESS;
}
@@ -267,6 +273,9 @@ static void producer_destroy_device(struct dev *device, const VkAllocationCallba
static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallbacks *alloc) {
struct dev *d = dev_for(device);
if (!d) return;
#ifdef DROIDDECK_WSI_CAPTURE
droiddeck_wsi_destroy(device);
#endif
if (d->producer) producer_destroy_device(d, alloc);
forget_device_semaphores(device);
PFN_vkDestroyDevice f = d->destroy;
@@ -281,6 +290,7 @@ static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallback
#define FAKE_DISPLAY ((VkDisplayKHR)(uintptr_t)0xdd0001)
#define FAKE_MODE ((VkDisplayModeKHR)(uintptr_t)0xdd0002)
static VkSurfaceKHR g_hmd_surface;
static uint32_t g_mode_w = 1920, g_mode_h = 1080, g_mode_mhz = 60000;
static void mode_init(void) {
@@ -383,6 +393,7 @@ static VkResult VKAPI_CALL CreateDisplayPlaneSurfaceKHR(VkInstance i, const VkDi
if (!headless) { say("%s: no headless surface to stand in", "vkCreateDisplayPlaneSurfaceKHR"); return VK_ERROR_INITIALIZATION_FAILED; }
VkHeadlessSurfaceCreateInfoEXT hci = {VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT, NULL, 0};
VkResult r = headless(i, &hci, a, s);
if (r == VK_SUCCESS) g_hmd_surface = *s;
say("the display's surface is a headless one (%s)", r == VK_SUCCESS ? "ok" : "failed");
return r;
}
@@ -441,12 +452,24 @@ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreat
VkSwapchainKHR *out) {
PFN_vkCreateSwapchainKHR next = NEXT(d, CreateSwapchainKHR);
VkSwapchainCreateInfoKHR sized = *ci;
#ifdef DROIDDECK_WSI_CAPTURE
const char *capture_mode = getenv("DROIDDECK_VR_PRESENT");
const int capture = capture_mode && !strcmp(capture_mode, "questwsi")
&& !strcmp(program_invocation_short_name, "vrcompositor") && ci->surface == g_hmd_surface;
if (capture)
sized.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
#endif
mode_init();
if (sized.imageExtent.width == UINT32_MAX || sized.imageExtent.height == UINT32_MAX)
sized.imageExtent = (VkExtent2D){g_mode_w, g_mode_h};
ci = &sized;
VkResult r = next(d, ci, a, out);
if (r == VK_SUCCESS) return r;
if (r == VK_SUCCESS) {
#ifdef DROIDDECK_WSI_CAPTURE
if (capture) droiddeck_wsi_swapchain(d, *out, ci);
#endif
return r;
}
char line[256];
int n = snprintf(line, sizeof(line), "format %d space %d mode %d images %u %ux%u usage 0x%x chain:", ci->imageFormat,
ci->imageColorSpace, ci->presentMode, ci->minImageCount, ci->imageExtent.width, ci->imageExtent.height,
@@ -462,16 +485,30 @@ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreat
if (r != VK_SUCCESS) {
mine.imageFormat = VK_FORMAT_B8G8R8A8_UNORM;
mine.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
#ifdef DROIDDECK_WSI_CAPTURE
if (capture) mine.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
#endif
mine.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
mine.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
r = next(d, &mine, a, out);
}
say("headless swapchain: %s", r == VK_SUCCESS ? "ok" : "still refused");
#ifdef DROIDDECK_WSI_CAPTURE
if (r == VK_SUCCESS && capture) droiddeck_wsi_swapchain(d, *out, &mine);
#endif
return r;
}
static void VKAPI_CALL DestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks *allocator) {
#ifdef DROIDDECK_WSI_CAPTURE
droiddeck_wsi_forget(device, swapchain);
#endif
NEXT(device, DestroySwapchainKHR)(device, swapchain, allocator);
}
static PFN_vkVoidFunction display_device_hook(const char *name) {
HOOK_AS("vkCreateSwapchainKHR", CreateSwapchainKHR)
HOOK_AS("vkDestroySwapchainKHR", DestroySwapchainKHR)
HOOK_AS("vkDisplayPowerControlEXT", DisplayPowerControlEXT)
HOOK_AS("vkRegisterDeviceEventEXT", RegisterDeviceEventEXT)
HOOK_AS("vkRegisterDisplayEventEXT", RegisterDisplayEventEXT)
@@ -873,7 +910,11 @@ static VkResult VKAPI_CALL QueuePresentKHR(VkQueue queue, const VkPresentInfoKHR
struct dev *device = dev_for(queue);
PFN_vkQueuePresentKHR next = (PFN_vkQueuePresentKHR)device->gdpa(device->handle, "vkQueuePresentKHR");
if (device->producer) { pthread_mutex_lock(&g_submission_lock); producer_record(device, queue); }
#ifdef DROIDDECK_WSI_CAPTURE
VkResult result = droiddeck_wsi_present(device->handle, queue, info, next);
#else
VkResult result = next(queue, info);
#endif
if (device->producer) pthread_mutex_unlock(&g_submission_lock);
return result;
}
@@ -928,9 +969,9 @@ static VkResult VKAPI_CALL CreateImage(VkDevice d, const VkImageCreateInfo *ci,
}
if (!ext) return r;
char line[256];
int n = snprintf(line, sizeof(line), "%s image %ux%u format %d flags 0x%x usage 0x%x tiling %d handles 0x%x list",
int n = snprintf(line, sizeof(line), "%s image %ux%u format %d flags 0x%x usage 0x%x tiling %d layers %u mips %u samples %u handles 0x%x list",
program_invocation_short_name, ci->extent.width, ci->extent.height, ci->format, ci->flags, ci->usage,
ci->tiling, ext->handleTypes);
ci->tiling, ci->arrayLayers, ci->mipLevels, ci->samples, ext->handleTypes);
for (uint32_t i = 0; list && i < list->viewFormatCount && n < (int)sizeof(line) - 8; i++)
n += snprintf(line + n, sizeof(line) - n, " %d", list->pViewFormats[i]);
say("%s", line);
+351
View File
@@ -0,0 +1,351 @@
#define VK_NO_PROTOTYPES
#include "wsi_capture.h"
#include "xr_tracking_file.h"
#include "xr_frame_channel.h"
#include <algorithm>
#include <cstdio>
#include <memory>
#include <unordered_map>
#include <condition_variable>
#include <array>
namespace {
#define FUNCTIONS(X) \
X(GetSwapchainImagesKHR) X(CreateCommandPool) X(DestroyCommandPool) X(AllocateCommandBuffers) \
X(ResetCommandBuffer) X(BeginCommandBuffer) X(EndCommandBuffer) X(CmdPipelineBarrier) \
X(CmdCopyImageToBuffer) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) \
X(AllocateMemory) X(FreeMemory) X(BindBufferMemory) X(MapMemory) X(UnmapMemory) \
X(InvalidateMappedMemoryRanges) X(CreateFence) X(DestroyFence) X(ResetFences) X(WaitForFences) X(QueueSubmit) \
X(CreateSemaphore) X(DestroySemaphore) X(GetMemoryFdKHR) X(CreateImage) X(DestroyImage) \
X(GetImageMemoryRequirements) X(BindImageMemory) X(CmdBlitImage)
struct Device {
VkDevice handle{};
VkPhysicalDeviceMemoryProperties memory{};
std::unordered_map<VkQueue, uint32_t> queues;
#define MEMBER(name) PFN_vk##name name{};
FUNCTIONS(MEMBER)
#undef MEMBER
};
struct CaptureSlot {
Device *device{};
VkCommandPool pool{};
VkCommandBuffer command{};
VkBuffer buffer{};
VkDeviceMemory memory{};
VkImage rgbaImage{};
VkDeviceMemory rgbaMemory{};
int sharedFd = -1;
bool rgbaInitialized{};
void *mapping{};
VkFence fence{};
bool busy{}, queued{};
DroidDeckXrFrameHeader header{};
~CaptureSlot() {
if (!device) return;
if (busy && fence) device->WaitForFences(device->handle, 1, &fence, VK_TRUE, UINT64_MAX);
if (sharedFd >= 0) close(sharedFd);
if (fence) device->DestroyFence(device->handle, fence, nullptr);
if (pool) device->DestroyCommandPool(device->handle, pool, nullptr);
if (buffer) device->DestroyBuffer(device->handle, buffer, nullptr);
if (rgbaImage) device->DestroyImage(device->handle, rgbaImage, nullptr);
if (rgbaMemory) device->FreeMemory(device->handle, rgbaMemory, nullptr);
if (mapping) device->UnmapMemory(device->handle, memory);
if (memory) device->FreeMemory(device->handle, memory, nullptr);
}
bool Initialize(Device &d, VkExtent2D extent, VkFormat format, uint32_t family) {
device = &d;
VkCommandPoolCreateInfo pc{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pc.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pc.queueFamilyIndex = family;
if (d.CreateCommandPool(d.handle, &pc, nullptr, &pool)) return false;
VkCommandBufferAllocateInfo ca{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ca.commandPool = pool; ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ca.commandBufferCount = 1;
if (d.AllocateCommandBuffers(d.handle, &ca, &command)) return false;
VkBufferCreateInfo bc{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; bc.pNext = &external;
bc.size = uint64_t(extent.width) * extent.height * 4; bc.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (d.CreateBuffer(d.handle, &bc, nullptr, &buffer)) return false;
VkMemoryRequirements requirements{}; d.GetBufferMemoryRequirements(d.handle, buffer, &requirements);
uint32_t type = d.memory.memoryTypeCount;
for (uint32_t i = 0; i < d.memory.memoryTypeCount; i++) {
auto flags = d.memory.memoryTypes[i].propertyFlags;
if (!(requirements.memoryTypeBits & (1u << i)) || !(flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) continue;
type = i;
if (flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) break;
}
if (type == d.memory.memoryTypeCount) return false;
VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
VkExportMemoryAllocateInfo exported{VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO};
exported.handleTypes = external.handleTypes; alloc.pNext = &exported;
alloc.allocationSize = requirements.size; alloc.memoryTypeIndex = type;
std::fprintf(stderr, "[droiddeck-vr-layer] WSI readback memory type %u flags %x\n", type, d.memory.memoryTypes[type].propertyFlags);
if (d.AllocateMemory(d.handle, &alloc, nullptr, &memory) || d.BindBufferMemory(d.handle, buffer, memory, 0)) return false;
if (d.MapMemory(d.handle, memory, 0, VK_WHOLE_SIZE, 0, &mapping)) return false;
VkMemoryGetFdInfoKHR getFd{VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR};
getFd.memory = memory; getFd.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
if (d.GetMemoryFdKHR(d.handle, &getFd, &sharedFd)) return false;
VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image.imageType = VK_IMAGE_TYPE_2D; image.extent = {extent.width, extent.height, 1};
image.mipLevels = image.arrayLayers = 1; image.samples = VK_SAMPLE_COUNT_1_BIT;
image.format = (format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_B8G8R8A8_SRGB)
? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
image.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
if (d.CreateImage(d.handle, &image, nullptr, &rgbaImage)) return false;
d.GetImageMemoryRequirements(d.handle, rgbaImage, &requirements);
type = 0;
while (type < d.memory.memoryTypeCount && !(requirements.memoryTypeBits & (1u << type))) type++;
if (type == d.memory.memoryTypeCount) return false;
alloc.pNext = nullptr; alloc.allocationSize = requirements.size; alloc.memoryTypeIndex = type;
if (d.AllocateMemory(d.handle, &alloc, nullptr, &rgbaMemory)
|| d.BindImageMemory(d.handle, rgbaImage, rgbaMemory, 0)) return false;
VkFenceCreateInfo fc{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (d.CreateFence(d.handle, &fc, nullptr, &fence)) return false;
return true;
}
};
struct Swapchain {
explicit Swapchain(const char *trackingPath = "/run/droiddeck/" DROIDDECK_XR_FILE) : tracking(trackingPath) {}
Device *device{};
VkExtent2D extent{};
VkFormat format{};
std::vector<VkImage> images;
std::vector<VkSemaphore> ready;
std::array<CaptureSlot, 3> slots;
uint64_t frame{};
XrTrackingFile tracking;
XrFrameSender sender;
bool initialized{}, attempted{};
uint32_t family{};
std::mutex sendMutex;
std::condition_variable sendReady;
bool stopping{}, failed{};
std::thread sendThread;
~Swapchain() {
{ std::lock_guard<std::mutex> lock(sendMutex); stopping = true; sendReady.notify_all(); }
if (sendThread.joinable()) sendThread.join();
for (auto semaphore : ready) if (semaphore) device->DestroySemaphore(device->handle, semaphore, nullptr);
}
void Run() noexcept {
try { Loop(); }
catch (...) {
std::lock_guard<std::mutex> lock(sendMutex); failed = true;
std::fprintf(stderr, "[droiddeck-vr-layer] WSI worker failed; restart SteamVR\n");
}
}
void Loop() {
for (;;) {
CaptureSlot *slot;
{
std::unique_lock<std::mutex> lock(sendMutex);
sendReady.wait(lock, [this] {
return stopping || std::any_of(slots.begin(), slots.end(), [](const auto &item) { return item.queued; });
});
slot = nullptr;
for (auto &item : slots) if (item.queued && (!slot || item.header.frame_id < slot->header.frame_id)) slot = &item;
if (!slot) return;
slot->queued = false;
}
auto &d = *device;
const auto header = slot->header;
auto &fence = slot->fence;
auto &memory = slot->memory;
auto sharedFd = slot->sharedFd;
int64_t begin = XrTrackingFile::Now();
VkResult result = d.WaitForFences(d.handle, 1, &fence, VK_TRUE, UINT64_MAX);
int64_t waited = XrTrackingFile::Now();
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = memory; range.size = VK_WHOLE_SIZE;
if (result == VK_SUCCESS) result = d.InvalidateMappedMemoryRanges(d.handle, 1, &range);
int64_t invalidated = XrTrackingFile::Now();
if (result == VK_SUCCESS && XrTrackingFile::Now() - header.captured_ns < 200000000LL) {
sender.SendShared("/run/droiddeck/" DROIDDECK_XR_FRAME_SOCKET, header, sharedFd);
}
if (header.frame_id % 120 == 0) std::fprintf(stderr, "[droiddeck-vr-layer] WSI ms wait=%.2f invalidate=%.2f shared=%.2f\n",
(waited-begin)/1e6, (invalidated-waited)/1e6, (XrTrackingFile::Now()-invalidated)/1e6);
std::lock_guard<std::mutex> lock(sendMutex);
slot->busy = false;
if (result != VK_SUCCESS || sender.SharedLeaseLost()) {
std::fprintf(stderr, "[droiddeck-vr-layer] WSI buffer quarantined: result=%d leaseLost=%d; restart SteamVR\n", result, sender.SharedLeaseLost());
failed = true; return;
}
}
}
bool Initialize(VkSwapchainKHR chain, VkQueue queue) {
if (initialized) return true;
if (attempted) return false;
attempted = true;
auto &d = *device;
auto queueFamily = d.queues.find(queue);
if (queueFamily == d.queues.end()) return false;
family = queueFamily->second;
uint32_t count = 0;
if (d.GetSwapchainImagesKHR(d.handle, chain, &count, nullptr) || !count) return false;
images.resize(count);
if (d.GetSwapchainImagesKHR(d.handle, chain, &count, images.data())) return false;
ready.resize(count);
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
for (auto &semaphore : ready) if (d.CreateSemaphore(d.handle, &semaphoreInfo, nullptr, &semaphore)) return false;
for (auto &slot : slots) if (!slot.Initialize(d, extent, format, family)) return false;
sendThread = std::thread([this] { Run(); });
initialized = true;
std::fprintf(stderr, "[droiddeck-vr-layer] capturing compositor swapchain %ux%u format %d\n", extent.width, extent.height, format);
return true;
}
VkResult Fail(VkResult result, CaptureSlot &slot) {
std::lock_guard<std::mutex> lock(sendMutex);
failed = true; slot.busy = false;
return result;
}
bool Header(DroidDeckXrFrameHeader &header) {
DroidDeckXrTracking packet{};
if (!tracking.Read(&packet) || !XrTrackingFile::Fresh(packet)) return false;
header = {};
header.magic = DROIDDECK_XR_FRAME_MAGIC; header.version = DROIDDECK_XR_FRAME_VERSION;
header.width = extent.width; header.height = extent.height;
header.bytes = extent.width * extent.height * 4;
header.color_space = DROIDDECK_XR_COLOR_SRGB_RGBA8;
header.frame_id = ++frame; header.captured_ns = XrTrackingFile::Now();
for (int eye = 0; eye < 2; eye++) {
const auto &pose = packet.eyes[eye].pose;
const float x = pose.orientation[0], y = pose.orientation[1], z = pose.orientation[2], w = pose.orientation[3];
float matrix[3][4] = {{1-2*(y*y+z*z),2*(x*y-w*z),2*(x*z+w*y),pose.position[0]},
{2*(x*y+w*z),1-2*(x*x+z*z),2*(y*z-w*x),pose.position[1]},
{2*(x*z-w*y),2*(y*z+w*x),1-2*(x*x+y*y),pose.position[2]}};
std::memcpy(header.eye_pose[eye], matrix, sizeof(matrix));
const auto *fov = packet.eyes[eye].fov;
float left = std::tan(fov[0]), right = std::tan(fov[1]), up = std::tan(fov[2]), down = std::tan(fov[3]);
auto &p = header.projection[eye];
p[0][0] = 2/(right-left); p[0][2] = (right+left)/(right-left);
p[1][1] = 2/(up-down); p[1][2] = (up+down)/(up-down);
p[2][2] = -1; p[2][3] = -0.2f; p[3][2] = -1;
}
return xr_frame_valid(header);
}
VkResult Capture(VkQueue queue, uint32_t index, const VkPresentInfoKHR &present, VkSemaphore *completion) {
auto &d = *device;
if (index >= images.size()) return VK_ERROR_OUT_OF_DATE_KHR;
CaptureSlot *selected = nullptr;
{
std::lock_guard<std::mutex> lock(sendMutex);
if (failed) return VK_ERROR_DEVICE_LOST;
for (auto &slot : slots) if (!slot.busy) { selected = &slot; slot.busy = true; break; }
}
if (!selected) return VK_SUCCESS;
auto &slot = *selected;
DroidDeckXrFrameHeader header{};
if (!Header(header)) { std::lock_guard<std::mutex> lock(sendMutex); slot.busy = false; return VK_SUCCESS; }
auto &command = slot.command;
auto &buffer = slot.buffer;
auto &fence = slot.fence;
auto &rgbaImage = slot.rgbaImage;
auto &rgbaInitialized = slot.rgbaInitialized;
VkResult result = d.ResetCommandBuffer(command, 0);
if (result) return Fail(result, slot);
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if ((result = d.BeginCommandBuffer(command, &begin))) return Fail(result, slot);
VkImageMemoryBarrier image{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
image.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; image.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
image.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; image.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
image.srcQueueFamilyIndex = image.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
image.image = images[index]; image.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image);
VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {extent.width, extent.height, 1};
VkImageMemoryBarrier rgba = image;
rgba.image = rgbaImage; rgba.srcAccessMask = rgbaInitialized ? VK_ACCESS_TRANSFER_READ_BIT : 0;
rgba.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
rgba.oldLayout = rgbaInitialized ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
rgba.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rgba);
VkImageBlit blit{}; blit.srcSubresource = blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.srcOffsets[1] = blit.dstOffsets[1] = {static_cast<int32_t>(extent.width), static_cast<int32_t>(extent.height), 1};
d.CmdBlitImage(command, images[index], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, rgbaImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST);
rgba.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rgba.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
rgba.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rgba.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rgba);
d.CmdCopyImageToBuffer(command, rgbaImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, &copy);
image.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; image.dstAccessMask = 0;
image.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; image.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkBufferMemoryBarrier host{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
host.srcQueueFamilyIndex = host.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; host.buffer = buffer; host.size = VK_WHOLE_SIZE;
d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, nullptr, 1, &host, 1, &image);
if ((result = d.EndCommandBuffer(command)) || (result = d.ResetFences(d.handle, 1, &fence))) return Fail(result, slot);
std::vector<VkPipelineStageFlags> stages(present.waitSemaphoreCount, VK_PIPELINE_STAGE_TRANSFER_BIT);
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.waitSemaphoreCount = present.waitSemaphoreCount; submit.pWaitSemaphores = present.pWaitSemaphores;
submit.pWaitDstStageMask = stages.data(); submit.commandBufferCount = 1; submit.pCommandBuffers = &command;
submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &ready[index];
if ((result = d.QueueSubmit(queue, 1, &submit, fence))) return Fail(result, slot);
rgbaInitialized = true;
{ std::lock_guard<std::mutex> lock(sendMutex); slot.header = header; slot.queued = true; sendReady.notify_all(); }
*completion = ready[index];
return VK_SUCCESS;
}
};
std::mutex mutex;
std::unordered_map<VkDevice, std::unique_ptr<Device>> devices;
std::unordered_map<VkSwapchainKHR, std::unique_ptr<Swapchain>> swapchains;
}
extern "C" void droiddeck_wsi_device(VkDevice device, VkPhysicalDevice physical, VkInstance instance,
PFN_vkGetInstanceProcAddr gipa, PFN_vkGetDeviceProcAddr gdpa, const VkDeviceCreateInfo *create) {
const char *mode = getenv("DROIDDECK_VR_PRESENT");
if (!mode || strcmp(mode, "questwsi") || strcmp(program_invocation_short_name, "vrcompositor")) return;
auto d = std::make_unique<Device>(); d->handle = device;
#define LOAD(name) d->name = reinterpret_cast<PFN_vk##name>(gdpa(device, "vk" #name)); if (!d->name) return;
FUNCTIONS(LOAD)
#undef LOAD
auto properties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(gipa(instance, "vkGetPhysicalDeviceMemoryProperties"));
properties(physical, &d->memory);
auto getQueue = reinterpret_cast<PFN_vkGetDeviceQueue>(gdpa(device, "vkGetDeviceQueue"));
for (uint32_t index = 0; index < create->queueCreateInfoCount; index++) {
const auto &info = create->pQueueCreateInfos[index];
if (info.flags) continue;
for (uint32_t number = 0; number < info.queueCount; number++) {
VkQueue queue{}; getQueue(device, info.queueFamilyIndex, number, &queue);
if (queue) d->queues[queue] = info.queueFamilyIndex;
}
}
std::lock_guard<std::mutex> lock(mutex); devices[device] = std::move(d);
}
extern "C" void droiddeck_wsi_destroy(VkDevice device) {
std::lock_guard<std::mutex> lock(mutex);
for (auto it = swapchains.begin(); it != swapchains.end();)
if (it->second->device->handle == device) it = swapchains.erase(it); else ++it;
devices.erase(device);
}
extern "C" void droiddeck_wsi_swapchain(VkDevice device, VkSwapchainKHR chain, const VkSwapchainCreateInfoKHR *create) {
std::lock_guard<std::mutex> lock(mutex);
auto it = devices.find(device); if (it == devices.end()) return;
if (create->imageFormat != VK_FORMAT_R8G8B8A8_UNORM && create->imageFormat != VK_FORMAT_R8G8B8A8_SRGB &&
create->imageFormat != VK_FORMAT_B8G8R8A8_UNORM && create->imageFormat != VK_FORMAT_B8G8R8A8_SRGB) return;
auto chainState = std::make_unique<Swapchain>(); chainState->device = it->second.get();
chainState->extent = create->imageExtent; chainState->format = create->imageFormat;
swapchains[chain] = std::move(chainState);
}
extern "C" void droiddeck_wsi_forget(VkDevice device, VkSwapchainKHR chain) {
(void)device; std::lock_guard<std::mutex> lock(mutex); swapchains.erase(chain);
}
extern "C" VkResult droiddeck_wsi_present(VkDevice device, VkQueue queue, const VkPresentInfoKHR *present, PFN_vkQueuePresentKHR next) try {
(void)device; std::lock_guard<std::mutex> lock(mutex);
if (present->swapchainCount != 1) return next(queue, present);
auto it = swapchains.find(present->pSwapchains[0]);
if (it == swapchains.end()) return next(queue, present);
if (!it->second->Initialize(present->pSwapchains[0], queue)) return VK_ERROR_INITIALIZATION_FAILED;
auto family = it->second->device->queues.find(queue);
if (family == it->second->device->queues.end() || family->second != it->second->family)
return VK_ERROR_INITIALIZATION_FAILED;
VkSemaphore completion{};
VkResult result = it->second->Capture(queue, present->pImageIndices[0], *present, &completion);
if (result) return result;
if (!completion) return next(queue, present);
// Reacquiring this swapchain image also proves its previous present wait semaphore is reusable.
VkPresentInfoKHR copy = *present; copy.waitSemaphoreCount = 1; copy.pWaitSemaphores = &completion;
return next(queue, &copy);
} catch (...) {
std::fprintf(stderr, "[droiddeck-vr-layer] WSI capture failed; restart SteamVR\n");
return VK_ERROR_OUT_OF_HOST_MEMORY;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <vulkan/vulkan.h>
#ifdef __cplusplus
extern "C" {
#endif
void droiddeck_wsi_device(VkDevice device, VkPhysicalDevice physical, VkInstance instance,
PFN_vkGetInstanceProcAddr gipa, PFN_vkGetDeviceProcAddr gdpa,
const VkDeviceCreateInfo *create);
void droiddeck_wsi_destroy(VkDevice device);
void droiddeck_wsi_swapchain(VkDevice device, VkSwapchainKHR swapchain, const VkSwapchainCreateInfoKHR *create);
void droiddeck_wsi_forget(VkDevice device, VkSwapchainKHR swapchain);
VkResult droiddeck_wsi_present(VkDevice device, VkQueue queue, const VkPresentInfoKHR *present,
PFN_vkQueuePresentKHR next);
#ifdef __cplusplus
}
#endif
+3 -1
View File
@@ -4,6 +4,7 @@
#define DROIDDECK_XR_FRAME_MAGIC 0x46584444u
#define DROIDDECK_XR_FRAME_VERSION 1u
#define DROIDDECK_XR_FRAME_SHARED_VERSION 2u
#define DROIDDECK_XR_FRAME_SOCKET "droiddeck-xr-frames"
#define DROIDDECK_XR_FRAME_MAX_BYTES (64u * 1024u * 1024u)
#define DROIDDECK_XR_COLOR_LINEAR_RGBA8 1u
@@ -22,7 +23,8 @@ static_assert(sizeof(DroidDeckXrFrameHeader) == 264, "XR frame protocol layout")
inline bool xr_frame_valid(const DroidDeckXrFrameHeader &header) {
uint64_t bytes = (uint64_t)header.width * header.height * 4;
if (header.magic != DROIDDECK_XR_FRAME_MAGIC || header.version != DROIDDECK_XR_FRAME_VERSION
if (header.magic != DROIDDECK_XR_FRAME_MAGIC || (header.version != DROIDDECK_XR_FRAME_VERSION
&& header.version != DROIDDECK_XR_FRAME_SHARED_VERSION)
|| !header.frame_id || header.width < 2 || (header.width & 1u) || !header.height
|| header.width > 8192 || header.height > 4096 || bytes > DROIDDECK_XR_FRAME_MAX_BYTES
|| header.bytes != bytes || (header.color_space != DROIDDECK_XR_COLOR_LINEAR_RGBA8
+111 -10
View File
@@ -12,6 +12,9 @@
#include <sys/file.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/dma-buf.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
@@ -46,6 +49,55 @@ inline bool xr_frame_peer(int descriptor) {
&& size == sizeof(peer) && peer.uid == getuid();
}
struct XrFrameDescriptor {
int fd = -1;
~XrFrameDescriptor() { if (fd >= 0) close(fd); }
XrFrameDescriptor() = default;
XrFrameDescriptor(const XrFrameDescriptor &) = delete;
XrFrameDescriptor &operator=(const XrFrameDescriptor &) = delete;
};
inline bool xr_frame_header(int socket, DroidDeckXrFrameHeader &header, XrFrameDescriptor &shared, int64_t deadline) {
for (;;) {
int64_t remaining = deadline - xr_frame_now();
if (remaining <= 0) return false;
pollfd ready{socket, POLLIN, 0};
int result = poll(&ready, 1, (int)((remaining + 999999) / 1000000));
if (result < 0 && errno == EINTR) continue;
if (result <= 0 || !(ready.revents & POLLIN)) return false;
alignas(cmsghdr) unsigned char control[CMSG_SPACE(8 * sizeof(int))]{};
iovec data{&header, sizeof(header)};
msghdr message{}; message.msg_iov = &data; message.msg_iovlen = 1;
message.msg_control = control; message.msg_controllen = sizeof(control);
ssize_t bytes = recvmsg(socket, &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
if (bytes < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (bytes <= 0) return false;
unsigned count = 0;
for (auto *item = CMSG_FIRSTHDR(&message); item; item = CMSG_NXTHDR(&message, item)) {
if (item->cmsg_level != SOL_SOCKET || item->cmsg_type != SCM_RIGHTS) continue;
size_t descriptors = (item->cmsg_len - CMSG_LEN(0)) / sizeof(int);
auto *fds = reinterpret_cast<int *>(CMSG_DATA(item));
for (size_t index = 0; index < descriptors; index++) {
if (++count == 1) shared.fd = fds[index]; else close(fds[index]);
}
}
if ((message.msg_flags & (MSG_CTRUNC | MSG_TRUNC)) || count > 1) return false;
return xr_frame_io(socket, reinterpret_cast<unsigned char *>(&header) + bytes, sizeof(header) - bytes, false, deadline);
}
}
inline bool xr_frame_shared_pixels(int descriptor, unsigned char *pixels, size_t bytes) {
struct stat status{};
if (fstat(descriptor, &status) != 0 || status.st_size < static_cast<int64_t>(bytes)) return false;
dma_buf_sync sync{DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ};
if (ioctl(descriptor, DMA_BUF_IOCTL_SYNC, &sync) != 0) return false;
void *mapping = mmap(nullptr, bytes, PROT_READ, MAP_SHARED, descriptor, 0);
bool success = mapping != MAP_FAILED;
if (success) { std::memcpy(pixels, mapping, bytes); munmap(mapping, bytes); }
sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
return ioctl(descriptor, DMA_BUF_IOCTL_SYNC, &sync) == 0 && success;
}
struct DroidDeckXrPixels {
DroidDeckXrFrameHeader header{};
std::vector<unsigned char> pixels;
@@ -88,6 +140,8 @@ public:
filename_.clear();
if (lock_ >= 0) close(lock_);
lock_ = -1;
std::lock_guard<std::mutex> lock(mutex_);
pending_ = {}; recycled_.clear();
}
bool Take(DroidDeckXrPixels *out) {
std::lock_guard<std::mutex> lock(mutex_);
@@ -96,6 +150,10 @@ public:
pending_ = {};
return true;
}
void Recycle(std::vector<unsigned char> pixels) {
std::lock_guard<std::mutex> lock(mutex_);
if (recycled_.size() < 3) recycled_.push_back(std::move(pixels));
}
uint64_t Rejected() const { return rejected_; }
uint64_t Received() const { return received_; }
private:
@@ -107,6 +165,7 @@ private:
std::thread thread_;
std::mutex mutex_;
DroidDeckXrPixels pending_;
std::vector<std::vector<unsigned char>> recycled_;
void Loop() {
while (!stopping_) {
pollfd ready{listener_, POLLIN, 0};
@@ -117,20 +176,33 @@ private:
uint64_t lastFrame = 0;
while (!stopping_) {
DroidDeckXrPixels frame;
XrFrameDescriptor shared;
int64_t deadline = xr_frame_now() + 200000000LL;
if (!xr_frame_io(client, &frame.header, sizeof(frame.header), false, deadline)) break;
if (!xr_frame_header(client, frame.header, shared, deadline)) break;
int64_t now = xr_frame_now();
if (!xr_frame_valid(frame.header) || frame.header.captured_ns > now
|| now - frame.header.captured_ns > 500000000LL || frame.header.frame_id <= lastFrame) { rejected_++; break; }
try { frame.pixels.resize(frame.header.bytes); }
|| now - frame.header.captured_ns > 500000000LL || frame.header.frame_id <= lastFrame
|| (frame.header.version == DROIDDECK_XR_FRAME_SHARED_VERSION) != (shared.fd >= 0)) { rejected_++; break; }
try {
{
std::lock_guard<std::mutex> lock(mutex_);
if (!recycled_.empty()) {
frame.pixels = std::move(recycled_.back()); recycled_.pop_back();
}
}
frame.pixels.resize(frame.header.bytes);
}
catch (...) { rejected_++; break; }
deadline = xr_frame_now() + 200000000LL;
if (!xr_frame_io(client, frame.pixels.data(), frame.pixels.size(), false, deadline)) { rejected_++; break; }
bool copied = shared.fd >= 0 ? xr_frame_shared_pixels(shared.fd, frame.pixels.data(), frame.pixels.size())
: xr_frame_io(client, frame.pixels.data(), frame.pixels.size(), false, deadline);
if (!copied) { rejected_++; break; }
if (xr_frame_now() - frame.header.captured_ns > 500000000LL) { rejected_++; break; }
uint64_t acknowledgement = frame.header.frame_id;
lastFrame = acknowledgement;
{
std::lock_guard<std::mutex> lock(mutex_);
if (!pending_.pixels.empty() && recycled_.size() < 3) recycled_.push_back(std::move(pending_.pixels));
pending_ = std::move(frame);
}
received_++;
@@ -148,7 +220,22 @@ public:
XrFrameSender &operator=(const XrFrameSender &) = delete;
~XrFrameSender() { Disconnect(); }
bool Send(const std::string &filename, const DroidDeckXrFrameHeader &header, const unsigned char *pixels) {
if (!pixels || !xr_frame_valid(header) || filename.empty() || filename.size() >= sizeof(sockaddr_un::sun_path)) return false;
return SendFrame(filename, header, pixels, -1);
}
bool SendShared(const std::string &filename, DroidDeckXrFrameHeader header, int shared) {
if (shared < 0 || sharedLeaseLost_) return false;
header.version = DROIDDECK_XR_FRAME_SHARED_VERSION;
return SendFrame(filename, header, nullptr, shared);
}
bool SharedLeaseLost() const { return sharedLeaseLost_; }
private:
int descriptor_ = -1;
bool sharedLeaseLost_ = false;
std::string filename_;
bool SendFrame(const std::string &filename, const DroidDeckXrFrameHeader &header, const unsigned char *pixels, int shared) {
if ((!pixels && shared < 0) || !xr_frame_valid(header) || filename.empty()
|| filename.size() >= sizeof(sockaddr_un::sun_path)
|| (header.version == DROIDDECK_XR_FRAME_SHARED_VERSION) != (shared >= 0)) return false;
if (filename_ != filename) Disconnect();
int64_t deadline = xr_frame_now() + 200000000LL;
if (descriptor_ < 0) {
@@ -169,14 +256,28 @@ public:
filename_ = filename;
}
uint64_t acknowledgement = 0;
if (!xr_frame_io(descriptor_, const_cast<DroidDeckXrFrameHeader *>(&header), sizeof(header), true, deadline)
|| !xr_frame_io(descriptor_, const_cast<unsigned char *>(pixels), header.bytes, true, deadline)
bool sent = false;
if (shared >= 0) {
iovec data{const_cast<DroidDeckXrFrameHeader *>(&header), sizeof(header)};
alignas(cmsghdr) unsigned char control[CMSG_SPACE(sizeof(int))]{};
msghdr message{}; message.msg_iov = &data; message.msg_iovlen = 1;
message.msg_control = control; message.msg_controllen = sizeof(control);
auto *item = CMSG_FIRSTHDR(&message); item->cmsg_level = SOL_SOCKET; item->cmsg_type = SCM_RIGHTS;
item->cmsg_len = CMSG_LEN(sizeof(int)); std::memcpy(CMSG_DATA(item), &shared, sizeof(shared));
ssize_t bytes;
do { bytes = sendmsg(descriptor_, &message, MSG_NOSIGNAL | MSG_DONTWAIT); } while (bytes < 0 && errno == EINTR);
if (bytes > 0) {
sharedLeaseLost_ = true;
sent = xr_frame_io(descriptor_, reinterpret_cast<unsigned char *>(data.iov_base) + bytes,
sizeof(header) - bytes, true, deadline);
}
} else sent = xr_frame_io(descriptor_, const_cast<DroidDeckXrFrameHeader *>(&header), sizeof(header), true, deadline)
&& xr_frame_io(descriptor_, const_cast<unsigned char *>(pixels), header.bytes, true, deadline);
if (!sent
|| !xr_frame_io(descriptor_, &acknowledgement, sizeof(acknowledgement), false, deadline)
|| acknowledgement != header.frame_id) { Disconnect(); return false; }
sharedLeaseLost_ = false;
return true;
}
private:
int descriptor_ = -1;
std::string filename_;
void Disconnect() { if (descriptor_ >= 0) close(descriptor_); descriptor_ = -1; filename_.clear(); }
};
+78
View File
@@ -0,0 +1,78 @@
#include "../src/wsi_capture.cpp"
#include <cassert>
static VkResult resetResult = VK_SUCCESS;
static unsigned submitted, presented, copied, blitted;
static VkSemaphore originalWaits[2]{(VkSemaphore)10, (VkSemaphore)11};
static VkSemaphore expectedPresent;
static VkResult VKAPI_CALL Present(VkQueue, const VkPresentInfoKHR *info) {
presented++;
if (expectedPresent) assert(info->waitSemaphoreCount == 1 && info->pWaitSemaphores[0] == expectedPresent);
else assert(info->waitSemaphoreCount == 2 && info->pWaitSemaphores[0] == originalWaits[0]);
return VK_SUCCESS;
}
int main() {
char path[] = "/tmp/droiddeck-wsi-XXXXXX"; int fd = mkstemp(path); assert(fd >= 0);
DroidDeckXrTracking tracking{};
tracking.magic = DROIDDECK_XR_MAGIC; tracking.version = DROIDDECK_XR_VERSION; tracking.active = 1;
tracking.sampled_ns = xr_frame_now(); tracking.period_ns = 11111111;
for (auto &eye : tracking.eyes) {
eye.width = eye.height = 4; eye.pose.orientation[3] = 1; eye.pose.flags = 15;
eye.fov[0] = eye.fov[3] = -0.7f; eye.fov[1] = eye.fov[2] = 0.7f;
}
assert(write(fd, &tracking, sizeof(tracking)) == sizeof(tracking)); close(fd);
auto device = std::make_unique<Device>();
device->ResetCommandBuffer = [](VkCommandBuffer, VkCommandBufferResetFlags) { return resetResult; };
device->BeginCommandBuffer = [](VkCommandBuffer, const VkCommandBufferBeginInfo *) { return VK_SUCCESS; };
device->EndCommandBuffer = [](VkCommandBuffer) { return VK_SUCCESS; };
device->ResetFences = [](VkDevice, uint32_t, const VkFence *) { return VK_SUCCESS; };
device->CmdPipelineBarrier = [](VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags,
VkDependencyFlags, uint32_t, const VkMemoryBarrier *, uint32_t buffers, const VkBufferMemoryBarrier *host,
uint32_t images, const VkImageMemoryBarrier *image) {
assert(images == 1 && image->srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED);
if (buffers) assert(host->dstAccessMask == VK_ACCESS_HOST_READ_BIT);
};
device->CmdBlitImage = [](VkCommandBuffer, VkImage, VkImageLayout source, VkImage, VkImageLayout destination,
uint32_t count, const VkImageBlit *blit, VkFilter filter) {
assert(source == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && destination == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
assert(count == 1 && filter == VK_FILTER_NEAREST && blit->srcOffsets[1].x == 8); blitted++;
};
device->CmdCopyImageToBuffer = [](VkCommandBuffer, VkImage, VkImageLayout layout, VkBuffer,
uint32_t count, const VkBufferImageCopy *copy) {
assert(layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && count == 1 && copy->imageExtent.width == 8); copied++;
};
device->QueueSubmit = [](VkQueue, uint32_t count, const VkSubmitInfo *info, VkFence) {
assert(count == 1 && info->waitSemaphoreCount == 2 && info->signalSemaphoreCount == 1);
for (int i = 0; i < 2; i++) assert(info->pWaitSemaphores[i] == originalWaits[i]
&& info->pWaitDstStageMask[i] == VK_PIPELINE_STAGE_TRANSFER_BIT);
submitted++; return VK_SUCCESS;
};
device->DestroySemaphore = [](VkDevice, VkSemaphore, const VkAllocationCallbacks *) {};
VkDevice handle = (VkDevice)1; VkQueue queue = (VkQueue)2;
device->handle = handle; device->queues[queue] = 7;
auto chain = std::make_unique<Swapchain>(path);
chain->device = device.get(); chain->family = 7; chain->initialized = true; chain->extent = {8, 4};
chain->images = {(VkImage)20, (VkImage)21, (VkImage)22};
chain->ready = {(VkSemaphore)30, (VkSemaphore)31, (VkSemaphore)32};
devices[handle] = std::move(device);
VkSwapchainKHR swapchain = (VkSwapchainKHR)3; swapchains[swapchain] = std::move(chain);
VkPresentInfoKHR info{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
uint32_t index = 0; info.pImageIndices = &index; info.swapchainCount = 1; info.pSwapchains = &swapchain;
info.waitSemaphoreCount = 2; info.pWaitSemaphores = originalWaits;
for (index = 0; index < 3; index++) {
expectedPresent = (VkSemaphore)(uintptr_t)(30 + index);
assert(droiddeck_wsi_present(handle, queue, &info, Present) == VK_SUCCESS);
}
index = 0; expectedPresent = VK_NULL_HANDLE;
assert(droiddeck_wsi_present(handle, queue, &info, Present) == VK_SUCCESS);
assert(submitted == 3 && presented == 4 && copied == 3 && blitted == 3);
for (const auto &slot : swapchains[swapchain]->slots) assert(slot.busy && slot.queued && xr_frame_valid(slot.header));
swapchains[swapchain]->slots[0].busy = false;
resetResult = VK_ERROR_DEVICE_LOST;
assert(droiddeck_wsi_present(handle, queue, &info, Present) == VK_ERROR_DEVICE_LOST);
assert(droiddeck_wsi_present(handle, queue, &info, Present) == VK_ERROR_DEVICE_LOST);
assert(submitted == 3 && presented == 4);
droiddeck_wsi_destroy(handle); unlink(path);
std::puts("WSI synchronization passed: original waits retained, per-image completion, bounded in-flight buffers and fatal error propagation");
}
+2 -2
View File
@@ -43,7 +43,7 @@ int main(int argc, char **argv) {
auto valid = Header(); assert(xr_frame_valid(valid));
valid.color_space = DROIDDECK_XR_COLOR_SRGB_RGBA8; assert(xr_frame_valid(valid));
auto unknownColor = valid; unknownColor.color_space = 3; assert(!xr_frame_valid(unknownColor));
auto invalid = valid; invalid.version++; assert(!xr_frame_valid(invalid));
auto invalid = valid; invalid.version = 3; assert(!xr_frame_valid(invalid));
invalid = valid; invalid.width = 3; assert(!xr_frame_valid(invalid));
invalid = valid; invalid.bytes--; assert(!xr_frame_valid(invalid));
invalid = valid; invalid.width = 8192; invalid.height = 4096; assert(!xr_frame_valid(invalid));
@@ -111,7 +111,7 @@ int main(int argc, char **argv) {
assert(acknowledged == delayedHeader.frame_id && receiver.Take(&taken));
close(delayed);
receiver.Stop(); assert(receiver.Start(socketPath));
int descriptor = Connect(socketPath); auto header = Header(); header.version++;
int descriptor = Connect(socketPath); auto header = Header(); header.version = 3;
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
close(descriptor); WaitRejected(receiver, 1); assert(!receiver.Take(&taken));
descriptor = Connect(socketPath); header = Header(); header.captured_ns += 1000000000LL;
+67
View File
@@ -0,0 +1,67 @@
#include "../src/xr_frame_channel.h"
#include <cassert>
#include <cstdio>
#include <sys/syscall.h>
static DroidDeckXrFrameHeader Header() {
DroidDeckXrFrameHeader header{};
header.magic = DROIDDECK_XR_FRAME_MAGIC; header.version = DROIDDECK_XR_FRAME_VERSION;
header.width = 8; header.height = 4; header.bytes = 128;
header.color_space = DROIDDECK_XR_COLOR_SRGB_RGBA8; header.frame_id = 1; header.captured_ns = xr_frame_now();
for (int eye = 0; eye < 2; eye++) {
for (int axis = 0; axis < 3; axis++) header.eye_pose[eye][axis][axis] = 1;
header.projection[eye][0][0] = header.projection[eye][1][1] = 1;
header.projection[eye][3][2] = -1;
}
return header;
}
int main() {
char directory[] = "/tmp/droiddeck-shared-XXXXXX"; assert(mkdtemp(directory));
std::string path = std::string(directory) + "/frames";
int listener = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); assert(listener >= 0);
sockaddr_un address{}; address.sun_family = AF_UNIX; std::strcpy(address.sun_path, path.c_str());
assert(bind(listener, reinterpret_cast<sockaddr *>(&address), sizeof(address)) == 0);
assert(listen(listener, 1) == 0);
int buffer = syscall(SYS_memfd_create, "frame-test", 1); assert(buffer >= 0);
assert(ftruncate(buffer, 128) == 0);
unsigned char pixels[128]; std::memset(pixels, 27, sizeof(pixels));
assert(pwrite(buffer, pixels, sizeof(pixels), 0) == sizeof(pixels));
auto header = Header();
XrFrameSender sender;
std::thread peer([&] {
int client = accept(listener, nullptr, nullptr); assert(client >= 0);
DroidDeckXrFrameHeader received{}; XrFrameDescriptor shared;
assert(xr_frame_header(client, received, shared, xr_frame_now() + 1000000000LL));
assert(received.version == DROIDDECK_XR_FRAME_SHARED_VERSION && xr_frame_valid(received));
assert(shared.fd >= 0 && (fcntl(shared.fd, F_GETFD) & FD_CLOEXEC));
unsigned char copy[128]; assert(pread(shared.fd, copy, sizeof(copy), 0) == sizeof(copy));
for (auto value : copy) assert(value == 27);
assert(xr_frame_io(client, &received.frame_id, sizeof(received.frame_id), true, xr_frame_now() + 1000000000LL));
close(client);
});
assert(sender.SendShared(path, header, buffer)); peer.join();
assert(!sender.SharedLeaseLost());
XrFrameSender lost;
std::thread disconnected([&] {
int client = accept(listener, nullptr, nullptr); assert(client >= 0);
DroidDeckXrFrameHeader received{}; XrFrameDescriptor shared;
assert(xr_frame_header(client, received, shared, xr_frame_now() + 1000000000LL));
assert(shared.fd >= 0);
close(client);
});
assert(!lost.SendShared(path, header, buffer)); disconnected.join();
assert(lost.SharedLeaseLost());
assert(!lost.SendShared(path, header, buffer));
close(listener); unlink(path.c_str());
XrFrameSender absent;
assert(!absent.SendShared(path, header, buffer)); assert(!absent.SharedLeaseLost());
XrFrameReceiver receiver; assert(receiver.Start(path));
XrFrameSender invalid;
// An ordinary file is not a DMA-BUF and must never be acknowledged as one.
assert(!invalid.SendShared(path, Header(), buffer));
assert(invalid.SharedLeaseLost() && receiver.Rejected() == 1);
DroidDeckXrPixels frame; assert(!receiver.Take(&frame));
receiver.Stop(); close(buffer); unlink((path + ".lock").c_str()); rmdir(directory);
std::puts("XR shared handoff passed: descriptor ownership, copied payload, acknowledgement, disconnect quarantine, absent peer and invalid buffer rejection");
}