mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
feat: add fenced SteamVR producer leases
This commit is contained in:
1 parent
bb58d0cc7b
commit
023daf57d2
14 files changed
+696
-51
No files matched your search
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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(); }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, ¤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 <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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in new issue
Block a user