mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
feat: add owned Quest frame transport
This commit is contained in:
1 parent
db0c32c72f
commit
040d31c1ee
13 files changed
+972
-32
No files matched your search
@@ -314,8 +314,11 @@ jobs:
|
||||
docker run --rm -v "$PWD:/src" -w /src public.ecr.aws/docker/library/debian:bullseye bash -c '
|
||||
set -euo pipefail
|
||||
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
apt-get -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
apt-get -o Acquire::Retries=3 -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 install -y -qq --no-install-recommends ca-certificates >/dev/null
|
||||
sed -i "s|http://archive.debian.org|https://archive.debian.org|g" /etc/apt/sources.list
|
||||
apt-get -o Acquire::Retries=3 -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
|
||||
'
|
||||
sudo chown -R "$(id -u):$(id -g)" app/src/main/assets/linuxfs
|
||||
@@ -400,6 +403,15 @@ jobs:
|
||||
run: |
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_tracking.cpp -o /tmp/test-xr-tracking
|
||||
/tmp/test-xr-tracking
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_frames.cpp -o /tmp/test-xr-frames
|
||||
/tmp/test-xr-frames
|
||||
cc -Itools/vr/third_party/wayland -c tools/vr/src/wayland_client_stub.c -o /tmp/test-wayland-stub.o
|
||||
cc -Itools/vr/third_party/wayland -c app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c -o /tmp/test-xdg-shell.o
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers -pthread -Ibuild/vr/include \
|
||||
-Itools/vr/third_party/wayland -Iapp/src/main/cpp/waylandcomp/generated \
|
||||
tools/vr/tests/test_presenter_shutdown.cpp tools/vr/src/presenter.cpp /tmp/test-wayland-stub.o /tmp/test-xdg-shell.o \
|
||||
-ldl -o /tmp/test-presenter-shutdown
|
||||
timeout 10 /tmp/test-presenter-shutdown
|
||||
|
||||
- name: Rust toolchain for the store engines (aarch64-linux-android)
|
||||
id: rust
|
||||
@@ -434,7 +446,7 @@ jobs:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/build/intermediates/cxx
|
||||
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**', 'questxr/src/main/cpp/**', 'tools/vr/src/xr_tracking.h') }}
|
||||
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**', 'questxr/src/main/cpp/**', 'tools/vr/src/xr_*.h') }}
|
||||
restore-keys: |
|
||||
cxx-${{ env.NDK_VERSION }}-
|
||||
|
||||
|
||||
@@ -130,6 +130,56 @@ conversion on the headset.
|
||||
|
||||
## Remaining stages
|
||||
|
||||
## Milestone 5: owned image and original render-pose protocol
|
||||
|
||||
The opt-in `DROIDDECK_VR_PRESENT=questframes` presenter captures an owned RGBA
|
||||
side-by-side image and its original SteamVR render geometry. The driver retains
|
||||
each submitted `mHmdPose` and `mProjection` and composes the same eye-to-head rig
|
||||
configured at HMD activation. It does not substitute the newest tracked pose
|
||||
for an older image. The versioned 264-byte header and pixels travel together
|
||||
over a private, same-UID Unix socket. Acknowledgement means the receiver owns a
|
||||
complete CPU copy, not that an XR layer was displayed.
|
||||
|
||||
The capture path holds `Present` until its Vulkan copy fence completes. It
|
||||
refuses missing, empty, errored or timed-out producer DMA fences, unsupported
|
||||
multiple layers and malformed render geometry. It never substitutes the old
|
||||
4 ms delay for synchronization. **Kernel producer-fence registration on the
|
||||
Quest graphics chain is not yet established**, and KGSL may reject the export
|
||||
entirely. Consequently this is an experimental correctness-first path, not a
|
||||
verified synchronization solution. The existing compatibility Vulkan layer
|
||||
still drops some imported semaphore waits; that also needs resolution before
|
||||
gameplay correctness can be claimed.
|
||||
|
||||
With `frame_input=true`, the native frontend uses explicitly acquired GLES XR
|
||||
swapchain images, uploads receiver-owned pixels, and submits the captured
|
||||
per-eye poses/FOV. It omits stale images. `frame_verify=true` reads uploaded
|
||||
pixels back from the GPU and compares every byte. This mode is separate from
|
||||
the asynchronous Android Surface diagnostic. It is not yet the normal Quest
|
||||
Start flow and does not provide a zero-copy/performance-ready bridge.
|
||||
|
||||
Host tests cover image/pose pairing, rotation and asymmetric projection math,
|
||||
latest-frame replacement, copying before acknowledgement, private socket
|
||||
ownership, restart/stale-socket recovery, replay rejection, malformed/truncated
|
||||
input and deadlines. Address/undefined-behavior sanitizer tests pass. A separate
|
||||
test confirms eight concurrent blocking presenters exit when Vulkan cannot
|
||||
initialize. Android test executables compile with NDK 27.2.12479018; the main
|
||||
native target and companion APK also compile. These are host/build results,
|
||||
not headset GPU evidence. The Quest subsequently disappeared from ADB, so the
|
||||
new path cannot yet be deployed or validated on-device.
|
||||
|
||||
`tools/test_quest_frame_channel.py --adb <adb> --native-tests <directory>` runs
|
||||
the Android protocol test and a known-pattern sender under the companion's own
|
||||
UID. It checks the final source frame ID and GPU checksum. Add
|
||||
`--require-rendering` to additionally require sustained tracked projection.
|
||||
Without that option it proves only private image transport/GLES upload, not
|
||||
6DoF, SteamVR or games. It never enables the main app's protected Debugging tools.
|
||||
|
||||
The failed full build at `7277386c` was a Debian archive HTTP package 404. CI now
|
||||
bootstraps CA certificates, uses HTTPS for compiler packages and retries
|
||||
downloads while retaining APT signature/package-hash verification.
|
||||
|
||||
## Remaining stages
|
||||
|
||||
- Android/Linux eye-image bridge and frame ownership/synchronization.
|
||||
- OpenVR driver IPC for predicted 6DoF head, per-eye views and tracked hands.
|
||||
- Real Touch input mapping and haptics returning through the bridge.
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include "../../../../tools/vr/src/xr_tracking.h"
|
||||
#include "../../../../tools/vr/src/xr_frame_channel.h"
|
||||
#include "../../../../tools/vr/src/xr_frame_math.h"
|
||||
|
||||
static void check(XrResult result, const char *operation) {
|
||||
if (XR_FAILED(result)) throw std::runtime_error(std::string(operation) + ": " + std::to_string(result));
|
||||
@@ -102,6 +104,7 @@ public:
|
||||
if (app_->destroyRequested) break;
|
||||
pollTimeout = 0;
|
||||
}
|
||||
receiveFrame();
|
||||
pollEvents();
|
||||
if (running_ && !app_->destroyRequested) frame();
|
||||
}
|
||||
@@ -121,12 +124,17 @@ public:
|
||||
std::fprintf(file,
|
||||
"{\"frames\":%llu,\"rendered\":%llu,\"sessionState\":%d,\"headFlags\":%llu,"
|
||||
"\"headPosition\":[%.6f,%.6f,%.6f],\"controllersActive\":[%s,%s],"
|
||||
"\"periodNs\":%lld,\"surfaceSwapchain\":%s,\"source\":\"%s\",\"guestFrames\":%lld,\"steamvrState\":%d,\"error\":\"%s\"}\n",
|
||||
"\"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",
|
||||
(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_, surfaceSupported_ ? "true" : "false", guestSurface_ ? "guest" : surfaceTest_ ? "surface" : "native", (long long)guestFrames_, steamvrState, error);
|
||||
(long long)period_, surfaceSupported_ ? "true" : "false", frameInput_ ? "frames" : guestSurface_ ? "guest" : surfaceTest_ ? "surface" : "native",
|
||||
(long long)guestFrames_, steamvrState, (unsigned long long)receiver_.Received(), (unsigned long long)receiver_.Rejected(),
|
||||
(unsigned long long)uploadedFrames_, (unsigned long long)sourceHeader_.frame_id,
|
||||
(unsigned long long)verifiedFrame_, gpuChecksum_, error);
|
||||
std::fclose(file);
|
||||
std::rename(temp.c_str(), path.c_str());
|
||||
}
|
||||
@@ -153,6 +161,12 @@ private:
|
||||
bool running_ = false, exit_ = false, surfaceSupported_ = false, quitRequested_ = false;
|
||||
bool surfaceTest_ = false;
|
||||
bool guestSurface_ = false, guestAttached_ = false;
|
||||
bool frameInput_ = false, verifyPixels_ = false;
|
||||
XrFrameReceiver receiver_;
|
||||
DroidDeckXrFrameHeader sourceHeader_{};
|
||||
uint64_t uploadedFrames_ = 0, verifiedFrame_ = 0;
|
||||
uint32_t gpuChecksum_ = 0;
|
||||
GLuint sourceTexture_ = 0, sourceProgram_ = 0;
|
||||
jmethodID surfaceCallback_ = nullptr, frameCallback_ = nullptr;
|
||||
jmethodID statusCallback_ = nullptr;
|
||||
jobject androidJavaSurface_ = nullptr;
|
||||
@@ -201,6 +215,12 @@ private:
|
||||
jstring extra = env_->NewStringUTF("surface_test");
|
||||
surfaceTest_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
|
||||
env_->DeleteLocalRef(extra);
|
||||
extra = env_->NewStringUTF("frame_input");
|
||||
frameInput_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
|
||||
env_->DeleteLocalRef(extra);
|
||||
extra = env_->NewStringUTF("frame_verify");
|
||||
verifyPixels_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
|
||||
env_->DeleteLocalRef(extra);
|
||||
extra = env_->NewStringUTF("guest_surface");
|
||||
guestSurface_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
|
||||
if (guestSurface_) {
|
||||
@@ -210,6 +230,7 @@ private:
|
||||
statusCallback_ = env_->GetMethodID(activityClass, "onXrStatus", "()I");
|
||||
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("XR activity bridge methods unavailable"); }
|
||||
}
|
||||
if (frameInput_ && (surfaceTest_ || guestSurface_)) throw std::runtime_error("Frame input cannot use an asynchronous surface projection");
|
||||
env_->DeleteLocalRef(extra); env_->DeleteLocalRef(intentClass);
|
||||
env_->DeleteLocalRef(intent); env_->DeleteLocalRef(activityClass);
|
||||
PFN_xrInitializeLoaderKHR initializeLoader = nullptr;
|
||||
@@ -292,6 +313,10 @@ private:
|
||||
if (surfaceTest_) initializeAndroidSurface(config);
|
||||
initializeGraphics();
|
||||
initializeTracking();
|
||||
if (frameInput_) {
|
||||
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");
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "initialized GLES; Android surface swapchain supported=%d", surfaceSupported_);
|
||||
health();
|
||||
}
|
||||
@@ -526,6 +551,51 @@ private:
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr); glEnableVertexAttribArray(0);
|
||||
glGenFramebuffers(1, &framebuffer_);
|
||||
if (frameInput_) {
|
||||
vertex = shader(GL_VERTEX_SHADER,
|
||||
"#version 300 es\nout vec2 uv; uniform float eye; void main(){vec2 p=vec2(float((gl_VertexID<<1)&2),float(gl_VertexID&2));uv=vec2(p.x*0.5+eye*0.5,1.0-p.y);gl_Position=vec4(p*2.0-1.0,0.0,1.0);}");
|
||||
fragment = shader(GL_FRAGMENT_SHADER,
|
||||
"#version 300 es\nprecision highp float; in vec2 uv; uniform sampler2D source; uniform float eye; out vec4 outputColor; void main(){float edge=0.5/float(textureSize(source,0).x);vec2 p=vec2(clamp(uv.x,eye*0.5+edge,(eye+1.0)*0.5-edge),uv.y);outputColor=texture(source,p);}");
|
||||
sourceProgram_ = glCreateProgram(); glAttachShader(sourceProgram_, vertex); glAttachShader(sourceProgram_, fragment);
|
||||
glLinkProgram(sourceProgram_); glDeleteShader(vertex); glDeleteShader(fragment);
|
||||
glGetProgramiv(sourceProgram_, GL_LINK_STATUS, &linked);
|
||||
if (!linked) throw std::runtime_error("Frame shader link failed");
|
||||
glGenTextures(1, &sourceTexture_); glBindTexture(GL_TEXTURE_2D, 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);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveFrame() {
|
||||
if (!frameInput_) 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");
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture_);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, frame.header.width, frame.header.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, frame.pixels.data());
|
||||
if (verifyPixels_) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture_, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
throw std::runtime_error("Frame readback framebuffer incomplete");
|
||||
std::vector<unsigned char> readback(frame.header.bytes);
|
||||
glReadPixels(0, 0, frame.header.width, frame.header.height, GL_RGBA, GL_UNSIGNED_BYTE, readback.data());
|
||||
if (readback != frame.pixels) throw std::runtime_error("Frame GPU readback does not match owned pixels");
|
||||
gpuChecksum_ = 2166136261u;
|
||||
for (unsigned char value : readback) gpuChecksum_ = (gpuChecksum_ ^ value) * 16777619u;
|
||||
verifiedFrame_ = frame.header.frame_id;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("Frame texture upload failed");
|
||||
sourceHeader_ = frame.header;
|
||||
uploadedFrames_++;
|
||||
if (verifyPixels_ || uploadedFrames_ % 72 == 1) health();
|
||||
}
|
||||
|
||||
void cube(const Matrix &camera, const XrPosef &pose, float size, float red, float green, float blue) {
|
||||
@@ -543,7 +613,14 @@ private:
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, eye.depth);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
throw std::runtime_error("Incomplete eye framebuffer");
|
||||
scene(view, eye.width, eye.height, 0);
|
||||
if (frameInput_) {
|
||||
glViewport(0, 0, eye.width, eye.height); glDisable(GL_DEPTH_TEST);
|
||||
glUseProgram(sourceProgram_); glBindVertexArray(vertexArray_);
|
||||
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, sourceTexture_);
|
||||
glUniform1i(glGetUniformLocation(sourceProgram_, "source"), 0);
|
||||
glUniform1f(glGetUniformLocation(sourceProgram_, "eye"), &eye == &eyes_[1] ? 1.f : 0.f);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
} else scene(view, eye.width, eye.height, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("OpenGL render error");
|
||||
glFlush();
|
||||
@@ -649,6 +726,15 @@ private:
|
||||
bool valid = frameState.shouldRender && (!surfaceTest_ || state_ == XR_SESSION_STATE_VISIBLE || state_ == XR_SESSION_STATE_FOCUSED) && count == 2 &&
|
||||
(viewState.viewStateFlags & (XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT))
|
||||
== (XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT);
|
||||
if (frameInput_) {
|
||||
valid = valid && sourceHeader_.frame_id && xr_frame_now() - sourceHeader_.captured_ns < 500000000LL;
|
||||
if (valid) for (int eye = 0; eye < 2; eye++) {
|
||||
auto source = xr_frame_view(sourceHeader_, eye);
|
||||
views[eye].pose.orientation = {source.orientation[0], source.orientation[1], source.orientation[2], source.orientation[3]};
|
||||
views[eye].pose.position = {source.position[0], source.position[1], source.position[2]};
|
||||
views[eye].fov = {source.fov[0], source.fov[1], source.fov[2], source.fov[3]};
|
||||
}
|
||||
}
|
||||
if (valid && surfaceTest_) {
|
||||
if (!guestSurface_ && !eglMakeCurrent(display_, androidSurface_, androidSurface_, context_))
|
||||
throw std::runtime_error("Surface context binding failed");
|
||||
@@ -691,6 +777,7 @@ private:
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
receiver_.Stop();
|
||||
try { attachGuest(false); } catch (const std::exception &error) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "cleanup: %s", error.what());
|
||||
}
|
||||
@@ -702,6 +789,7 @@ private:
|
||||
if (display_ != EGL_NO_DISPLAY && context_ != EGL_NO_CONTEXT) {
|
||||
glDeleteFramebuffers(1, &framebuffer_); glDeleteProgram(program_);
|
||||
glDeleteBuffers(1, &buffer_); glDeleteVertexArrays(1, &vertexArray_);
|
||||
glDeleteTextures(1, &sourceTexture_); glDeleteProgram(sourceProgram_);
|
||||
for (auto &eye : eyes_) glDeleteRenderbuffers(1, &eye.depth);
|
||||
}
|
||||
if (androidSurface_ != EGL_NO_SURFACE) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from adb_device import resolve_serial
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Validate private owned-frame transport and GLES upload, not SteamVR gameplay')
|
||||
parser.add_argument('--adb', default=os.environ.get('ADB', 'adb'))
|
||||
parser.add_argument('--serial')
|
||||
parser.add_argument('--native-tests', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, default=Path('questxr-validation/frames'))
|
||||
parser.add_argument('--require-rendering', action='store_true')
|
||||
args = parser.parse_args()
|
||||
serial = resolve_serial(args.adb, args.serial)
|
||||
package = 'com.droiddeck.questxr'
|
||||
health_path = f'/sdcard/Android/data/{package}/files/xr-health.json'
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def adb(*command):
|
||||
return subprocess.run([args.adb, '-s', serial, *command], check=True,
|
||||
capture_output=True, text=True, timeout=30).stdout
|
||||
|
||||
for name in ('test_xr_frames', 'send_xr_frame'):
|
||||
binary = args.native_tests / name
|
||||
if not binary.is_file():
|
||||
parser.error(f'Missing Android executable: {binary}')
|
||||
remote = f'/data/local/tmp/droiddeck-{name}'
|
||||
adb('push', str(binary), remote)
|
||||
adb('shell', 'run-as', package, 'cp', remote, f'./{name}')
|
||||
adb('shell', 'run-as', package, 'chmod', '700', f'./{name}')
|
||||
unit_output = adb('shell', 'run-as', package, './test_xr_frames', './files')
|
||||
adb('shell', 'am', 'force-stop', package)
|
||||
adb('shell', 'rm', '-f', health_path)
|
||||
failure = None
|
||||
samples = []
|
||||
sent = {}
|
||||
try:
|
||||
adb('shell', 'input', 'keyevent', 'KEYCODE_WAKEUP')
|
||||
adb('shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.prox_close')
|
||||
adb('shell', 'am', 'start', '-n', f'{package}/android.app.NativeActivity',
|
||||
'--ez', 'frame_input', 'true', '--ez', 'frame_verify', 'true')
|
||||
sent = json.loads(adb('shell', 'run-as', package, './send_xr_frame',
|
||||
'./files/.wayland-rt/droiddeck-xr-frames'))
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
sample = json.loads(adb('shell', 'cat', health_path))
|
||||
samples.append(sample)
|
||||
if sample.get('error'):
|
||||
failure = sample['error']
|
||||
break
|
||||
if sample.get('gpuVerifiedFrameId') == sent['lastFrameId']:
|
||||
break
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as error:
|
||||
failure = f'Frame channel operation failed: {error}'
|
||||
finally:
|
||||
try:
|
||||
adb('shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.prox_far')
|
||||
adb('shell', 'am', 'force-stop', package)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
failure = failure or 'Could not restore headset proximity after testing'
|
||||
latest = samples[-1] if samples else {}
|
||||
if not failure:
|
||||
if latest.get('source') != 'frames' or latest.get('receivedFrames') != sent.get('sent'):
|
||||
failure = 'Not all complete frames arrived in the private receiver'
|
||||
elif latest.get('gpuVerifiedFrameId') != sent.get('lastFrameId') or latest.get('gpuChecksum') != sent.get('checksum'):
|
||||
failure = 'GPU readback does not prove the final image and frame ID match'
|
||||
elif latest.get('rejectedFrames') != 0 or latest.get('uploadedFrames', 0) < 1:
|
||||
failure = 'Malformed frames or no actual GPU upload'
|
||||
elif args.require_rendering and (latest.get('rendered', 0) < 72 or latest.get('headFlags', 0) & 15 != 15):
|
||||
failure = 'Owned upload passed but sustained tracked immersive projection did not'
|
||||
report = {'ok': failure is None, 'scope': 'owned-frame GLES upload', 'error': failure,
|
||||
'unitTests': unit_output, 'sender': sent, 'samples': samples}
|
||||
(args.output / 'report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0 if report['ok'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "../../../app/src/main/cpp/waylandcomp/src/headpose_shm.h"
|
||||
#include "presenter.h"
|
||||
#include "xr_tracking_file.h"
|
||||
#include "xr_frame_math.h"
|
||||
|
||||
#define DD_EXPORT extern "C" __attribute__((visibility("default")))
|
||||
|
||||
@@ -186,6 +187,10 @@ private:
|
||||
|
||||
class DirectMode : public IVRDriverDirectModeComponent {
|
||||
public:
|
||||
void ConfigureXr(const DroidDeckXrTracking &tracking) {
|
||||
xrWidth_ = tracking.eyes[0].width * 2; xrHeight_ = tracking.eyes[0].height;
|
||||
for (int eye = 0; eye < 2; eye++) eyeToHead_[eye] = XrEyeToHead(tracking.head, tracking.eyes[eye].pose);
|
||||
}
|
||||
struct Set {
|
||||
SharedTextureHandle_t handles[3] = {};
|
||||
int fds[3] = {-1, -1, -1};
|
||||
@@ -254,10 +259,18 @@ public:
|
||||
void SubmitLayer(const SubmitLayerPerEye_t (&eyes)[2]) override {
|
||||
layers_++;
|
||||
if (layerInFrame_++ == 0) {
|
||||
frame_.render = {};
|
||||
if (xrWidth_) {
|
||||
frame_.render.magic = DROIDDECK_XR_FRAME_MAGIC; frame_.render.version = DROIDDECK_XR_FRAME_VERSION;
|
||||
frame_.render.width = xrWidth_; frame_.render.height = xrHeight_;
|
||||
frame_.render.bytes = xrWidth_ * xrHeight_ * 4; frame_.render.color_space = 1;
|
||||
}
|
||||
for (int e = 0; e < 2; e++) {
|
||||
frame_.texture[e] = eyes[e].hTexture;
|
||||
frame_.bounds[e][0] = eyes[e].bounds.uMin; frame_.bounds[e][1] = eyes[e].bounds.vMin;
|
||||
frame_.bounds[e][2] = eyes[e].bounds.uMax; frame_.bounds[e][3] = eyes[e].bounds.vMax;
|
||||
std::memcpy(frame_.render.projection[e], eyes[e].mProjection.m, sizeof(eyes[e].mProjection.m));
|
||||
xr_frame_eye_pose(frame_.render.eye_pose[e], eyes[e].mHmdPose.m, eyeToHead_[e].m);
|
||||
}
|
||||
}
|
||||
if (frames_ < 3)
|
||||
@@ -268,6 +281,8 @@ public:
|
||||
|
||||
void Present(SharedTextureHandle_t sync) override {
|
||||
frames_++;
|
||||
frame_.render.frame_id = frames_; frame_.render.captured_ns = XrTrackingFile::Now();
|
||||
frame_.layers = layerInFrame_;
|
||||
if (layerInFrame_) droiddeck::Presenter::Submit(frame_);
|
||||
if (layerInFrame_ != lastLayerCount_) {
|
||||
static int said;
|
||||
@@ -308,6 +323,8 @@ private:
|
||||
uint64_t frames_ = 0, layers_ = 0;
|
||||
int layerInFrame_ = 0, lastLayerCount_ = -1;
|
||||
droiddeck::EyeFrame frame_{};
|
||||
uint32_t xrWidth_ = 0, xrHeight_ = 0;
|
||||
HmdMatrix34_t eyeToHead_[2]{};
|
||||
};
|
||||
|
||||
struct PadState {
|
||||
@@ -500,6 +517,7 @@ public:
|
||||
VRProperties()->SetFloatProperty(c, Prop_UserIpdMeters_Float, 0.064f);
|
||||
if (xrMode_) {
|
||||
VRServerDriverHost()->SetDisplayEyeToHead(id_, XrEyeToHead(xr.head, xr.eyes[0].pose), XrEyeToHead(xr.head, xr.eyes[1].pose));
|
||||
dm_.ConfigureXr(xr);
|
||||
float distanceSquared = 0;
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
float difference = xr.eyes[0].pose.position[axis] - xr.eyes[1].pose.position[axis];
|
||||
|
||||
+235
-26
@@ -10,6 +10,7 @@ typedef unsigned long VisualID;
|
||||
|
||||
#include "presenter.h"
|
||||
#include "xr_tracking_file.h"
|
||||
#include "xr_frame_channel.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -21,6 +22,7 @@ typedef unsigned long VisualID;
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <sys/ioctl.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
@@ -37,7 +39,7 @@ void Log(const char *fmt, ...) {
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
if (vr::VRDriverLog()) vr::VRDriverLog()->Log(buf);
|
||||
if (vr::VRDriverContext() && vr::VRDriverLog()) vr::VRDriverLog()->Log(buf);
|
||||
fprintf(stderr, "[droiddeck] %s\n", buf);
|
||||
}
|
||||
|
||||
@@ -74,13 +76,15 @@ struct X11 {
|
||||
X(GetPhysicalDeviceMemoryProperties) X(CreateDevice) X(GetDeviceProcAddr) X(CreateXlibSurfaceKHR) \
|
||||
X(CreateWaylandSurfaceKHR) \
|
||||
X(GetPhysicalDeviceSurfaceSupportKHR) X(GetPhysicalDeviceSurfaceCapabilitiesKHR) \
|
||||
X(GetPhysicalDeviceSurfaceFormatsKHR) X(GetPhysicalDeviceSurfacePresentModesKHR) X(GetPhysicalDeviceProperties)
|
||||
X(GetPhysicalDeviceSurfaceFormatsKHR) X(GetPhysicalDeviceSurfacePresentModesKHR) X(GetPhysicalDeviceProperties) X(DestroySurfaceKHR)
|
||||
#define VK_DEVICE(X) \
|
||||
X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) X(AcquireNextImageKHR) \
|
||||
X(QueuePresentKHR) X(CreateCommandPool) X(AllocateCommandBuffers) X(BeginCommandBuffer) X(EndCommandBuffer) \
|
||||
X(ResetCommandBuffer) X(CmdPipelineBarrier) X(CmdBlitImage) X(CmdClearColorImage) X(QueueSubmit) X(CreateFence) \
|
||||
X(WaitForFences) X(ResetFences) X(CreateSemaphore) X(CreateImage) X(DestroyImage) X(GetImageMemoryRequirements) \
|
||||
X(AllocateMemory) X(FreeMemory) X(BindImageMemory) X(DeviceWaitIdle)
|
||||
X(AllocateMemory) X(FreeMemory) X(BindImageMemory) X(DeviceWaitIdle) X(DestroyDevice) X(DestroyFence) X(DestroySemaphore) \
|
||||
X(DestroyCommandPool) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) X(BindBufferMemory) \
|
||||
X(MapMemory) X(UnmapMemory) X(InvalidateMappedMemoryRanges) X(CmdCopyImageToBuffer)
|
||||
|
||||
struct Vk {
|
||||
void *lib = nullptr;
|
||||
@@ -95,6 +99,7 @@ struct Imported {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
uint32_t width = 0, height = 0;
|
||||
int dmaFd = -1;
|
||||
};
|
||||
|
||||
struct PendingImport {
|
||||
@@ -120,63 +125,87 @@ public:
|
||||
|
||||
void Add(const uint64_t handles[3], const int fds[3], uint32_t format, uint32_t w, uint32_t h) {
|
||||
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});
|
||||
}
|
||||
EnsureThread();
|
||||
cv_.notify_one();
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
void Remove(uint64_t handle) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
removals_.push_back(handle);
|
||||
cv_.notify_one();
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
void Submit(const EyeFrame &f) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
if (failed_ || exit_) return;
|
||||
bool blocking = FrameChannelRequested();
|
||||
if (blocking) cv_.wait(lock, [this] { return failed_ || exit_ || (!haveFrame_ && !inFlight_); });
|
||||
if (failed_ || exit_) return;
|
||||
EnsureThread();
|
||||
frame_ = f;
|
||||
frameAt_ = std::chrono::steady_clock::now();
|
||||
haveFrame_ = true;
|
||||
submitted_++;
|
||||
cv_.notify_one();
|
||||
uint64_t ticket = ++ticket_;
|
||||
cv_.notify_all();
|
||||
if (blocking) cv_.wait(lock, [this, ticket] { return failed_ || completed_ >= ticket; });
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
exit_ = true;
|
||||
cv_.notify_one();
|
||||
cv_.notify_all();
|
||||
}
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool FrameChannelRequested() {
|
||||
const char *mode = getenv("DROIDDECK_VR_PRESENT");
|
||||
return mode && !strcmp(mode, "questframes");
|
||||
}
|
||||
|
||||
void Fail() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
failed_ = true;
|
||||
for (const auto &pending : pending_) close(pending.fd);
|
||||
pending_.clear(); cv_.notify_all();
|
||||
}
|
||||
void EnsureThread() {
|
||||
if (!started_) {
|
||||
started_ = true;
|
||||
thread_ = std::thread([this] { Run(); });
|
||||
thread_ = std::thread([this] {
|
||||
try { Run(); }
|
||||
catch (const std::exception &error) { Log("presenter: %s", error.what()); Fail(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Run() {
|
||||
const char *mode = getenv("DROIDDECK_VR_PRESENT");
|
||||
wayland_ = !(mode && !strcmp(mode, "x11")) && InitWayland();
|
||||
if (!wayland_ && !InitWindow()) {
|
||||
frameChannel_ = FrameChannelRequested();
|
||||
if (!frameChannel_) wayland_ = !(mode && !strcmp(mode, "x11")) && InitWayland();
|
||||
if (!frameChannel_ && !wayland_ && !InitWindow()) {
|
||||
Log("presenter: unavailable, frames are not shown");
|
||||
return;
|
||||
Fail(); return;
|
||||
}
|
||||
if (!InitVulkan()) {
|
||||
Log("presenter: unavailable, frames are not shown");
|
||||
return;
|
||||
Fail(); return;
|
||||
}
|
||||
uint64_t shown = 0;
|
||||
auto lastLog = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
EyeFrame f;
|
||||
uint64_t ticket = 0;
|
||||
std::chrono::steady_clock::time_point at;
|
||||
std::vector<PendingImport> imports;
|
||||
std::vector<uint64_t> removals;
|
||||
@@ -190,13 +219,19 @@ private:
|
||||
at = frameAt_;
|
||||
bool draw = haveFrame_;
|
||||
haveFrame_ = false;
|
||||
inFlight_ = draw; ticket = ticket_;
|
||||
if (!draw) f.texture[0] = f.texture[1] = 0;
|
||||
}
|
||||
if (wl_) { wl_display_dispatch_pending(wl_); wl_display_flush(wl_); }
|
||||
for (auto &p : imports) Import(p);
|
||||
for (auto h : removals) Forget(h);
|
||||
if (f.texture[0]) std::this_thread::sleep_until(at + guard_);
|
||||
if (f.texture[0] && Draw(f)) shown++;
|
||||
if (f.texture[0] && !frameChannel_) std::this_thread::sleep_until(at + guard_);
|
||||
if (f.texture[0] && (frameChannel_ ? Capture(f) : Draw(f))) shown++;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
inFlight_ = false; completed_ = ticket; cv_.notify_all();
|
||||
if (failed_) break;
|
||||
}
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now - lastLog > std::chrono::seconds(10)) {
|
||||
Log("presenter: %llu frames shown, %llu submitted (last 10 s)", (unsigned long long)shown,
|
||||
@@ -206,6 +241,19 @@ private:
|
||||
}
|
||||
}
|
||||
if (dev_) vk_.DeviceWaitIdle(dev_);
|
||||
DestroyCapture();
|
||||
while (!textures_.empty()) Forget(textures_.back().handle);
|
||||
if (dev_) {
|
||||
if (swapchain_) vk_.DestroySwapchainKHR(dev_, swapchain_, nullptr);
|
||||
if (acquired_) vk_.DestroySemaphore(dev_, acquired_, nullptr);
|
||||
if (rendered_) vk_.DestroySemaphore(dev_, rendered_, nullptr);
|
||||
if (fence_) vk_.DestroyFence(dev_, fence_, nullptr);
|
||||
if (pool_) vk_.DestroyCommandPool(dev_, pool_, nullptr);
|
||||
vk_.DestroyDevice(dev_, nullptr); dev_ = VK_NULL_HANDLE;
|
||||
}
|
||||
if (surface_) vk_.DestroySurfaceKHR(inst_, surface_, nullptr);
|
||||
if (inst_) vk_.DestroyInstance(inst_, nullptr);
|
||||
Fail();
|
||||
}
|
||||
|
||||
std::chrono::microseconds guard_{4000};
|
||||
@@ -313,7 +361,8 @@ private:
|
||||
#undef X
|
||||
const char *iexts[] = {"VK_KHR_surface", wayland_ ? "VK_KHR_wayland_surface" : "VK_KHR_xlib_surface"};
|
||||
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr, "droiddeck-vr-presenter", 1, nullptr, 0, VK_API_VERSION_1_1};
|
||||
VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, nullptr, 0, &app, 0, nullptr, 2, iexts};
|
||||
VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, nullptr, 0, &app, 0, nullptr,
|
||||
frameChannel_ ? 0u : 2u, frameChannel_ ? nullptr : iexts};
|
||||
VkResult r = vk_.CreateInstance(&ici, nullptr, &inst_);
|
||||
if (r != VK_SUCCESS) { Log("presenter: vkCreateInstance %d", r); return false; }
|
||||
#define X(n) vk_.n = (PFN_vk##n)vk_.GetInstanceProcAddr(inst_, "vk" #n);
|
||||
@@ -328,10 +377,10 @@ private:
|
||||
vk_.GetPhysicalDeviceProperties(pd_, &props);
|
||||
vk_.GetPhysicalDeviceMemoryProperties(pd_, &mem_);
|
||||
|
||||
if (wayland_) {
|
||||
if (!frameChannel_ && wayland_) {
|
||||
VkWaylandSurfaceCreateInfoKHR wci = {VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, nullptr, 0, wl_, wlSurf_};
|
||||
if ((r = vk_.CreateWaylandSurfaceKHR(inst_, &wci, nullptr, &surface_)) != VK_SUCCESS) { Log("presenter: wayland surface %d", r); return false; }
|
||||
} else {
|
||||
} else if (!frameChannel_) {
|
||||
VkXlibSurfaceCreateInfoKHR sci = {VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, nullptr, 0, dpy_, win_};
|
||||
if ((r = vk_.CreateXlibSurfaceKHR(inst_, &sci, nullptr, &surface_)) != VK_SUCCESS) { Log("presenter: xlib surface %d", r); return false; }
|
||||
}
|
||||
@@ -343,14 +392,15 @@ private:
|
||||
qfam_ = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < nq; i++) {
|
||||
VkBool32 present = VK_FALSE;
|
||||
vk_.GetPhysicalDeviceSurfaceSupportKHR(pd_, i, surface_, &present);
|
||||
if (frameChannel_) present = VK_TRUE;
|
||||
else vk_.GetPhysicalDeviceSurfaceSupportKHR(pd_, i, surface_, &present);
|
||||
if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) { qfam_ = i; break; }
|
||||
}
|
||||
if (qfam_ == UINT32_MAX) { Log("presenter: no queue can present"); return false; }
|
||||
float prio = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, nullptr, 0, qfam_, 1, &prio};
|
||||
const char *dexts[] = {"VK_KHR_swapchain", "VK_KHR_external_memory_fd"};
|
||||
VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, nullptr, 0, 1, &qci, 0, nullptr, 2, dexts, nullptr};
|
||||
const char *dexts[] = {"VK_KHR_external_memory_fd", "VK_KHR_swapchain"};
|
||||
VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, nullptr, 0, 1, &qci, 0, nullptr, frameChannel_ ? 1u : 2u, dexts, nullptr};
|
||||
if ((r = vk_.CreateDevice(pd_, &dci, nullptr, &dev_)) != VK_SUCCESS) { Log("presenter: vkCreateDevice %d", r); return false; }
|
||||
#define X(n) vk_.n = (PFN_vk##n)vk_.GetDeviceProcAddr(dev_, "vk" #n);
|
||||
VK_DEVICE(X)
|
||||
@@ -367,7 +417,7 @@ private:
|
||||
VkSemaphoreCreateInfo semci = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0};
|
||||
vk_.CreateSemaphore(dev_, &semci, nullptr, &acquired_);
|
||||
vk_.CreateSemaphore(dev_, &semci, nullptr, &rendered_);
|
||||
if (!CreateSwapchain()) return false;
|
||||
if (!frameChannel_ && !CreateSwapchain()) return false;
|
||||
Log("presenter: Vulkan on %s, queue family %u", props.deviceName, qfam_);
|
||||
return true;
|
||||
}
|
||||
@@ -429,6 +479,7 @@ private:
|
||||
Imported im{p.handle};
|
||||
im.width = p.width;
|
||||
im.height = p.height;
|
||||
if (frameChannel_) im.dmaFd = dup(p.fd);
|
||||
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)};
|
||||
@@ -440,20 +491,27 @@ 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); return; }
|
||||
if (r != VK_SUCCESS) { Log("presenter: import image %d", r); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); 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};
|
||||
VkMemoryAllocateInfo mai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &ded, req.size, (uint32_t)MemoryType(req.memoryTypeBits)};
|
||||
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; }
|
||||
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;
|
||||
}
|
||||
vk_.BindImageMemory(dev_, im.image, im.memory, 0);
|
||||
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);
|
||||
Log("presenter: imported texture 0x%llx (%ux%u, format %u)", (unsigned long long)p.handle, p.width, p.height, p.format);
|
||||
}
|
||||
@@ -464,6 +522,7 @@ 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;
|
||||
}
|
||||
@@ -482,6 +541,149 @@ 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);
|
||||
if (captureMemory_) vk_.FreeMemory(dev_, captureMemory_, nullptr);
|
||||
if (readbackBuffer_) vk_.DestroyBuffer(dev_, readbackBuffer_, nullptr);
|
||||
if (readbackMemory_) vk_.FreeMemory(dev_, readbackMemory_, nullptr);
|
||||
captureImage_ = VK_NULL_HANDLE; captureMemory_ = readbackMemory_ = VK_NULL_HANDLE;
|
||||
readbackBuffer_ = VK_NULL_HANDLE; captureWidth_ = captureHeight_ = 0;
|
||||
}
|
||||
|
||||
bool PrepareCapture(const DroidDeckXrFrameHeader &header) {
|
||||
if (captureWidth_ == header.width && captureHeight_ == header.height) return true;
|
||||
DestroyCapture();
|
||||
auto fail = [this](VkResult result) { Log("presenter: capture allocation failed %d", result); DestroyCapture(); return false; };
|
||||
VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
image.imageType = VK_IMAGE_TYPE_2D; image.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
image.extent = {header.width, header.height, 1}; image.mipLevels = image.arrayLayers = 1;
|
||||
image.samples = VK_SAMPLE_COUNT_1_BIT; image.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
image.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
VkResult result = vk_.CreateImage(dev_, &image, nullptr, &captureImage_);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
VkMemoryRequirements requirements{}; vk_.GetImageMemoryRequirements(dev_, captureImage_, &requirements);
|
||||
int type = MemoryType(requirements.memoryTypeBits);
|
||||
if (type < 0) return fail(VK_ERROR_FEATURE_NOT_PRESENT);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type;
|
||||
result = vk_.AllocateMemory(dev_, &allocation, nullptr, &captureMemory_);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
result = vk_.BindImageMemory(dev_, captureImage_, captureMemory_, 0);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
VkBufferCreateInfo buffer{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; buffer.size = header.bytes;
|
||||
buffer.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
result = vk_.CreateBuffer(dev_, &buffer, nullptr, &readbackBuffer_);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
vk_.GetBufferMemoryRequirements(dev_, readbackBuffer_, &requirements);
|
||||
type = -1;
|
||||
for (uint32_t index = 0; index < mem_.memoryTypeCount; index++) {
|
||||
if ((requirements.memoryTypeBits & (1u << index)) && (mem_.memoryTypes[index].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) {
|
||||
type = index;
|
||||
if (mem_.memoryTypes[index].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) break;
|
||||
}
|
||||
}
|
||||
if (type < 0) return fail(VK_ERROR_FEATURE_NOT_PRESENT);
|
||||
allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type;
|
||||
result = vk_.AllocateMemory(dev_, &allocation, nullptr, &readbackMemory_);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
result = vk_.BindBufferMemory(dev_, readbackBuffer_, readbackMemory_, 0);
|
||||
if (result != VK_SUCCESS) return fail(result);
|
||||
captureWidth_ = header.width; captureHeight_ = header.height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Capture(const EyeFrame &frame) {
|
||||
if (frame.layers != 1 || !xr_frame_valid(frame.render)) {
|
||||
static unsigned said = 0;
|
||||
if (said++ < 3) Log("presenter: refusing Quest frame with unsupported layer count or render geometry (%u layers)", frame.layers);
|
||||
return false;
|
||||
}
|
||||
const Imported *eye[2] = {Find(frame.texture[0]), Find(frame.texture[1])};
|
||||
if (!eye[0] || !eye[1]) return false;
|
||||
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;
|
||||
}
|
||||
VkResult result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX);
|
||||
if (result != VK_SUCCESS) { Fail(); return false; }
|
||||
if (!PrepareCapture(frame.render)) return false;
|
||||
if (vk_.ResetCommandBuffer(cmd_, 0) != VK_SUCCESS) { Fail(); return false; }
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if (vk_.BeginCommandBuffer(cmd_, &begin) != VK_SUCCESS) { Fail(); return false; }
|
||||
VkImageMemoryBarrier before[] = {
|
||||
Barrier(captureImage_, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT),
|
||||
Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_),
|
||||
Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_)};
|
||||
vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
|
||||
eye[0] == eye[1] ? 2 : 3, before);
|
||||
int32_t half = frame.render.width / 2;
|
||||
for (int index = 0; index < 2; index++) {
|
||||
const float *bounds = frame.bounds[index];
|
||||
VkImageBlit blit{}; blit.srcSubresource = blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
blit.srcOffsets[0] = {static_cast<int32_t>(bounds[0] * eye[index]->width), static_cast<int32_t>(bounds[1] * eye[index]->height), 0};
|
||||
blit.srcOffsets[1] = {static_cast<int32_t>(bounds[2] * eye[index]->width), static_cast<int32_t>(bounds[3] * eye[index]->height), 1};
|
||||
blit.dstOffsets[0] = {index * half, 0, 0}; blit.dstOffsets[1] = {(index + 1) * half, static_cast<int32_t>(frame.render.height), 1};
|
||||
vk_.CmdBlitImage(cmd_, eye[index]->image, VK_IMAGE_LAYOUT_GENERAL, captureImage_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
|
||||
}
|
||||
VkImageMemoryBarrier after[] = {
|
||||
Barrier(captureImage_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT),
|
||||
Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL),
|
||||
Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL)};
|
||||
vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
|
||||
eye[0] == eye[1] ? 2 : 3, after);
|
||||
VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {frame.render.width, frame.render.height, 1};
|
||||
vk_.CmdCopyImageToBuffer(cmd_, captureImage_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readbackBuffer_, 1, ©);
|
||||
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 = readbackBuffer_; host.size = VK_WHOLE_SIZE;
|
||||
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_;
|
||||
result = vk_.QueueSubmit(queue_, 1, &submit, fence_);
|
||||
if (result != VK_SUCCESS) { Fail(); return false; }
|
||||
result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX);
|
||||
if (result != VK_SUCCESS) { vk_.DeviceWaitIdle(dev_); Fail(); 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;
|
||||
result = vk_.InvalidateMappedMemoryRanges(dev_, 1, &range);
|
||||
bool sent = result == VK_SUCCESS && sender_.Send("/run/droiddeck/" DROIDDECK_XR_FRAME_SOCKET,
|
||||
frame.render, static_cast<unsigned char *>(mapping));
|
||||
vk_.UnmapMemory(dev_, readbackMemory_);
|
||||
return sent;
|
||||
}
|
||||
|
||||
bool Draw(const EyeFrame &f) {
|
||||
const Imported *eye[2] = {Find(f.texture[0]), Find(f.texture[1])};
|
||||
if (!eye[0] || !eye[1]) return false;
|
||||
@@ -551,7 +753,14 @@ private:
|
||||
std::vector<uint64_t> removals_;
|
||||
EyeFrame frame_{};
|
||||
std::chrono::steady_clock::time_point frameAt_{};
|
||||
bool haveFrame_ = false, exit_ = false, started_ = false;
|
||||
bool haveFrame_ = false, exit_ = false, started_ = false, inFlight_ = false, failed_ = false;
|
||||
uint64_t ticket_ = 0, completed_ = 0;
|
||||
bool frameChannel_ = false;
|
||||
XrFrameSender sender_;
|
||||
VkImage captureImage_ = VK_NULL_HANDLE;
|
||||
VkDeviceMemory captureMemory_ = VK_NULL_HANDLE, readbackMemory_ = VK_NULL_HANDLE;
|
||||
VkBuffer readbackBuffer_ = VK_NULL_HANDLE;
|
||||
uint32_t captureWidth_ = 0, captureHeight_ = 0;
|
||||
std::atomic<uint64_t> submitted_{0};
|
||||
std::thread thread_;
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "xr_frame.h"
|
||||
|
||||
namespace droiddeck {
|
||||
|
||||
struct EyeFrame {
|
||||
uint64_t texture[2];
|
||||
float bounds[2][4];
|
||||
DroidDeckXrFrameHeader render;
|
||||
uint32_t layers = 0;
|
||||
};
|
||||
|
||||
class Presenter {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <cmath>
|
||||
|
||||
#define DROIDDECK_XR_FRAME_MAGIC 0x46584444u
|
||||
#define DROIDDECK_XR_FRAME_VERSION 1u
|
||||
#define DROIDDECK_XR_FRAME_SOCKET "droiddeck-xr-frames"
|
||||
#define DROIDDECK_XR_FRAME_MAX_BYTES (64u * 1024u * 1024u)
|
||||
|
||||
struct DroidDeckXrFrameHeader {
|
||||
uint32_t magic, version, width, height;
|
||||
uint64_t frame_id;
|
||||
int64_t captured_ns;
|
||||
uint32_t bytes, color_space;
|
||||
float eye_pose[2][3][4];
|
||||
float projection[2][4][4];
|
||||
};
|
||||
|
||||
static_assert(sizeof(DroidDeckXrFrameHeader) == 264, "XR frame protocol layout");
|
||||
|
||||
inline bool xr_frame_valid(const DroidDeckXrFrameHeader &header) {
|
||||
uint64_t bytes = (uint64_t)header.width * header.height * 4;
|
||||
if (header.magic != DROIDDECK_XR_FRAME_MAGIC || header.version != DROIDDECK_XR_FRAME_VERSION
|
||||
|| !header.frame_id || header.width < 2 || (header.width & 1u) || !header.height
|
||||
|| header.width > 8192 || header.height > 4096 || bytes > DROIDDECK_XR_FRAME_MAX_BYTES
|
||||
|| header.bytes != bytes || header.color_space != 1 || header.captured_ns <= 0) return false;
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
for (const auto &row : header.eye_pose[eye]) for (float value : row) if (!std::isfinite(value)) return false;
|
||||
for (const auto &row : header.projection[eye]) for (float value : row) if (!std::isfinite(value)) return false;
|
||||
const auto &projection = header.projection[eye];
|
||||
if (projection[0][0] < 0.0001f || projection[1][1] < 0.0001f
|
||||
|| std::abs(projection[3][2] + 1.f) > 0.0001f || std::abs(projection[3][3]) > 0.0001f
|
||||
|| std::abs(projection[3][0]) > 0.0001f || std::abs(projection[3][1]) > 0.0001f
|
||||
|| std::abs(projection[0][1]) > 0.0001f || std::abs(projection[0][3]) > 0.0001f
|
||||
|| std::abs(projection[1][0]) > 0.0001f || std::abs(projection[1][3]) > 0.0001f) return false;
|
||||
for (int row = 0; row < 3; row++) for (int other = 0; other < 3; other++) {
|
||||
float product = 0;
|
||||
for (int column = 0; column < 3; column++) product += header.eye_pose[eye][row][column] * header.eye_pose[eye][other][column];
|
||||
if (std::abs(product - (row == other ? 1.f : 0.f)) > 0.01f) return false;
|
||||
}
|
||||
const auto &rotation = header.eye_pose[eye];
|
||||
float determinant = rotation[0][0] * (rotation[1][1] * rotation[2][2] - rotation[1][2] * rotation[2][1])
|
||||
- rotation[0][1] * (rotation[1][0] * rotation[2][2] - rotation[1][2] * rotation[2][0])
|
||||
+ rotation[0][2] * (rotation[1][0] * rotation[2][1] - rotation[1][1] * rotation[2][0]);
|
||||
if (std::abs(determinant - 1.f) > 0.01f) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#pragma once
|
||||
#include "xr_frame.h"
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <poll.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
inline int64_t xr_frame_now() {
|
||||
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
|
||||
}
|
||||
|
||||
inline bool xr_frame_io(int descriptor, void *data, size_t size, bool write, int64_t deadline) {
|
||||
auto *bytes = static_cast<unsigned char *>(data);
|
||||
while (size) {
|
||||
int64_t remaining = deadline - xr_frame_now();
|
||||
if (remaining <= 0) return false;
|
||||
pollfd ready{descriptor, (short)(write ? 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 false;
|
||||
ssize_t done = write ? send(descriptor, bytes, size, MSG_NOSIGNAL | MSG_DONTWAIT)
|
||||
: recv(descriptor, bytes, size, MSG_DONTWAIT);
|
||||
if (done < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
|
||||
if (done <= 0) return false;
|
||||
bytes += done; size -= done;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool xr_frame_peer(int descriptor) {
|
||||
struct { pid_t pid; uid_t uid; gid_t gid; } peer{};
|
||||
socklen_t size = sizeof(peer);
|
||||
return getsockopt(descriptor, SOL_SOCKET, SO_PEERCRED, &peer, &size) == 0
|
||||
&& size == sizeof(peer) && peer.uid == getuid();
|
||||
}
|
||||
|
||||
struct DroidDeckXrPixels {
|
||||
DroidDeckXrFrameHeader header{};
|
||||
std::vector<unsigned char> pixels;
|
||||
};
|
||||
|
||||
class XrFrameReceiver {
|
||||
public:
|
||||
~XrFrameReceiver() { Stop(); }
|
||||
bool Start(const std::string &filename) {
|
||||
if (listener_ >= 0 || filename.empty() || filename.size() >= sizeof(sockaddr_un::sun_path)) return false;
|
||||
lock_ = open((filename + ".lock").c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||
struct stat status{};
|
||||
if (lock_ < 0) return false;
|
||||
if (fstat(lock_, &status) != 0 || !S_ISREG(status.st_mode) || status.st_uid != getuid()
|
||||
|| flock(lock_, LOCK_EX | LOCK_NB) != 0) { close(lock_); lock_ = -1; return false; }
|
||||
if (lstat(filename.c_str(), &status) == 0) {
|
||||
if (!S_ISSOCK(status.st_mode) || status.st_uid != getuid() || unlink(filename.c_str()) != 0) {
|
||||
close(lock_); lock_ = -1; return false;
|
||||
}
|
||||
} else if (errno != ENOENT) { close(lock_); lock_ = -1; return false; }
|
||||
sockaddr_un address{}; address.sun_family = AF_UNIX;
|
||||
std::strcpy(address.sun_path, filename.c_str());
|
||||
listener_ = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
if (listener_ < 0) { Stop(); return false; }
|
||||
if (bind(listener_, reinterpret_cast<sockaddr *>(&address), sizeof(address)) != 0) { Stop(); return false; }
|
||||
filename_ = filename;
|
||||
if (chmod(filename.c_str(), 0600) != 0 || listen(listener_, 2) != 0) { Stop(); return false; }
|
||||
{ std::lock_guard<std::mutex> lock(mutex_); pending_ = {}; }
|
||||
received_ = rejected_ = 0;
|
||||
stopping_ = false;
|
||||
thread_ = std::thread([this] { Loop(); });
|
||||
return true;
|
||||
}
|
||||
void Stop() {
|
||||
stopping_ = true;
|
||||
if (thread_.joinable()) thread_.join();
|
||||
if (listener_ >= 0) close(listener_);
|
||||
listener_ = -1;
|
||||
if (!filename_.empty()) unlink(filename_.c_str());
|
||||
filename_.clear();
|
||||
if (lock_ >= 0) close(lock_);
|
||||
lock_ = -1;
|
||||
}
|
||||
bool Take(DroidDeckXrPixels *out) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (pending_.pixels.empty()) return false;
|
||||
*out = std::move(pending_);
|
||||
pending_ = {};
|
||||
return true;
|
||||
}
|
||||
uint64_t Rejected() const { return rejected_; }
|
||||
uint64_t Received() const { return received_; }
|
||||
private:
|
||||
int listener_ = -1;
|
||||
int lock_ = -1;
|
||||
std::string filename_;
|
||||
std::atomic<bool> stopping_{false};
|
||||
std::atomic<uint64_t> rejected_{0}, received_{0};
|
||||
std::thread thread_;
|
||||
std::mutex mutex_;
|
||||
DroidDeckXrPixels pending_;
|
||||
void Loop() {
|
||||
while (!stopping_) {
|
||||
pollfd ready{listener_, POLLIN, 0};
|
||||
if (poll(&ready, 1, 50) <= 0) continue;
|
||||
int client = accept4(listener_, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
|
||||
if (client < 0) continue;
|
||||
if (!xr_frame_peer(client)) { rejected_++; close(client); continue; }
|
||||
uint64_t lastFrame = 0;
|
||||
while (!stopping_) {
|
||||
DroidDeckXrPixels frame;
|
||||
int64_t deadline = xr_frame_now() + 200000000LL;
|
||||
if (!xr_frame_io(client, &frame.header, sizeof(frame.header), false, deadline)) break;
|
||||
int64_t now = xr_frame_now();
|
||||
if (!xr_frame_valid(frame.header) || frame.header.captured_ns > now
|
||||
|| now - frame.header.captured_ns > 500000000LL || frame.header.frame_id <= lastFrame) { rejected_++; break; }
|
||||
try { frame.pixels.resize(frame.header.bytes); }
|
||||
catch (...) { rejected_++; break; }
|
||||
if (!xr_frame_io(client, frame.pixels.data(), frame.pixels.size(), false, deadline)) { rejected_++; break; }
|
||||
if (xr_frame_now() - frame.header.captured_ns > 500000000LL) { rejected_++; break; }
|
||||
uint64_t acknowledgement = frame.header.frame_id;
|
||||
lastFrame = acknowledgement;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
pending_ = std::move(frame);
|
||||
}
|
||||
received_++;
|
||||
if (!xr_frame_io(client, &acknowledgement, sizeof(acknowledgement), true, deadline)) break;
|
||||
}
|
||||
close(client);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class XrFrameSender {
|
||||
public:
|
||||
XrFrameSender() = default;
|
||||
XrFrameSender(const XrFrameSender &) = delete;
|
||||
XrFrameSender &operator=(const XrFrameSender &) = delete;
|
||||
~XrFrameSender() { Disconnect(); }
|
||||
bool Send(const std::string &filename, const DroidDeckXrFrameHeader &header, const unsigned char *pixels) {
|
||||
if (!pixels || !xr_frame_valid(header) || filename.empty() || filename.size() >= sizeof(sockaddr_un::sun_path)) return false;
|
||||
if (filename_ != filename) Disconnect();
|
||||
int64_t deadline = xr_frame_now() + 200000000LL;
|
||||
if (descriptor_ < 0) {
|
||||
sockaddr_un address{}; address.sun_family = AF_UNIX;
|
||||
std::strcpy(address.sun_path, filename.c_str());
|
||||
descriptor_ = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
if (descriptor_ < 0) return false;
|
||||
int result = connect(descriptor_, reinterpret_cast<sockaddr *>(&address), sizeof(address));
|
||||
if (result != 0 && errno == EINPROGRESS) {
|
||||
pollfd ready{descriptor_, POLLOUT, 0};
|
||||
int error = 0; socklen_t size = sizeof(error);
|
||||
result = poll(&ready, 1, 200) > 0 && getsockopt(descriptor_, SOL_SOCKET, SO_ERROR, &error, &size) == 0
|
||||
&& error == 0 ? 0 : -1;
|
||||
}
|
||||
if (result != 0 || !xr_frame_peer(descriptor_)) {
|
||||
Disconnect(); return false;
|
||||
}
|
||||
filename_ = filename;
|
||||
}
|
||||
uint64_t acknowledgement = 0;
|
||||
if (!xr_frame_io(descriptor_, const_cast<DroidDeckXrFrameHeader *>(&header), sizeof(header), true, deadline)
|
||||
|| !xr_frame_io(descriptor_, const_cast<unsigned char *>(pixels), header.bytes, true, deadline)
|
||||
|| !xr_frame_io(descriptor_, &acknowledgement, sizeof(acknowledgement), false, deadline)
|
||||
|| acknowledgement != header.frame_id) { Disconnect(); return false; }
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
int descriptor_ = -1;
|
||||
std::string filename_;
|
||||
void Disconnect() { if (descriptor_ >= 0) close(descriptor_); descriptor_ = -1; filename_.clear(); }
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include "xr_frame.h"
|
||||
|
||||
struct DroidDeckXrRenderView {
|
||||
float orientation[4];
|
||||
float position[3];
|
||||
float fov[4];
|
||||
};
|
||||
|
||||
inline void xr_frame_eye_pose(float result[3][4], const float head[3][4], const float eyeToHead[3][4]) {
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 3; column++) {
|
||||
result[row][column] = 0;
|
||||
for (int inner = 0; inner < 3; inner++) result[row][column] += head[row][inner] * eyeToHead[inner][column];
|
||||
}
|
||||
result[row][3] = head[row][3];
|
||||
for (int inner = 0; inner < 3; inner++) result[row][3] += head[row][inner] * eyeToHead[inner][3];
|
||||
}
|
||||
}
|
||||
|
||||
inline DroidDeckXrRenderView xr_frame_view(const DroidDeckXrFrameHeader &header, int eye) {
|
||||
const auto &matrix = header.eye_pose[eye];
|
||||
DroidDeckXrRenderView view{};
|
||||
float trace = matrix[0][0] + matrix[1][1] + matrix[2][2];
|
||||
if (trace > 0) {
|
||||
float scale = std::sqrt(trace + 1.f) * 2.f;
|
||||
view.orientation[3] = scale * 0.25f;
|
||||
view.orientation[0] = (matrix[2][1] - matrix[1][2]) / scale;
|
||||
view.orientation[1] = (matrix[0][2] - matrix[2][0]) / scale;
|
||||
view.orientation[2] = (matrix[1][0] - matrix[0][1]) / scale;
|
||||
} else {
|
||||
int axis = matrix[1][1] > matrix[0][0] ? 1 : 0;
|
||||
if (matrix[2][2] > matrix[axis][axis]) axis = 2;
|
||||
int next = (axis + 1) % 3, last = (axis + 2) % 3;
|
||||
float scale = std::sqrt(1.f + matrix[axis][axis] - matrix[next][next] - matrix[last][last]) * 2.f;
|
||||
view.orientation[axis] = scale * 0.25f;
|
||||
view.orientation[next] = (matrix[axis][next] + matrix[next][axis]) / scale;
|
||||
view.orientation[last] = (matrix[axis][last] + matrix[last][axis]) / scale;
|
||||
view.orientation[3] = (matrix[last][next] - matrix[next][last]) / scale;
|
||||
}
|
||||
float lengthSquared = 0;
|
||||
for (float value : view.orientation) lengthSquared += value * value;
|
||||
for (float &value : view.orientation) value /= std::sqrt(lengthSquared);
|
||||
for (int axis = 0; axis < 3; axis++) view.position[axis] = matrix[axis][3];
|
||||
const auto &projection = header.projection[eye];
|
||||
view.fov[0] = std::atan((projection[0][2] - 1.f) / projection[0][0]);
|
||||
view.fov[1] = std::atan((projection[0][2] + 1.f) / projection[0][0]);
|
||||
view.fov[2] = std::atan((projection[1][2] + 1.f) / projection[1][1]);
|
||||
view.fov[3] = std::atan((projection[1][2] - 1.f) / projection[1][1]);
|
||||
return view;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "../src/xr_frame_channel.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) { std::fprintf(stderr, "usage: send_xr_frame <socket>\n"); return 2; }
|
||||
DroidDeckXrFrameHeader header{};
|
||||
header.magic = DROIDDECK_XR_FRAME_MAGIC; header.version = DROIDDECK_XR_FRAME_VERSION;
|
||||
header.width = 64; header.height = 32; header.bytes = header.width * header.height * 4; header.color_space = 1;
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
for (int axis = 0; axis < 3; axis++) header.eye_pose[eye][axis][axis] = 1;
|
||||
header.eye_pose[eye][0][3] = eye == 0 ? -0.032f : 0.032f; header.eye_pose[eye][1][3] = 1.6f;
|
||||
header.projection[eye][0][0] = header.projection[eye][1][1] = 1;
|
||||
header.projection[eye][2][2] = -1.002f; header.projection[eye][2][3] = -0.1001f;
|
||||
header.projection[eye][3][2] = -1;
|
||||
}
|
||||
std::vector<unsigned char> pixels(header.bytes);
|
||||
for (uint32_t row = 0; row < header.height; row++) for (uint32_t column = 0; column < header.width; column++) {
|
||||
size_t offset = (row * header.width + column) * 4;
|
||||
pixels[offset] = column < header.width / 2 ? 211 : 19;
|
||||
pixels[offset + 1] = row * 7;
|
||||
pixels[offset + 2] = column < header.width / 2 ? 31 : 239;
|
||||
pixels[offset + 3] = 255;
|
||||
}
|
||||
uint32_t checksum = 2166136261u;
|
||||
for (unsigned char value : pixels) checksum = (checksum ^ value) * 16777619u;
|
||||
XrFrameSender sender;
|
||||
int64_t deadline = xr_frame_now() + 10000000000LL;
|
||||
uint64_t sent = 0;
|
||||
for (uint64_t id = 1; id <= 144; id++) {
|
||||
header.frame_id = id; header.captured_ns = xr_frame_now();
|
||||
bool delivered = sender.Send(argv[1], header, pixels.data());
|
||||
while (!delivered && sent == 0 && xr_frame_now() < deadline) {
|
||||
usleep(20000); header.captured_ns = xr_frame_now();
|
||||
delivered = sender.Send(argv[1], header, pixels.data());
|
||||
}
|
||||
if (!delivered) { std::fprintf(stderr, "frame %llu was not acknowledged\n", (unsigned long long)id); return 1; }
|
||||
sent++; usleep(13889);
|
||||
}
|
||||
std::printf("{\"sent\":%llu,\"lastFrameId\":%llu,\"checksum\":%u}\n",
|
||||
(unsigned long long)sent, (unsigned long long)header.frame_id, checksum);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "../src/presenter.h"
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
int main() {
|
||||
setenv("DROIDDECK_VR_PRESENT", "questframes", 1);
|
||||
setenv("VK_ICD_FILENAMES", "/droiddeck-test-no-icd.json", 1);
|
||||
setenv("VK_DRIVER_FILES", "/droiddeck-test-no-icd.json", 1);
|
||||
auto started = std::chrono::steady_clock::now();
|
||||
std::vector<std::thread> callers;
|
||||
for (int index = 0; index < 8; index++) callers.emplace_back([] {
|
||||
droiddeck::EyeFrame frame{};
|
||||
for (int count = 0; count < 1000; count++) droiddeck::Presenter::Submit(frame);
|
||||
});
|
||||
for (auto &caller : callers) caller.join();
|
||||
droiddeck::Presenter::Shutdown();
|
||||
assert(std::chrono::steady_clock::now() - started < std::chrono::seconds(5));
|
||||
std::puts("Presenter validation passed: unavailable GPU releases concurrent blocking callers and shutdown");
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "../src/xr_frame_channel.h"
|
||||
#include "../src/xr_frame_math.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
static DroidDeckXrFrameHeader Header(uint64_t id = 1) {
|
||||
DroidDeckXrFrameHeader header{};
|
||||
header.magic = DROIDDECK_XR_FRAME_MAGIC; header.version = DROIDDECK_XR_FRAME_VERSION;
|
||||
header.width = 8; header.height = 4; header.bytes = 128; header.color_space = 1;
|
||||
header.frame_id = id; 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.eye_pose[eye][0][3] = eye == 0 ? -0.032f : 0.032f;
|
||||
header.eye_pose[eye][1][3] = 1.6f;
|
||||
header.projection[eye][0][0] = header.projection[eye][1][1] = 1;
|
||||
header.projection[eye][2][2] = -1.002f; header.projection[eye][2][3] = -0.1001f;
|
||||
header.projection[eye][3][2] = -1;
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
static int Connect(const std::string &path) {
|
||||
int descriptor = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
assert(descriptor >= 0);
|
||||
sockaddr_un address{}; address.sun_family = AF_UNIX;
|
||||
std::strcpy(address.sun_path, path.c_str());
|
||||
assert(connect(descriptor, reinterpret_cast<sockaddr *>(&address), sizeof(address)) == 0);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
static void WaitRejected(XrFrameReceiver &receiver, uint64_t count) {
|
||||
int64_t deadline = xr_frame_now() + 1000000000LL;
|
||||
while (receiver.Rejected() < count && xr_frame_now() < deadline) usleep(1000);
|
||||
assert(receiver.Rejected() == count);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char directory[256];
|
||||
std::snprintf(directory, sizeof(directory), "%s/droiddeck-frames-XXXXXX", argc > 1 ? argv[1] : "/tmp");
|
||||
assert(mkdtemp(directory));
|
||||
std::string socketPath = std::string(directory) + "/frames";
|
||||
auto valid = Header(); assert(xr_frame_valid(valid));
|
||||
auto invalid = valid; invalid.version++; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.width = 3; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.bytes--; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.width = 8192; invalid.height = 4096; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.eye_pose[0][0][0] = -1; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.eye_pose[0][1][1] = 2; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.projection[0][0][0] = std::numeric_limits<float>::quiet_NaN(); assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.projection[0][3][2] = 0; assert(!xr_frame_valid(invalid));
|
||||
invalid = valid; invalid.projection[0][1][1] = -1; assert(!xr_frame_valid(invalid));
|
||||
auto view = xr_frame_view(valid, 0);
|
||||
assert(view.orientation[3] == 1 && view.position[0] == -0.032f && view.position[1] == 1.6f);
|
||||
assert(std::abs(view.fov[0] + 0.785398f) < 0.00001f && std::abs(view.fov[2] - 0.785398f) < 0.00001f);
|
||||
auto rotated = valid;
|
||||
rotated.eye_pose[0][0][0] = rotated.eye_pose[0][2][2] = 0;
|
||||
rotated.eye_pose[0][0][2] = 1; rotated.eye_pose[0][2][0] = -1;
|
||||
assert(xr_frame_valid(rotated)); view = xr_frame_view(rotated, 0);
|
||||
assert(std::abs(view.orientation[1] - 0.70710678f) < 0.00001f);
|
||||
float eyeToHead[3][4]{{1, 0, 0, 0.032f}, {0, 1, 0, 0}, {0, 0, 1, 0}};
|
||||
auto paired = rotated;
|
||||
xr_frame_eye_pose(paired.eye_pose[0], rotated.eye_pose[0], eyeToHead);
|
||||
rotated.eye_pose[0][1][3] = 9;
|
||||
assert(paired.eye_pose[0][1][3] == 1.6f && paired.eye_pose[0][2][3] == -0.032f);
|
||||
assert(xr_frame_valid(paired));
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
rotated = valid;
|
||||
for (int other = 0; other < 3; other++) if (other != axis) rotated.eye_pose[0][other][other] = -1;
|
||||
assert(xr_frame_valid(rotated)); view = xr_frame_view(rotated, 0);
|
||||
assert(view.orientation[axis] == 1 && view.orientation[3] == 0);
|
||||
}
|
||||
valid.projection[0][0][2] = 0.2f; valid.projection[0][1][2] = -0.1f;
|
||||
view = xr_frame_view(valid, 0);
|
||||
assert(std::abs(std::tan(view.fov[0]) + 0.8f) < 0.00001f && std::abs(std::tan(view.fov[2]) - 0.9f) < 0.00001f);
|
||||
XrFrameReceiver receiver; assert(receiver.Start(socketPath));
|
||||
struct stat status{}; assert(stat(socketPath.c_str(), &status) == 0 && (status.st_mode & 0777) == 0600);
|
||||
XrFrameReceiver other; assert(!other.Start(socketPath));
|
||||
XrFrameSender sender;
|
||||
std::vector<unsigned char> pixels(valid.bytes, 17);
|
||||
assert(sender.Send(socketPath, valid, pixels.data()));
|
||||
pixels.assign(valid.bytes, 99);
|
||||
DroidDeckXrPixels taken; assert(receiver.Take(&taken));
|
||||
assert(taken.header.frame_id == 1 && taken.pixels[0] == 17 && taken.header.eye_pose[0][0][3] == -0.032f);
|
||||
assert(!receiver.Take(&taken));
|
||||
for (uint64_t id = 2; id <= 100; id++) {
|
||||
auto header = Header(id); header.eye_pose[0][0][3] = static_cast<float>(id);
|
||||
pixels.assign(header.bytes, static_cast<unsigned char>(id));
|
||||
assert(sender.Send(socketPath, header, pixels.data()));
|
||||
}
|
||||
assert(receiver.Take(&taken));
|
||||
assert(taken.header.frame_id == 100 && taken.header.eye_pose[0][0][3] == 100 && taken.pixels[0] == 100);
|
||||
assert(receiver.Received() == 100);
|
||||
assert(!sender.Send(socketPath, Header(100), pixels.data())); WaitRejected(receiver, 1);
|
||||
assert(sender.Send(socketPath, Header(101), pixels.data())); assert(receiver.Take(&taken));
|
||||
receiver.Stop(); assert(!sender.Send(socketPath, Header(102), pixels.data()));
|
||||
assert(receiver.Start(socketPath)); assert(sender.Send(socketPath, Header(1), pixels.data()));
|
||||
assert(receiver.Take(&taken));
|
||||
receiver.Stop(); assert(receiver.Start(socketPath));
|
||||
int descriptor = Connect(socketPath); auto header = Header(); header.version++;
|
||||
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
|
||||
close(descriptor); WaitRejected(receiver, 1); assert(!receiver.Take(&taken));
|
||||
descriptor = Connect(socketPath); header = Header(); header.captured_ns += 1000000000LL;
|
||||
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
|
||||
close(descriptor); WaitRejected(receiver, 2);
|
||||
descriptor = Connect(socketPath); header = Header(); header.captured_ns -= 1000000000LL;
|
||||
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
|
||||
close(descriptor); WaitRejected(receiver, 3);
|
||||
descriptor = Connect(socketPath); header = Header();
|
||||
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
|
||||
assert(send(descriptor, pixels.data(), 1, MSG_NOSIGNAL) == 1);
|
||||
close(descriptor); WaitRejected(receiver, 4); assert(!receiver.Take(&taken));
|
||||
descriptor = Connect(socketPath); header = Header();
|
||||
assert(xr_frame_io(descriptor, &header, sizeof(header), true, xr_frame_now() + 200000000LL));
|
||||
WaitRejected(receiver, 5); close(descriptor);
|
||||
assert(!sender.Send(socketPath, Header(200), pixels.data()));
|
||||
assert(sender.Send(socketPath, Header(201), pixels.data()));
|
||||
receiver.Stop();
|
||||
int stale = socket(AF_UNIX, SOCK_STREAM, 0); sockaddr_un address{}; address.sun_family = AF_UNIX;
|
||||
std::strcpy(address.sun_path, socketPath.c_str());
|
||||
assert(bind(stale, reinterpret_cast<sockaddr *>(&address), sizeof(address)) == 0); close(stale);
|
||||
assert(receiver.Start(socketPath)); receiver.Stop();
|
||||
int regular = open(socketPath.c_str(), O_CREAT | O_RDWR, 0600); assert(regular >= 0); close(regular);
|
||||
assert(!receiver.Start(socketPath)); assert(stat(socketPath.c_str(), &status) == 0 && S_ISREG(status.st_mode));
|
||||
unlink(socketPath.c_str()); unlink((socketPath + ".lock").c_str()); rmdir(directory);
|
||||
std::puts("XR frame validation passed: owned pixels, paired render pose, latest mailbox, bounds, projection, restart, stale socket, replay, malformed and timeout rejection");
|
||||
}
|
||||
Reference in new issue
Block a user