/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <math.h>
#include "prlink.h"
#include "prenv.h"
#include "gfxPrefs.h"
#include "mozilla/Preferences.h"
#include "mozilla/gfx/Quaternion.h"
#ifdef XP_WIN
#include "CompositorD3D11.h"
#include "TextureD3D11.h"
#elif defined(XP_MACOSX)
#include "mozilla/gfx/MacIOSurface.h"
#endif
#include "gfxVROpenVR.h"
#include "VRManagerParent.h"
#include "VRManager.h"
#include "VRThread.h"
#include "nsServiceManagerUtils.h"
#include "nsIScreenManager.h"
#include "mozilla/dom/GamepadEventTypes.h"
#include "mozilla/dom/GamepadBinding.h"
#include "mozilla/Telemetry.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::gfx::impl;
using namespace mozilla::layers;
using namespace mozilla::dom;
#define BTN_MASK_FROM_ID(_id) ::vr::ButtonMaskFromId(vr::EVRButtonId::_id)
static const uint32_t kNumOpenVRHaptcs = 1;
VRDisplayOpenVR::VRDisplayOpenVR(::vr::IVRSystem* aVRSystem,
::vr::IVRChaperone* aVRChaperone,
::vr::IVRCompositor* aVRCompositor)
: VRDisplayHost(VRDeviceType::OpenVR),
mVRSystem(aVRSystem),
mVRChaperone(aVRChaperone),
mVRCompositor(aVRCompositor),
mIsPresenting(false) {
MOZ_COUNT_CTOR_INHERITED(VRDisplayOpenVR, VRDisplayHost);
mDisplayInfo.mDisplayName.AssignLiteral("OpenVR HMD");
mDisplayInfo.mIsConnected =
mVRSystem->IsTrackedDeviceConnected(::vr::k_unTrackedDeviceIndex_Hmd);
mDisplayInfo.mIsMounted = false;
mDisplayInfo.mCapabilityFlags = VRDisplayCapabilityFlags::Cap_None |
VRDisplayCapabilityFlags::Cap_Orientation |
VRDisplayCapabilityFlags::Cap_Position |
VRDisplayCapabilityFlags::Cap_External |
VRDisplayCapabilityFlags::Cap_Present |
VRDisplayCapabilityFlags::Cap_StageParameters;
mIsHmdPresent = ::vr::VR_IsHmdPresent();
::vr::ETrackedPropertyError err;
bool bHasProximitySensor = mVRSystem->GetBoolTrackedDeviceProperty(
::vr::k_unTrackedDeviceIndex_Hmd, ::vr::Prop_ContainsProximitySensor_Bool,
&err);
if (err == ::vr::TrackedProp_Success && bHasProximitySensor) {
mDisplayInfo.mCapabilityFlags |=
VRDisplayCapabilityFlags::Cap_MountDetection;
}
mVRCompositor->SetTrackingSpace(::vr::TrackingUniverseSeated);
uint32_t w, h;
mVRSystem->GetRecommendedRenderTargetSize(&w, &h);
mDisplayInfo.mEyeResolution.width = w;
mDisplayInfo.mEyeResolution.height = h;
// SteamVR gives the application a single FOV to use; it's not configurable as
// with Oculus
for (uint32_t eye = 0; eye < 2; ++eye) {
// get l/r/t/b clip plane coordinates
float l, r, t, b;
mVRSystem->GetProjectionRaw(static_cast<::vr::Hmd_Eye>(eye), &l, &r, &t,
&b);
mDisplayInfo.mEyeFOV[eye].SetFromTanRadians(-t, r, b, -l);
}
UpdateEyeParameters();
UpdateStageParameters();
}
VRDisplayOpenVR::~VRDisplayOpenVR() {
Destroy();
MOZ_COUNT_DTOR_INHERITED(VRDisplayOpenVR, VRDisplayHost);
}
void VRDisplayOpenVR::Destroy() {
StopPresentation();
::vr::VR_Shutdown();
}
void VRDisplayOpenVR::UpdateEyeParameters(
gfx::Matrix4x4* aHeadToEyeTransforms /* = nullptr */) {
// Note this must be called every frame, as the IPD adjustment can be changed
// by the user during a VR session.
for (uint32_t eye = 0; eye < VRDisplayInfo::NumEyes; eye++) {
::vr::HmdMatrix34_t eyeToHead =
mVRSystem->GetEyeToHeadTransform(static_cast<::vr::Hmd_Eye>(eye));
mDisplayInfo.mEyeTranslation[eye].x = eyeToHead.m[0][3];
mDisplayInfo.mEyeTranslation[eye].y = eyeToHead.m[1][3];
mDisplayInfo.mEyeTranslation[eye].z = eyeToHead.m[2][3];
if (aHeadToEyeTransforms) {
Matrix4x4 pose;
// NOTE! eyeToHead.m is a 3x4 matrix, not 4x4. But
// because of its arrangement, we can copy the 12 elements in and
// then transpose them to the right place.
memcpy(&pose._11, &eyeToHead.m, sizeof(eyeToHead.m));
pose.Transpose();
pose.Invert();
aHeadToEyeTransforms[eye] = pose;
}
}
}
void VRDisplayOpenVR::UpdateStageParameters() {
float sizeX = 0.0f;
float sizeZ = 0.0f;
if (mVRChaperone->GetPlayAreaSize(&sizeX, &sizeZ)) {
::vr::HmdMatrix34_t t =
mVRSystem->GetSeatedZeroPoseToStandingAbsoluteTrackingPose();
mDisplayInfo.mStageSize.width = sizeX;
mDisplayInfo.mStageSize.height = sizeZ;
mDisplayInfo.mSittingToStandingTransform._11 = t.m[0][0];
mDisplayInfo.mSittingToStandingTransform._12 = t.m[1][0];
mDisplayInfo.mSittingToStandingTransform._13 = t.m[2][0];
mDisplayInfo.mSittingToStandingTransform._14 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._21 = t.m[0][1];
mDisplayInfo.mSittingToStandingTransform._22 = t.m[1][1];
mDisplayInfo.mSittingToStandingTransform._23 = t.m[2][1];
mDisplayInfo.mSittingToStandingTransform._24 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._31 = t.m[0][2];
mDisplayInfo.mSittingToStandingTransform._32 = t.m[1][2];
mDisplayInfo.mSittingToStandingTransform._33 = t.m[2][2];
mDisplayInfo.mSittingToStandingTransform._34 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._41 = t.m[0][3];
mDisplayInfo.mSittingToStandingTransform._42 = t.m[1][3];
mDisplayInfo.mSittingToStandingTransform._43 = t.m[2][3];
mDisplayInfo.mSittingToStandingTransform._44 = 1.0f;
} else {
// If we fail, fall back to reasonable defaults.
// 1m x 1m space, 0.75m high in seated position
mDisplayInfo.mStageSize.width = 1.0f;
mDisplayInfo.mStageSize.height = 1.0f;
mDisplayInfo.mSittingToStandingTransform._11 = 1.0f;
mDisplayInfo.mSittingToStandingTransform._12 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._13 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._14 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._21 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._22 = 1.0f;
mDisplayInfo.mSittingToStandingTransform._23 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._24 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._31 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._32 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._33 = 1.0f;
mDisplayInfo.mSittingToStandingTransform._34 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._41 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._42 = 0.75f;
mDisplayInfo.mSittingToStandingTransform._43 = 0.0f;
mDisplayInfo.mSittingToStandingTransform._44 = 1.0f;
}
}
void VRDisplayOpenVR::ZeroSensor() {
mVRSystem->ResetSeatedZeroPose();
UpdateStageParameters();
}
bool VRDisplayOpenVR::GetIsHmdPresent() { return mIsHmdPresent; }
void VRDisplayOpenVR::Refresh() {
mIsHmdPresent = ::vr::VR_IsHmdPresent();
::vr::VREvent_t event;
while (mVRSystem && mVRSystem->PollNextEvent(&event, sizeof(event))) {
switch (event.eventType) {
case ::vr::VREvent_TrackedDeviceUserInteractionStarted:
if (event.trackedDeviceIndex == ::vr::k_unTrackedDeviceIndex_Hmd) {
mDisplayInfo.mIsMounted = true;
}
break;
case ::vr::VREvent_TrackedDeviceUserInteractionEnded:
if (event.trackedDeviceIndex == ::vr::k_unTrackedDeviceIndex_Hmd) {
mDisplayInfo.mIsMounted = false;
}
break;
case ::vr::EVREventType::VREvent_TrackedDeviceActivated:
if (event.trackedDeviceIndex == ::vr::k_unTrackedDeviceIndex_Hmd) {
mDisplayInfo.mIsConnected = true;
}
break;
case ::vr::EVREventType::VREvent_TrackedDeviceDeactivated:
if (event.trackedDeviceIndex == ::vr::k_unTrackedDeviceIndex_Hmd) {
mDisplayInfo.mIsConnected = false;
}
break;
case ::vr::EVREventType::VREvent_DriverRequestedQuit:
case ::vr::EVREventType::VREvent_Quit:
case ::vr::EVREventType::VREvent_ProcessQuit:
case ::vr::EVREventType::VREvent_QuitAcknowledged:
case ::vr::EVREventType::VREvent_QuitAborted_UserPrompt:
mIsHmdPresent = false;
break;
default:
// ignore
break;
}
}
}
VRHMDSensorState VRDisplayOpenVR::GetSensorState() {
const uint32_t posesSize = ::vr::k_unTrackedDeviceIndex_Hmd + 1;
::vr::TrackedDevicePose_t poses[posesSize];
// Note: We *must* call WaitGetPoses in order for any rendering to happen at
// all.
mVRCompositor->WaitGetPoses(nullptr, 0, poses, posesSize);
gfx::Matrix4x4 headToEyeTransforms[2];
UpdateEyeParameters(headToEyeTransforms);
VRHMDSensorState result{};
::vr::Compositor_FrameTiming timing;
timing.m_nSize = sizeof(::vr::Compositor_FrameTiming);
if (mVRCompositor->GetFrameTiming(&timing)) {
result.timestamp = timing.m_flSystemTimeInSeconds;
} else {
// This should not happen, but log it just in case
NS_WARNING("OpenVR - IVRCompositor::GetFrameTiming failed");
}
if (poses[::vr::k_unTrackedDeviceIndex_Hmd].bDeviceIsConnected &&
poses[::vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid &&
poses[::vr::k_unTrackedDeviceIndex_Hmd].eTrackingResult ==
::vr::TrackingResult_Running_OK) {
const ::vr::TrackedDevicePose_t& pose =
poses[::vr::k_unTrackedDeviceIndex_Hmd];
gfx::Matrix4x4 m;
// NOTE! mDeviceToAbsoluteTracking is a 3x4 matrix, not 4x4. But
// because of its arrangement, we can copy the 12 elements in and
// then transpose them to the right place. We do this so we can
// pull out a Quaternion.
memcpy(&m._11, &pose.mDeviceToAbsoluteTracking,
sizeof(pose.mDeviceToAbsoluteTracking));
m.Transpose();
gfx::Quaternion rot;
rot.SetFromRotationMatrix(m);
rot.Invert();
result.flags |= VRDisplayCapabilityFlags::Cap_Orientation;
result.orientation[0] = rot.x;
result.orientation[1] = rot.y;
result.orientation[2] = rot.z;
result.orientation[3] = rot.w;
result.angularVelocity[0] = pose.vAngularVelocity.v[0];
result.angularVelocity[1] = pose.vAngularVelocity.v[1];
result.angularVelocity[2] = pose.vAngularVelocity.v[2];
result.flags |= VRDisplayCapabilityFlags::Cap_Position;
result.position[0] = m._41;
result.position[1] = m._42;
result.position[2] = m._43;
result.linearVelocity[0] = pose.vVelocity.v[0];
result.linearVelocity[1] = pose.vVelocity.v[1];
result.linearVelocity[2] = pose.vVelocity.v[2];
} else {
// default to an identity quaternion
result.orientation[3] = 1.0f;
}
result.CalcViewMatrices(headToEyeTransforms);
result.inputFrameID = mDisplayInfo.mFrameId;
return result;
}
void VRDisplayOpenVR::StartPresentation() {
if (mIsPresenting) {
return;
}
mIsPresenting = true;
mTelemetry.Clear();
mTelemetry.mPresentationStart = TimeStamp::Now();
::vr::Compositor_CumulativeStats stats;
mVRCompositor->GetCumulativeStats(&stats,
sizeof(::vr::Compositor_CumulativeStats));
mTelemetry.mLastDroppedFrameCount = stats.m_nNumReprojectedFrames;
}
void VRDisplayOpenVR::StopPresentation() {
if (!mIsPresenting) {
return;
}
mVRCompositor->ClearLastSubmittedFrame();
mIsPresenting = false;
const TimeDuration duration =
TimeStamp::Now() - mTelemetry.mPresentationStart;
Telemetry::Accumulate(Telemetry::WEBVR_USERS_VIEW_IN, 2);
Telemetry::Accumulate(Telemetry::WEBVR_TIME_SPENT_VIEWING_IN_OPENVR,
duration.ToMilliseconds());
::vr::Compositor_CumulativeStats stats;
mVRCompositor->GetCumulativeStats(&stats,
sizeof(::vr::Compositor_CumulativeStats));
const uint32_t droppedFramesPerSec =
(stats.m_nNumReprojectedFrames - mTelemetry.mLastDroppedFrameCount) /
duration.ToSeconds();
Telemetry::Accumulate(Telemetry::WEBVR_DROPPED_FRAMES_IN_OPENVR,
droppedFramesPerSec);
}
bool VRDisplayOpenVR::SubmitFrame(void* aTextureHandle,
::vr::ETextureType aTextureType,
const IntSize& aSize,
const gfx::Rect& aLeftEyeRect,
const gfx::Rect& aRightEyeRect) {
MOZ_ASSERT(mSubmitThread->GetThread() == NS_GetCurrentThread());
if (!mIsPresenting) {
return false;
}
::vr::Texture_t tex;
tex.handle = aTextureHandle;
tex.eType = aTextureType;
tex.eColorSpace = ::vr::EColorSpace::ColorSpace_Auto;
::vr::VRTextureBounds_t bounds;
bounds.uMin = aLeftEyeRect.X();
bounds.vMin = 1.0 - aLeftEyeRect.Y();
bounds.uMax = aLeftEyeRect.XMost();
bounds.vMax = 1.0 - aLeftEyeRect.YMost();
::vr::EVRCompositorError err;
err = mVRCompositor->Submit(::vr::EVREye::Eye_Left, &tex, &bounds);
if (err != ::vr::EVRCompositorError::VRCompositorError_None) {
printf_stderr("OpenVR Compositor Submit() failed.\n");
}
bounds.uMin = aRightEyeRect.X();
bounds.vMin = 1.0 - aRightEyeRect.Y();
bounds.uMax = aRightEyeRect.XMost();
bounds.vMax = 1.0 - aRightEyeRect.YMost();
err = mVRCompositor->Submit(::vr::EVREye::Eye_Right, &tex, &bounds);
if (err != ::vr::EVRCompositorError::VRCompositorError_None) {
printf_stderr("OpenVR Compositor Submit() failed.\n");
}
mVRCompositor->PostPresentHandoff();
return true;
}
#if defined(XP_WIN)
bool VRDisplayOpenVR::SubmitFrame(ID3D11Texture2D* aSource,
const IntSize& aSize,
const gfx::Rect& aLeftEyeRect,
const gfx::Rect& aRightEyeRect) {
return SubmitFrame((void*)aSource, ::vr::ETextureType::TextureType_DirectX,
aSize, aLeftEyeRect, aRightEyeRect);
}
#elif defined(XP_MACOSX)
bool VRDisplayOpenVR::SubmitFrame(MacIOSurface* aMacIOSurface,
const IntSize& aSize,
const gfx::Rect& aLeftEyeRect,
const gfx::Rect& aRightEyeRect) {
const void* ioSurface = aMacIOSurface->GetIOSurfacePtr();
bool result = false;
if (ioSurface == nullptr) {
NS_WARNING("VRDisplayOpenVR::SubmitFrame() could not get an IOSurface");
} else {
result =
SubmitFrame((void*)ioSurface, ::vr::ETextureType::TextureType_IOSurface,
aSize, aLeftEyeRect, aRightEyeRect);
}
return result;
}
#endif
VRControllerOpenVR::VRControllerOpenVR(dom::GamepadHand aHand,
uint32_t aDisplayID,
uint32_t aNumButtons,
uint32_t aNumTriggers, uint32_t aNumAxes,
const nsCString& aId)
: VRControllerHost(VRDeviceType::OpenVR, aHand, aDisplayID),
mTrigger(aNumTriggers),
mAxisMove(aNumAxes),
mVibrateThread(nullptr),
mIsVibrateStopped(false) {
MOZ_COUNT_CTOR_INHERITED(VRControllerOpenVR, VRControllerHost);
mAxisMove.SetLengthAndRetainStorage(aNumAxes);
mTrigger.SetLengthAndRetainStorage(aNumTriggers);
mControllerInfo.mControllerName = aId;
mControllerInfo.mNumButtons = aNumButtons;
mControllerInfo.mNumAxes = aNumAxes;
mControllerInfo.mNumHaptics = kNumOpenVRHaptcs;
}
VRControllerOpenVR::~VRControllerOpenVR() {
ShutdownVibrateHapticThread();
MOZ_COUNT_DTOR_INHERITED(VRControllerOpenVR, VRControllerHost);
}
void VRControllerOpenVR::SetTrackedIndex(uint32_t aTrackedIndex) {
mTrackedIndex = aTrackedIndex;
}
uint32_t VRControllerOpenVR::GetTrackedIndex() { return mTrackedIndex; }
float VRControllerOpenVR::GetAxisMove(uint32_t aAxis) {
return mAxisMove[aAxis];
}
void VRControllerOpenVR::SetAxisMove(uint32_t aAxis, float aValue) {
mAxisMove[aAxis] = aValue;
}
void VRControllerOpenVR::SetTrigger(uint32_t aButton, float aValue) {
mTrigger[aButton] = aValue;
}
float VRControllerOpenVR::GetTrigger(uint32_t aButton) {
return mTrigger[aButton];
}
void VRControllerOpenVR::SetHand(dom::GamepadHand aHand) {
mControllerInfo.mHand = aHand;
}
void VRControllerOpenVR::UpdateVibrateHaptic(::vr::IVRSystem* aVRSystem,
uint32_t aHapticIndex,
double aIntensity,
double aDuration,
uint64_t aVibrateIndex,
const VRManagerPromise& aPromise) {
// UpdateVibrateHaptic() only can be called by mVibrateThread
MOZ_ASSERT(mVibrateThread->GetThread() == NS_GetCurrentThread());
// It has been interrupted by loss focus.
if (mIsVibrateStopped) {
VibrateHapticComplete(aPromise);
return;
}
// Avoid the previous vibrate event to override the new one.
if (mVibrateIndex != aVibrateIndex) {
VibrateHapticComplete(aPromise);
return;
}
const double duration = (aIntensity == 0) ? 0 : aDuration;
// We expect OpenVR to vibrate for 5 ms, but we found it only response the
// commend ~ 3.9 ms. For duration time longer than 3.9 ms, we separate them
// to a loop of 3.9 ms for make users feel that is a continuous events.
const uint32_t microSec =
(duration < 3.9 ? duration : 3.9) * 1000 * aIntensity;
aVRSystem->TriggerHapticPulse(GetTrackedIndex(), aHapticIndex, microSec);
// In OpenVR spec, it mentions TriggerHapticPulse() may not trigger another
// haptic pulse on this controller and axis combination for 5ms.
const double kVibrateRate = 5.0;
if (duration >= kVibrateRate) {
MOZ_ASSERT(mVibrateThread);
MOZ_ASSERT(mVibrateThread->IsActive());
RefPtr<Runnable> runnable =
NewRunnableMethod<::vr::IVRSystem*, uint32_t, double, double, uint64_t,
StoreCopyPassByConstLRef<VRManagerPromise>>(
"VRControllerOpenVR::UpdateVibrateHaptic", this,
&VRControllerOpenVR::UpdateVibrateHaptic, aVRSystem, aHapticIndex,
aIntensity, duration - kVibrateRate, aVibrateIndex, aPromise);
mVibrateThread->PostDelayedTask(runnable.forget(), kVibrateRate);
} else {
// The pulse has completed
VibrateHapticComplete(aPromise);
}
}
void VRControllerOpenVR::VibrateHapticComplete(
const VRManagerPromise& aPromise) {
VRManager* vm = VRManager::Get();
VRListenerThreadHolder::Loop()->PostTask(
NewRunnableMethod<StoreCopyPassByConstLRef<VRManagerPromise>>(
"VRManager::NotifyVibrateHapticCompleted", vm,
&VRManager::NotifyVibrateHapticCompleted, aPromise));
}
void VRControllerOpenVR::VibrateHaptic(::vr::IVRSystem* aVRSystem,
uint32_t aHapticIndex, double aIntensity,
double aDuration,
const VRManagerPromise& aPromise) {
// Spinning up the haptics thread at the first haptics call.
if (!mVibrateThread) {
mVibrateThread = new VRThread(NS_LITERAL_CSTRING("OpenVR_Vibration"));
}
mVibrateThread->Start();
++mVibrateIndex;
mIsVibrateStopped = false;
RefPtr<Runnable> runnable =
NewRunnableMethod<::vr::IVRSystem*, uint32_t, double, double, uint64_t,
StoreCopyPassByConstLRef<VRManagerPromise>>(
"VRControllerOpenVR::UpdateVibrateHaptic", this,
&VRControllerOpenVR::UpdateVibrateHaptic, aVRSystem, aHapticIndex,
aIntensity, aDuration, mVibrateIndex, aPromise);
mVibrateThread->PostTask(runnable.forget());
}
void VRControllerOpenVR::StopVibrateHaptic() { mIsVibrateStopped = true; }
void VRControllerOpenVR::ShutdownVibrateHapticThread() {
StopVibrateHaptic();
if (mVibrateThread) {
mVibrateThread->Shutdown();
mVibrateThread = nullptr;
}
}
VRSystemManagerOpenVR::VRSystemManagerOpenVR()
: mVRSystem(nullptr), mIsWindowsMR(false) {}
/*static*/ already_AddRefed<VRSystemManagerOpenVR>
VRSystemManagerOpenVR::Create() {
MOZ_ASSERT(NS_IsMainThread());
if (!gfxPrefs::VREnabled() || !gfxPrefs::VROpenVREnabled()) {
return nullptr;
}
if (!::vr::VR_IsRuntimeInstalled()) {
return nullptr;
}
RefPtr<VRSystemManagerOpenVR> manager = new VRSystemManagerOpenVR();
return manager.forget();
}
void VRSystemManagerOpenVR::Destroy() { Shutdown(); }
void VRSystemManagerOpenVR::Shutdown() {
if (mOpenVRHMD) {
mOpenVRHMD = nullptr;
}
RemoveControllers();
mVRSystem = nullptr;
}
void VRSystemManagerOpenVR::NotifyVSync() {
VRSystemManager::NotifyVSync();
// Avoid doing anything unless we have already
// successfully enumerated and loaded the OpenVR
// runtime.
if (mVRSystem == nullptr) {
return;
}
if (mOpenVRHMD) {
mOpenVRHMD->Refresh();
if (!mOpenVRHMD->GetIsHmdPresent()) {
// OpenVR runtime could be quit accidentally
// or a device could be disconnected.
// We free up resources and must re-initialize
// if a device is detected again later.
mOpenVRHMD = nullptr;
mVRSystem = nullptr;
}
}
}
void VRSystemManagerOpenVR::Enumerate() {
if (mOpenVRHMD == nullptr && ::vr::VR_IsHmdPresent()) {
::vr::HmdError err;
::vr::VR_Init(&err, ::vr::EVRApplicationType::VRApplication_Scene);
if (err) {
return;
}
::vr::IVRSystem* system = (::vr::IVRSystem*)::vr::VR_GetGenericInterface(
::vr::IVRSystem_Version, &err);
if (err || !system) {
::vr::VR_Shutdown();
return;
}
::vr::IVRChaperone* chaperone =
(::vr::IVRChaperone*)::vr::VR_GetGenericInterface(
::vr::IVRChaperone_Version, &err);
if (err || !chaperone) {
::vr::VR_Shutdown();
return;
}
::vr::IVRCompositor* compositor =
(::vr::IVRCompositor*)::vr::VR_GetGenericInterface(
::vr::IVRCompositor_Version, &err);
if (err || !compositor) {
::vr::VR_Shutdown();
return;
}
mVRSystem = system;
mOpenVRHMD = new VRDisplayOpenVR(system, chaperone, compositor);
}
}
bool VRSystemManagerOpenVR::ShouldInhibitEnumeration() {
if (VRSystemManager::ShouldInhibitEnumeration()) {
return true;
}
if (mOpenVRHMD) {
// When we find an a VR device, don't
// allow any further enumeration as it
// may get picked up redundantly by other
// API's.
return true;
}
return false;
}
void VRSystemManagerOpenVR::GetHMDs(
nsTArray<RefPtr<VRDisplayHost>>& aHMDResult) {
if (mOpenVRHMD) {
aHMDResult.AppendElement(mOpenVRHMD);
}
}
bool VRSystemManagerOpenVR::GetIsPresenting() {
if (mOpenVRHMD) {
VRDisplayInfo displayInfo(mOpenVRHMD->GetDisplayInfo());
return displayInfo.GetPresentingGroups() != kVRGroupNone;
}
return false;
}
void VRSystemManagerOpenVR::HandleInput() {
// mVRSystem is available after VRDisplay is created
// at GetHMDs().
if (!mVRSystem) {
return;
}
RefPtr<impl::VRControllerOpenVR> controller;
// Compare with Edge, we have a wrong implementation for the vertical axis
// value. In order to not affect the current VR content, we add a workaround
// for yAxis.
const float yAxisInvert = (mIsWindowsMR) ? -1.0f : 1.0f;
::vr::VRControllerState_t state;
::vr::TrackedDevicePose_t poses[::vr::k_unMaxTrackedDeviceCount];
mVRSystem->GetDeviceToAbsoluteTrackingPose(::vr::TrackingUniverseSeated, 0.0f,
poses,
::vr::k_unMaxTrackedDeviceCount);
// Process OpenVR controller state
for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
uint32_t axisIdx = 0;
uint32_t buttonIdx = 0;
uint32_t triggerIdx = 0;
controller = mOpenVRController[i];
const uint32_t trackedIndex = controller->GetTrackedIndex();
MOZ_ASSERT(mVRSystem->GetTrackedDeviceClass(trackedIndex) ==
::vr::TrackedDeviceClass_Controller ||
mVRSystem->GetTrackedDeviceClass(trackedIndex) ==
::vr::TrackedDeviceClass_GenericTracker);
// Sometimes, OpenVR controllers are not located by HMD at the initial time.
// That makes us have to update the hand info at runtime although switching
// controllers to the other hand does not have new changes at the current
// OpenVR SDK. But, it makes sense to detect hand changing at runtime.
const ::vr::ETrackedControllerRole role =
mVRSystem->GetControllerRoleForTrackedDeviceIndex(trackedIndex);
const dom::GamepadHand hand = GetGamepadHandFromControllerRole(role);
if (hand != controller->GetHand()) {
controller->SetHand(hand);
NewHandChangeEvent(i, hand);
}
if (mVRSystem->GetControllerState(trackedIndex, &state, sizeof(state))) {
for (uint32_t j = 0; j < ::vr::k_unControllerStateAxisCount; ++j) {
const uint32_t axisType = mVRSystem->GetInt32TrackedDeviceProperty(
trackedIndex, static_cast<::vr::TrackedDeviceProperty>(
::vr::Prop_Axis0Type_Int32 + j));
switch (axisType) {
case ::vr::EVRControllerAxisType::k_eControllerAxis_Joystick:
case ::vr::EVRControllerAxisType::k_eControllerAxis_TrackPad:
if (mIsWindowsMR) {
// Adjust the input mapping for Windows MR which has
// different order.
axisIdx = (axisIdx == 0) ? 2 : 0;
buttonIdx = (buttonIdx == 0) ? 4 : 0;
}
HandleAxisMove(i, axisIdx, state.rAxis[j].x);
++axisIdx;
HandleAxisMove(i, axisIdx, state.rAxis[j].y * yAxisInvert);
++axisIdx;
HandleButtonPress(
i, buttonIdx,
::vr::ButtonMaskFromId(
static_cast<::vr::EVRButtonId>(::vr::k_EButton_Axis0 + j)),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
if (mIsWindowsMR) {
axisIdx = (axisIdx == 4) ? 2 : 4;
buttonIdx = (buttonIdx == 5) ? 1 : 2;
}
break;
case vr::EVRControllerAxisType::k_eControllerAxis_Trigger:
if (j <= 2) {
HandleTriggerPress(i, buttonIdx, triggerIdx, state.rAxis[j].x);
++buttonIdx;
++triggerIdx;
} else {
// For SteamVR Knuckles.
HandleTriggerPress(i, buttonIdx, triggerIdx, state.rAxis[j].x);
++buttonIdx;
++triggerIdx;
HandleTriggerPress(i, buttonIdx, triggerIdx, state.rAxis[j].y);
++buttonIdx;
++triggerIdx;
}
break;
}
}
MOZ_ASSERT(axisIdx == controller->GetControllerInfo().GetNumAxes());
const uint64_t supportedButtons =
mVRSystem->GetUint64TrackedDeviceProperty(
trackedIndex, ::vr::Prop_SupportedButtons_Uint64);
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_A)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_A),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_Grip)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_Grip),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_ApplicationMenu)) {
HandleButtonPress(i, buttonIdx,
BTN_MASK_FROM_ID(k_EButton_ApplicationMenu),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (mIsWindowsMR) {
// button 4 in Windows MR has already been assigned
// to k_eControllerAxis_TrackPad.
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Left)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_DPad_Left),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Up)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_DPad_Up),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Right)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_DPad_Right),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
if (supportedButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Down)) {
HandleButtonPress(i, buttonIdx, BTN_MASK_FROM_ID(k_EButton_DPad_Down),
state.ulButtonPressed, state.ulButtonTouched);
++buttonIdx;
}
MOZ_ASSERT(buttonIdx == controller->GetControllerInfo().GetNumButtons());
controller->SetButtonPressed(state.ulButtonPressed);
controller->SetButtonTouched(state.ulButtonTouched);
// Start to process pose
const ::vr::TrackedDevicePose_t& pose = poses[trackedIndex];
GamepadPoseState poseState;
if (pose.bDeviceIsConnected) {
poseState.flags |= (GamepadCapabilityFlags::Cap_Orientation |
GamepadCapabilityFlags::Cap_Position);
}
if (pose.bPoseIsValid &&
pose.eTrackingResult == ::vr::TrackingResult_Running_OK) {
gfx::Matrix4x4 m;
// NOTE! mDeviceToAbsoluteTracking is a 3x4 matrix, not 4x4. But
// because of its arrangement, we can copy the 12 elements in and
// then transpose them to the right place. We do this so we can
// pull out a Quaternion.
memcpy(&m.components, &pose.mDeviceToAbsoluteTracking,
sizeof(pose.mDeviceToAbsoluteTracking));
m.Transpose();
gfx::Quaternion rot;
rot.SetFromRotationMatrix(m);
rot.Invert();
poseState.orientation[0] = rot.x;
poseState.orientation[1] = rot.y;
poseState.orientation[2] = rot.z;
poseState.orientation[3] = rot.w;
poseState.angularVelocity[0] = pose.vAngularVelocity.v[0];
poseState.angularVelocity[1] = pose.vAngularVelocity.v[1];
poseState.angularVelocity[2] = pose.vAngularVelocity.v[2];
poseState.isOrientationValid = true;
poseState.position[0] = m._41;
poseState.position[1] = m._42;
poseState.position[2] = m._43;
poseState.linearVelocity[0] = pose.vVelocity.v[0];
poseState.linearVelocity[1] = pose.vVelocity.v[1];
poseState.linearVelocity[2] = pose.vVelocity.v[2];
poseState.isPositionValid = true;
}
HandlePoseTracking(i, poseState, controller);
}
}
}
void VRSystemManagerOpenVR::HandleButtonPress(uint32_t aControllerIdx,
uint32_t aButton,
uint64_t aButtonMask,
uint64_t aButtonPressed,
uint64_t aButtonTouched) {
RefPtr<impl::VRControllerOpenVR> controller(
mOpenVRController[aControllerIdx]);
MOZ_ASSERT(controller);
const uint64_t pressedDiff =
(controller->GetButtonPressed() ^ aButtonPressed);
const uint64_t touchedDiff =
(controller->GetButtonTouched() ^ aButtonTouched);
if (!pressedDiff && !touchedDiff) {
return;
}
if (pressedDiff & aButtonMask || touchedDiff & aButtonMask) {
// diff & (aButtonPressed, aButtonTouched) would be true while a new button
// pressed or touched event, otherwise it is an old event and needs to
// notify the button has been released.
NewButtonEvent(aControllerIdx, aButton, aButtonMask & aButtonPressed,
aButtonMask & aButtonTouched,
(aButtonMask & aButtonPressed) ? 1.0L : 0.0L);
}
}
void VRSystemManagerOpenVR::HandleTriggerPress(uint32_t aControllerIdx,
uint32_t aButton,
uint32_t aTrigger,
float aValue) {
RefPtr<impl::VRControllerOpenVR> controller(
mOpenVRController[aControllerIdx]);
MOZ_ASSERT(controller);
const float oldValue = controller->GetTrigger(aTrigger);
// For OpenVR, the threshold value of ButtonPressed and ButtonTouched is 0.55.
// We prefer to let developers to set their own threshold for the adjustment.
// Therefore, we don't check ButtonPressed and ButtonTouched with ButtonMask
// here. we just check the button value is larger than the threshold value or
// not.
const float threshold = gfxPrefs::VRControllerTriggerThreshold();
// Avoid sending duplicated events in IPC channels.
if (oldValue != aValue) {
NewButtonEvent(aControllerIdx, aButton, aValue > threshold,
aValue > threshold, aValue);
controller->SetTrigger(aTrigger, aValue);
}
}
void VRSystemManagerOpenVR::HandleAxisMove(uint32_t aControllerIdx,
uint32_t aAxis, float aValue) {
RefPtr<impl::VRControllerOpenVR> controller(
mOpenVRController[aControllerIdx]);
MOZ_ASSERT(controller);
if (controller->GetAxisMove(aAxis) != aValue) {
NewAxisMove(aControllerIdx, aAxis, aValue);
controller->SetAxisMove(aAxis, aValue);
}
}
void VRSystemManagerOpenVR::HandlePoseTracking(uint32_t aControllerIdx,
const GamepadPoseState& aPose,
VRControllerHost* aController) {
MOZ_ASSERT(aController);
if (aPose != aController->GetPose()) {
aController->SetPose(aPose);
NewPoseState(aControllerIdx, aPose);
}
}
dom::GamepadHand VRSystemManagerOpenVR::GetGamepadHandFromControllerRole(
::vr::ETrackedControllerRole aRole) {
dom::GamepadHand hand;
switch (aRole) {
case ::vr::ETrackedControllerRole::TrackedControllerRole_Invalid:
hand = dom::GamepadHand::_empty;
break;
case ::vr::ETrackedControllerRole::TrackedControllerRole_LeftHand:
hand = dom::GamepadHand::Left;
break;
case ::vr::ETrackedControllerRole::TrackedControllerRole_RightHand:
hand = dom::GamepadHand::Right;
break;
default:
MOZ_ASSERT(false);
break;
}
return hand;
}
void VRSystemManagerOpenVR::VibrateHaptic(uint32_t aControllerIdx,
uint32_t aHapticIndex,
double aIntensity, double aDuration,
const VRManagerPromise& aPromise) {
// mVRSystem is available after VRDisplay is created
// at GetHMDs().
if (!mVRSystem || (aControllerIdx >= mOpenVRController.Length())) {
return;
}
RefPtr<impl::VRControllerOpenVR> controller =
mOpenVRController[aControllerIdx];
MOZ_ASSERT(controller);
controller->VibrateHaptic(mVRSystem, aHapticIndex, aIntensity, aDuration,
aPromise);
}
void VRSystemManagerOpenVR::StopVibrateHaptic(uint32_t aControllerIdx) {
// mVRSystem is available after VRDisplay is created
// at GetHMDs().
if (!mVRSystem || (aControllerIdx >= mOpenVRController.Length())) {
return;
}
RefPtr<impl::VRControllerOpenVR> controller =
mOpenVRController[aControllerIdx];
MOZ_ASSERT(controller);
controller->StopVibrateHaptic();
}
void VRSystemManagerOpenVR::GetControllers(
nsTArray<RefPtr<VRControllerHost>>& aControllerResult) {
aControllerResult.Clear();
for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
aControllerResult.AppendElement(mOpenVRController[i]);
}
}
void VRSystemManagerOpenVR::ScanForControllers() {
// mVRSystem is available after VRDisplay is created
// at GetHMDs().
if (!mVRSystem) {
return;
}
::vr::TrackedDeviceIndex_t trackedIndexArray[::vr::k_unMaxTrackedDeviceCount];
uint32_t newControllerCount = 0;
// Basically, we would have HMDs in the tracked devices,
// but we are just interested in the controllers.
for (::vr::TrackedDeviceIndex_t trackedDevice =
::vr::k_unTrackedDeviceIndex_Hmd + 1;
trackedDevice < ::vr::k_unMaxTrackedDeviceCount; ++trackedDevice) {
if (!mVRSystem->IsTrackedDeviceConnected(trackedDevice)) {
continue;
}
const ::vr::ETrackedDeviceClass deviceType =
mVRSystem->GetTrackedDeviceClass(trackedDevice);
if (deviceType != ::vr::TrackedDeviceClass_Controller &&
deviceType != ::vr::TrackedDeviceClass_GenericTracker) {
continue;
}
trackedIndexArray[newControllerCount] = trackedDevice;
++newControllerCount;
}
if (newControllerCount != mControllerCount) {
RemoveControllers();
// Re-adding controllers to VRControllerManager.
for (::vr::TrackedDeviceIndex_t i = 0; i < newControllerCount; ++i) {
const ::vr::TrackedDeviceIndex_t trackedDevice = trackedIndexArray[i];
const ::vr::ETrackedDeviceClass deviceType =
mVRSystem->GetTrackedDeviceClass(trackedDevice);
const ::vr::ETrackedControllerRole role =
mVRSystem->GetControllerRoleForTrackedDeviceIndex(trackedDevice);
const GamepadHand hand = GetGamepadHandFromControllerRole(role);
uint32_t numButtons = 0;
uint32_t numTriggers = 0;
uint32_t numAxes = 0;
// Scan the axes that the controllers support
for (uint32_t j = 0; j < ::vr::k_unControllerStateAxisCount; ++j) {
const uint32_t supportAxis = mVRSystem->GetInt32TrackedDeviceProperty(
trackedDevice, static_cast<vr::TrackedDeviceProperty>(
::vr::Prop_Axis0Type_Int32 + j));
switch (supportAxis) {
case ::vr::EVRControllerAxisType::k_eControllerAxis_Joystick:
case ::vr::EVRControllerAxisType::k_eControllerAxis_TrackPad:
numAxes += 2; // It has x and y axes.
++numButtons;
break;
case ::vr::k_eControllerAxis_Trigger:
if (j <= 2) {
++numButtons;
++numTriggers;
} else {
#ifdef DEBUG
// SteamVR Knuckles is the only special case for using 2D axis
// values on triggers.
::vr::ETrackedPropertyError err;
uint32_t requiredBufferLen;
char charBuf[128];
requiredBufferLen = mVRSystem->GetStringTrackedDeviceProperty(
trackedDevice, ::vr::Prop_RenderModelName_String, charBuf,
128, &err);
MOZ_ASSERT(requiredBufferLen && err == ::vr::TrackedProp_Success);
nsCString deviceId(charBuf);
MOZ_ASSERT(deviceId.Find("knuckles") != kNotFound);
#endif // #ifdef DEBUG
numButtons += 2;
numTriggers += 2;
}
break;
}
}
// Scan the buttons that the controllers support
const uint64_t supportButtons = mVRSystem->GetUint64TrackedDeviceProperty(
trackedDevice, ::vr::Prop_SupportedButtons_Uint64);
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_A)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_Grip)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_ApplicationMenu)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Left)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Up)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Right)) {
++numButtons;
}
if (supportButtons & BTN_MASK_FROM_ID(k_EButton_DPad_Down)) {
++numButtons;
}
nsCString deviceId;
GetControllerDeviceId(deviceType, trackedDevice, deviceId);
RefPtr<VRControllerOpenVR> openVRController = new VRControllerOpenVR(
hand, mOpenVRHMD->GetDisplayInfo().GetDisplayID(), numButtons,
numTriggers, numAxes, deviceId);
openVRController->SetTrackedIndex(trackedDevice);
mOpenVRController.AppendElement(openVRController);
// If the Windows MR controller doesn't has the amount
// of buttons or axes as our expectation, switching off
// the workaround for Windows MR.
if (mIsWindowsMR && (numAxes < 4 || numButtons < 5)) {
mIsWindowsMR = false;
NS_WARNING("OpenVR - Switching off Windows MR mode.");
}
// Not already present, add it.
AddGamepad(openVRController->GetControllerInfo());
++mControllerCount;
}
}
}
void VRSystemManagerOpenVR::RemoveControllers() {
// The controller count is changed, removing the existing gamepads first.
for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
mOpenVRController[i]->ShutdownVibrateHapticThread();
RemoveGamepad(i);
}
mOpenVRController.Clear();
mControllerCount = 0;
}
void VRSystemManagerOpenVR::GetControllerDeviceId(
::vr::ETrackedDeviceClass aDeviceType,
::vr::TrackedDeviceIndex_t aDeviceIndex, nsCString& aId) {
switch (aDeviceType) {
case ::vr::TrackedDeviceClass_Controller: {
::vr::ETrackedPropertyError err;
uint32_t requiredBufferLen;
bool isFound = false;
char charBuf[128];
requiredBufferLen = mVRSystem->GetStringTrackedDeviceProperty(
aDeviceIndex, ::vr::Prop_RenderModelName_String, charBuf, 128, &err);
if (requiredBufferLen > 128) {
MOZ_CRASH("Larger than the buffer size.");
}
MOZ_ASSERT(requiredBufferLen && err == ::vr::TrackedProp_Success);
nsCString deviceId(charBuf);
if (deviceId.Find("knuckles") != kNotFound) {
aId.AssignLiteral("OpenVR Knuckles");
isFound = true;
}
requiredBufferLen = mVRSystem->GetStringTrackedDeviceProperty(
aDeviceIndex, ::vr::Prop_SerialNumber_String, charBuf, 128, &err);
if (requiredBufferLen > 128) {
MOZ_CRASH("Larger than the buffer size.");
}
MOZ_ASSERT(requiredBufferLen && err == ::vr::TrackedProp_Success);
deviceId.Assign(charBuf);
if (deviceId.Find("MRSOURCE") != kNotFound) {
aId.AssignLiteral("Spatial Controller (Spatial Interaction Source) ");
mIsWindowsMR = true;
isFound = true;
}
if (!isFound) {
aId.AssignLiteral("OpenVR Gamepad");
}
break;
}
case ::vr::TrackedDeviceClass_GenericTracker: {
aId.AssignLiteral("OpenVR Tracker");
break;
}
default:
MOZ_ASSERT(false);
break;
}
}