feat: bridge Quest projection surface and tracking

This commit is contained in:
XxJSONDeruloxX committed 2026-10-14 17:20:13 -04:00
1 parent 07f5e5f83d
commit f0934018df
19 files changed
+569 -35

No files matched your search

+2
View File
@@ -150,6 +150,7 @@ android {
}
buildFeatures {
prefab true
compose true
buildConfig true
}
@@ -225,6 +226,7 @@ if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
dependencies {
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
implementation 'org.tukaani:xz:1.10'
+14
View File
@@ -22,6 +22,9 @@
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="false" />
<uses-feature android:name="oculus.software.handtracking" android:required="false" />
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
<application
android:name=".App"
@@ -37,6 +40,17 @@
android:isGame="true">
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
<activity android:name=".QuestXrActivity" android:exported="true" android:launchMode="singleTask"
android:resizeableActivity="false" android:screenOrientation="landscape"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|uiMode"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<meta-data android:name="android.app.lib_name" android:value="droiddeckxr" />
<intent-filter>
<action android:name="com.droiddeck.launcher.XR" />
<category android:name="com.oculus.intent.category.VR" />
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
+1
View File
@@ -30,6 +30,7 @@ string(SUBSTRING "${WINFG_CHAIN_HASH_FULL}" 0 12 WINFG_CHAIN_HASH)
message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
add_subdirectory(waylandcomp)
add_subdirectory(../../../../questxr/src/main/cpp questxr)
add_library(sdnative SHARED sdnative/sdnative.c)
target_link_libraries(sdnative log)
+35 -1
View File
@@ -121,6 +121,11 @@ static pthread_mutex_t g_req_lock = PTHREAD_MUTEX_INITIALIZER;
static ANativeWindow *g_req_window;
static int g_req_pending;
static int g_req_detached;
static uint64_t g_req_generation, g_req_applied;
static pthread_cond_t g_req_condition = PTHREAD_COND_INITIALIZER;
static uint64_t g_presented_frames;
uint64_t vkp_presented_frames(void) { return __atomic_load_n(&g_presented_frames, __ATOMIC_ACQUIRE); }
static volatile int g_mode = VKP_MODE_STRETCH, g_align = VKP_ALIGN_CENTER;
@@ -198,26 +203,53 @@ void vk_present_set_window(ANativeWindow *window) {
if (g_req_pending && g_req_window && g_req_window != window) superseded = g_req_window;
g_req_window = window;
g_req_pending = 1;
g_req_generation++;
if (!window) g_req_detached = 1;
pthread_mutex_unlock(&g_req_lock);
if (superseded) ANativeWindow_release(superseded);
}
static void window_request_applied(uint64_t generation) {
pthread_mutex_lock(&g_req_lock);
g_req_applied = generation;
pthread_cond_broadcast(&g_req_condition);
pthread_mutex_unlock(&g_req_lock);
}
int vkp_wait_window_request(int timeout_ms) {
if (timeout_ms <= 0 || timeout_ms > 10000) return 0;
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += timeout_ms / 1000;
deadline.tv_nsec += (timeout_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
pthread_mutex_lock(&g_req_lock);
const uint64_t generation = g_req_generation;
int error = 0;
while (g_req_applied < generation && error == 0)
error = pthread_cond_timedwait(&g_req_condition, &g_req_lock, &deadline);
int applied = g_req_applied >= generation;
pthread_mutex_unlock(&g_req_lock);
return applied;
}
int vkp_apply_window_request(void) {
ANativeWindow *w;
pthread_mutex_lock(&g_req_lock);
if (!g_req_pending) { pthread_mutex_unlock(&g_req_lock); return 0; }
w = g_req_window;
const int detached = g_req_detached;
const uint64_t generation = g_req_generation;
g_req_window = NULL;
g_req_pending = 0;
g_req_detached = 0;
pthread_mutex_unlock(&g_req_lock);
if (w == g_window) {
if (w) ANativeWindow_release(w);
if (!w || !detached) return 0;
if (!w || !detached) { window_request_applied(generation); return 0; }
sc_layer_window_gone();
destroy_swapchain();
window_request_applied(generation);
droiddeck_log("gpu", "screen surface rebound: rebuilding the swapchain at its new size");
return 1;
}
@@ -228,6 +260,7 @@ int vkp_apply_window_request(void) {
g_window = w;
g_swap_retry_at_ns = 0;
g_swap_fail_logged = 0;
window_request_applied(generation);
droiddeck_log("gpu", w ? "screen surface attached" : "screen surface gone: presenting paused");
return 1;
}
@@ -1638,6 +1671,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
}
g_base_black = (g_plain_frame && n == 0 && !hf) ? 1 : 0;
if (fr == VK_SUCCESS) __atomic_add_fetch(&g_presented_frames, 1u, __ATOMIC_RELEASE);
if (drawn && !g_first_frame_done) {
g_first_frame_done = 1;
@@ -48,6 +48,8 @@ const char *vkp_gpu_name(void);
int vkp_ready(void);
int vkp_has_window(void);
int vkp_wait_window_request(int timeout_ms);
uint64_t vkp_presented_frames(void);
int vkp_device_lost(void);
int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n);
@@ -582,6 +582,17 @@ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) {
return buf;
}
JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeWaitSurface(
JNIEnv *env, jclass clazz, jint timeout_ms) {
return vkp_wait_window_request(timeout_ms) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jlong JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativePresentedFrames(JNIEnv *env, jclass clazz) {
return (jlong)vkp_presented_frames();
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) {
int len;
@@ -0,0 +1,53 @@
package com.droiddeck.launcher
import android.app.NativeActivity
import android.os.Bundle
import android.view.Surface
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.wayland.CompositorHost
import com.droiddeck.launcher.wayland.WaylandCompositor
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class QuestXrActivity : NativeActivity() {
private var presentedBaseline = 0L
override fun onCreate(savedInstanceState: Bundle?) {
if (intent.getBooleanExtra("guest_surface", false)) {
require(CompositorHost.isStarted) { "Start a DroidDeck session before attaching XR" }
intent.putExtra("surface_test", true)
}
super.onCreate(savedInstanceState)
}
fun onXrSurface(surface: Surface?, width: Int, height: Int) {
val complete = CountDownLatch(1)
var failure: Throwable? = null
runOnUiThread {
try {
if (surface != null) {
presentedBaseline = WaylandCompositor.nativePresentedFrames()
WaylandCompositor.nativeSetCardboard(false, 1, 0.064f, 3f, 4f, 0f, 0f, 90f, 0f, 0f)
WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
WaylandCompositor.nativeSetZeroCopy(false)
WaylandCompositor.nativeSetOutputSize(width, height)
WaylandCompositor.nativeSetFpsLimit(0)
}
CompositorHost.attachXr(surface)
SessionService.setActivityVisible(this, surface != null)
WaylandCompositor.nativeVsync(System.nanoTime())
} catch (error: Throwable) {
failure = error
} finally {
complete.countDown()
}
}
check(complete.await(3, TimeUnit.SECONDS)) { "XR surface UI handshake timed out" }
failure?.let { throw it }
check(WaylandCompositor.nativeWaitSurface(3000)) { "XR surface GPU handshake timed out" }
}
fun onXrFrame(frameTimeNanos: Long): Long {
WaylandCompositor.nativeVsync(frameTimeNanos)
return (WaylandCompositor.nativePresentedFrames() - presentedBaseline).coerceAtLeast(0)
}
}
@@ -71,6 +71,14 @@ object CompositorHost {
private var attached: Surface? = null
@Synchronized
fun attachXr(surface: Surface?) {
check(started) { "DroidDeck compositor is not running" }
pauseVsync()
attached = surface
WaylandCompositor.nativeSetSurface(surface)
}
@Synchronized
fun detach(surface: Surface?) {
if (!started) return
@@ -148,6 +148,8 @@ public final class WaylandCompositor {
String nativeLibDir);
public static native void nativeSetSurface(Surface surface);
public static native boolean nativeWaitSurface(int timeoutMs);
public static native long nativePresentedFrames();
public static native void nativeSendPointer(int action, int x, int y);
+29
View File
@@ -48,6 +48,35 @@ treating a successful `am start` as successful rendering.
## Remaining stages
## Milestone 2: Android projection surface and guest integration
The Android Surface producer test passed repeatedly, including 1,079 submitted
stereo frames in a 15-second test at 72 Hz. The new stage-space test reports
tracked floor-relative position, and the runtime supports monotonic-time
conversion. A JNI abort during the first surface test was traced to treating
the runtime's global Surface reference as a local reference and corrected.
The same native frontend is now compiled into the main app behind the explicit
`QuestXrActivity` entrypoint. With `guest_surface=true`, it hands its Android
projection Surface to the existing compositor. Surface transitions wait for a
compositor acknowledgement after the old Vulkan swapchain's GPU work drains.
Successful guest GPU presentations are counted separately from XR submissions.
The main app's Kotlin and native targets compile locally; an on-device main-app
guest test is still required before considering this bridge functional.
The frontend publishes a versioned 296-byte seqlock tracking record under the
session runtime directory. It contains floor-relative head/controller poses,
per-eye view geometry, predicted display time converted to CLOCK_MONOTONIC,
display period, and Touch action values. The OpenVR driver consumes fresh
records for real head/controller poses and asymmetric FOV; it marks stale
tracking invalid. The modified ARM64 Linux driver links successfully in WSL.
Driver timing, eye-to-head transforms, input completeness, render-pose/image
association and return haptics still need implementation/validation.
The companion's release build now targets Android 14 and passes release lint.
## 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.
+1 -1
View File
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId 'com.droiddeck.questxr'
minSdk 26
targetSdk 32
targetSdk 34
versionCode 1
versionName '0.1.0'
ndk { abiFilters 'arm64-v8a' }
+1 -1
View File
@@ -5,7 +5,7 @@ add_library(native_app_glue STATIC ${ANDROID_NDK}/sources/android/native_app_glu
target_include_directories(native_app_glue PUBLIC ${ANDROID_NDK}/sources/android/native_app_glue)
add_library(droiddeckxr SHARED quest_xr.cpp)
target_compile_features(droiddeckxr PRIVATE cxx_std_17)
target_compile_definitions(droiddeckxr PRIVATE XR_USE_PLATFORM_ANDROID XR_USE_GRAPHICS_API_OPENGL_ES)
target_compile_definitions(droiddeckxr PRIVATE XR_USE_PLATFORM_ANDROID XR_USE_GRAPHICS_API_OPENGL_ES XR_USE_TIMESPEC)
target_compile_options(droiddeckxr PRIVATE -Wall -Wextra -Wno-missing-field-initializers -Werror=return-type)
target_link_options(droiddeckxr PRIVATE -Wl,-z,max-page-size=16384 -uANativeActivity_onCreate)
target_link_libraries(droiddeckxr PRIVATE native_app_glue OpenXR::openxr_loader EGL GLESv3 android log)
+230 -17
View File
@@ -2,7 +2,9 @@
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android_native_app_glue.h>
#include <time.h>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <array>
@@ -13,6 +15,14 @@
#include <cstring>
#include <cmath>
#include <chrono>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <cstddef>
#include <ctime>
#include "../../../../tools/vr/src/xr_tracking.h"
static void check(XrResult result, const char *operation) {
if (XR_FAILED(result)) throw std::runtime_error(std::string(operation) + ": " + std::to_string(result));
@@ -103,12 +113,12 @@ public:
std::fprintf(file,
"{\"frames\":%llu,\"rendered\":%llu,\"sessionState\":%d,\"headFlags\":%llu,"
"\"headPosition\":[%.6f,%.6f,%.6f],\"controllersActive\":[%s,%s],"
"\"periodNs\":%lld,\"surfaceSwapchain\":%s,\"error\":\"%s\"}\n",
"\"periodNs\":%lld,\"surfaceSwapchain\":%s,\"source\":\"%s\",\"guestFrames\":%lld,\"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", error);
(long long)period_, surfaceSupported_ ? "true" : "false", guestSurface_ ? "guest" : surfaceTest_ ? "surface" : "native", (long long)guestFrames_, error);
std::fclose(file);
std::rename(temp.c_str(), path.c_str());
}
@@ -122,14 +132,28 @@ private:
XrSpace world_ = XR_NULL_HANDLE, view_ = XR_NULL_HANDLE;
XrActionSet actions_ = XR_NULL_HANDLE;
XrAction grip_ = XR_NULL_HANDLE, trigger_ = XR_NULL_HANDLE, vibration_ = XR_NULL_HANDLE;
XrAction squeeze_ = XR_NULL_HANDLE, stick_ = XR_NULL_HANDLE, stickClick_ = XR_NULL_HANDLE;
XrAction primary_ = XR_NULL_HANDLE, secondary_ = XR_NULL_HANDLE, menu_ = XR_NULL_HANDLE;
DroidDeckXrHand inputs_[2]{};
PFN_xrConvertTimeToTimespecTimeKHR convertTime_ = nullptr;
bool timespecSupported_ = false;
XrPath hands_[2]{};
XrSpace handSpaces_[2]{};
XrSpaceLocation head_{XR_TYPE_SPACE_LOCATION};
XrSpaceLocation handLocations_[2]{{XR_TYPE_SPACE_LOCATION}, {XR_TYPE_SPACE_LOCATION}};
bool handActive_[2]{}, pressed_[2]{};
bool running_ = false, exit_ = false, surfaceSupported_ = false;
bool surfaceTest_ = false;
bool guestSurface_ = false, guestAttached_ = false;
jmethodID surfaceCallback_ = nullptr, frameCallback_ = nullptr;
jobject androidJavaSurface_ = nullptr;
DroidDeckXrTracking *tracking_ = nullptr;
XrSwapchain androidSwapchain_ = XR_NULL_HANDLE;
ANativeWindow *androidWindow_ = nullptr;
EGLSurface androidSurface_ = EGL_NO_SURFACE;
XrSessionState state_ = XR_SESSION_STATE_UNKNOWN;
uint64_t frames_ = 0, rendered_ = 0;
int64_t guestFrames_ = 0;
XrDuration period_ = 0;
EGLDisplay display_ = EGL_NO_DISPLAY;
EGLContext context_ = EGL_NO_CONTEXT;
@@ -146,6 +170,24 @@ private:
void initialize() {
app_->activity->vm->AttachCurrentThread(&env_, nullptr);
jclass activityClass = env_->GetObjectClass(app_->activity->clazz);
jmethodID getIntent = env_->GetMethodID(activityClass, "getIntent", "()Landroid/content/Intent;");
jobject intent = env_->CallObjectMethod(app_->activity->clazz, getIntent);
jclass intentClass = env_->GetObjectClass(intent);
jmethodID getBoolean = env_->GetMethodID(intentClass, "getBooleanExtra", "(Ljava/lang/String;Z)Z");
jstring extra = env_->NewStringUTF("surface_test");
surfaceTest_ = 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_) {
surfaceTest_ = true;
surfaceCallback_ = env_->GetMethodID(activityClass, "onXrSurface", "(Landroid/view/Surface;II)V");
frameCallback_ = env_->GetMethodID(activityClass, "onXrFrame", "(J)J");
if (env_->ExceptionCheck()) { env_->ExceptionClear(); throw std::runtime_error("XR activity bridge methods unavailable"); }
}
env_->DeleteLocalRef(extra); env_->DeleteLocalRef(intentClass);
env_->DeleteLocalRef(intent); env_->DeleteLocalRef(activityClass);
PFN_xrInitializeLoaderKHR initializeLoader = nullptr;
check(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
reinterpret_cast<PFN_xrVoidFunction *>(&initializeLoader)), "loader init entrypoint");
@@ -160,8 +202,13 @@ private:
for (const auto &extension : extensions) {
if (!std::strcmp(extension.extensionName, XR_KHR_ANDROID_SURFACE_SWAPCHAIN_EXTENSION_NAME))
surfaceSupported_ = true;
if (!std::strcmp(extension.extensionName, XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME))
timespecSupported_ = true;
}
const char *enabled[] = {XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME};
if (surfaceTest_ && !surfaceSupported_) throw std::runtime_error("Android surface swapchains unavailable");
std::vector<const char *> enabled{XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME};
if (surfaceSupported_) enabled.push_back(XR_KHR_ANDROID_SURFACE_SWAPCHAIN_EXTENSION_NAME);
if (timespecSupported_) enabled.push_back(XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME);
XrInstanceCreateInfoAndroidKHR androidInfo{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
androidInfo.applicationVM = app_->activity->vm;
androidInfo.applicationActivity = app_->activity->clazz;
@@ -170,9 +217,11 @@ private:
std::strcpy(create.applicationInfo.applicationName, "DroidDeck XR");
create.applicationInfo.applicationVersion = 1;
create.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
create.enabledExtensionCount = 2;
create.enabledExtensionNames = enabled;
create.enabledExtensionCount = enabled.size();
create.enabledExtensionNames = enabled.data();
check(xrCreateInstance(&create, &instance_), "xrCreateInstance");
if (timespecSupported_) check(xrGetInstanceProcAddr(instance_, "xrConvertTimeToTimespecTimeKHR",
reinterpret_cast<PFN_xrVoidFunction *>(&convertTime_)), "time conversion entrypoint");
XrSystemGetInfo systemInfo{XR_TYPE_SYSTEM_GET_INFO};
systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
check(xrGetSystem(instance_, &systemInfo, &system_), "xrGetSystem");
@@ -186,8 +235,8 @@ private:
throw std::runtime_error("Runtime does not support GLES 3.0");
display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!eglInitialize(display_, nullptr, nullptr)) throw std::runtime_error("eglInitialize failed");
const EGLint attributes[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
const EGLint attributes[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT | EGL_WINDOW_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_NONE};
EGLConfig config{};
EGLint found = 0;
if (!eglChooseConfig(display_, attributes, &config, 1, &found) || found != 1)
@@ -204,13 +253,21 @@ private:
check(xrCreateSession(instance_, &sessionInfo, &session_), "xrCreateSession");
XrReferenceSpaceCreateInfo reference{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
reference.poseInReferenceSpace.orientation.w = 1;
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
check(xrCreateReferenceSpace(session_, &reference, &world_), "local space");
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;
XrResult referenceResult = xrCreateReferenceSpace(session_, &reference, &world_);
if (referenceResult == XR_ERROR_REFERENCE_SPACE_UNSUPPORTED) {
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
reference.poseInReferenceSpace.position.y = -1.6f;
check(xrCreateReferenceSpace(session_, &reference, &world_), "local standing space");
} else check(referenceResult, "stage space");
reference.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
reference.poseInReferenceSpace.position.y = 0;
check(xrCreateReferenceSpace(session_, &reference, &view_), "view space");
initializeActions();
initializeImages();
if (surfaceTest_) initializeAndroidSurface(config);
initializeGraphics();
initializeTracking();
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "initialized GLES; Android surface swapchain supported=%d", surfaceSupported_);
health();
}
@@ -228,14 +285,26 @@ private:
};
action("grip_pose", XR_ACTION_TYPE_POSE_INPUT, &grip_);
action("trigger", XR_ACTION_TYPE_FLOAT_INPUT, &trigger_);
action("squeeze", XR_ACTION_TYPE_FLOAT_INPUT, &squeeze_);
action("stick", XR_ACTION_TYPE_VECTOR2F_INPUT, &stick_);
action("stick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, &stickClick_);
action("primary", XR_ACTION_TYPE_BOOLEAN_INPUT, &primary_);
action("secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, &secondary_);
action("menu", XR_ACTION_TYPE_BOOLEAN_INPUT, &menu_);
action("vibration", XR_ACTION_TYPE_VIBRATION_OUTPUT, &vibration_);
std::vector<XrActionSuggestedBinding> bindings;
for (const char *hand : {"left", "right"}) {
std::string root = std::string("/user/hand/") + hand;
bindings.push_back({grip_, path((root + "/input/grip/pose").c_str())});
bindings.push_back({trigger_, path((root + "/input/trigger/value").c_str())});
bindings.push_back({squeeze_, path((root + "/input/squeeze/value").c_str())});
bindings.push_back({stick_, path((root + "/input/thumbstick").c_str())});
bindings.push_back({stickClick_, path((root + "/input/thumbstick/click").c_str())});
bindings.push_back({primary_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/x/click" : "/input/a/click")).c_str())});
bindings.push_back({secondary_, path((root + (std::strcmp(hand, "left") == 0 ? "/input/y/click" : "/input/b/click")).c_str())});
bindings.push_back({vibration_, path((root + "/output/haptic").c_str())});
}
bindings.push_back({menu_, path("/user/hand/left/input/menu/click")});
XrInteractionProfileSuggestedBinding suggestion{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
suggestion.interactionProfile = path("/interaction_profiles/oculus/touch_controller");
suggestion.countSuggestedBindings = bindings.size(); suggestion.suggestedBindings = bindings.data();
@@ -268,6 +337,7 @@ private:
Eye &target = eyes_[eye];
target.width = views[eye].recommendedImageRectWidth;
target.height = views[eye].recommendedImageRectHeight;
if (surfaceTest_) continue;
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
create.format = format; create.sampleCount = 1;
@@ -284,6 +354,94 @@ private:
}
}
void initializeAndroidSurface(EGLConfig config) {
PFN_xrCreateSwapchainAndroidSurfaceKHR createSurface = nullptr;
check(xrGetInstanceProcAddr(instance_, "xrCreateSwapchainAndroidSurfaceKHR",
reinterpret_cast<PFN_xrVoidFunction *>(&createSurface)), "surface swapchain entrypoint");
if (eyes_[0].width != eyes_[1].width || eyes_[0].height != eyes_[1].height)
throw std::runtime_error("Surface path requires matching eye dimensions");
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
info.width = eyes_[0].width * 2; info.height = eyes_[0].height;
jobject surface = nullptr;
check(createSurface(session_, &info, &androidSwapchain_, &surface), "Android surface swapchain");
androidWindow_ = ANativeWindow_fromSurface(env_, surface);
androidJavaSurface_ = env_->NewGlobalRef(surface);
if (env_->GetObjectRefType(surface) == JNILocalRefType) env_->DeleteLocalRef(surface);
if (!androidWindow_) throw std::runtime_error("Android swapchain has no native window");
if (!guestSurface_) {
androidSurface_ = eglCreateWindowSurface(display_, config, androidWindow_, nullptr);
if (androidSurface_ == EGL_NO_SURFACE) throw std::runtime_error("Android EGL window surface failed");
}
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "Android projection surface %ux%u", info.width, info.height);
}
void initializeTracking() {
std::string directory = std::string(app_->activity->internalDataPath) + "/.wayland-rt";
if (mkdir(directory.c_str(), 0700) != 0 && errno != EEXIST)
throw std::runtime_error("XR tracking directory unavailable");
std::string filename = directory + "/" DROIDDECK_XR_FILE;
int descriptor = open(filename.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0600);
if (descriptor < 0) throw std::runtime_error("XR tracking file unavailable");
if (ftruncate(descriptor, sizeof(DroidDeckXrTracking)) != 0) {
close(descriptor); throw std::runtime_error("XR tracking file size failed");
}
void *mapping = mmap(nullptr, sizeof(DroidDeckXrTracking), PROT_READ | PROT_WRITE, MAP_SHARED, descriptor, 0);
close(descriptor);
if (mapping == MAP_FAILED) throw std::runtime_error("XR tracking map failed");
tracking_ = static_cast<DroidDeckXrTracking *>(mapping);
}
static DroidDeckXrPose posePacket(const XrPosef &pose, XrSpaceLocationFlags flags) {
return {{pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w},
{pose.position.x, pose.position.y, pose.position.z}, (uint32_t)flags};
}
void publishTracking(const XrView views[2], XrTime predicted) {
if (!tracking_) return;
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
DroidDeckXrTracking packet{};
packet.magic = DROIDDECK_XR_MAGIC; packet.version = DROIDDECK_XR_VERSION;
packet.active = state_ == XR_SESSION_STATE_VISIBLE || state_ == XR_SESSION_STATE_FOCUSED;
packet.sampled_ns = (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
if (convertTime_) {
timespec displayTime{};
check(convertTime_(instance_, predicted, &displayTime), "display time conversion");
packet.predicted_ns = (int64_t)displayTime.tv_sec * 1000000000LL + displayTime.tv_nsec;
}
packet.period_ns = period_;
packet.head = posePacket(head_.pose, head_.locationFlags);
for (int eye = 0; eye < 2; eye++) {
packet.eyes[eye].pose = posePacket(views[eye].pose, head_.locationFlags);
packet.eyes[eye].fov[0] = views[eye].fov.angleLeft;
packet.eyes[eye].fov[1] = views[eye].fov.angleRight;
packet.eyes[eye].fov[2] = views[eye].fov.angleUp;
packet.eyes[eye].fov[3] = views[eye].fov.angleDown;
packet.eyes[eye].width = eyes_[eye].width; packet.eyes[eye].height = eyes_[eye].height;
packet.hands[eye] = inputs_[eye];
packet.hands[eye].pose = posePacket(handLocations_[eye].pose, handLocations_[eye].locationFlags);
packet.hands[eye].active = handActive_[eye];
}
uint32_t sequence = __atomic_load_n(&tracking_->seq, __ATOMIC_RELAXED);
__atomic_store_n(&tracking_->seq, sequence | 1u, __ATOMIC_SEQ_CST);
__atomic_thread_fence(__ATOMIC_SEQ_CST);
std::memcpy(tracking_, &packet, offsetof(DroidDeckXrTracking, seq));
std::memcpy(reinterpret_cast<char *>(tracking_) + offsetof(DroidDeckXrTracking, active),
reinterpret_cast<const char *>(&packet) + offsetof(DroidDeckXrTracking, active),
sizeof(packet) - offsetof(DroidDeckXrTracking, active));
__atomic_store_n(&tracking_->seq, (sequence | 1u) + 1u, __ATOMIC_RELEASE);
}
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 (env_->ExceptionCheck()) {
env_->ExceptionDescribe(); env_->ExceptionClear(); throw std::runtime_error("Guest surface handshake failed");
}
guestAttached_ = attach;
}
GLuint shader(GLenum type, const char *text) {
GLuint handle = glCreateShader(type);
glShaderSource(handle, 1, &text, nullptr); glCompileShader(handle);
@@ -331,23 +489,29 @@ 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");
glViewport(0, 0, eye.width, eye.height);
scene(view, eye.width, eye.height, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("OpenGL render error");
glFlush();
}
void scene(const XrView &view, int width, int height, int offset) {
glViewport(offset, 0, width, height);
glEnable(GL_SCISSOR_TEST); glScissor(offset, 0, width, height);
glEnable(GL_DEPTH_TEST); glClearColor(0.015f, 0.025f, 0.06f, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(program_); glBindVertexArray(vertexArray_);
Matrix camera = viewProjection(view);
XrPosef pose{}; pose.orientation.w = 1;
for (int column = -4; column <= 4; column++) {
pose.position = {column * 0.6f, -1.2f, -3};
pose.position = {column * 0.6f, 0.2f, -3};
cube(camera, pose, 0.08f, 0.1f, 0.3f, 0.8f);
}
pose.position = {0, 0, -2}; cube(camera, pose, 0.2f, 0.1f, 0.9f, 0.4f);
pose.position = {0, 1.4f, -2}; cube(camera, pose, 0.2f, 0.1f, 0.9f, 0.4f);
for (int hand = 0; hand < 2; hand++)
if (handActive_[hand] && (handLocations_[hand].locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT))
cube(camera, handLocations_[hand].pose, 0.06f, hand == 0 ? 0.9f : 0.2f, 0.3f, hand == 1 ? 0.9f : 0.2f);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("OpenGL render error");
glFlush();
glDisable(GL_SCISSOR_TEST);
}
void pollEvents() {
@@ -357,6 +521,7 @@ private:
if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
state_ = reinterpret_cast<XrEventDataSessionStateChanged *>(&event)->state;
__android_log_print(ANDROID_LOG_INFO, "DroidDeckXR", "session state=%d", state_);
attachGuest(state_ == XR_SESSION_STATE_VISIBLE || state_ == XR_SESSION_STATE_FOCUSED);
if (state_ == XR_SESSION_STATE_READY) {
XrSessionBeginInfo begin{XR_TYPE_SESSION_BEGIN_INFO};
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
@@ -388,6 +553,21 @@ private:
check(xrLocateSpace(handSpaces_[hand], world_, frameState.predictedDisplayTime, &handLocations_[hand]), "hand pose");
get.action = trigger_;
XrActionStateFloat trigger{XR_TYPE_ACTION_STATE_FLOAT}; check(xrGetActionStateFloat(session_, &get, &trigger), "trigger state");
inputs_[hand].trigger = trigger.isActive ? trigger.currentState : 0;
get.action = squeeze_;
XrActionStateFloat squeeze{XR_TYPE_ACTION_STATE_FLOAT}; check(xrGetActionStateFloat(session_, &get, &squeeze), "squeeze state");
inputs_[hand].squeeze = squeeze.isActive ? squeeze.currentState : 0;
get.action = stick_;
XrActionStateVector2f stick{XR_TYPE_ACTION_STATE_VECTOR2F}; check(xrGetActionStateVector2f(session_, &get, &stick), "stick state");
inputs_[hand].stick[0] = stick.isActive ? stick.currentState.x : 0;
inputs_[hand].stick[1] = stick.isActive ? stick.currentState.y : 0;
inputs_[hand].buttons = 0;
XrAction buttons[] = {stickClick_, primary_, secondary_, menu_};
for (int button = 0; button < 4; button++) {
get.action = buttons[button];
XrActionStateBoolean value{XR_TYPE_ACTION_STATE_BOOLEAN}; check(xrGetActionStateBoolean(session_, &get, &value), "button state");
if (value.isActive && value.currentState) inputs_[hand].buttons |= 1u << button;
}
bool pressed = trigger.isActive && trigger.currentState > 0.8f;
if (pressed && !pressed_[hand]) {
XrHapticActionInfo info{XR_TYPE_HAPTIC_ACTION_INFO}; info.action = vibration_; info.subactionPath = hands_[hand];
@@ -402,11 +582,30 @@ private:
XrViewState viewState{XR_TYPE_VIEW_STATE}; uint32_t count = 0;
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_) {
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"); }
}
XrCompositionLayerProjectionView projection[2]{{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}, {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
bool valid = frameState.shouldRender && count == 2 &&
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 (valid) for (int eye = 0; eye < 2; eye++) {
if (valid && surfaceTest_) {
if (!guestSurface_ && !eglMakeCurrent(display_, androidSurface_, androidSurface_, context_))
throw std::runtime_error("Surface context binding failed");
glBindFramebuffer(GL_FRAMEBUFFER, 0);
for (int eye = 0; eye < 2; eye++) {
if (!guestSurface_) scene(views[eye], eyes_[eye].width, eyes_[eye].height, eye * eyes_[eye].width);
projection[eye].pose = views[eye].pose; projection[eye].fov = views[eye].fov;
projection[eye].subImage.swapchain = androidSwapchain_;
projection[eye].subImage.imageRect.offset = {eye * eyes_[eye].width, 0};
projection[eye].subImage.imageRect.extent = {eyes_[eye].width, eyes_[eye].height};
}
if (glGetError() != GL_NO_ERROR) throw std::runtime_error("Surface rendering error");
if (!guestSurface_ && !eglSwapBuffers(display_, androidSurface_)) throw std::runtime_error("Surface presentation failed");
} else if (valid) for (int eye = 0; eye < 2; eye++) {
uint32_t image = 0;
XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
check(xrAcquireSwapchainImage(eyes_[eye].swapchain, &acquire, &image), "acquire image");
@@ -435,11 +634,25 @@ private:
}
void cleanup() {
try { attachGuest(false); } catch (const std::exception &error) {
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckXR", "cleanup: %s", error.what());
}
if (tracking_) {
__atomic_store_n(&tracking_->active, 0u, __ATOMIC_RELEASE);
munmap(tracking_, sizeof(DroidDeckXrTracking));
}
if (display_ != EGL_NO_DISPLAY && context_ != EGL_NO_CONTEXT) {
glDeleteFramebuffers(1, &framebuffer_); glDeleteProgram(program_);
glDeleteBuffers(1, &buffer_); glDeleteVertexArrays(1, &vertexArray_);
for (auto &eye : eyes_) glDeleteRenderbuffers(1, &eye.depth);
}
if (androidSurface_ != EGL_NO_SURFACE) {
eglMakeCurrent(display_, surface_, surface_, context_);
eglDestroySurface(display_, androidSurface_);
}
if (androidWindow_) ANativeWindow_release(androidWindow_);
if (androidSwapchain_) xrDestroySwapchain(androidSwapchain_);
if (androidJavaSurface_) env_->DeleteGlobalRef(androidJavaSurface_);
for (auto &eye : eyes_) if (eye.swapchain) xrDestroySwapchain(eye.swapchain);
for (auto space : handSpaces_) if (space) xrDestroySpace(space);
if (view_) xrDestroySpace(view_); if (world_) xrDestroySpace(world_);
+10 -10
View File
@@ -6,24 +6,24 @@ param(
[string]$BuildTools = '35.0.0'
)
$ErrorActionPreference = 'Stop'
function Invoke-Checked([string]$Program, [string[]]$Arguments) {
$ErrorActionPreference = 'Continue'
& $Program @Arguments
if ($LASTEXITCODE -ne 0) { throw "$Program exited with $LASTEXITCODE" }
}
$repo = Split-Path $PSScriptRoot -Parent
Push-Location $repo
try {
& .\gradlew.bat :questxr:assembleDebug "-PndkVersion=$NdkVersion" '-PskipRust=true' --console=plain
if ($LASTEXITCODE) { throw 'Quest XR build failed' }
Invoke-Checked .\gradlew.bat @(':questxr:assembleDebug', "-PndkVersion=$NdkVersion", '-PskipRust=true', '--console=plain')
$tools = Join-Path $Sdk "build-tools\$BuildTools"
$aligned = 'questxr\build\questxr-aligned.apk'
$signed = 'questxr\build\questxr-signed.apk'
& "$tools\zipalign.exe" -f -p 4 questxr\build\outputs\apk\debug\questxr-debug.apk $aligned
if ($LASTEXITCODE) { throw 'APK alignment failed' }
& "$tools\apksigner.bat" sign --key "$KeyDirectory\droiddeck-release.pk8" --cert "$KeyDirectory\droiddeck-release.x509.pem" --out $signed $aligned
if ($LASTEXITCODE) { throw 'APK signing failed' }
$certificate = & "$tools\apksigner.bat" verify --print-certs $signed
if ($LASTEXITCODE) { throw 'APK verification failed' }
Invoke-Checked "$tools\zipalign.exe" @('-f', '-p', '4', 'questxr\build\outputs\apk\debug\questxr-debug.apk', $aligned)
Invoke-Checked "$tools\apksigner.bat" @('sign', '--key', "$KeyDirectory\droiddeck-release.pk8", '--cert', "$KeyDirectory\droiddeck-release.x509.pem", '--out', $signed, $aligned)
$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' }
& $Adb install --no-incremental -r $signed
if ($LASTEXITCODE) { throw 'Quest XR deployment failed' }
Invoke-Checked $Adb @('install', '--no-incremental', '-r', $signed)
} finally {
Pop-Location
}
+17 -3
View File
@@ -15,6 +15,8 @@ def main():
parser.add_argument('--seconds', type=int, default=20)
parser.add_argument('--output', type=Path, default=Path('questxr-validation'))
parser.add_argument('--require-controllers', action='store_true')
parser.add_argument('--surface', action='store_true')
parser.add_argument('--guest', action='store_true')
args = parser.parse_args()
if args.seconds < 5 or args.seconds > 600:
parser.error('--seconds must be between 5 and 600')
@@ -25,12 +27,20 @@ def main():
return subprocess.run([args.adb, '-s', serial, *command], check=True,
capture_output=True, text=True).stdout
health_path = '/sdcard/Android/data/com.droiddeck.questxr/files/xr-health.json'
adb('shell', 'am', 'force-stop', 'com.droiddeck.questxr')
package = 'com.droiddeck.launcher' if args.guest else 'com.droiddeck.questxr'
health_path = f'/sdcard/Android/data/{package}/files/xr-health.json'
if not args.guest:
adb('shell', 'am', 'force-stop', package)
adb('shell', 'rm', '-f', health_path)
adb('shell', 'input', 'keyevent', 'KEYCODE_WAKEUP')
adb('shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.prox_close')
adb('shell', 'am', 'start', '-n', 'com.droiddeck.questxr/android.app.NativeActivity')
component = f'{package}/.QuestXrActivity' if args.guest else f'{package}/android.app.NativeActivity'
launch = ['shell', 'am', 'start', '-n', component]
if args.surface:
launch += ['--ez', 'surface_test', 'true']
if args.guest:
launch += ['--ez', 'guest_surface', 'true']
adb(*launch)
samples = []
failure = None
deadline = time.monotonic() + args.seconds
@@ -55,6 +65,10 @@ def main():
failure = failure or 'No focused session with valid 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 samples[-1].get('source') != ('guest' if args.guest else 'surface' if args.surface else 'native'):
failure = failure or 'Wrong projection source'
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),
+62 -2
View File
@@ -17,6 +17,7 @@
#include "../../../app/src/main/cpp/waylandcomp/src/headpose_shm.h"
#include "presenter.h"
#include "xr_tracking_file.h"
#define DD_EXPORT extern "C" __attribute__((visibility("default")))
@@ -372,6 +373,24 @@ public:
DriverPose_t GetPose() override { return pose_; }
void UpdateXr(const DroidDeckXrHand &hand) {
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.deviceIsConnected = hand.active;
pose.poseIsValid = hand.active && (hand.pose.flags & 3u) == 3u;
pose.result = pose.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange;
pose_ = pose;
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, pose, sizeof(pose));
VRDriverInput()->UpdateScalarComponent(trigger_, hand.active ? hand.trigger : 0, 0);
VRDriverInput()->UpdateBooleanComponent(triggerClick_, hand.active && hand.trigger > 0.6f, 0);
VRDriverInput()->UpdateBooleanComponent(grip_, hand.active && hand.squeeze > 0.6f, 0);
VRDriverInput()->UpdateScalarComponent(padX_, hand.active ? hand.stick[0] : 0, 0);
VRDriverInput()->UpdateScalarComponent(padY_, hand.active ? hand.stick[1] : 0, 0);
}
void Update(const HmdQuaternion_t &head, const PadState *pad) {
if (id_ == k_unTrackedDeviceIndexInvalid) return;
DriverPose_t p = {};
@@ -426,6 +445,12 @@ public:
if (height_ <= 0) height_ = 1080;
float hz = VRSettings()->GetFloat(kSettingsSection, "displayFrequency");
if (hz <= 0.0f) hz = 60.0f;
DroidDeckXrTracking xr;
xrMode_ = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
if (xrMode_) {
width_ = xr.eyes[0].width * 2; height_ = xr.eyes[0].height;
hz = 1000000000.f / xr.period_ns;
}
tanHalf_ = 1.0f;
eyeAspect_ = (width_ * 0.5f) / height_;
droiddeck_headpose hp;
@@ -433,7 +458,7 @@ public:
tanHalf_ = hp.tan_half_fov;
if (hp.eye_aspect > 0.0f) eyeAspect_ = hp.eye_aspect;
}
VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, "DroidDeck Cardboard");
VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, xrMode_ ? "DroidDeck Quest OpenXR" : "DroidDeck Cardboard");
VRProperties()->SetStringProperty(c, Prop_ManufacturerName_String, "DroidDeck");
VRProperties()->SetStringProperty(c, Prop_TrackingSystemName_String, "droiddeck");
VRProperties()->SetFloatProperty(c, Prop_UserIpdMeters_Float, 0.064f);
@@ -488,6 +513,18 @@ public:
p.qWorldFromDriverRotation.w = 1;
p.qDriverFromHeadRotation.w = 1;
p.qRotation.w = 1;
DroidDeckXrTracking xr;
bool freshXr = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
if (freshXr || xrMode_) {
p.deviceIsConnected = true;
p.poseIsValid = freshXr && (xr.head.flags & 3u) == 3u;
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]};
for (int axis = 0; axis < 3; axis++) p.vecPosition[axis] = xr.head.position[axis];
}
return p;
}
p.vecPosition[1] = 1.6;
droiddeck_headpose hp;
bool ok = pose_.Read(&hp);
@@ -509,6 +546,7 @@ public:
bool IsDisplayOnDesktop() override { return !directMode_; }
bool IsDisplayRealDisplay() override { return false; }
void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override {
if (xrMode_) { *w = width_ / 2; *h = height_; return; }
*w = (uint32_t)(width_ / 2 * 1.4f);
*h = (uint32_t)(height_ * 1.4f);
}
@@ -516,7 +554,14 @@ public:
*y = 0; *w = (uint32_t)width_ / 2; *h = (uint32_t)height_;
*x = eye == Eye_Left ? 0 : (uint32_t)width_ / 2;
}
void GetProjectionRaw(EVREye, float *left, float *right, float *top, float *bottom) override {
void GetProjectionRaw(EVREye eye, float *left, float *right, float *top, float *bottom) override {
DroidDeckXrTracking xr;
if (xrMode_ && xr_.Read(&xr) && XrTrackingFile::Fresh(xr)) {
const float *fov = xr.eyes[eye == Eye_Right ? 1 : 0].fov;
*left = std::tan(fov[0]); *right = std::tan(fov[1]);
*top = -std::tan(fov[2]); *bottom = -std::tan(fov[3]);
return;
}
*left = -tanHalf_ * eyeAspect_; *right = tanHalf_ * eyeAspect_;
*top = -tanHalf_; *bottom = tanHalf_;
}
@@ -536,6 +581,19 @@ private:
uint32_t lastSeq = 0;
auto lastSent = std::chrono::steady_clock::now();
while (running_) {
DroidDeckXrTracking xr;
if (xrMode_) {
bool fresh = xr_.Read(&xr) && XrTrackingFile::Fresh(xr);
DriverPose_t head = GetPose();
VRServerDriverHost()->TrackedDevicePoseUpdated(id_, head, sizeof(head));
for (int hand = 0; hand < 2; hand++) if (hands_[hand]) {
DroidDeckXrHand state = fresh ? xr.hands[hand] : DroidDeckXrHand{};
hands_[hand]->UpdateXr(state);
}
VRDriverInput()->UpdateBooleanComponent(proximity_, fresh, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
continue;
}
droiddeck_headpose hp;
bool fresh = pose_.Read(&hp) && hp.seq != lastSeq;
auto now = std::chrono::steady_clock::now();
@@ -570,6 +628,8 @@ public:
private:
PoseFile pose_;
XrTrackingFile xr_;
bool xrMode_ = false;
PadRing pad_;
PadState lastPad_;
VRInputComponentHandle_t proximity_ = 0;
+6
View File
@@ -9,6 +9,7 @@ typedef unsigned long VisualID;
#include "xdg-shell-client-protocol.h"
#include "presenter.h"
#include "xr_tracking_file.h"
#include <atomic>
#include <chrono>
@@ -268,6 +269,11 @@ private:
if (pread(fd, eye, sizeof(eye), 72) == (ssize_t)sizeof(eye) && eye[0] && eye[1]) { width_ = eye[0] * 2; height_ = eye[1]; }
close(fd);
}
XrTrackingFile xrFile;
DroidDeckXrTracking tracking;
if (xrFile.Read(&tracking) && XrTrackingFile::Fresh(tracking)) {
width_ = tracking.eyes[0].width * 2; height_ = tracking.eyes[0].height;
}
if (!width_ && cfgW_ > 0 && cfgH_ > 0) { width_ = (uint32_t)cfgW_; height_ = (uint32_t)cfgH_; }
if (!width_) { width_ = 1920; height_ = 1080; }
Log("presenter: %ux%u fullscreen window on Wayland display %s (configured %s)", width_, height_, name, configured_ ? "yes" : "no");
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#define DROIDDECK_XR_MAGIC 0x52584444u
#define DROIDDECK_XR_VERSION 1u
#define DROIDDECK_XR_FILE "droiddeck-xr-tracking"
struct DroidDeckXrPose {
float orientation[4];
float position[3];
uint32_t flags;
};
struct DroidDeckXrEye {
DroidDeckXrPose pose;
float fov[4];
uint32_t width, height;
};
struct DroidDeckXrHand {
DroidDeckXrPose pose;
float trigger, squeeze, stick[2];
uint32_t buttons, active;
};
struct DroidDeckXrTracking {
uint32_t magic, version, seq, active;
int64_t sampled_ns, predicted_ns, period_ns;
DroidDeckXrPose head;
DroidDeckXrEye eyes[2];
DroidDeckXrHand hands[2];
};
static_assert(sizeof(DroidDeckXrTracking) == 296, "XR tracking IPC layout");
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "xr_tracking.h"
#include <cstring>
#include <mutex>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
class XrTrackingFile {
public:
~XrTrackingFile() { if (mapping_) munmap(mapping_, sizeof(*mapping_)); }
bool Read(DroidDeckXrTracking *packet) {
std::lock_guard<std::mutex> lock(mutex_);
if (!mapping_) {
int descriptor = open("/run/droiddeck/" DROIDDECK_XR_FILE, O_RDONLY | O_CLOEXEC);
if (descriptor < 0) return false;
struct stat status{};
if (fstat(descriptor, &status) != 0 || status.st_size != sizeof(*mapping_)) {
close(descriptor); return false;
}
void *mapping = mmap(nullptr, sizeof(*mapping_), PROT_READ, MAP_SHARED, descriptor, 0);
close(descriptor);
if (mapping == MAP_FAILED) return false;
mapping_ = static_cast<DroidDeckXrTracking *>(mapping);
}
for (int attempt = 0; attempt < 8; attempt++) {
uint32_t sequence = __atomic_load_n(&mapping_->seq, __ATOMIC_ACQUIRE);
if (sequence & 1u) continue;
std::memcpy(packet, mapping_, sizeof(*packet));
__atomic_thread_fence(__ATOMIC_ACQUIRE);
if (__atomic_load_n(&mapping_->seq, __ATOMIC_RELAXED) == sequence)
return packet->magic == DROIDDECK_XR_MAGIC && packet->version == DROIDDECK_XR_VERSION;
}
return false;
}
static int64_t Now() {
timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
}
static bool Fresh(const DroidDeckXrTracking &packet) {
int64_t age = Now() - packet.sampled_ns;
return packet.active && age >= 0 && age < 500000000LL && packet.period_ns >= 1000000LL
&& packet.period_ns <= 50000000LL && packet.eyes[0].width > 0 && packet.eyes[0].width <= 8192
&& packet.eyes[0].height > 0 && packet.eyes[0].height <= 8192;
}
private:
DroidDeckXrTracking *mapping_ = nullptr;
std::mutex mutex_;
};