From 023daf57d211ee4c3acdf560e820efae2c4e25fe Mon Sep 17 00:00:00 2001 From: XxJSONDeruloxX Date: Wed, 14 Oct 2026 22:03:05 -0400 Subject: [PATCH] feat: add fenced SteamVR producer leases --- .github/workflows/build.yml | 2 +- docs/development/quest-openxr.md | 29 +++ .../overlay/usr/local/bin/droiddeck-steamvr | 2 + tools/tests/test_steamvr_helper.py | 1 + tools/tests/test_vr_producer_lease.py | 45 ++++ tools/vr/build.sh | 2 +- tools/vr/src/driver_droiddeck.cpp | 2 +- tools/vr/src/presenter.cpp | 87 ++++---- tools/vr/src/presenter.h | 3 +- tools/vr/src/producer_lease.h | 192 ++++++++++++++++++ tools/vr/src/vk_layer_droiddeck_vr.c | 150 +++++++++++++- tools/vr/tests/test_producer_gpu.c | 143 +++++++++++++ tools/vr/tests/test_producer_lease.c | 87 ++++++++ tools/vr/tests/test_semaphore_layer.c | 2 +- 14 files changed, 696 insertions(+), 51 deletions(-) create mode 100644 tools/tests/test_vr_producer_lease.py create mode 100644 tools/vr/src/producer_lease.h create mode 100644 tools/vr/tests/test_producer_gpu.c create mode 100644 tools/vr/tests/test_producer_lease.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28d17965a..94195bab1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,7 +193,7 @@ jobs: run: | set -euo pipefail sudo apt-get update -qq - sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null + sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libvulkan1 mesa-vulkan-drivers >/dev/null D=app/src/main/assets/linuxfs mkdir -p "$D" aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \ diff --git a/docs/development/quest-openxr.md b/docs/development/quest-openxr.md index 0c4bde818..199d9e5aa 100644 --- a/docs/development/quest-openxr.md +++ b/docs/development/quest-openxr.md @@ -309,6 +309,35 @@ bundled into the user-facing runtime. A valid already-signaled SYNC_FD value of ## Remaining stages +### Experimental producer lease + +`DROIDDECK_VR_PRODUCER_LEASE=1` opts the compositor into a private, same-UID, +PID-authenticated producer endpoint. It serializes producer queue submissions, +inserts real GPU fence markers on every observed producer queue, and grants a +lease only after those fences complete. Producer submissions remain frozen +until the presenter's GPU readback finishes and the release handshake succeeds. +The presenter device is excluded from the producer lock. Multiple producer +PIDs in one eye pair are rejected. Pending timeout fences are retained, not +destroyed while the GPU can still use them. Losing a granted lease quarantines +submissions instead of resuming writes against a potentially incomplete copy; +restarting SteamVR is required in that case. The layer remains resident for its +process lifetime so its service threads cannot execute unloaded code. + +The protocol regression verifies private sockets, peer PID, completion gating, +retention beyond client acquisition deadlines, release acknowledgement and +disconnect quarantine. A real Vulkan Lavapipe regression waits for deliberately +delayed GPU work, verifies all 1,024 buffer words, blocks a concurrent overwriter +through the lease, then verifies its writes after release; it also exercises +an outstanding fence through repeated timeouts. CI installs the software Vulkan +driver so this is not merely a source-shaped or mocked GPU test. + +This route is intentionally opt-in, not declared complete. Live Quest validation +must establish that the swap-set PID identifies the actual final-image writer, +audit the compositor's external-memory image-layout/queue-family release +contract, and validate the matching acquire/release barriers with real images. +The host buffer test does not establish any of those image-sharing properties. +Owned-image submission still fails closed without a successfully retained lease. + The Vulkan layer no longer drops imported binary waits or unsignaled timeline waits after a three-millisecond grace period. Queue submissions and host waits retain their original arguments, deadlines and downstream results. The diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr index abc369a82..89479bac4 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr @@ -7,7 +7,9 @@ driver=$vrdir/droiddeck layer=$vrdir/layer logs=$HOME/.local/share/Steam/logs state=${XDG_RUNTIME_DIR:-/tmp}/droiddeck-steamvr +umask 077 mkdir -p "$state" +chmod 700 "$state" PKG_URL=https://steamdeck-packages.steamos.cloud/archlinux-deckard-hotfixes/release/0.3.x/deckard-steamvr-rel-r25358740%2B28b72a4f-1-aarch64.pkg.tar.zst PKG_SHA256=90a600775eb3e8856dc475f83166a93a290a0b1f3b4e82a69c8e2a8f06b9ddc9 diff --git a/tools/tests/test_steamvr_helper.py b/tools/tests/test_steamvr_helper.py index 87d343a6f..2d5aa11d0 100644 --- a/tools/tests/test_steamvr_helper.py +++ b/tools/tests/test_steamvr_helper.py @@ -57,6 +57,7 @@ class SteamVrHelperTest(unittest.TestCase): def test_fresh_runtime_mode_is_selected_and_log_survives_runtime_cleanup(self): log = self.start() + self.assertEqual((self.runtime / 'droiddeck-steamvr').stat().st_mode & 0o777, 0o700) self.assertEqual(log.read_text().strip(), 'display=3360x1760@120') self.assertEqual((self.runtime / 'droiddeck-steamvr/vrstartup.log').resolve(), log) shutil.rmtree(self.runtime) diff --git a/tools/tests/test_vr_producer_lease.py b/tools/tests/test_vr_producer_lease.py new file mode 100644 index 000000000..d11721ed9 --- /dev/null +++ b/tools/tests/test_vr_producer_lease.py @@ -0,0 +1,45 @@ +import ctypes.util +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +@unittest.skipUnless(shutil.which("gcc"), "needs gcc on a Linux host") +class VrProducerLeaseTest(unittest.TestCase): + def compile_and_run(self, source, headers=None, gpu=False): + repository = Path(__file__).resolve().parents[2] + with tempfile.TemporaryDirectory() as directory: + executable = Path(directory) / "producer-test" + command = ["gcc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-pthread"] + if headers: + command += ["-I" + str(headers)] + command += [str(repository / "tools/vr/tests" / source), "-ldl"] + if gpu: + command += ["-l:libvulkan.so.1"] + subprocess.run(command + ["-o", str(executable)], check=True) + environment = dict(os.environ) + if gpu: + manifests = sorted(Path("/usr/share/vulkan/icd.d").glob("lvp_icd*.json")) + environment["VK_DRIVER_FILES"] = str(manifests[0]) + environment["VK_ICD_FILENAMES"] = str(manifests[0]) + result = subprocess.run([str(executable)], env=environment, capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return result.stdout + + def test_protocol_and_retained_ownership(self): + self.assertIn("disconnect quarantine passed", self.compile_and_run("test_producer_lease.c")) + + def test_real_gpu_completion_and_frozen_overwrite(self): + if not ctypes.util.find_library("vulkan") or not list(Path("/usr/share/vulkan/icd.d").glob("lvp_icd*.json")): + self.skipTest("needs Vulkan loader and Lavapipe") + repository = Path(__file__).resolve().parents[2] + candidates = [os.environ.get("VK_INCLUDE", ""), "/usr/include", repository / "build/vr/include"] + candidates += [path for root in (os.environ.get("ANDROID_HOME"), os.environ.get("ANDROID_SDK_ROOT")) if root + for path in Path(root).glob("ndk/*/toolchains/llvm/prebuilt/*/sysroot/usr/include")] + headers = next((Path(path) for path in candidates if path and (Path(path) / "vulkan/vk_layer.h").is_file()), None) + if headers is None: + self.skipTest("needs Vulkan headers") + self.assertIn("1024 buffer values passed", self.compile_and_run("test_producer_gpu.c", headers=headers, gpu=True)) diff --git a/tools/vr/build.sh b/tools/vr/build.sh index dbb1e03d7..7aefd5510 100755 --- a/tools/vr/build.sh +++ b/tools/vr/build.sh @@ -43,7 +43,7 @@ compile() { -o build/vr/out/droiddeck/bin/linuxarm64/driver_droiddeck.so tools/vr/src/driver_droiddeck.cpp tools/vr/src/presenter.cpp \ build/vr/xdg-shell-protocol.o -Lbuild/vr/stub -lwayland-client -ldl aarch64-linux-gnu-gcc -std=c11 -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \ - -Wl,-z,defs -Ibuild/vr/include -o build/vr/out/layer/libVkLayer_droiddeck_vr.so tools/vr/src/vk_layer_droiddeck_vr.c -ldl + -Wl,-z,defs -Wl,-z,nodelete -Ibuild/vr/include -o build/vr/out/layer/libVkLayer_droiddeck_vr.so tools/vr/src/vk_layer_droiddeck_vr.c -ldl aarch64-linux-gnu-strip --strip-unneeded build/vr/out/droiddeck/bin/linuxarm64/driver_droiddeck.so \ build/vr/out/layer/libVkLayer_droiddeck_vr.so') if [ "$native" = 1 ]; then diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index b9e58ef5d..e9b4b432e 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -219,7 +219,7 @@ public: out->unTextureFlags = 0; Log(" -> handles 0x%llx 0x%llx 0x%llx, fds %d %d %d", (unsigned long long)set.handles[0], (unsigned long long)set.handles[1], (unsigned long long)set.handles[2], set.fds[0], set.fds[1], set.fds[2]); - droiddeck::Presenter::AddTextures(set.handles, set.fds, d->nFormat, d->nWidth, d->nHeight); + droiddeck::Presenter::AddTextures(set.handles, set.fds, d->nFormat, d->nWidth, d->nHeight, pid); std::lock_guard lock(mutex_); sets_.push_back(set); } diff --git a/tools/vr/src/presenter.cpp b/tools/vr/src/presenter.cpp index c1dafecd0..939a000c6 100644 --- a/tools/vr/src/presenter.cpp +++ b/tools/vr/src/presenter.cpp @@ -11,6 +11,7 @@ typedef unsigned long VisualID; #include "presenter.h" #include "xr_tracking_file.h" #include "xr_frame_channel.h" +#include "producer_lease.h" #include #include @@ -22,7 +23,6 @@ typedef unsigned long VisualID; #include #include #include -#include #include #include #include @@ -99,13 +99,31 @@ struct Imported { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; uint32_t width = 0, height = 0; - int dmaFd = -1; + uint32_t producerPid = 0; }; struct PendingImport { uint64_t handle; int fd; - uint32_t format, width, height; + uint32_t format, width, height, producerPid; +}; + +class CaptureLease { +public: + explicit CaptureLease(uint32_t pid) : acquired_(droiddeck_producer_acquire(pid, &lease_)) {} + ~CaptureLease() { + if (lease_.descriptor < 0) return; + if (gpuComplete_) droiddeck_producer_release(&lease_); + else close(lease_.descriptor); + } + bool Acquired() const { return acquired_; } + void BeginGpu() { gpuComplete_ = false; } + void EndGpu() { gpuComplete_ = true; } + bool Release() { return gpuComplete_ && droiddeck_producer_release(&lease_); } +private: + droiddeck_producer_lease lease_{-1, 0}; + bool acquired_; + bool gpuComplete_ = true; }; VkFormat UnormOf(uint32_t f) { @@ -123,13 +141,13 @@ public: return impl; } - void Add(const uint64_t handles[3], const int fds[3], uint32_t format, uint32_t w, uint32_t h) { + void Add(const uint64_t handles[3], const int fds[3], uint32_t format, uint32_t w, uint32_t h, uint32_t producerPid) { std::lock_guard lock(mutex_); if (failed_ || exit_) return; for (int i = 0; i < 3; i++) { if (fds[i] < 0) continue; int fd = dup(fds[i]); - if (fd >= 0) pending_.push_back({handles[i], fd, format, w, h}); + if (fd >= 0) pending_.push_back({handles[i], fd, format, w, h, producerPid}); } EnsureThread(); cv_.notify_all(); @@ -483,7 +501,7 @@ private: Imported im{p.handle}; im.width = p.width; im.height = p.height; - if (frameChannel_) im.dmaFd = dup(p.fd); + im.producerPid = p.producerPid; const char *fl = getenv("DROIDDECK_VR_IMPORT_FORMAT_LIST"); const bool formatList = !fl || strcmp(fl, "0") != 0; VkFormat formats[2] = {(VkFormat)p.format, UnormOf(p.format)}; @@ -495,25 +513,23 @@ private: VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, VK_IMAGE_LAYOUT_UNDEFINED}; VkResult r = vk_.CreateImage(dev_, &ici, nullptr, &im.image); - if (r != VK_SUCCESS) { Log("presenter: import image %d", r); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); return; } + if (r != VK_SUCCESS) { Log("presenter: import image %d", r); close(p.fd); return; } VkMemoryRequirements req; vk_.GetImageMemoryRequirements(dev_, im.image, &req); VkImportMemoryFdInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, p.fd}; VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, &imp, im.image, VK_NULL_HANDLE}; int memoryType = MemoryType(req.memoryTypeBits); - if (memoryType < 0) { vk_.DestroyImage(dev_, im.image, nullptr); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); return; } + if (memoryType < 0) { vk_.DestroyImage(dev_, im.image, nullptr); close(p.fd); return; } VkMemoryAllocateInfo mai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &ded, req.size, (uint32_t)memoryType}; r = vk_.AllocateMemory(dev_, &mai, nullptr, &im.memory); if (r != VK_SUCCESS) { Log("presenter: import memory (fd %d, %llu bytes) %d", p.fd, (unsigned long long)req.size, r); close(p.fd); - if (im.dmaFd >= 0) close(im.dmaFd); vk_.DestroyImage(dev_, im.image, nullptr); return; } if (vk_.BindImageMemory(dev_, im.image, im.memory, 0) != VK_SUCCESS) { vk_.DestroyImage(dev_, im.image, nullptr); vk_.FreeMemory(dev_, im.memory, nullptr); - if (im.dmaFd >= 0) close(im.dmaFd); return; } textures_.push_back(im); @@ -526,7 +542,6 @@ private: vk_.DeviceWaitIdle(dev_); vk_.DestroyImage(dev_, it->image, nullptr); vk_.FreeMemory(dev_, it->memory, nullptr); - if (it->dmaFd >= 0) close(it->dmaFd); textures_.erase(it); return; } @@ -545,33 +560,6 @@ private: return b; } - bool ProducerReady(const Imported &image) { - struct { uint32_t flags; int32_t fd; } exported{1u, -1}; - bool ready = false; - if (image.dmaFd >= 0 && ioctl(image.dmaFd, _IOWR('b', 2, decltype(exported)), &exported) == 0 && exported.fd >= 0) { - struct SyncInfo { char name[32]; int32_t status; uint32_t flags, count, padding; uint64_t fences; } info{}; - if (ioctl(exported.fd, _IOWR('>', 4, SyncInfo), &info) == 0 && info.count > 0) { - pollfd signal{exported.fd, POLLIN, 0}; - int64_t deadline = xr_frame_now() + 100000000LL; - while (xr_frame_now() < deadline) { - int64_t remaining = deadline - xr_frame_now(); - if (remaining <= 0) break; - int result = poll(&signal, 1, static_cast((remaining + 999999) / 1000000)); - if (result < 0 && errno == EINTR) continue; - ready = result > 0 && (signal.revents & POLLIN) && !(signal.revents & (POLLERR | POLLNVAL)); - break; - } - } - close(exported.fd); - } - if (!ready) { - static unsigned said = 0; - if (said++ < 3) Log("presenter: refusing Quest frame without a nonempty successful producer fence (texture 0x%llx)", - (unsigned long long)image.handle); - } - return ready; - } - void DestroyCapture() { if (!dev_) return; if (captureImage_) vk_.DestroyImage(dev_, captureImage_, nullptr); @@ -635,7 +623,13 @@ private: for (int index = 0; index < 2; index++) { for (float value : frame.bounds[index]) if (!std::isfinite(value) || value < 0 || value > 1) return false; if (frame.bounds[index][0] == frame.bounds[index][2] || frame.bounds[index][1] == frame.bounds[index][3]) return false; - if ((index == 0 || eye[0] != eye[1]) && !ProducerReady(*eye[index])) return false; + } + if (!eye[0]->producerPid || eye[0]->producerPid != eye[1]->producerPid) return false; + CaptureLease lease(eye[0]->producerPid); + if (!lease.Acquired()) { + static unsigned said = 0; + if (said++ < 3) Log("presenter: refusing Quest frame without a completed, retained producer lease (pid %u)", eye[0]->producerPid); + return false; } VkResult result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX); if (result != VK_SUCCESS) { Fail(); return false; } @@ -674,10 +668,19 @@ private: vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &host, 0, nullptr); if (vk_.EndCommandBuffer(cmd_) != VK_SUCCESS || vk_.ResetFences(dev_, 1, &fence_) != VK_SUCCESS) { Fail(); return false; } VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd_; + lease.BeginGpu(); result = vk_.QueueSubmit(queue_, 1, &submit, fence_); - if (result != VK_SUCCESS) { Fail(); return false; } + if (result != VK_SUCCESS) { + if (vk_.DeviceWaitIdle(dev_) == VK_SUCCESS) lease.EndGpu(); + Fail(); return false; + } result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX); - if (result != VK_SUCCESS) { vk_.DeviceWaitIdle(dev_); Fail(); return false; } + if (result != VK_SUCCESS) { + if (vk_.DeviceWaitIdle(dev_) == VK_SUCCESS) lease.EndGpu(); + Fail(); return false; + } + lease.EndGpu(); + if (!lease.Release()) return false; void *mapping = nullptr; if (vk_.MapMemory(dev_, readbackMemory_, 0, VK_WHOLE_SIZE, 0, &mapping) != VK_SUCCESS) return false; VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = readbackMemory_; range.size = VK_WHOLE_SIZE; @@ -792,7 +795,7 @@ private: } -void Presenter::AddTextures(const uint64_t h[3], const int fds[3], uint32_t f, uint32_t w, uint32_t hh) { Impl::Get().Add(h, fds, f, w, hh); } +void Presenter::AddTextures(const uint64_t h[3], const int fds[3], uint32_t f, uint32_t w, uint32_t hh, uint32_t pid) { Impl::Get().Add(h, fds, f, w, hh, pid); } void Presenter::RemoveTexture(uint64_t h) { Impl::Get().Remove(h); } void Presenter::Submit(const EyeFrame &f) { Impl::Get().Submit(f); } void Presenter::Shutdown() { Impl::Get().Shutdown(); } diff --git a/tools/vr/src/presenter.h b/tools/vr/src/presenter.h index 8858abe06..3a934a722 100644 --- a/tools/vr/src/presenter.h +++ b/tools/vr/src/presenter.h @@ -13,7 +13,8 @@ struct EyeFrame { class Presenter { public: - static void AddTextures(const uint64_t handles[3], const int fds[3], uint32_t vkFormat, uint32_t width, uint32_t height); + static void AddTextures(const uint64_t handles[3], const int fds[3], uint32_t vkFormat, uint32_t width, uint32_t height, + uint32_t producerPid); static void RemoveTexture(uint64_t handle); static void Submit(const EyeFrame &frame); static void Shutdown(); diff --git a/tools/vr/src/producer_lease.h b/tools/vr/src/producer_lease.h new file mode 100644 index 000000000..f5b8a7ef4 --- /dev/null +++ b/tools/vr/src/producer_lease.h @@ -0,0 +1,192 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DROIDDECK_PRODUCER_MAGIC 0x44504c31u +#define DROIDDECK_PRODUCER_ACQUIRE 1u +#define DROIDDECK_PRODUCER_READY 2u +#define DROIDDECK_PRODUCER_RELEASE 3u +#define DROIDDECK_PRODUCER_RELEASED 4u + +struct droiddeck_producer_message { + uint32_t magic; + uint32_t command; + uint64_t ticket; +}; + +static inline int64_t droiddeck_producer_now(void) { + struct timespec current; + clock_gettime(CLOCK_MONOTONIC, ¤t); + return (int64_t)current.tv_sec * 1000000000LL + current.tv_nsec; +} + +static inline int droiddeck_producer_path(char *path, size_t capacity, uint32_t pid) { + const char *runtime = getenv("XDG_RUNTIME_DIR"); + int length = snprintf(path, capacity, "%s/droiddeck-steamvr/producer-%u.sock", + runtime && *runtime ? runtime : "/run/droiddeck", pid); + return pid && length > 0 && (size_t)length < capacity; +} + +static inline int droiddeck_producer_private_parent(const char *path) { + char parent[sizeof(((struct sockaddr_un *)0)->sun_path)]; + if (strlen(path) >= sizeof(parent)) return 0; + strcpy(parent, path); + char *separator = strrchr(parent, '/'); + if (!separator) return 0; + *separator = 0; + struct stat metadata; + return lstat(parent, &metadata) == 0 && S_ISDIR(metadata.st_mode) && + metadata.st_uid == getuid() && !(metadata.st_mode & 0077); +} + +static inline int droiddeck_producer_peer(int descriptor, uint32_t expected_pid) { + struct ucred peer; + socklen_t length = sizeof(peer); + return getsockopt(descriptor, SOL_SOCKET, SO_PEERCRED, &peer, &length) == 0 && + peer.uid == getuid() && (!expected_pid || (uint32_t)peer.pid == expected_pid); +} + +static inline int droiddeck_producer_io(int descriptor, struct droiddeck_producer_message *message, + int writing, int64_t deadline) { + for (;;) { + int64_t remaining = deadline - droiddeck_producer_now(); + if (remaining <= 0) return 0; + struct pollfd ready = {descriptor, (short)(writing ? POLLOUT : POLLIN), 0}; + int result = poll(&ready, 1, (int)((remaining + 999999) / 1000000)); + if (result < 0 && errno == EINTR) continue; + if (result <= 0 || !(ready.revents & ready.events)) return 0; + ssize_t transferred = writing ? send(descriptor, message, sizeof(*message), MSG_NOSIGNAL | MSG_DONTWAIT) + : recv(descriptor, message, sizeof(*message), MSG_TRUNC | MSG_DONTWAIT); + if (transferred < 0 && (errno == EAGAIN || errno == EINTR)) continue; + return transferred == (ssize_t)sizeof(*message) && message->magic == DROIDDECK_PRODUCER_MAGIC; + } +} + +struct droiddeck_producer_lease { + int descriptor; + uint64_t ticket; +}; + +static inline int droiddeck_producer_acquire(uint32_t pid, struct droiddeck_producer_lease *lease) { + lease->descriptor = -1; + lease->ticket = 0; + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + if (!droiddeck_producer_path(address.sun_path, sizeof(address.sun_path), pid) || + !droiddeck_producer_private_parent(address.sun_path)) return 0; + int descriptor = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (descriptor < 0) return 0; + int64_t deadline = droiddeck_producer_now() + 250000000LL; + struct droiddeck_producer_message message = {DROIDDECK_PRODUCER_MAGIC, DROIDDECK_PRODUCER_ACQUIRE, 0}; + if (connect(descriptor, (struct sockaddr *)&address, sizeof(address)) != 0 || + !droiddeck_producer_peer(descriptor, pid) || + !droiddeck_producer_io(descriptor, &message, 1, deadline) || + !droiddeck_producer_io(descriptor, &message, 0, deadline) || + message.command != DROIDDECK_PRODUCER_READY || !message.ticket) { + close(descriptor); + return 0; + } + lease->descriptor = descriptor; + lease->ticket = message.ticket; + return 1; +} + +static inline int droiddeck_producer_release(struct droiddeck_producer_lease *lease) { + if (lease->descriptor < 0) return 0; + int64_t deadline = droiddeck_producer_now() + 250000000LL; + struct droiddeck_producer_message message = {DROIDDECK_PRODUCER_MAGIC, DROIDDECK_PRODUCER_RELEASE, lease->ticket}; + int released = droiddeck_producer_io(lease->descriptor, &message, 1, deadline) && + droiddeck_producer_io(lease->descriptor, &message, 0, deadline) && + message.command == DROIDDECK_PRODUCER_RELEASED && message.ticket == lease->ticket; + close(lease->descriptor); + lease->descriptor = -1; + return released; +} + +#ifdef DROIDDECK_PRODUCER_SERVER +#include + +struct droiddeck_producer_server { + int listener; + pthread_t thread; + int (*acquire)(int64_t deadline); + void (*release)(void); + void (*quarantine)(void); + char path[sizeof(((struct sockaddr_un *)0)->sun_path)]; +}; + +static void *droiddeck_producer_serve(void *argument) { + struct droiddeck_producer_server *server = argument; + uint64_t ticket = 0; + for (;;) { + int connection = accept4(server->listener, NULL, NULL, SOCK_CLOEXEC | SOCK_NONBLOCK); + if (connection < 0) { + if (errno == EINTR) continue; + break; + } + struct droiddeck_producer_message message; + int64_t deadline = droiddeck_producer_now() + 150000000LL; + if (!droiddeck_producer_peer(connection, 0) || + !droiddeck_producer_io(connection, &message, 0, deadline) || + message.command != DROIDDECK_PRODUCER_ACQUIRE || message.ticket || !server->acquire(deadline)) { + close(connection); + continue; + } + uint64_t granted = ++ticket; + message = (struct droiddeck_producer_message){DROIDDECK_PRODUCER_MAGIC, DROIDDECK_PRODUCER_READY, granted}; + int valid = droiddeck_producer_io(connection, &message, 1, deadline); + while (valid) { + struct pollfd request = {connection, POLLIN, 0}; + int ready = poll(&request, 1, -1); + if (ready < 0 && errno == EINTR) continue; + if (ready <= 0 || !(request.revents & POLLIN)) { valid = 0; break; } + valid = droiddeck_producer_io(connection, &message, 0, droiddeck_producer_now() + 100000000LL) && + message.command == DROIDDECK_PRODUCER_RELEASE && message.ticket == granted; + break; + } + if (!valid) { + server->quarantine(); + close(connection); + break; + } + server->release(); + message = (struct droiddeck_producer_message){DROIDDECK_PRODUCER_MAGIC, DROIDDECK_PRODUCER_RELEASED, granted}; + droiddeck_producer_io(connection, &message, 1, droiddeck_producer_now() + 100000000LL); + close(connection); + } + return NULL; +} + +static int droiddeck_producer_start(struct droiddeck_producer_server *server) { + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + if (!droiddeck_producer_path(address.sun_path, sizeof(address.sun_path), getpid()) || + !droiddeck_producer_private_parent(address.sun_path)) return 0; + server->listener = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0); + if (server->listener < 0) return 0; + if (bind(server->listener, (struct sockaddr *)&address, sizeof(address)) != 0) { + close(server->listener); + return 0; + } + if (chmod(address.sun_path, 0600) == 0 && listen(server->listener, 4) == 0 && + pthread_create(&server->thread, NULL, droiddeck_producer_serve, server) == 0) { + strcpy(server->path, address.sun_path); + return 1; + } + close(server->listener); + unlink(address.sun_path); + return 0; +} +#endif diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index f824c52ba..7b9edbc94 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -11,6 +11,8 @@ #include #include #include +#define DROIDDECK_PRODUCER_SERVER +#include "producer_lease.h" #define LAYER_NAME "VK_LAYER_DROIDDECK_vr" #define EXPORT __attribute__((visibility("default"))) @@ -87,6 +89,7 @@ struct inst { PFN_vkDestroyInstance destroy; int stripped; int active; + int producer; }; struct dev { VkDevice handle; @@ -94,6 +97,7 @@ struct dev { PFN_vkDestroyDevice destroy; int active; int display; + int producer; }; static int steamvr_running(void) { @@ -194,8 +198,12 @@ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const pthread_mutex_lock(&g_lock); for (int i = 0; i < MAX_OBJS; i++) { if (!g_inst[i].handle) { + const char *lease_mode = getenv("DROIDDECK_VR_PRODUCER_LEASE"); + int producer = lease_mode && !strcmp(lease_mode, "1") && !strcmp(program_invocation_short_name, "vrcompositor") && + (!ci->pApplicationInfo || !ci->pApplicationInfo->pApplicationName || + strcmp(ci->pApplicationInfo->pApplicationName, "droiddeck-vr-presenter")); g_inst[i] = (struct inst){*out, next_gipa, - (PFN_vkDestroyInstance)next_gipa(*out, "vkDestroyInstance"), stripped, active}; + (PFN_vkDestroyInstance)next_gipa(*out, "vkDestroyInstance"), stripped, active, producer}; break; } } @@ -244,7 +252,8 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat pthread_mutex_lock(&g_lock); for (int i = 0; i < MAX_OBJS; i++) { if (!g_dev[i].handle) { - g_dev[i] = (struct dev){*out, next_gdpa, (PFN_vkDestroyDevice)next_gdpa(*out, "vkDestroyDevice"), active, display}; + g_dev[i] = (struct dev){*out, next_gdpa, (PFN_vkDestroyDevice)next_gdpa(*out, "vkDestroyDevice"), active, display, + in && in->producer}; break; } } @@ -253,10 +262,12 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat } static void forget_device_semaphores(VkDevice dev); +static void producer_destroy_device(struct dev *device, const VkAllocationCallbacks *allocator); static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallbacks *alloc) { struct dev *d = dev_for(device); if (!d) return; + if (d->producer) producer_destroy_device(d, alloc); forget_device_semaphores(device); PFN_vkDestroyDevice f = d->destroy; pthread_mutex_lock(&g_lock); @@ -477,6 +488,109 @@ static PFN_vkVoidFunction display_device_hook(const char *name) { #define SHARED_NAME "droiddeck-shared-sem" #define POLL_US 300 +static pthread_mutex_t g_submission_lock = PTHREAD_MUTEX_INITIALIZER; +struct producer_queue { + VkQueue queue; + VkDevice device; + VkFence fence; +}; +static struct producer_queue g_producer_queues[MAX_OBJS]; +static int g_producer_overflow; +static int g_producer_started; +static struct droiddeck_producer_server g_producer_server; + +__attribute__((destructor)) static void producer_process_exit(void) { + if (g_producer_started && g_producer_server.path[0]) unlink(g_producer_server.path); +} + +static int producer_acquire(int64_t deadline) { + while (pthread_mutex_trylock(&g_submission_lock) != 0) { + if (droiddeck_producer_now() >= deadline) return 0; + usleep(100); + } + int queues = 0; + if (g_producer_overflow) goto fail; + for (unsigned index = 0; index < MAX_OBJS; index++) { + struct producer_queue *entry = &g_producer_queues[index]; + if (!entry->queue) continue; + queues++; + struct dev *device = dev_for(entry->device); + if (!device) goto fail; + PFN_vkGetFenceStatus status = (PFN_vkGetFenceStatus)device->gdpa(device->handle, "vkGetFenceStatus"); + PFN_vkDestroyFence destroy = (PFN_vkDestroyFence)device->gdpa(device->handle, "vkDestroyFence"); + if (entry->fence) { + if (status(device->handle, entry->fence) != VK_SUCCESS) goto fail; + destroy(device->handle, entry->fence, NULL); + entry->fence = VK_NULL_HANDLE; + } + VkFenceCreateInfo create = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, NULL, 0}; + PFN_vkCreateFence create_fence = (PFN_vkCreateFence)device->gdpa(device->handle, "vkCreateFence"); + if (create_fence(device->handle, &create, NULL, &entry->fence) != VK_SUCCESS) goto fail; + PFN_vkQueueSubmit submit = (PFN_vkQueueSubmit)device->gdpa(device->handle, "vkQueueSubmit"); + if (submit(entry->queue, 0, NULL, entry->fence) != VK_SUCCESS) { + destroy(device->handle, entry->fence, NULL); + entry->fence = VK_NULL_HANDLE; + goto fail; + } + } + if (!queues) goto fail; + for (unsigned index = 0; index < MAX_OBJS; index++) { + struct producer_queue *entry = &g_producer_queues[index]; + if (!entry->queue) continue; + int64_t remaining = deadline - droiddeck_producer_now(); + if (remaining <= 0) goto fail; + struct dev *device = dev_for(entry->device); + PFN_vkWaitForFences wait = (PFN_vkWaitForFences)device->gdpa(device->handle, "vkWaitForFences"); + if (wait(device->handle, 1, &entry->fence, VK_TRUE, (uint64_t)remaining) != VK_SUCCESS) goto fail; + } + return 1; +fail: + pthread_mutex_unlock(&g_submission_lock); + return 0; +} + +static void producer_release(void) { + pthread_mutex_unlock(&g_submission_lock); +} + +static void producer_quarantine(void) { + say("%s", "producer lease lost before capture completion: submissions quarantined; restart SteamVR"); +} + +static void producer_record(struct dev *device, VkQueue queue) { + if (!g_producer_started) { + g_producer_server.acquire = producer_acquire; + g_producer_server.release = producer_release; + g_producer_server.quarantine = producer_quarantine; + if (droiddeck_producer_start(&g_producer_server)) { + pthread_detach(g_producer_server.thread); + g_producer_started = 1; + } + } + for (unsigned index = 0; index < MAX_OBJS; index++) { + struct producer_queue *entry = &g_producer_queues[index]; + if (entry->queue == queue) return; + if (!entry->queue) { + *entry = (struct producer_queue){queue, device->handle, VK_NULL_HANDLE}; + return; + } + } + g_producer_overflow = 1; +} + +static void producer_destroy_device(struct dev *device, const VkAllocationCallbacks *allocator) { + (void)allocator; + pthread_mutex_lock(&g_submission_lock); + PFN_vkDestroyFence destroy = (PFN_vkDestroyFence)device->gdpa(device->handle, "vkDestroyFence"); + for (unsigned index = 0; index < MAX_OBJS; index++) { + struct producer_queue *entry = &g_producer_queues[index]; + if (entry->device != device->handle) continue; + if (entry->fence) destroy(device->handle, entry->fence, NULL); + memset(entry, 0, sizeof(*entry)); + } + pthread_mutex_unlock(&g_submission_lock); +} + struct bridged { VkSemaphore sem; VkDevice dev; @@ -729,7 +843,11 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit for (uint32_t j = 0; j < submits[i].signalSemaphoreCount; j++) trace_sem("signals", submits[i].pSignalSemaphores[j], t && j < t->signalSemaphoreValueCount ? t->pSignalSemaphoreValues[j] : 0); } - return next(q, count, submits, fence); + struct dev *device = dev_for(q); + if (device->producer) { pthread_mutex_lock(&g_submission_lock); producer_record(device, q); } + VkResult result = next(q, count, submits, fence); + if (device->producer) pthread_mutex_unlock(&g_submission_lock); + return result; } static VkResult VKAPI_CALL QueueSubmit2(VkQueue q, uint32_t count, const VkSubmitInfo2 *submits, VkFence fence) { @@ -742,7 +860,29 @@ static VkResult VKAPI_CALL QueueSubmit2(VkQueue q, uint32_t count, const VkSubmi for (uint32_t j = 0; j < submits[i].signalSemaphoreInfoCount; j++) trace_sem("signals", submits[i].pSignalSemaphoreInfos[j].semaphore, submits[i].pSignalSemaphoreInfos[j].value); } - return next(q, count, submits, fence); + struct dev *device = dev_for(q); + if (device->producer) { pthread_mutex_lock(&g_submission_lock); producer_record(device, q); } + VkResult result = next(q, count, submits, fence); + if (device->producer) pthread_mutex_unlock(&g_submission_lock); + return result; +} + +static VkResult VKAPI_CALL QueuePresentKHR(VkQueue queue, const VkPresentInfoKHR *info) { + struct dev *device = dev_for(queue); + PFN_vkQueuePresentKHR next = (PFN_vkQueuePresentKHR)device->gdpa(device->handle, "vkQueuePresentKHR"); + if (device->producer) { pthread_mutex_lock(&g_submission_lock); producer_record(device, queue); } + VkResult result = next(queue, info); + if (device->producer) pthread_mutex_unlock(&g_submission_lock); + return result; +} + +static VkResult VKAPI_CALL QueueBindSparse(VkQueue queue, uint32_t count, const VkBindSparseInfo *binds, VkFence fence) { + struct dev *device = dev_for(queue); + PFN_vkQueueBindSparse next = (PFN_vkQueueBindSparse)device->gdpa(device->handle, "vkQueueBindSparse"); + if (device->producer) { pthread_mutex_lock(&g_submission_lock); producer_record(device, queue); } + VkResult result = next(queue, count, binds, fence); + if (device->producer) pthread_mutex_unlock(&g_submission_lock); + return result; } static VkResult VKAPI_CALL WaitSemaphores(VkDevice d, const VkSemaphoreWaitInfo *info, uint64_t timeout) { @@ -786,6 +926,8 @@ static PFN_vkVoidFunction semaphore_hook(const char *name) { HOOK_AS("vkQueueSubmit", QueueSubmit) HOOK_AS("vkQueueSubmit2", QueueSubmit2) HOOK_AS("vkQueueSubmit2KHR", QueueSubmit2) + HOOK_AS("vkQueuePresentKHR", QueuePresentKHR) + HOOK_AS("vkQueueBindSparse", QueueBindSparse) HOOK_AS("vkWaitSemaphores", WaitSemaphores) HOOK_AS("vkWaitSemaphoresKHR", WaitSemaphores) HOOK_AS("vkGetSemaphoreCounterValue", GetSemaphoreCounterValue) diff --git a/tools/vr/tests/test_producer_gpu.c b/tools/vr/tests/test_producer_gpu.c new file mode 100644 index 000000000..2c683b4a9 --- /dev/null +++ b/tools/vr/tests/test_producer_gpu.c @@ -0,0 +1,143 @@ +#define _GNU_SOURCE +#include +#include +#include "../src/vk_layer_droiddeck_vr.c" + +static VkDevice test_device; +static VkQueue test_queue; +static VkSemaphore gate; +static VkCommandBuffer commands[2]; +static atomic_int second_returned; +static atomic_int second_entered; + +static void *signal_gate(void *argument) { + (void)argument; + usleep(30000); + VkSemaphoreSignalInfo signal_info = {VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, NULL, gate, 1}; + assert(vkSignalSemaphore(test_device, &signal_info) == VK_SUCCESS); + return NULL; +} + +static void *second_submit(void *argument) { + (void)argument; + VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO, NULL, 0, NULL, NULL, 1, &commands[1], 0, NULL}; + atomic_store(&second_entered, 1); + assert(QueueSubmit(test_queue, 1, &submit, VK_NULL_HANDLE) == VK_SUCCESS); + atomic_store(&second_returned, 1); + return NULL; +} + +static void check_pixels(const uint32_t *pixels, uint32_t expected) { + for (unsigned index = 0; index < 1024; index++) assert(pixels[index] == expected); +} + +int main(void) { + setenv("DROIDDECK_VR_LAYER_QUIET", "1", 1); + VkApplicationInfo application = {VK_STRUCTURE_TYPE_APPLICATION_INFO, NULL, "DroidDeck producer GPU test", 1, NULL, 0, VK_API_VERSION_1_2}; + VkInstanceCreateInfo create_instance = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, NULL, 0, &application, 0, NULL, 0, NULL}; + VkInstance instance; + assert(vkCreateInstance(&create_instance, NULL, &instance) == VK_SUCCESS); + uint32_t count = 1; + VkPhysicalDevice physical; + VkResult enumeration = vkEnumeratePhysicalDevices(instance, &count, &physical); + assert((enumeration == VK_SUCCESS || enumeration == VK_INCOMPLETE) && count); + VkQueueFamilyProperties families[32]; + count = 32; + vkGetPhysicalDeviceQueueFamilyProperties(physical, &count, families); + uint32_t family = UINT32_MAX; + for (uint32_t index = 0; index < count; index++) + if (families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) { family = index; break; } + assert(family != UINT32_MAX); + float priority = 1; + VkDeviceQueueCreateInfo queue_info = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, NULL, 0, family, 1, &priority}; + VkPhysicalDeviceTimelineSemaphoreFeatures timeline_feature = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES, NULL, VK_TRUE}; + VkDeviceCreateInfo create_device = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &timeline_feature, 0, 1, &queue_info, 0, NULL, 0, NULL, NULL}; + assert(vkCreateDevice(physical, &create_device, NULL, &test_device) == VK_SUCCESS); + vkGetDeviceQueue(test_device, family, 0, &test_queue); + g_dev[0] = (struct dev){test_device, vkGetDeviceProcAddr, NULL, 1, 0, 1}; + g_producer_started = 1; + VkBufferCreateInfo create_buffer = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0, 4096, VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_SHARING_MODE_EXCLUSIVE, 0, NULL}; + VkBuffer buffer; + assert(vkCreateBuffer(test_device, &create_buffer, NULL, &buffer) == VK_SUCCESS); + VkMemoryRequirements requirements; + vkGetBufferMemoryRequirements(test_device, buffer, &requirements); + VkPhysicalDeviceMemoryProperties properties; + vkGetPhysicalDeviceMemoryProperties(physical, &properties); + uint32_t memory_type = UINT32_MAX; + VkMemoryPropertyFlags required = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + for (uint32_t index = 0; index < properties.memoryTypeCount; index++) + if ((requirements.memoryTypeBits & (1u << index)) && (properties.memoryTypes[index].propertyFlags & required) == required) { + memory_type = index; + break; + } + assert(memory_type != UINT32_MAX); + VkMemoryAllocateInfo allocate = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, requirements.size, memory_type}; + VkDeviceMemory memory; + assert(vkAllocateMemory(test_device, &allocate, NULL, &memory) == VK_SUCCESS); + assert(vkBindBufferMemory(test_device, buffer, memory, 0) == VK_SUCCESS); + uint32_t *pixels; + assert(vkMapMemory(test_device, memory, 0, VK_WHOLE_SIZE, 0, (void **)&pixels) == VK_SUCCESS); + VkCommandPoolCreateInfo create_pool = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, NULL, 0, family}; + VkCommandPool pool; + assert(vkCreateCommandPool(test_device, &create_pool, NULL, &pool) == VK_SUCCESS); + VkCommandBufferAllocateInfo allocate_commands = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, NULL, pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 2}; + assert(vkAllocateCommandBuffers(test_device, &allocate_commands, commands) == VK_SUCCESS); + const uint32_t patterns[2] = {0x13579bdf, 0x2468ace0}; + for (unsigned index = 0; index < 2; index++) { + VkCommandBufferBeginInfo begin = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL, 0, NULL}; + assert(vkBeginCommandBuffer(commands[index], &begin) == VK_SUCCESS); + vkCmdFillBuffer(commands[index], buffer, 0, 4096, patterns[index]); + VkMemoryBarrier barrier = {VK_STRUCTURE_TYPE_MEMORY_BARRIER, NULL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT}; + vkCmdPipelineBarrier(commands[index], VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0, NULL, 0, NULL); + assert(vkEndCommandBuffer(commands[index]) == VK_SUCCESS); + } + VkSemaphoreTypeCreateInfo type = {VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, NULL, VK_SEMAPHORE_TYPE_TIMELINE, 0}; + VkSemaphoreCreateInfo create_gate = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type, 0}; + assert(vkCreateSemaphore(test_device, &create_gate, NULL, &gate) == VK_SUCCESS); + uint64_t value = 1; + VkTimelineSemaphoreSubmitInfo timeline = {VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO, NULL, 1, &value, 0, NULL}; + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + VkSubmitInfo first = {VK_STRUCTURE_TYPE_SUBMIT_INFO, &timeline, 1, &gate, &stage, 1, &commands[0], 0, NULL}; + assert(QueueSubmit(test_queue, 1, &first, VK_NULL_HANDLE) == VK_SUCCESS); + pthread_t signal_thread; + int64_t began = droiddeck_producer_now(); + assert(pthread_create(&signal_thread, NULL, signal_gate, NULL) == 0); + assert(producer_acquire(began + 1000000000LL)); + assert(droiddeck_producer_now() - began >= 20000000LL); + assert(pthread_join(signal_thread, NULL) == 0); + check_pixels(pixels, patterns[0]); + pthread_t writer; + assert(pthread_create(&writer, NULL, second_submit, NULL) == 0); + while (!atomic_load(&second_entered)) usleep(100); + usleep(50000); + assert(!atomic_load(&second_returned)); + check_pixels(pixels, patterns[0]); + producer_release(); + assert(pthread_join(writer, NULL) == 0); + assert(vkQueueWaitIdle(test_queue) == VK_SUCCESS); + check_pixels(pixels, patterns[1]); + value = 2; + assert(QueueSubmit(test_queue, 1, &first, VK_NULL_HANDLE) == VK_SUCCESS); + assert(!producer_acquire(droiddeck_producer_now() + 20000000LL)); + VkFence pending = g_producer_queues[0].fence; + assert(pending && vkGetFenceStatus(test_device, pending) == VK_NOT_READY); + assert(!producer_acquire(droiddeck_producer_now() + 20000000LL)); + assert(g_producer_queues[0].fence == pending); + VkSemaphoreSignalInfo release_gate = {VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, NULL, gate, 2}; + assert(vkSignalSemaphore(test_device, &release_gate) == VK_SUCCESS); + assert(vkQueueWaitIdle(test_queue) == VK_SUCCESS); + assert(producer_acquire(droiddeck_producer_now() + 1000000000LL)); + producer_release(); + check_pixels(pixels, patterns[0]); + producer_destroy_device(&g_dev[0], NULL); + vkDestroySemaphore(test_device, gate, NULL); + vkDestroyCommandPool(test_device, pool, NULL); + vkUnmapMemory(test_device, memory); + vkDestroyBuffer(test_device, buffer, NULL); + vkFreeMemory(test_device, memory, NULL); + vkDestroyDevice(test_device, NULL); + vkDestroyInstance(instance, NULL); + puts("producer GPU: actual completion, frozen overwrite, timeout retention and 1024 buffer values passed"); + return 0; +} diff --git a/tools/vr/tests/test_producer_lease.c b/tools/vr/tests/test_producer_lease.c new file mode 100644 index 000000000..481585ab2 --- /dev/null +++ b/tools/vr/tests/test_producer_lease.c @@ -0,0 +1,87 @@ +#define _GNU_SOURCE +#define DROIDDECK_PRODUCER_SERVER +#include "../src/producer_lease.h" +#include +#include + +static pthread_mutex_t queue_lock = PTHREAD_MUTEX_INITIALIZER; +static atomic_int completed; +static atomic_int released; +static atomic_int quarantined; +static atomic_int available; + +static int acquire(int64_t deadline) { + assert(deadline > droiddeck_producer_now()); + if (!atomic_load(&available)) return 0; + assert(pthread_mutex_lock(&queue_lock) == 0); + atomic_fetch_add(&completed, 1); + return 1; +} + +static void release(void) { + atomic_fetch_add(&released, 1); + assert(pthread_mutex_unlock(&queue_lock) == 0); +} + +static void quarantine(void) { + atomic_store(&quarantined, 1); +} + +static void wait_count(atomic_int *counter, int expected) { + int64_t deadline = droiddeck_producer_now() + 1000000000LL; + while (atomic_load(counter) != expected && droiddeck_producer_now() < deadline) usleep(1000); + assert(atomic_load(counter) == expected); +} + +int main(void) { + char directory[] = "/tmp/droiddeck-producer-XXXXXX"; + assert(mkdtemp(directory)); + setenv("XDG_RUNTIME_DIR", directory, 1); + char parent[128]; + assert(snprintf(parent, sizeof(parent), "%s/droiddeck-steamvr", directory) > 0); + assert(mkdir(parent, 0755) == 0); + struct droiddeck_producer_server server = {.acquire = acquire, .release = release, .quarantine = quarantine}; + assert(!droiddeck_producer_start(&server)); + assert(chmod(parent, 0700) == 0); + assert(droiddeck_producer_start(&server)); + struct stat metadata; + assert(stat(server.path, &metadata) == 0 && (metadata.st_mode & 0777) == 0600); + struct droiddeck_producer_server duplicate = server; + assert(!droiddeck_producer_start(&duplicate)); + struct droiddeck_producer_lease lease; + assert(!droiddeck_producer_acquire(getpid(), &lease)); + assert(!atomic_load(&completed)); + atomic_store(&available, 1); + assert(droiddeck_producer_acquire(getpid(), &lease)); + assert(fcntl(lease.descriptor, F_GETFD) & FD_CLOEXEC); + assert(pthread_mutex_trylock(&queue_lock) == EBUSY); + uint64_t first_ticket = lease.ticket; + usleep(300000); + assert(pthread_mutex_trylock(&queue_lock) == EBUSY && !atomic_load(&released)); + assert(droiddeck_producer_release(&lease)); + assert(lease.descriptor == -1 && atomic_load(&released) == 1); + assert(pthread_mutex_trylock(&queue_lock) == 0); + assert(pthread_mutex_unlock(&queue_lock) == 0); + assert(droiddeck_producer_acquire(getpid(), &lease)); + assert(lease.ticket > first_ticket); + assert(droiddeck_producer_release(&lease)); + char wrong_path[128]; + assert(droiddeck_producer_path(wrong_path, sizeof(wrong_path), (uint32_t)getpid() + 1)); + assert(link(server.path, wrong_path) == 0); + assert(!droiddeck_producer_acquire((uint32_t)getpid() + 1, &lease)); + assert(unlink(wrong_path) == 0); + assert(chmod(parent, 0755) == 0); + assert(!droiddeck_producer_acquire(getpid(), &lease)); + assert(chmod(parent, 0700) == 0); + assert(droiddeck_producer_acquire(getpid(), &lease)); + close(lease.descriptor); + wait_count(&quarantined, 1); + assert(atomic_load(&released) == 2); + assert(pthread_mutex_trylock(&queue_lock) == EBUSY); + assert(pthread_join(server.thread, NULL) == 0); + close(server.listener); + assert(unlink(server.path) == 0); + assert(rmdir(parent) == 0 && rmdir(directory) == 0); + puts("producer lease: peer PID, private socket, completion gate, retained ownership and disconnect quarantine passed"); + return 0; +} diff --git a/tools/vr/tests/test_semaphore_layer.c b/tools/vr/tests/test_semaphore_layer.c index 1b02e0d85..ffb869e78 100644 --- a/tools/vr/tests/test_semaphore_layer.c +++ b/tools/vr/tests/test_semaphore_layer.c @@ -64,7 +64,7 @@ int main(void) { void *dispatch = &dispatch; VkDevice device = (VkDevice)&dispatch; VkQueue queue = (VkQueue)&dispatch; - g_dev[0] = (struct dev){device, mock_proc, NULL, 1, 0}; + g_dev[0] = (struct dev){device, mock_proc, NULL, 1, 0, 0}; g_poller_started = 1; VkExportSemaphoreCreateInfo export_info = {VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO, NULL, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT};