diff --git a/docs/development/quest-openxr.md b/docs/development/quest-openxr.md index 247d71d00..3d9f5cd41 100644 --- a/docs/development/quest-openxr.md +++ b/docs/development/quest-openxr.md @@ -321,6 +321,10 @@ thumbrest and face-button touch actions in previously unused button bits, keepin the 296-byte tracking layout and existing low click bits compatible. Inactive hands clear every input; invalid analog values are sanitized. The flat Cardboard gamepad path retains its existing Vive profile and mappings. +Hand poses now carry the converted OpenXR prediction timestamp relative to +SteamVR's clock, matching the headset instead of reporting a future hand pose +as a present-time sample. The common timestamp conversion is host-tested for +future, past and unavailable prediction times. Host tests cover the input decoder's independent clicks, touch-versus-press, handed menu, analog ranges and inactive reset. The Android native frontend and diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index 273c3c157..6bcd9537f 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -431,7 +431,7 @@ public: DriverPose_t GetPose() override { return pose_; } VRInputComponentHandle_t HapticHandle() const { return haptic_; } - void UpdateXr(const DroidDeckXrHand &hand) { + void UpdateXr(const DroidDeckXrHand &hand, int64_t predicted) { if (id_ == k_unTrackedDeviceIndexInvalid) return; DriverPose_t pose{}; pose.qWorldFromDriverRotation.w = pose.qDriverFromHeadRotation.w = 1; @@ -441,6 +441,7 @@ public: 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.poseTimeOffset = XrTrackingFile::PoseTimeOffset(predicted, XrTrackingFile::Now()); } pose.result = pose.poseIsValid ? TrackingResult_Running_OK : TrackingResult_Running_OutOfRange; pose_ = pose; @@ -611,7 +612,7 @@ public: 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]; - if (xr.predicted_ns > 0) p.poseTimeOffset = (xr.predicted_ns - XrTrackingFile::Now()) / 1000000000.0; + p.poseTimeOffset = XrTrackingFile::PoseTimeOffset(xr.predicted_ns, XrTrackingFile::Now()); } return p; } @@ -678,7 +679,7 @@ private: 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); + hands_[hand]->UpdateXr(state, fresh ? xr.predicted_ns : 0); } VRDriverInput()->UpdateBooleanComponent(proximity_, fresh, 0); std::this_thread::sleep_for(std::chrono::milliseconds(2)); diff --git a/tools/vr/src/xr_tracking_file.h b/tools/vr/src/xr_tracking_file.h index 308cfcc59..1b122d412 100644 --- a/tools/vr/src/xr_tracking_file.h +++ b/tools/vr/src/xr_tracking_file.h @@ -42,6 +42,9 @@ public: timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now); return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec; } + static double PoseTimeOffset(int64_t predicted, int64_t now) { + return predicted > 0 ? (static_cast(predicted) - static_cast(now)) / 1000000000.0 : 0; + } static bool Fresh(const DroidDeckXrTracking &packet) { int64_t age = Now() - packet.sampled_ns; return packet.active && age >= 0 && age < 500000000LL && packet.period_ns >= 1000000LL diff --git a/tools/vr/tests/test_xr_tracking.cpp b/tools/vr/tests/test_xr_tracking.cpp index 527240260..950f83f5d 100644 --- a/tools/vr/tests/test_xr_tracking.cpp +++ b/tools/vr/tests/test_xr_tracking.cpp @@ -5,6 +5,10 @@ #include int main(int argc, char **argv) { + assert(XrTrackingFile::PoseTimeOffset(1010000000LL, 1000000000LL) == 0.01); + assert(XrTrackingFile::PoseTimeOffset(990000000LL, 1000000000LL) == -0.01); + assert(XrTrackingFile::PoseTimeOffset(0, 1000000000LL) == 0); + assert(XrTrackingFile::PoseTimeOffset(-1, 1000000000LL) == 0); char filename[256]; std::snprintf(filename, sizeof(filename), "%s/droiddeck-xr-test-XXXXXX", argc > 1 ? argv[1] : "/tmp"); int descriptor = mkstemp(filename);