feat: validate Quest guest frame entrypoint

This commit is contained in:
XxJSONDeruloxX committed 2026-10-14 19:07:40 -04:00
1 parent 040d31c1ee
commit 2ef2d4b53d
13 files changed
+245 -45

No files matched your search

@@ -15,8 +15,8 @@ class QuestXrActivity : NativeActivity() {
@Volatile private var closing = false
@Volatile private var steamVrStatus = SteamVr.Status.UNAVAILABLE
override fun onCreate(savedInstanceState: Bundle?) {
if (intent.getBooleanExtra("guest_surface", false)) {
intent.putExtra("surface_test", true)
if (intent.getBooleanExtra("guest_surface", false) || intent.getBooleanExtra("guest_frames", false)) {
if (intent.getBooleanExtra("guest_surface", false)) intent.putExtra("surface_test", true)
SessionService.resume(this)
}
super.onCreate(savedInstanceState)
@@ -35,7 +35,26 @@ class QuestXrActivity : NativeActivity() {
SessionService.setActivityVisible(this, surface != null)
WaylandCompositor.nativeVsync(System.nanoTime())
check(WaylandCompositor.nativeWaitSurface(3000)) { "XR surface GPU handshake timed out" }
if (surface != null && intent.getBooleanExtra("start_steamvr", false) && !steamVrStarted) {
if (surface != null) startSteamVr()
}
fun onXrActive(active: Boolean) {
if (active) {
check(CompositorHost.isStarted) { "DroidDeck compositor is not running" }
SessionService.resume(this)
WaylandCompositor.nativeSetCardboard(false, 1, 0.064f, 3f, 4f, 0f, 0f, 90f, 0f, 0f)
WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
WaylandCompositor.nativeSetZeroCopy(false)
CompositorHost.attachXr(null)
WaylandCompositor.nativeVsync(System.nanoTime())
check(WaylandCompositor.nativeWaitSurface(3000)) { "XR frame GPU handshake timed out" }
}
SessionService.setActivityVisible(this, active)
if (active) startSteamVr()
}
private fun startSteamVr() {
if (intent.getBooleanExtra("start_steamvr", false) && !steamVrStarted) {
steamVrStarted = true
Thread({
if (!SteamVr.run(this, "start")) Log.e("DroidDeckXR", "SteamVR start failed")
@@ -57,7 +76,6 @@ class QuestXrActivity : NativeActivity() {
override fun onDestroy() {
closing = true
if (steamVrStarted) Thread({ SteamVr.run(this, "stop") }, "quest-steamvr-stop").start()
super.onDestroy()
}
}
+39 -4
View File
@@ -128,8 +128,6 @@ The actual shared tracking file was read through the debuggable companion:
21 ms beyond the monotonic sample time. This confirms the IPC time-domain
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
@@ -154,8 +152,14 @@ 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.
the asynchronous Android Surface diagnostic. The main activity's explicit
`guest_frames=true` entrypoint detaches the desktop Surface, keeps the guest
active and starts the same SteamVR helper when requested. The Linux helper
selects the experimental presenter only when this private receiver exists.
`tools/test_quest_xr.py --guest --steamvr` uses that entrypoint and requires owned
source images/frame IDs to advance, not generic desktop presentations. It is
not yet the normal Quest Start flow and does not provide a zero-copy or
performance-ready bridge.
Host tests cover image/pose pairing, rotation and asymmetric projection math,
latest-frame replacement, copying before acknowledgement, private socket
@@ -167,6 +171,7 @@ 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/build_quest_xr_tests.ps1` builds the three Android test executables.
`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
@@ -178,6 +183,36 @@ The failed full build at `7277386c` was a Debian archive HTTP package 404. CI no
bootstraps CA certificates, uses HTTPS for compiler packages and retries
downloads while retaining APT signature/package-hash verification.
The signed companion for this stage is retained locally under ignored
`build/questxr-owned-frames-signed.apk`. Deployment supports `-SignOnly` while
disconnected; before installing it now requires an authorized, explicitly
selected Quest 3, rather than risking an unrelated Android device.
### Reconnected device evidence
Full CI run 38649944478 passed at `040d31c1` and its APK was downloaded and
release-signed locally. After USB reconnection, the updated diagnostic companion
installed successfully. The private image test passed on the actual Quest:
144 complete frames received, zero rejections, 137 uploaded, final source and
GPU-verified frame ID 144, and expected GPU checksum 1815165381. Every byte of
the owned image matched GLES readback. The protocol unit executable also passed
on Android. These prove image ownership/transport and real GPU upload, not
Linux producer synchronization or SteamVR gameplay.
A later repeat passed both tracking and frame protocol unit executables on
Android but lost the sustained session at image 138. The native tracking test
also failed: 9 rendered frames, session stopped to IDLE, head flags 7 and no
active controllers. State reports still show Debugging tools off and no running
guest. Steam sign-in is pending. Test failures are retained, not reclassified
as passes. The user cannot attend these prerequisites yet and requested a pause
after the remaining independent work.
The driver now marks position/orientation that are merely valid but untracked
as out-of-range and rejects nonfinite or non-unit poses. The importer requests
Vulkan 1.2 for its core image-format-list use. Focused Kotlin tests and the new
validation classifier tests pass. The broad Linux suite had one unrelated
AppImage log-tail failure under WSL; the full CI suite passed at `040d31c1`.
## Remaining stages
- Android/Linux eye-image bridge and frame ownership/synchronization.
+17 -9
View File
@@ -113,7 +113,7 @@ public:
void health(const char *error = "") {
int steamvrState = -1;
if (guestSurface_ && statusCallback_) {
if ((guestSurface_ || guestFramesInput_) && statusCallback_) {
steamvrState = env_->CallIntMethod(app_->activity->clazz, statusCallback_);
if (env_->ExceptionCheck()) { env_->ExceptionClear(); steamvrState = -1; }
}
@@ -161,13 +161,14 @@ private:
bool running_ = false, exit_ = false, surfaceSupported_ = false, quitRequested_ = false;
bool surfaceTest_ = false;
bool guestSurface_ = false, guestAttached_ = false;
bool frameInput_ = false, verifyPixels_ = false;
bool frameInput_ = false, verifyPixels_ = false, guestFramesInput_ = 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 activeCallback_ = nullptr;
jmethodID statusCallback_ = nullptr;
jobject androidJavaSurface_ = nullptr;
DroidDeckXrTracking *tracking_ = nullptr;
@@ -223,9 +224,15 @@ private:
env_->DeleteLocalRef(extra);
extra = env_->NewStringUTF("guest_surface");
guestSurface_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
if (guestSurface_) {
surfaceTest_ = true;
surfaceCallback_ = env_->GetMethodID(activityClass, "onXrSurface", "(Landroid/view/Surface;II)V");
env_->DeleteLocalRef(extra);
extra = env_->NewStringUTF("guest_frames");
guestFramesInput_ = env_->CallBooleanMethod(intent, getBoolean, extra, JNI_FALSE);
if (guestFramesInput_) frameInput_ = true;
if (guestSurface_ || guestFramesInput_) {
if (guestSurface_) {
surfaceTest_ = true;
surfaceCallback_ = env_->GetMethodID(activityClass, "onXrSurface", "(Landroid/view/Surface;II)V");
} else activeCallback_ = env_->GetMethodID(activityClass, "onXrActive", "(Z)V");
frameCallback_ = env_->GetMethodID(activityClass, "onXrFrame", "(J)J");
statusCallback_ = env_->GetMethodID(activityClass, "onXrStatus", "()I");
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("XR activity bridge methods unavailable"); }
@@ -488,9 +495,10 @@ private:
}
void attachGuest(bool attach) {
if (!guestSurface_ || attach == guestAttached_) return;
env_->CallVoidMethod(app_->activity->clazz, surfaceCallback_, attach ? androidJavaSurface_ : nullptr,
eyes_[0].width * 2, eyes_[0].height);
if ((!guestSurface_ && !guestFramesInput_) || attach == guestAttached_) return;
if (guestFramesInput_) env_->CallVoidMethod(app_->activity->clazz, activeCallback_, static_cast<jboolean>(attach));
else env_->CallVoidMethod(app_->activity->clazz, surfaceCallback_, attach ? androidJavaSurface_ : nullptr,
eyes_[0].width * 2, eyes_[0].height);
if (env_->ExceptionCheck()) {
env_->ExceptionDescribe(); env_->ExceptionClear(); throw std::runtime_error("Guest surface handshake failed");
}
@@ -717,7 +725,7 @@ private:
XrView views[2]{{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
check(xrLocateViews(session_, &locate, &viewState, 2, &count, views), "xrLocateViews");
publishTracking(views, frameState.predictedDisplayTime);
if (guestSurface_ && guestAttached_) {
if ((guestSurface_ || guestFramesInput_) && guestAttached_) {
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
guestFrames_ = env_->CallLongMethod(app_->activity->clazz, frameCallback_, (jlong)now.tv_sec * 1000000000LL + now.tv_nsec);
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("Guest frame callback failed"); }
+19
View File
@@ -0,0 +1,19 @@
param(
[string]$Sdk = "$env:USERPROFILE\android-sdk",
[string]$NdkVersion = '27.3.13750724'
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$compiler = Join-Path $Sdk "ndk\$NdkVersion\toolchains\llvm\prebuilt\windows-x86_64\bin\aarch64-linux-android26-clang++.cmd"
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw 'Android ARM64 compiler is unavailable' }
Push-Location $repo
try {
New-Item -ItemType Directory -Path build\vr\android-tests -Force | Out-Null
foreach ($name in @('test_xr_tracking', 'test_xr_frames', 'send_xr_frame')) {
& $compiler -std=c++17 -Wall -Wextra -Werror -static-libstdc++ -pthread "tools/vr/tests/$name.cpp" -o "build/vr/android-tests/$name"
if ($LASTEXITCODE -ne 0) { throw "Android test compilation failed: $name" }
}
Write-Output "Built Android test executables: $(Join-Path $repo 'build\vr\android-tests')"
} finally {
Pop-Location
}
+17 -2
View File
@@ -4,7 +4,9 @@ param(
[string]$Sdk = "$env:USERPROFILE\android-sdk",
[string]$NdkVersion = '27.3.13750724',
[string]$BuildTools = '35.0.0',
[string]$Apk
[string]$Apk,
[string]$Serial,
[switch]$SignOnly
)
$ErrorActionPreference = 'Stop'
function Invoke-Checked([string]$Program, [string[]]$Arguments) {
@@ -29,7 +31,20 @@ try {
$certificate = Invoke-Checked "$tools\apksigner.bat" @('verify', '--print-certs', $signed)
$expected = (Get-Content keystore\release-signer.sha256 -Raw).Trim()
if (-not ($certificate -match "certificate SHA-256 digest: $expected")) { throw 'Unexpected signing certificate' }
Invoke-Checked $Adb @('install', '--no-incremental', '-r', $signed)
if (-not $SignOnly) {
$connected = @(Invoke-Checked $Adb @('devices') | ForEach-Object {
if ($_ -match '^(\S+)\s+device\s*$') { $matches[1] }
})
if (-not $Serial) {
if ($connected.Count -ne 1) { throw 'Specify -Serial with exactly one connected, authorized Quest target' }
$Serial = $connected[0]
}
if ($Serial -notin $connected) { throw 'The selected headset is not connected and authorized' }
$model = (Invoke-Checked $Adb @('-s', $Serial, 'shell', 'getprop', 'ro.product.model') | Out-String).Trim()
if ($model -notmatch '^Quest[ _]3$') { throw "Refusing Quest 3 deployment to model: $model" }
Invoke-Checked $Adb @('-s', $Serial, 'install', '--no-incremental', '-r', $signed)
}
Write-Output "Verified signed APK: $(Join-Path $repo $signed)"
} finally {
Pop-Location
}
@@ -75,6 +75,9 @@ JSON
export VK_LAYER_PATH="$layer${VK_LAYER_PATH:+:$VK_LAYER_PATH}"
export VK_INSTANCE_LAYERS="VK_LAYER_DROIDDECK_vr${VK_INSTANCE_LAYERS:+:$VK_INSTANCE_LAYERS}"
export VK_LOADER_LAYERS_DISABLE="${VK_LOADER_LAYERS_DISABLE:-~implicit~}"
if [ -S /run/droiddeck/droiddeck-xr-frames ] && [ -z "${DROIDDECK_VR_PRESENT:-}" ]; then
export DROIDDECK_VR_PRESENT=questframes
fi
shims=$state/shims
mkdir -p "$shims"
+16 -2
View File
@@ -25,7 +25,7 @@ def main():
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'):
for name in ('test_xr_tracking', 'test_xr_frames', 'send_xr_frame'):
binary = args.native_tests / name
if not binary.is_file():
parser.error(f'Missing Android executable: {binary}')
@@ -33,7 +33,8 @@ def main():
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')
unit_output = adb('shell', 'run-as', package, './test_xr_tracking', './files')
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
@@ -61,12 +62,25 @@ def main():
time.sleep(0.2)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as error:
failure = f'Frame channel operation failed: {error}'
if isinstance(error, subprocess.CalledProcessError) and error.stderr:
failure += f' {error.stderr.strip()}'
finally:
try:
sample = json.loads(adb('shell', 'cat', health_path))
if not samples or sample != samples[-1]:
samples.append(sample)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError):
pass
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'
try:
log = adb('logcat', '-d', '-s', 'DroidDeckXR:I', 'AndroidRuntime:E')
(args.output / 'logcat.txt').write_text(log, encoding='utf-8')
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass
latest = samples[-1] if samples else {}
if not failure:
if latest.get('source') != 'frames' or latest.get('receivedFrames') != sent.get('sent'):
+27 -15
View File
@@ -8,6 +8,29 @@ from pathlib import Path
from adb_device import resolve_serial
def validate_samples(samples, require_controllers=False, guest=False, steamvr=False, surface=False):
if len(samples) < 2:
return 'No advancing telemetry; check runtime initialization and headset wake/proximity'
if samples[-1]['rendered'] - samples[0]['rendered'] < 72:
return 'Fewer than 72 additional stereo frames were submitted'
if not any(sample['sessionState'] == 5 and sample['headFlags'] & 15 == 15 for sample in samples):
return 'No focused session with tracked 6DoF position and orientation'
if require_controllers and not any(all(sample['controllersActive']) for sample in samples):
return 'Both controllers did not become active'
if steamvr and samples[-1].get('uploadedFrames', 0) - samples[0].get('uploadedFrames', 0) < 72:
return 'No advancing owned SteamVR images; XR submissions alone are insufficient'
if steamvr and samples[-1].get('sourceFrameId', 0) <= samples[0].get('sourceFrameId', 0):
return 'SteamVR source frame IDs did not advance'
if guest and not steamvr and samples[-1].get('guestFrames', 0) - samples[0].get('guestFrames', 0) < 72:
return 'No advancing guest GPU presentation; submitting an XR layer alone is insufficient'
if steamvr and samples[-1].get('steamvrState') != 4:
return 'SteamVR did not report RUNNING'
expected = 'frames' if steamvr else 'guest' if guest else 'surface' if surface else 'native'
if samples[-1].get('source') != expected:
return 'Wrong projection source'
return None
def main():
parser = argparse.ArgumentParser(description='Validate immersive DroidDeck XR on a connected Quest')
parser.add_argument('--adb', default=os.environ.get('ADB', 'adb'))
@@ -24,6 +47,8 @@ def main():
parser.error('--seconds must be between 5 and 600')
if args.steamvr and not args.guest:
parser.error('--steamvr requires --guest')
if args.steamvr and args.surface:
parser.error('--steamvr uses owned frames and cannot use --surface')
serial = resolve_serial(args.adb, args.serial)
args.output.mkdir(parents=True, exist_ok=True)
@@ -42,7 +67,7 @@ def main():
if args.surface:
launch += ['--ez', 'surface_test', 'true']
if args.guest:
launch += ['--ez', 'guest_surface', 'true']
launch += ['--ez', 'guest_frames' if args.steamvr else 'guest_surface', 'true']
if args.steamvr:
launch += ['--ez', 'start_steamvr', 'true']
samples = []
@@ -75,20 +100,7 @@ def main():
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
log = str(error)
(args.output / 'logcat.txt').write_text(log, encoding='utf-8')
if len(samples) < 2:
failure = failure or 'No advancing telemetry; check runtime initialization and headset wake/proximity'
elif samples[-1]['rendered'] - samples[0]['rendered'] < 72:
failure = failure or 'Fewer than 72 additional stereo frames were submitted'
elif not any(sample['sessionState'] == 5 and sample['headFlags'] & 15 == 15 for sample in samples):
failure = failure or 'No focused session with tracked 6DoF position and orientation'
elif args.require_controllers and not any(all(sample['controllersActive']) for sample in samples):
failure = failure or 'Both controllers did not become active'
elif args.guest and samples[-1].get('guestFrames', 0) - samples[0].get('guestFrames', 0) < 72:
failure = failure or 'No advancing guest GPU presentation; submitting an XR layer alone is insufficient'
elif args.steamvr and samples[-1].get('steamvrState') != 4:
failure = failure or 'SteamVR did not report RUNNING'
elif samples[-1].get('source') != ('guest' if args.guest else 'surface' if args.surface else 'native'):
failure = failure or 'Wrong projection source'
failure = failure or validate_samples(samples, args.require_controllers, args.guest, args.steamvr, args.surface)
report = {'ok': failure is None, 'error': failure, 'samples': samples}
(args.output / 'report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps({'ok': report['ok'], 'error': failure, 'samples': len(samples),
+49
View File
@@ -0,0 +1,49 @@
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from test_quest_xr import validate_samples
class QuestXrValidationTest(unittest.TestCase):
def samples(self, source='native'):
return [{'rendered': count, 'sessionState': 5, 'headFlags': 15, 'source': source,
'controllersActive': [False, False], 'guestFrames': count,
'uploadedFrames': count, 'sourceFrameId': count + 1, 'steamvrState': 4}
for count in (0, 144)]
def test_native_and_surface_are_separate_diagnostics(self):
self.assertIsNone(validate_samples(self.samples()))
self.assertIsNone(validate_samples(self.samples('surface'), surface=True))
self.assertEqual('Wrong projection source', validate_samples(self.samples('surface')))
def test_position_must_be_tracked_not_merely_valid(self):
samples = self.samples()
for sample in samples:
sample['headFlags'] = 7
self.assertIn('tracked 6DoF', validate_samples(samples))
def test_optional_controllers_cannot_be_claimed_as_tested(self):
self.assertIn('controllers', validate_samples(self.samples(), require_controllers=True))
def test_steamvr_requires_owned_images_and_source_frame_ids(self):
samples = self.samples('frames')
self.assertIsNone(validate_samples(samples, guest=True, steamvr=True))
samples[-1]['uploadedFrames'] = 0
self.assertIn('owned SteamVR images', validate_samples(samples, guest=True, steamvr=True))
samples[-1]['uploadedFrames'] = 144
samples[-1]['sourceFrameId'] = samples[0]['sourceFrameId']
self.assertIn('frame IDs', validate_samples(samples, guest=True, steamvr=True))
def test_running_process_alone_is_not_success(self):
samples = self.samples('frames')
samples[-1]['rendered'] = 0
self.assertIn('stereo frames', validate_samples(samples, guest=True, steamvr=True))
samples[-1]['rendered'] = 144
samples[-1]['steamvrState'] = 3
self.assertIn('RUNNING', validate_samples(samples, guest=True, steamvr=True))
if __name__ == '__main__':
unittest.main()
+7 -4
View File
@@ -425,10 +425,13 @@ public:
if (id_ == k_unTrackedDeviceIndexInvalid) return;
DriverPose_t pose{};
pose.qWorldFromDriverRotation.w = pose.qDriverFromHeadRotation.w = 1;
pose.qRotation = {hand.pose.orientation[3], hand.pose.orientation[0], hand.pose.orientation[1], hand.pose.orientation[2]};
for (int axis = 0; axis < 3; axis++) pose.vecPosition[axis] = hand.pose.position[axis];
pose.qRotation.w = 1;
pose.deviceIsConnected = hand.active;
pose.poseIsValid = hand.active && (hand.pose.flags & 3u) == 3u;
pose.poseIsValid = hand.active && XrTrackingFile::Tracked(hand.pose);
if (pose.poseIsValid) {
pose.qRotation = {hand.pose.orientation[3], hand.pose.orientation[0], hand.pose.orientation[1], hand.pose.orientation[2]};
for (int axis = 0; axis < 3; axis++) pose.vecPosition[axis] = hand.pose.position[axis];
}
pose.result = pose.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
pose_ = pose;
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, pose, sizeof(pose));
@@ -580,7 +583,7 @@ public:
bool freshXr = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
if (freshXr || xrMode_) {
p.deviceIsConnected = true;
p.poseIsValid = freshXr && (xr.head.flags & 3u) == 3u;
p.poseIsValid = freshXr && XrTrackingFile::Tracked(xr.head);
p.result = p.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
if (p.poseIsValid) {
p.qRotation = {xr.head.orientation[3], xr.head.orientation[0], xr.head.orientation[1], xr.head.orientation[2]};
+7 -3
View File
@@ -183,7 +183,8 @@ private:
started_ = true;
thread_ = std::thread([this] {
try { Run(); }
catch (const std::exception &error) { Log("presenter: %s", error.what()); Fail(); }
catch (const std::exception &error) { Log("presenter: %s", error.what()); }
Cleanup(); Fail();
});
}
}
@@ -240,6 +241,9 @@ private:
lastLog = now;
}
}
}
void Cleanup() {
if (dev_) vk_.DeviceWaitIdle(dev_);
DestroyCapture();
while (!textures_.empty()) Forget(textures_.back().handle);
@@ -253,7 +257,7 @@ private:
}
if (surface_) vk_.DestroySurfaceKHR(inst_, surface_, nullptr);
if (inst_) vk_.DestroyInstance(inst_, nullptr);
Fail();
surface_ = VK_NULL_HANDLE; inst_ = VK_NULL_HANDLE;
}
std::chrono::microseconds guard_{4000};
@@ -360,7 +364,7 @@ private:
VK_GLOBAL(X)
#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};
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr, "droiddeck-vr-presenter", 1, nullptr, 0, VK_API_VERSION_1_2};
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_);
+11
View File
@@ -1,6 +1,7 @@
#pragma once
#include "xr_tracking.h"
#include <cstring>
#include <cmath>
#include <mutex>
#include <string>
#include <fcntl.h>
@@ -47,6 +48,16 @@ public:
&& packet.period_ns <= 50000000LL && packet.eyes[0].width > 0 && packet.eyes[0].width <= 8192
&& packet.eyes[0].height > 0 && packet.eyes[0].height <= 8192;
}
static bool Tracked(const DroidDeckXrPose &pose) {
if ((pose.flags & 15u) != 15u) return false;
float lengthSquared = 0;
for (float value : pose.orientation) {
if (!std::isfinite(value)) return false;
lengthSquared += value * value;
}
for (float value : pose.position) if (!std::isfinite(value)) return false;
return std::abs(lengthSquared - 1.f) < 0.01f;
}
private:
DroidDeckXrTracking *mapping_ = nullptr;
std::mutex mutex_;
+11 -2
View File
@@ -1,9 +1,11 @@
#include "../src/xr_tracking_file.h"
#include <cassert>
#include <cstdio>
#include <limits>
int main() {
char filename[] = "/tmp/droiddeck-xr-test-XXXXXX";
int main(int argc, char **argv) {
char filename[256];
std::snprintf(filename, sizeof(filename), "%s/droiddeck-xr-test-XXXXXX", argc > 1 ? argv[1] : "/tmp");
int descriptor = mkstemp(filename);
assert(descriptor >= 0);
assert(ftruncate(descriptor, sizeof(DroidDeckXrTracking)) == 0);
@@ -20,6 +22,13 @@ int main() {
writer->head.orientation[3] = 1; writer->head.position[1] = 1.6f; writer->head.flags = 15;
assert(reader.Read(&packet)); assert(XrTrackingFile::Fresh(packet));
assert(packet.head.position[1] == 1.6f);
assert(XrTrackingFile::Tracked(packet.head));
auto invalidPose = packet.head; invalidPose.flags = 7;
assert(!XrTrackingFile::Tracked(invalidPose));
invalidPose = packet.head; invalidPose.orientation[3] = 0;
assert(!XrTrackingFile::Tracked(invalidPose));
invalidPose = packet.head; invalidPose.position[0] = std::numeric_limits<float>::quiet_NaN();
assert(!XrTrackingFile::Tracked(invalidPose));
__atomic_store_n(&writer->seq, 1u, __ATOMIC_RELEASE);
assert(!reader.Read(&packet));
__atomic_store_n(&writer->seq, 2u, __ATOMIC_RELEASE);