mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
feat: validate Quest guest frame entrypoint
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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"); }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
@@ -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]};
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user