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