diff --git a/app/src/main/cpp/questxr/gpu_frames.h b/app/src/main/cpp/questxr/gpu_frames.h new file mode 100644 index 000000000..dc69dbbfb --- /dev/null +++ b/app/src/main/cpp/questxr/gpu_frames.h @@ -0,0 +1,201 @@ +#pragma once +#include +#include +#include +#include +#include "xr_gpu_channel.h" + +class QuestGpuFrames { +public: + bool Start(const std::string &filename, EGLDisplay display) { + const char *extensions = reinterpret_cast(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(eglGetProcAddress("glBufferStorageExternalEXT")); + client_ = reinterpret_cast(eglGetProcAddress("eglGetNativeClientBufferANDROID")); + if (strstr(extensions, "GL_OES_texture_view")) view_ = reinterpret_cast(eglGetProcAddress("glTextureViewOES")); + else if (strstr(extensions, "GL_EXT_texture_view")) view_ = reinterpret_cast(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(&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 hardware_{}; + std::array buffers_{}; + std::array fds_{{-1, -1, -1}}; + std::array 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(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(bytes) + && status.st_size <= static_cast(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(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; + } +}; diff --git a/app/src/main/cpp/questxr/quest_xr.cpp b/app/src/main/cpp/questxr/quest_xr.cpp index 3dbfc5e67..09423c7d1 100644 --- a/app/src/main/cpp/questxr/quest_xr.cpp +++ b/app/src/main/cpp/questxr/quest_xr.cpp @@ -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(maximum) || frame.header.height > static_cast(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); diff --git a/docs/quest.md b/docs/quest.md index 8d4b2f4cd..bc50031cc 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -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 diff --git a/tools/tests/test_vr_wsi.py b/tools/tests/test_vr_wsi.py index 89cdf5220..9e93eb9ba 100644 --- a/tools/tests/test_vr_wsi.py +++ b/tools/tests/test_vr_wsi.py @@ -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', diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index c66157491..4ddbca5de 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -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; } diff --git a/tools/vr/src/wsi_capture.cpp b/tools/vr/src/wsi_capture.cpp index ab874ce84..11f4d4e2b 100644 --- a/tools/vr/src/wsi_capture.cpp +++ b/tools/vr/src/wsi_capture.cpp @@ -2,6 +2,7 @@ #include "wsi_capture.h" #include "xr_tracking_file.h" #include "xr_frame_channel.h" +#include "xr_gpu_channel.h" #include #include #include @@ -21,6 +22,8 @@ namespace { struct Device { VkDevice handle{}; VkPhysicalDeviceMemoryProperties memory{}; + bool foreign{}; + PFN_vkGetMemoryFdPropertiesKHR GetMemoryFdPropertiesKHR{}; std::unordered_map 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(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 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 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(extent.width), static_cast(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, ©); + 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(extent.width), static_cast(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, ©); 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 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(); d->handle = device; + d->GetMemoryFdPropertiesKHR = reinterpret_cast(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(gdpa(device, "vk" #name)); if (!d->name) return; FUNCTIONS(LOAD) #undef LOAD diff --git a/tools/vr/src/xr_gpu_channel.h b/tools/vr/src/xr_gpu_channel.h new file mode 100644 index 000000000..8735d3dc5 --- /dev/null +++ b/tools/vr/src/xr_gpu_channel.h @@ -0,0 +1,118 @@ +#pragma once +#include "xr_frame_channel.h" +#include + +#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(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(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 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(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(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 &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(&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(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; +}; diff --git a/tools/vr/tests/test_wsi_capture.cpp b/tools/vr/tests/test_wsi_capture.cpp index 7b26adfee..d5ab2f038 100644 --- a/tools/vr/tests/test_wsi_capture.cpp +++ b/tools/vr/tests/test_wsi_capture.cpp @@ -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"); } diff --git a/tools/vr/tests/test_xr_gpu.cpp b/tools/vr/tests/test_xr_gpu.cpp new file mode 100644 index 000000000..b41420a03 --- /dev/null +++ b/tools/vr/tests/test_xr_gpu.cpp @@ -0,0 +1,80 @@ +#include "../src/xr_gpu_channel.h" +#include +#include +#include + +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 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(&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"); +}