feat: add owned Quest frame transport

This commit is contained in:
XxJSONDeruloxX committed 2026-10-14 18:46:34 -04:00
1 parent db0c32c72f
commit 040d31c1ee
13 files changed
+972 -32

No files matched your search

+15 -3
View File
@@ -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 }}-
+50
View File
@@ -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.
+91 -3
View File
@@ -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) {
+88
View File
@@ -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
View File
@@ -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
View File
@@ -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, &copy);
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_;
+3
View File
@@ -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 {
+48
View File
@@ -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;
}
+181
View File
@@ -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(); }
};
+51
View File
@@ -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;
}
+42
View File
@@ -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");
}
+127
View File
@@ -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");
}