feat: add fenced SteamVR producer leases

This commit is contained in:
XxJSONDeruloxX committed 2026-10-14 22:03:05 -04:00
1 parent bb58d0cc7b
commit 023daf57d2
14 files changed
+696 -51

No files matched your search

+1 -1
View File
@@ -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 \
+29
View File
@@ -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
@@ -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
+1
View File
@@ -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)
+45
View File
@@ -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))
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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<std::mutex> lock(mutex_);
sets_.push_back(set);
}
+45 -42
View File
@@ -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 <atomic>
#include <chrono>
@@ -22,7 +23,6 @@ typedef unsigned long VisualID;
#include <dlfcn.h>
#include <fcntl.h>
#include <mutex>
#include <sys/ioctl.h>
#include <thread>
#include <unistd.h>
#include <vector>
@@ -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<std::mutex> 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<int>((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(); }
+2 -1
View File
@@ -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();
+192
View File
@@ -0,0 +1,192 @@
#pragma once
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#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, &current);
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 <pthread.h>
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
+146 -4
View File
@@ -11,6 +11,8 @@
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#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)
+143
View File
@@ -0,0 +1,143 @@
#define _GNU_SOURCE
#include <assert.h>
#include <stdatomic.h>
#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;
}
+87
View File
@@ -0,0 +1,87 @@
#define _GNU_SOURCE
#define DROIDDECK_PRODUCER_SERVER
#include "../src/producer_lease.h"
#include <assert.h>
#include <stdatomic.h>
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;
}
+1 -1
View File
@@ -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};