perf: share Quest frames through GPU buffers

This commit is contained in:
XxJSONDeruloxX committed 2026-10-15 20:25:21 -04:00
1 parent d8de6895c1
commit e9a199285e
9 files changed
+585 -59

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
#pragma once
#include <EGL/eglext.h>
#include <GLES3/gl31.h>
#include <GLES2/gl2ext.h>
#include <android/hardware_buffer.h>
#include "xr_gpu_channel.h"
class QuestGpuFrames {
public:
bool Start(const std::string &filename, EGLDisplay display) {
const char *extensions = reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS));
const char *eglExtensions = eglQueryString(display, EGL_EXTENSIONS);
if (!extensions || !eglExtensions || !strstr(extensions, "GL_EXT_external_buffer")
|| !strstr(eglExtensions, "EGL_ANDROID_get_native_client_buffer")) return false;
storage_ = reinterpret_cast<PFNGLBUFFERSTORAGEEXTERNALEXTPROC>(eglGetProcAddress("glBufferStorageExternalEXT"));
client_ = reinterpret_cast<PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC>(eglGetProcAddress("eglGetNativeClientBufferANDROID"));
if (strstr(extensions, "GL_OES_texture_view")) view_ = reinterpret_cast<PFNGLTEXTUREVIEWOESPROC>(eglGetProcAddress("glTextureViewOES"));
else if (strstr(extensions, "GL_EXT_texture_view")) view_ = reinterpret_cast<PFNGLTEXTUREVIEWOESPROC>(eglGetProcAddress("glTextureViewEXT"));
if (!storage_ || !client_ || !view_ || filename.size() >= sizeof(sockaddr_un::sun_path)) return false;
const char *source = "#version 310 es\nlayout(local_size_x=8,local_size_y=8)in;"
"layout(std430,binding=0)readonly buffer Source{uint pixels[];};"
"layout(rgba8,binding=0)writeonly uniform highp image2D target;uniform bool bgra;"
"void main(){ivec2 p=ivec2(gl_GlobalInvocationID.xy),size=imageSize(target);"
"if(any(greaterThanEqual(p,size)))return;vec4 value=unpackUnorm4x8(pixels[p.y*size.x+p.x]);"
"imageStore(target,p,bgra?value.bgra:value);}";
GLuint shader = glCreateShader(GL_COMPUTE_SHADER); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader);
program_ = glCreateProgram(); glAttachShader(program_, shader); glLinkProgram(program_); glDeleteShader(shader);
GLint linked{}; glGetProgramiv(program_, GL_LINK_STATUS, &linked);
if (!linked) { Stop(); return false; }
lock_ = open((filename + ".lock").c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
struct stat status{};
if (lock_ < 0 || fstat(lock_, &status) || !S_ISREG(status.st_mode) || status.st_uid != getuid()
|| flock(lock_, LOCK_EX | LOCK_NB)) { Stop(); return false; }
if (lstat(filename.c_str(), &status) == 0) {
if (!S_ISSOCK(status.st_mode) || status.st_uid != getuid() || unlink(filename.c_str())) { Stop(); return false; }
} else if (errno != ENOENT) { Stop(); return false; }
listener_ = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
sockaddr_un address{}; address.sun_family = AF_UNIX; strcpy(address.sun_path, filename.c_str());
if (listener_ < 0 || bind(listener_, reinterpret_cast<sockaddr *>(&address), sizeof(address))) { Stop(); return false; }
filename_ = filename;
if (chmod(filename.c_str(), 0600) || listen(listener_, 2)) { Stop(); return false; }
return true;
}
bool Take(DroidDeckXrFrameHeader &header, GLuint &texture) {
if (listener_ < 0) return false;
if (peer_ < 0) {
peer_ = accept4(listener_, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
if (peer_ < 0) return false;
if (!xr_frame_peer(peer_)) { Disconnect(); return false; }
configured_ = false; deadline_ = xr_frame_now() + 2000000000LL;
}
pollfd ready{peer_, POLLIN, 0};
if (poll(&ready, 1, 0) <= 0) {
if (!configured_ && xr_frame_now() > deadline_) Disconnect();
return false;
}
if (!(ready.revents & POLLIN)) { Disconnect(); return false; }
if (!configured_) {
XrGpuRequest request;
if (!xr_gpu_receive(peer_, &request, sizeof(request)) || request.magic != XR_GPU_MAGIC
|| request.version != XR_GPU_VERSION || !xr_gpu_dimensions(request.width, request.height)
|| !Allocate(request.width, request.height)) { Disconnect(); return false; }
XrGpuPoolInfo info; info.width = width_; info.height = height_;
for (unsigned index = 0; index < XR_GPU_SLOTS; index++) info.sizes[index] = sizes_[index];
configured_ = xr_gpu_send(peer_, &info, sizeof(info), fds_.data(), fds_.size());
for (int &fd : fds_) { if (fd >= 0) close(fd); fd = -1; }
if (!configured_) Disconnect();
return false;
}
XrGpuFrame frame;
if (!xr_gpu_receive(peer_, &frame, sizeof(frame))) { Disconnect(); return false; }
auto state = xr_gpu_frame_state(frame, width_, height_, lastFrame_, xr_frame_now());
if (state == XrGpuFrameState::Invalid) {
rejected_++; Disconnect(); return false;
}
lastFrame_ = frame.header.frame_id;
if (state == XrGpuFrameState::Stale) {
// No GPU read was submitted, so the producer can safely reclaim a dropped frame.
dropped_++;
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
return false;
}
if (!Output()) { Disconnect(); return false; }
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(program_); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffers_[frame.slot]);
glUniform1i(glGetUniformLocation(program_, "bgra"), !!(frame.flags & XR_GPU_BGRA));
glBindImageTexture(0, raw_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glDispatchCompute((width_ + 7) / 8, (height_ + 7) / 8, 1);
glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
// The producer may overwrite this slot only after the native GPU has finished reading it.
glFinish();
if (glGetError() != GL_NO_ERROR) { rejected_++; Disconnect(); return false; }
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
received_++;
header = frame.header;
texture = header.color_space == DROIDDECK_XR_COLOR_SRGB_RGBA8 ? srgb_ : raw_;
return true;
}
uint64_t Received() const { return received_; }
uint64_t Rejected() const { return rejected_; }
uint64_t Dropped() const { return dropped_; }
void Stop() {
Disconnect(); DropPool();
glDeleteTextures(1, &srgb_); glDeleteTextures(1, &raw_); srgb_ = raw_ = 0;
if (program_) glDeleteProgram(program_); program_ = 0;
if (listener_ >= 0) close(listener_); listener_ = -1;
if (!filename_.empty()) unlink(filename_.c_str()); filename_.clear();
if (lock_ >= 0) close(lock_); lock_ = -1;
}
private:
int listener_ = -1, peer_ = -1, lock_ = -1;
std::string filename_;
bool configured_ = false;
int64_t deadline_{};
uint32_t width_{}, height_{}, outputWidth_{}, outputHeight_{};
uint64_t lastFrame_{}, received_{}, rejected_{}, dropped_{};
std::array<AHardwareBuffer *, XR_GPU_SLOTS> hardware_{};
std::array<GLuint, XR_GPU_SLOTS> buffers_{};
std::array<int, XR_GPU_SLOTS> fds_{{-1, -1, -1}};
std::array<uint64_t, XR_GPU_SLOTS> sizes_{};
GLuint raw_{}, srgb_{}, program_{};
PFNGLBUFFERSTORAGEEXTERNALEXTPROC storage_{};
PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC client_{};
PFNGLTEXTUREVIEWOESPROC view_{};
void Disconnect() { if (peer_ >= 0) close(peer_); peer_ = -1; configured_ = false; lastFrame_ = 0; }
void DropPool() {
if (buffers_[0]) glFinish();
glDeleteBuffers(buffers_.size(), buffers_.data()); buffers_.fill(0);
for (auto *&buffer : hardware_) { if (buffer) AHardwareBuffer_release(buffer); buffer = nullptr; }
for (int &fd : fds_) { if (fd >= 0) close(fd); fd = -1; }
}
static int AllocationFd(AHardwareBuffer *buffer, uint64_t bytes, uint64_t &allocation) {
int pair[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair)) return -1;
if (AHardwareBuffer_sendHandleToUnixSocket(buffer, pair[0])) { close(pair[0]); close(pair[1]); return -1; }
close(pair[0]);
int selected = -1; unsigned candidates = 0;
for (;;) {
unsigned char data[1024]; iovec bytesReceived{data, sizeof(data)};
alignas(cmsghdr) unsigned char control[CMSG_SPACE(32 * sizeof(int))]{};
msghdr message{}; message.msg_iov = &bytesReceived; message.msg_iovlen = 1;
message.msg_control = control; message.msg_controllen = sizeof(control);
ssize_t result = recvmsg(pair[1], &message, MSG_CMSG_CLOEXEC);
if (result < 0 && errno == EINTR) continue;
if (result <= 0) break;
for (auto *item = CMSG_FIRSTHDR(&message); item; item = CMSG_NXTHDR(&message, item)) {
if (item->cmsg_level != SOL_SOCKET || item->cmsg_type != SCM_RIGHTS) continue;
auto *fds = reinterpret_cast<int *>(CMSG_DATA(item));
for (unsigned index = 0; index < (item->cmsg_len - CMSG_LEN(0)) / sizeof(int); index++) {
struct stat status{};
if (!fstat(fds[index], &status) && status.st_size >= static_cast<int64_t>(bytes)
&& status.st_size <= static_cast<int64_t>(bytes + 65536)) {
candidates++; if (selected >= 0) close(selected);
selected = fds[index]; allocation = status.st_size;
} else close(fds[index]);
}
}
if (message.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) { candidates = 0; break; }
}
close(pair[1]);
if (candidates != 1) { if (selected >= 0) close(selected); return -1; }
return selected;
}
bool Allocate(uint32_t width, uint32_t height) {
DropPool(); width_ = width; height_ = height;
const uint64_t bytes = uint64_t(width) * height * 4;
GLint64 limit{}; glGetInteger64v(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &limit);
if (bytes > static_cast<uint64_t>(limit)) return false;
AHardwareBuffer_Desc description{}; description.width = bytes; description.height = description.layers = 1;
description.format = AHARDWAREBUFFER_FORMAT_BLOB;
description.usage = AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER;
glGenBuffers(buffers_.size(), buffers_.data());
for (unsigned index = 0; index < XR_GPU_SLOTS; index++) {
if (AHardwareBuffer_allocate(&description, &hardware_[index])) { DropPool(); return false; }
fds_[index] = AllocationFd(hardware_[index], bytes, sizes_[index]);
if (fds_[index] < 0) { DropPool(); return false; }
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers_[index]);
storage_(GL_SHADER_STORAGE_BUFFER, 0, bytes, client_(hardware_[index]), 0);
if (glGetError() != GL_NO_ERROR) { DropPool(); return false; }
}
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
return true;
}
bool Output() {
if (raw_ && outputWidth_ == width_ && outputHeight_ == height_) return true;
glDeleteTextures(1, &srgb_); glDeleteTextures(1, &raw_); srgb_ = raw_ = 0;
glGenTextures(1, &raw_); glBindTexture(GL_TEXTURE_2D, raw_);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width_, height_);
glGenTextures(1, &srgb_); view_(srgb_, GL_TEXTURE_2D, raw_, GL_SRGB8_ALPHA8, 0, 1, 0, 1);
for (GLuint texture : {raw_, srgb_}) {
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
outputWidth_ = width_; outputHeight_ = height_;
return glGetError() == GL_NO_ERROR;
}
};
+18 -3
View File
@@ -25,6 +25,7 @@
#include "xr_tracking.h"
#include "xr_frame_channel.h"
#include "xr_frame_math.h"
#include "gpu_frames.h"
static void check(XrResult result, const char *operation) {
if (XR_FAILED(result)) throw std::runtime_error(std::string(operation) + ": " + std::to_string(result));
@@ -75,14 +76,16 @@ public:
"\"headPosition\":[%.6f,%.6f,%.6f],\"controllersActive\":[%s,%s],"
"\"periodNs\":%lld,\"surfaceSwapchain\":%s,\"source\":\"%s\",\"guestFrames\":%lld,\"steamvrState\":%d,"
"\"receivedFrames\":%llu,\"rejectedFrames\":%llu,\"uploadedFrames\":%llu,\"sourceFrameId\":%llu,"
"\"gpuVerifiedFrameId\":%llu,\"gpuChecksum\":%u,\"error\":\"%s\"}\n",
"\"gpuReceivedFrames\":%llu,\"gpuDroppedFrames\":%llu,\"gpuVerifiedFrameId\":%llu,\"gpuChecksum\":%u,\"error\":\"%s\"}\n",
(unsigned long long)frames_, (unsigned long long)rendered_, state_,
(unsigned long long)head_.locationFlags,
head_.pose.position.x, head_.pose.position.y, head_.pose.position.z,
handActive_[0] ? "true" : "false", handActive_[1] ? "true" : "false",
(long long)period_, "false", "frames",
0LL, steamvrState, (unsigned long long)receiver_.Received(), (unsigned long long)receiver_.Rejected(),
0LL, steamvrState, (unsigned long long)(receiver_.Received() + gpuFrames_.Received()),
(unsigned long long)(receiver_.Rejected() + gpuFrames_.Rejected()),
(unsigned long long)uploadedFrames_, (unsigned long long)sourceHeader_.frame_id,
(unsigned long long)gpuFrames_.Received(), (unsigned long long)gpuFrames_.Dropped(),
(unsigned long long)verifiedFrame_, gpuChecksum_, error);
std::fclose(file);
std::rename(temp.c_str(), path.c_str());
@@ -114,10 +117,12 @@ private:
bool running_ = false, exit_ = false, quitRequested_ = false;
bool guestAttached_ = false, verifyPixels_ = false;
XrFrameReceiver receiver_;
QuestGpuFrames gpuFrames_;
DroidDeckXrFrameHeader sourceHeader_{};
uint64_t uploadedFrames_ = 0, verifiedFrame_ = 0;
uint32_t gpuChecksum_ = 0;
GLuint sourceTexture_ = 0, sourceProgram_ = 0;
GLuint cpuTexture_ = 0;
jmethodID frameCallback_ = nullptr;
jmethodID activeCallback_ = nullptr;
jmethodID statusCallback_ = nullptr;
@@ -326,6 +331,8 @@ private:
{
std::string filename = std::string(app_->activity->internalDataPath) + "/.wayland-rt/" DROIDDECK_XR_FRAME_SOCKET;
if (!receiver_.Start(filename)) throw std::runtime_error("XR frame receiver unavailable");
bool gpu = !verifyPixels_ && gpuFrames_.Start(std::string(app_->activity->internalDataPath) + "/.wayland-rt/" DROIDDECK_XR_GPU_SOCKET, display_);
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "GPU frame channel available=%d", gpu);
}
health();
}
@@ -533,6 +540,7 @@ private:
GLint linked = 0; glGetProgramiv(sourceProgram_, GL_LINK_STATUS, &linked);
if (!linked) throw std::runtime_error("Frame shader link failed");
glGenTextures(1, &sourceTexture_); glBindTexture(GL_TEXTURE_2D, sourceTexture_);
cpuTexture_ = sourceTexture_;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
@@ -540,11 +548,17 @@ private:
}
void receiveFrame() {
if (gpuFrames_.Take(sourceHeader_, sourceTexture_)) {
uploadedFrames_++;
if (uploadedFrames_ % 72 == 1) health();
return;
}
DroidDeckXrPixels frame;
if (!receiver_.Take(&frame)) return;
GLint maximum = 0; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum);
if (frame.header.width > static_cast<uint32_t>(maximum) || frame.header.height > static_cast<uint32_t>(maximum))
throw std::runtime_error("XR source exceeds GPU texture limit");
sourceTexture_ = cpuTexture_;
glBindTexture(GL_TEXTURE_2D, sourceTexture_);
GLenum encoding = frame.header.color_space == DROIDDECK_XR_COLOR_SRGB_RGBA8 ? GL_SRGB8_ALPHA8 : GL_RGBA8;
glTexImage2D(GL_TEXTURE_2D, 0, encoding, frame.header.width, frame.header.height, 0,
@@ -723,6 +737,7 @@ private:
void cleanup() {
receiver_.Stop();
gpuFrames_.Stop();
try { attachGuest(false); } catch (const std::exception &error) {
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "cleanup: %s", error.what());
}
@@ -733,7 +748,7 @@ private:
if (haptics_) munmap(haptics_, sizeof(DroidDeckXrHaptics));
if (display_ != EGL_NO_DISPLAY && context_ != EGL_NO_CONTEXT) {
glDeleteFramebuffers(1, &framebuffer_); glDeleteVertexArrays(1, &vertexArray_);
glDeleteTextures(1, &sourceTexture_); glDeleteProgram(sourceProgram_);
glDeleteTextures(1, &cpuTexture_); glDeleteProgram(sourceProgram_);
}
for (auto &eye : eyes_) if (eye.swapchain) xrDestroySwapchain(eye.swapchain);
for (auto space : handSpaces_) if (space) xrDestroySpace(space);
+26 -6
View File
@@ -89,12 +89,22 @@ started within a millimetre of zero while standing and raw height stayed unchang
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.
allow rendering and transfer to overlap. Where native GLES supports
`GL_EXT_external_buffer` and texture views, Android allocates three
`AHardwareBuffer` BLOBs and passes their DMA-BUF allocations to Linux through
`SCM_RIGHTS`. Vulkan imports them as buffers and transfers foreign queue-family
ownership before and after copying the display image. A native compute shader
reads each shared buffer into a private RGBA8 texture; an sRGB texture view
preserves hardware transfer decoding. Native GPU completion precedes the
acknowledgement, so Linux cannot overwrite an active reader. This removes the CPU
pixel copy and upload, but still includes GPU copies and explicit completion waits.
If capability negotiation or buffer import fails before frames are submitted,
the existing CPU transport remains available. It brackets the DMA-BUF read with
`DMA_BUF_IOCTL_SYNC`, copies to owned pixels, then acknowledges and uploads them.
Both channels authenticate the peer UID and bound buffer sizes. Missing
acknowledgements after submission quarantine the transfer and fail presentation
instead of falling back while a reader might still own a buffer.
The helper temporarily enables SteamVR's Linux Vulkan asynchronous queue and disables motion
smoothing, backing up those preferences and restoring them on stop or the next start
@@ -147,6 +157,16 @@ focused tracking and no rejected frames. These dashboard results do not establis
sustained game performance, and exact association with SteamVR's final compositor
pose still require validation.
The native GPU-buffer channel passed 256 full-size buffer reuse cycles, including
head/tail pixel checks and sRGB texture-view decoding. A subsequent SUPERHOT
scene run delivered about 43 new stereo images/s and 58 XR draws/s over one
minute, with focused tracking and no rejected frames. The earlier CPU-path
SUPERHOT run uploaded about 28 images/s; these are separate runs, not a controlled
benchmark. Capturing the source and eye images can delay frame consumption;
the GPU receiver acknowledges stale frames without reading them and keeps the
last private image until a fresh frame arrives. Missing completion still fails
closed. `frame_verify` explicitly selects the CPU path for byte comparison.
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
+1 -1
View File
@@ -14,7 +14,7 @@ class VrWsiTest(unittest.TestCase):
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'):
for name in ('test_wsi_capture', 'test_xr_shared', 'test_xr_frames', 'test_xr_gpu'):
with self.subTest(name=name):
executable = Path(directory) / name
subprocess.run(['g++', '-std=c++17', '-O2', '-pthread', '-Wall', '-Wextra', '-Werror',
+26 -4
View File
@@ -242,17 +242,38 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat
const int display = in && in->stripped;
VkDeviceCreateInfo mine = *ci;
const char **names = calloc(ci->enabledExtensionCount + 1, sizeof(*names));
const char **names = calloc(ci->enabledExtensionCount + 4, sizeof(*names));
if (!names) return VK_ERROR_OUT_OF_HOST_MEMORY;
uint32_t n = 0;
for (uint32_t i = 0; i < ci->enabledExtensionCount; i++) {
if (display && !strcmp(ci->ppEnabledExtensionNames[i], "VK_EXT_display_control")) { say("taking %s off the device", ci->ppEnabledExtensionNames[i]); continue; }
names[n++] = ci->ppEnabledExtensionNames[i];
}
#ifdef DROIDDECK_WSI_CAPTURE
const char *present_mode = getenv("DROIDDECK_VR_PRESENT");
if (present_mode && !strcmp(present_mode, "questwsi") && !strcmp(program_invocation_short_name, "vrcompositor")) {
PFN_vkEnumerateDeviceExtensionProperties enumerate = (PFN_vkEnumerateDeviceExtensionProperties)
next_gipa(in ? in->handle : VK_NULL_HANDLE, "vkEnumerateDeviceExtensionProperties");
uint32_t count = 0;
if (enumerate && enumerate(pd, NULL, &count, NULL) == VK_SUCCESS && count) {
VkExtensionProperties *supported = calloc(count, sizeof(*supported));
if (supported && enumerate(pd, NULL, &count, supported) == VK_SUCCESS) {
const char *required[] = {"VK_KHR_external_memory_fd", "VK_EXT_external_memory_dma_buf", "VK_EXT_queue_family_foreign"};
for (unsigned j = 0; j < 3; j++) {
int enabled = 0, available = 0;
for (uint32_t k = 0; k < n; k++) if (!strcmp(names[k], required[j])) enabled = 1;
for (uint32_t k = 0; k < count; k++) if (!strcmp(supported[k].extensionName, required[j])) available = 1;
if (!enabled && available) names[n++] = required[j];
}
}
free(supported);
}
}
#endif
mine.enabledExtensionCount = n;
mine.ppEnabledExtensionNames = names;
VkResult r = next_create(pd, &mine, alloc, out);
free(names);
if (r != VK_SUCCESS) return r;
if (r != VK_SUCCESS) { free(names); return r; }
pthread_mutex_lock(&g_lock);
for (int i = 0; i < MAX_OBJS; i++) {
if (!g_dev[i].handle) {
@@ -263,8 +284,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);
if (in) droiddeck_wsi_device(*out, pd, in->handle, in->gipa, next_gdpa, &mine);
#endif
free(names);
return VK_SUCCESS;
}
+96 -43
View File
@@ -2,6 +2,7 @@
#include "wsi_capture.h"
#include "xr_tracking_file.h"
#include "xr_frame_channel.h"
#include "xr_gpu_channel.h"
#include <algorithm>
#include <cstdio>
#include <memory>
@@ -21,6 +22,8 @@ namespace {
struct Device {
VkDevice handle{};
VkPhysicalDeviceMemoryProperties memory{};
bool foreign{};
PFN_vkGetMemoryFdPropertiesKHR GetMemoryFdPropertiesKHR{};
std::unordered_map<VkQueue, uint32_t> queues;
#define MEMBER(name) PFN_vk##name name{};
FUNCTIONS(MEMBER)
@@ -40,7 +43,8 @@ struct CaptureSlot {
VkFence fence{};
bool busy{}, queued{};
DroidDeckXrFrameHeader header{};
~CaptureSlot() {
~CaptureSlot() { Clear(); }
void Clear() {
if (!device) return;
if (busy && fence) device->WaitForFences(device->handle, 1, &fence, VK_TRUE, UINT64_MAX);
if (sharedFd >= 0) close(sharedFd);
@@ -51,8 +55,11 @@ struct CaptureSlot {
if (rgbaMemory) device->FreeMemory(device->handle, rgbaMemory, nullptr);
if (mapping) device->UnmapMemory(device->handle, memory);
if (memory) device->FreeMemory(device->handle, memory, nullptr);
device = nullptr; pool = VK_NULL_HANDLE; command = VK_NULL_HANDLE; buffer = VK_NULL_HANDLE;
memory = VK_NULL_HANDLE; rgbaImage = VK_NULL_HANDLE; rgbaMemory = VK_NULL_HANDLE;
sharedFd = -1; mapping = nullptr; fence = VK_NULL_HANDLE; busy = queued = rgbaInitialized = false;
}
bool Initialize(Device &d, VkExtent2D extent, VkFormat format, uint32_t family) {
bool Initialize(Device &d, VkExtent2D extent, VkFormat format, uint32_t family, int imported = -1, uint64_t size = 0) {
device = &d;
VkCommandPoolCreateInfo pc{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pc.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pc.queueFamilyIndex = family;
@@ -66,10 +73,17 @@ struct CaptureSlot {
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);
VkImportMemoryFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR};
VkMemoryFdPropertiesKHR fdProperties{VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
if (imported >= 0) {
if (!d.GetMemoryFdPropertiesKHR || size < requirements.size
|| d.GetMemoryFdPropertiesKHR(d.handle, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, imported, &fdProperties)) return false;
requirements.memoryTypeBits &= fdProperties.memoryTypeBits;
}
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;
if (!(requirements.memoryTypeBits & (1u << i)) || (imported < 0 && !(flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT))) continue;
type = i;
if (flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) break;
}
@@ -78,26 +92,36 @@ struct CaptureSlot {
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;
if (imported >= 0) {
import.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; import.fd = dup(imported);
if (import.fd < 0) return false;
alloc.pNext = &import; alloc.allocationSize = size;
}
VkResult allocated = d.AllocateMemory(d.handle, &alloc, nullptr, &memory);
if (allocated && import.fd >= 0 && imported >= 0) close(import.fd);
if (allocated || d.BindBufferMemory(d.handle, buffer, memory, 0)) return false;
if (imported < 0) {
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;
}
if (imported < 0) {
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;
@@ -114,6 +138,8 @@ struct Swapchain {
uint64_t frame{};
XrTrackingFile tracking;
XrFrameSender sender;
XrGpuSender gpuSender;
bool gpu{};
bool initialized{}, attempted{};
uint32_t family{};
std::mutex sendMutex;
@@ -154,19 +180,22 @@ struct Swapchain {
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);
if (result == VK_SUCCESS && !gpu) 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 (gpu) {
uint32_t flags = (format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB) ? XR_GPU_BGRA : 0;
if (!gpuSender.Send(header, static_cast<uint32_t>(slot - slots.data()), flags)) result = VK_ERROR_DEVICE_LOST;
} else 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;
}
slot->busy = false;
}
}
bool Initialize(VkSwapchainKHR chain, VkQueue queue) {
@@ -184,10 +213,21 @@ struct Swapchain {
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;
XrGpuPoolInfo info;
std::array<int, XR_GPU_SLOTS> fds{{-1, -1, -1}};
gpu = d.foreign && gpuSender.Connect("/run/droiddeck/" DROIDDECK_XR_GPU_SOCKET, extent.width, extent.height, info, fds);
if (gpu) {
for (unsigned index = 0; index < slots.size(); index++)
if (!slots[index].Initialize(d, extent, format, family, fds[index], info.sizes[index])) { gpu = false; break; }
for (int fd : fds) if (fd >= 0) close(fd);
}
if (!gpu) {
gpuSender.Close();
for (auto &slot : slots) { slot.Clear(); 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);
std::fprintf(stderr, "[droiddeck-vr-layer] capturing compositor swapchain %ux%u format %d channel=%s\n", extent.width, extent.height, format, gpu ? "gpu" : "cpu");
return true;
}
VkResult Fail(VkResult result, CaptureSlot &slot) {
@@ -251,26 +291,36 @@ struct Swapchain {
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);
if (!gpu) {
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);
}
VkBufferMemoryBarrier ownership{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
ownership.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; ownership.dstQueueFamilyIndex = family;
ownership.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; ownership.buffer = buffer; ownership.size = VK_WHOLE_SIZE;
if (gpu) d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 1, &ownership, 0, nullptr);
d.CmdCopyImageToBuffer(command, gpu ? images[index] : 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,
if (gpu) {
host.dstAccessMask = 0; host.srcQueueFamilyIndex = family; host.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
}
d.CmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, (gpu ? 0 : 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);
@@ -295,6 +345,9 @@ extern "C" void droiddeck_wsi_device(VkDevice device, VkPhysicalDevice physical,
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;
d->GetMemoryFdPropertiesKHR = reinterpret_cast<PFN_vkGetMemoryFdPropertiesKHR>(gdpa(device, "vkGetMemoryFdPropertiesKHR"));
for (uint32_t index = 0; index < create->enabledExtensionCount; index++)
if (!strcmp(create->ppEnabledExtensionNames[index], "VK_EXT_queue_family_foreign")) d->foreign = true;
#define LOAD(name) d->name = reinterpret_cast<PFN_vk##name>(gdpa(device, "vk" #name)); if (!d->name) return;
FUNCTIONS(LOAD)
#undef LOAD
+118
View File
@@ -0,0 +1,118 @@
#pragma once
#include "xr_frame_channel.h"
#include <array>
#define DROIDDECK_XR_GPU_SOCKET "droiddeck-xr-gpu"
constexpr uint32_t XR_GPU_MAGIC = 0x47584444u;
constexpr uint32_t XR_GPU_VERSION = 1;
constexpr unsigned XR_GPU_SLOTS = 3;
constexpr uint32_t XR_GPU_BGRA = 1;
struct XrGpuRequest { uint32_t magic = XR_GPU_MAGIC, version = XR_GPU_VERSION, width{}, height{}; };
struct XrGpuPoolInfo {
uint32_t magic = XR_GPU_MAGIC, version = XR_GPU_VERSION, width{}, height{};
uint64_t sizes[XR_GPU_SLOTS]{};
};
struct XrGpuFrame { DroidDeckXrFrameHeader header{}; uint32_t slot{}, flags{}; };
static_assert(sizeof(XrGpuRequest) == 16 && sizeof(XrGpuPoolInfo) == 40 && sizeof(XrGpuFrame) == 272,
"XR GPU protocol layout");
inline bool xr_gpu_dimensions(uint32_t width, uint32_t height) {
return width >= 2 && !(width & 1) && height && width <= 8192 && height <= 4096
&& uint64_t(width) * height * 4 <= DROIDDECK_XR_FRAME_MAX_BYTES;
}
enum class XrGpuFrameState { Invalid, Stale, Ready };
inline XrGpuFrameState xr_gpu_frame_state(const XrGpuFrame &frame, uint32_t width, uint32_t height,
uint64_t previous, int64_t now) {
if (frame.slot >= XR_GPU_SLOTS || (frame.flags & ~XR_GPU_BGRA) || !xr_frame_valid(frame.header)
|| frame.header.width != width || frame.header.height != height || frame.header.frame_id <= previous
|| frame.header.captured_ns > now + 100000000LL) return XrGpuFrameState::Invalid;
return now - frame.header.captured_ns > 200000000LL ? XrGpuFrameState::Stale : XrGpuFrameState::Ready;
}
// SEQPACKET keeps descriptor transfer and its metadata in one indivisible message.
inline bool xr_gpu_send(int socket, const void *data, size_t size, const int *fds = nullptr, unsigned count = 0) {
if (count > XR_GPU_SLOTS) return false;
iovec bytes{const_cast<void *>(data), size};
alignas(cmsghdr) unsigned char control[CMSG_SPACE(XR_GPU_SLOTS * sizeof(int))]{};
msghdr message{}; message.msg_iov = &bytes; message.msg_iovlen = 1;
if (count) {
message.msg_control = control; message.msg_controllen = CMSG_SPACE(count * sizeof(int));
auto *item = CMSG_FIRSTHDR(&message); item->cmsg_level = SOL_SOCKET; item->cmsg_type = SCM_RIGHTS;
item->cmsg_len = CMSG_LEN(count * sizeof(int)); std::memcpy(CMSG_DATA(item), fds, count * sizeof(int));
}
ssize_t result;
do { result = sendmsg(socket, &message, MSG_NOSIGNAL | MSG_DONTWAIT); } while (result < 0 && errno == EINTR);
return result == static_cast<ssize_t>(size);
}
inline bool xr_gpu_receive(int socket, void *data, size_t size, int *fds = nullptr, unsigned count = 0) {
if (count > XR_GPU_SLOTS) return false;
iovec bytes{data, size};
alignas(cmsghdr) unsigned char control[CMSG_SPACE(16 * sizeof(int))]{};
msghdr message{}; message.msg_iov = &bytes; message.msg_iovlen = 1;
message.msg_control = control; message.msg_controllen = sizeof(control);
ssize_t result;
do { result = recvmsg(socket, &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC); } while (result < 0 && errno == EINTR);
unsigned received = 0;
std::array<int, XR_GPU_SLOTS> owned{{-1, -1, -1}};
for (auto *item = CMSG_FIRSTHDR(&message); result >= 0 && item; item = CMSG_NXTHDR(&message, item)) {
if (item->cmsg_level != SOL_SOCKET || item->cmsg_type != SCM_RIGHTS) continue;
auto *handles = reinterpret_cast<int *>(CMSG_DATA(item));
for (unsigned index = 0; index < (item->cmsg_len - CMSG_LEN(0)) / sizeof(int); index++) {
if (received < owned.size()) owned[received] = handles[index]; else close(handles[index]);
received++;
}
}
bool valid = result == static_cast<ssize_t>(size) && !(message.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) && received == count;
for (unsigned index = 0; index < owned.size(); index++) {
if (valid && index < count) fds[index] = owned[index];
else if (owned[index] >= 0) close(owned[index]);
}
return valid;
}
inline bool xr_gpu_wait(int socket, int timeout) {
pollfd ready{socket, POLLIN, 0};
int result;
do { result = poll(&ready, 1, timeout); } while (result < 0 && errno == EINTR);
return result > 0 && (ready.revents & POLLIN);
}
class XrGpuSender {
public:
~XrGpuSender() { Close(); }
bool Connect(const char *filename, uint32_t width, uint32_t height, XrGpuPoolInfo &pool,
std::array<int, XR_GPU_SLOTS> &fds) {
Close();
if (!xr_gpu_dimensions(width, height) || !filename || strlen(filename) >= sizeof(sockaddr_un::sun_path)) return false;
socket_ = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (socket_ < 0) return false;
sockaddr_un address{}; address.sun_family = AF_UNIX; std::strcpy(address.sun_path, filename);
if (connect(socket_, reinterpret_cast<sockaddr *>(&address), sizeof(address)) || !xr_frame_peer(socket_)) { Close(); return false; }
XrGpuRequest request; request.width = width; request.height = height;
if (!xr_gpu_send(socket_, &request, sizeof(request)) || !xr_gpu_wait(socket_, 2000)
|| !xr_gpu_receive(socket_, &pool, sizeof(pool), fds.data(), fds.size())) { Close(); return false; }
bool valid = pool.magic == XR_GPU_MAGIC && pool.version == XR_GPU_VERSION && pool.width == width && pool.height == height;
const uint64_t bytes = uint64_t(width) * height * 4;
for (unsigned index = 0; index < fds.size(); index++) {
struct stat status{};
if (fstat(fds[index], &status) || pool.sizes[index] < bytes || pool.sizes[index] > bytes + 65536
|| status.st_size != static_cast<int64_t>(pool.sizes[index])) valid = false;
}
if (!valid) { for (int &fd : fds) { close(fd); fd = -1; } Close(); }
return valid;
}
bool Send(const DroidDeckXrFrameHeader &header, uint32_t slot, uint32_t flags = 0) {
if (socket_ < 0 || slot >= XR_GPU_SLOTS || (flags & ~XR_GPU_BGRA) || !xr_frame_valid(header)) return false;
XrGpuFrame frame; frame.header = header; frame.slot = slot; frame.flags = flags;
uint64_t ack{};
if (!xr_gpu_send(socket_, &frame, sizeof(frame)) || !xr_gpu_wait(socket_, 5000)
|| !xr_gpu_receive(socket_, &ack, sizeof(ack)) || ack != header.frame_id) { Close(); return false; }
return true;
}
void Close() { if (socket_ >= 0) close(socket_); socket_ = -1; }
private:
int socket_ = -1;
};
+19 -2
View File
@@ -3,6 +3,8 @@
static VkResult resetResult = VK_SUCCESS;
static unsigned submitted, presented, copied, blitted;
static bool gpuTest;
static unsigned acquired, released;
static VkSemaphore originalWaits[2]{(VkSemaphore)10, (VkSemaphore)11};
static VkSemaphore expectedPresent;
static VkResult VKAPI_CALL Present(VkQueue, const VkPresentInfoKHR *info) {
@@ -30,8 +32,18 @@ int main() {
device->CmdPipelineBarrier = [](VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags,
VkDependencyFlags, uint32_t, const VkMemoryBarrier *, uint32_t buffers, const VkBufferMemoryBarrier *host,
uint32_t images, const VkImageMemoryBarrier *image) {
if (gpuTest && !images) {
assert(buffers == 1 && host->srcQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT
&& host->dstQueueFamilyIndex == 7 && host->dstAccessMask == VK_ACCESS_TRANSFER_WRITE_BIT);
acquired++; return;
}
assert(images == 1 && image->srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED);
if (buffers) assert(host->dstAccessMask == VK_ACCESS_HOST_READ_BIT);
if (buffers) {
if (gpuTest) {
assert(host->srcQueueFamilyIndex == 7 && host->dstQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT
&& !host->dstAccessMask); released++;
} else 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) {
@@ -68,11 +80,16 @@ int main() {
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));
gpuTest = swapchains[swapchain]->gpu = true;
swapchains[swapchain]->slots[0].busy = false;
expectedPresent = (VkSemaphore)30;
assert(droiddeck_wsi_present(handle, queue, &info, Present) == VK_SUCCESS);
assert(acquired == 1 && released == 1 && copied == 4 && blitted == 3);
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);
assert(submitted == 4 && presented == 5);
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");
}
+80
View File
@@ -0,0 +1,80 @@
#include "../src/xr_gpu_channel.h"
#include <cassert>
#include <cstdio>
#include <dirent.h>
static unsigned descriptors() {
DIR *directory = opendir("/proc/self/fd"); assert(directory);
unsigned count = 0; while (readdir(directory)) count++; closedir(directory); return count;
}
int main() {
assert(xr_gpu_dimensions(3360, 1760));
assert(!xr_gpu_dimensions(3, 4) && !xr_gpu_dimensions(8192, 4096));
int pair[2]; assert(!socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, pair));
int fd = memfd_create("xr-gpu-test", MFD_CLOEXEC); assert(fd >= 0 && !ftruncate(fd, 4096));
XrGpuPoolInfo pool; int sent[3]{fd, fd, fd}; std::array<int, 3> received{{-1, -1, -1}};
assert(xr_gpu_send(pair[0], &pool, sizeof(pool), sent, 3));
assert(xr_gpu_receive(pair[1], &pool, sizeof(pool), received.data(), 3));
for (int handle : received) { assert(fcntl(handle, F_GETFD) & FD_CLOEXEC); close(handle); }
auto count = descriptors();
assert(xr_gpu_send(pair[0], &pool, sizeof(pool), sent, 3));
assert(!xr_gpu_receive(pair[1], &pool, sizeof(pool)) && descriptors() == count);
assert(xr_gpu_send(pair[0], &pool, sizeof(pool), sent, 2));
assert(!xr_gpu_receive(pair[1], &pool, sizeof(pool), received.data(), 3) && descriptors() == count);
assert(xr_gpu_send(pair[0], &pool, sizeof(pool), sent, 3));
assert(!xr_gpu_receive(pair[1], &pool, sizeof(pool) - 1, received.data(), 3) && descriptors() == count);
close(pair[0]); close(pair[1]);
char directory[] = "/tmp/dd-xr-gpu-XXXXXX"; assert(mkdtemp(directory));
std::string filename = std::string(directory) + "/gpu";
int listener = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0); assert(listener >= 0);
sockaddr_un address{}; address.sun_family = AF_UNIX; strcpy(address.sun_path, filename.c_str());
assert(!bind(listener, reinterpret_cast<sockaddr *>(&address), sizeof(address)) && !listen(listener, 1));
for (unsigned attempt = 0; attempt < 3; attempt++) {
std::thread server([&] {
int peer = accept4(listener, nullptr, nullptr, SOCK_CLOEXEC); assert(peer >= 0 && xr_frame_peer(peer));
XrGpuRequest request; assert(xr_gpu_wait(peer, 1000) && xr_gpu_receive(peer, &request, sizeof(request)));
assert(request.width == 32 && request.height == 32);
XrGpuPoolInfo reply; reply.width = reply.height = 32;
for (auto &size : reply.sizes) size = attempt == 2 ? 4095 : 4096;
assert(xr_gpu_send(peer, &reply, sizeof(reply), sent, 3));
if (attempt != 2) {
XrGpuFrame frame; assert(xr_gpu_wait(peer, 1000) && xr_gpu_receive(peer, &frame, sizeof(frame)));
assert(frame.slot == 2 && frame.header.frame_id == 7);
auto now = xr_frame_now();
frame.header.captured_ns = now;
assert(xr_gpu_frame_state(frame, 32, 32, 0, now) == XrGpuFrameState::Ready);
assert(xr_gpu_frame_state(frame, 32, 32, 7, now) == XrGpuFrameState::Invalid);
frame.header.captured_ns = now - 1000000000LL;
assert(xr_gpu_frame_state(frame, 32, 32, 0, now) == XrGpuFrameState::Stale);
frame.slot = 3;
assert(xr_gpu_frame_state(frame, 32, 32, 0, now) == XrGpuFrameState::Invalid);
if (!attempt) std::this_thread::sleep_for(std::chrono::milliseconds(700));
uint64_t ack = attempt ? 8 : 7;
assert(xr_gpu_send(peer, &ack, sizeof(ack)));
}
close(peer);
});
XrGpuSender sender; received.fill(-1);
bool connected = sender.Connect(filename.c_str(), 32, 32, pool, received);
assert(connected == (attempt != 2));
if (connected) {
for (int handle : received) close(handle);
DroidDeckXrFrameHeader header{};
header.magic = DROIDDECK_XR_FRAME_MAGIC; header.version = DROIDDECK_XR_FRAME_VERSION;
header.width = header.height = 32; header.bytes = 4096; header.color_space = DROIDDECK_XR_COLOR_SRGB_RGBA8;
header.frame_id = 7; 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;
}
assert(sender.Send(header, 2) == (attempt == 0));
if (attempt) assert(!sender.Send(header, 2));
} else for (int handle : received) assert(handle == -1);
server.join();
}
close(fd); close(listener); unlink(filename.c_str()); rmdir(directory);
puts("XR GPU transport passed: atomic FD ownership, malformed pool rejection, truncation cleanup and exact completion acknowledgement");
}