diff options
Diffstat (limited to 'gfx/vr/service/VRService.cpp')
-rw-r--r-- | gfx/vr/service/VRService.cpp | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/gfx/vr/service/VRService.cpp b/gfx/vr/service/VRService.cpp new file mode 100644 index 0000000000..2b774c2531 --- /dev/null +++ b/gfx/vr/service/VRService.cpp @@ -0,0 +1,418 @@ +/* -*- 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 "VRService.h" + +#include <cstring> // for memcmp + +#include "../VRShMem.h" +#include "../gfxVRMutex.h" +#include "PuppetSession.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsThread.h" +#include "nsXULAppAPI.h" + +#if defined(XP_WIN) +# include "OculusSession.h" +#endif + +#if defined(XP_WIN) || defined(XP_MACOSX) || \ + (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)) +# include "OpenVRSession.h" +#endif +#if !defined(MOZ_WIDGET_ANDROID) +# include "OSVRSession.h" +#endif + +using namespace mozilla; +using namespace mozilla::gfx; + +namespace { + +int64_t FrameIDFromBrowserState(const mozilla::gfx::VRBrowserState& aState) { + for (const auto& layer : aState.layerState) { + if (layer.type == VRLayerType::LayerType_Stereo_Immersive) { + return layer.layer_stereo_immersive.frameId; + } + } + return 0; +} + +bool IsImmersiveContentActive(const mozilla::gfx::VRBrowserState& aState) { + for (const auto& layer : aState.layerState) { + if (layer.type == VRLayerType::LayerType_Stereo_Immersive) { + return true; + } + } + return false; +} + +} // anonymous namespace + +/*static*/ +already_AddRefed<VRService> VRService::Create( + volatile VRExternalShmem* aShmem) { + RefPtr<VRService> service = new VRService(aShmem); + return service.forget(); +} + +VRService::VRService(volatile VRExternalShmem* aShmem) + : mSystemState{}, + mBrowserState{}, + mShutdownRequested(false), + mLastHapticState{}, + mFrameStartTime{} { + // When we have the VR process, we map the memory + // of mAPIShmem from GPU process and pass it to the CTOR. + // If we don't have the VR process, we will instantiate + // mAPIShmem in VRService. + mShmem = new VRShMem(aShmem, aShmem == nullptr /*aRequiresMutex*/); +} + +VRService::~VRService() { + // PSA: We must store the value of any staticPrefs preferences as this + // destructor will be called after staticPrefs has been shut down. + StopInternal(true /*aFromDtor*/); +} + +void VRService::Refresh() { + if (mShmem != nullptr && mShmem->IsDisplayStateShutdown()) { + Stop(); + } +} + +void VRService::Start() { + if (!mServiceThread) { + /** + * We must ensure that any time the service is re-started, that + * the VRSystemState is reset, including mSystemState.enumerationCompleted + * This must happen before VRService::Start returns to the caller, in order + * to prevent the WebVR/WebXR promises from being resolved before the + * enumeration has been completed. + */ + memset(&mSystemState, 0, sizeof(mSystemState)); + PushState(mSystemState); + RefPtr<VRService> self = this; + nsCOMPtr<nsIThread> thread; + nsresult rv = NS_NewNamedThread( + "VRService", getter_AddRefs(thread), + NS_NewRunnableFunction("VRService::ServiceThreadStartup", [self]() { + self->mBackgroundHangMonitor = + MakeUnique<mozilla::BackgroundHangMonitor>( + "VRService", + /* Timeout values are powers-of-two to enable us get better + data. 128ms is chosen for transient hangs because 8Hz + should be the minimally acceptable goal for Compositor + responsiveness (normal goal is 60Hz). */ + 128, + /* 2048ms is chosen for permanent hangs because it's longer + * than most Compositor hangs seen in the wild, but is short + * enough to not miss getting native hang stacks. */ + 2048); + static_cast<nsThread*>(NS_GetCurrentThread()) + ->SetUseHangMonitor(true); + })); + + if (NS_FAILED(rv)) { + return; + } + thread.swap(mServiceThread); + // ServiceInitialize needs mServiceThread to be set in order to be able to + // assert that it's running on the right thread as well as dispatching new + // tasks. It can't be run within the NS_NewRunnableFunction initial event. + MOZ_ALWAYS_SUCCEEDS(mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceInitialize", this, + &VRService::ServiceInitialize))); + } +} + +void VRService::Stop() { StopInternal(false /*aFromDtor*/); } + +void VRService::StopInternal(bool aFromDtor) { + if (mServiceThread) { + // We must disable the background hang monitor before we can shutdown this + // thread. Dispatched a last task to do so. No task will be allowed to run + // on the service thread after this one. + mServiceThread->Dispatch(NS_NewRunnableFunction( + "VRService::StopInternal", [self = RefPtr<VRService>(this), this] { + static_cast<nsThread*>(NS_GetCurrentThread()) + ->SetUseHangMonitor(false); + mBackgroundHangMonitor = nullptr; + })); + mShutdownRequested = true; + mServiceThread->Shutdown(); + mServiceThread = nullptr; + } + + if (mShmem != nullptr && (aFromDtor || !mShmem->IsSharedExternalShmem())) { + // Only leave the VRShMem and clean up the pointer when the struct + // was not passed in. Otherwise, VRService will no longer have a + // way to access that struct if VRService starts again. + mShmem->LeaveShMem(); + delete mShmem; + mShmem = nullptr; + } + + mSession = nullptr; +} + +bool VRService::InitShmem() { return mShmem->JoinShMem(); } + +bool VRService::IsInServiceThread() { + return mServiceThread && mServiceThread->IsOnCurrentThread(); +} + +void VRService::ServiceInitialize() { + MOZ_ASSERT(IsInServiceThread()); + + if (!InitShmem()) { + return; + } + + mShutdownRequested = false; + // Get initial state from the browser + PullState(mBrowserState); + + // Try to start a VRSession + UniquePtr<VRSession> session; + + if (StaticPrefs::dom_vr_puppet_enabled()) { + // When the VR Puppet is enabled, we don't want + // to enumerate any real devices + session = MakeUnique<PuppetSession>(); + if (!session->Initialize(mSystemState, mBrowserState.detectRuntimesOnly)) { + session = nullptr; + } + } else { + // We try Oculus first to ensure we use Oculus + // devices trough the most native interface + // when possible. +#if defined(XP_WIN) + // Try Oculus + if (!session) { + session = MakeUnique<OculusSession>(); + if (!session->Initialize(mSystemState, + mBrowserState.detectRuntimesOnly)) { + session = nullptr; + } + } +#endif + +#if defined(XP_WIN) || defined(XP_MACOSX) || \ + (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)) + // Try OpenVR + if (!session) { + session = MakeUnique<OpenVRSession>(); + if (!session->Initialize(mSystemState, + mBrowserState.detectRuntimesOnly)) { + session = nullptr; + } + } +#endif +#if !defined(MOZ_WIDGET_ANDROID) + // Try OSVR + if (!session) { + session = MakeUnique<OSVRSession>(); + if (!session->Initialize(mSystemState, + mBrowserState.detectRuntimesOnly)) { + session = nullptr; + } + } +#endif + + } // if (staticPrefs:VRPuppetEnabled()) + + if (session) { + mSession = std::move(session); + // Setting enumerationCompleted to true indicates to the browser + // that it should resolve any promises in the WebVR/WebXR API + // waiting for hardware detection. + mSystemState.enumerationCompleted = true; + PushState(mSystemState); + + mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this, + &VRService::ServiceWaitForImmersive)); + } else { + // VR hardware was not detected. + // We must inform the browser of the failure so it may try again + // later and resolve WebVR promises. A failure or shutdown is + // indicated by enumerationCompleted being set to true, with all + // other fields remaining zeroed out. + VRDisplayCapabilityFlags capFlags = + mSystemState.displayState.capabilityFlags; + memset(&mSystemState, 0, sizeof(mSystemState)); + mSystemState.enumerationCompleted = true; + + if (mBrowserState.detectRuntimesOnly) { + mSystemState.displayState.capabilityFlags = capFlags; + } else { + mSystemState.displayState.minRestartInterval = + StaticPrefs::dom_vr_external_notdetected_timeout(); + } + mSystemState.displayState.shutdown = true; + PushState(mSystemState); + } +} + +void VRService::ServiceShutdown() { + MOZ_ASSERT(IsInServiceThread()); + + // Notify the browser that we have shut down. + // This is indicated by enumerationCompleted being set + // to true, with all other fields remaining zeroed out. + memset(&mSystemState, 0, sizeof(mSystemState)); + mSystemState.enumerationCompleted = true; + mSystemState.displayState.shutdown = true; + if (mSession && mSession->ShouldQuit()) { + mSystemState.displayState.minRestartInterval = + StaticPrefs::dom_vr_external_quit_timeout(); + } + PushState(mSystemState); + mSession = nullptr; +} + +void VRService::ServiceWaitForImmersive() { + MOZ_ASSERT(IsInServiceThread()); + MOZ_ASSERT(mSession); + + mSession->ProcessEvents(mSystemState); + PushState(mSystemState); + PullState(mBrowserState); + + if (mSession->ShouldQuit() || mShutdownRequested) { + // Shut down + mServiceThread->Dispatch(NewRunnableMethod( + "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown)); + } else if (IsImmersiveContentActive(mBrowserState)) { + // Enter Immersive Mode + mSession->StartPresentation(); + mSession->StartFrame(mSystemState); + PushState(mSystemState); + + mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this, + &VRService::ServiceImmersiveMode)); + } else { + // Continue waiting for immersive mode + mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this, + &VRService::ServiceWaitForImmersive)); + } +} + +void VRService::ServiceImmersiveMode() { + MOZ_ASSERT(IsInServiceThread()); + MOZ_ASSERT(mSession); + + mSession->ProcessEvents(mSystemState); + UpdateHaptics(); + PushState(mSystemState); + PullState(mBrowserState); + + if (mSession->ShouldQuit() || mShutdownRequested) { + // Shut down + mServiceThread->Dispatch(NewRunnableMethod( + "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown)); + return; + } + + if (!IsImmersiveContentActive(mBrowserState)) { + // Exit immersive mode + mSession->StopAllHaptics(); + mSession->StopPresentation(); + mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this, + &VRService::ServiceWaitForImmersive)); + return; + } + + uint64_t newFrameId = FrameIDFromBrowserState(mBrowserState); + if (newFrameId != mSystemState.displayState.lastSubmittedFrameId) { + // A new immersive frame has been received. + // Submit the textures to the VR system compositor. + bool success = false; + for (const auto& layer : mBrowserState.layerState) { + if (layer.type == VRLayerType::LayerType_Stereo_Immersive) { + // SubmitFrame may block in order to control the timing for + // the next frame start + success = mSession->SubmitFrame(layer.layer_stereo_immersive); + break; + } + } + + // Changing mLastSubmittedFrameId triggers a new frame to start + // rendering. Changes to mLastSubmittedFrameId and the values + // used for rendering, such as headset pose, must be pushed + // atomically to the browser. + mSystemState.displayState.lastSubmittedFrameId = newFrameId; + mSystemState.displayState.lastSubmittedFrameSuccessful = success; + + // StartFrame may block to control the timing for the next frame start + mSession->StartFrame(mSystemState); + mSystemState.sensorState.inputFrameID++; + size_t historyIndex = + mSystemState.sensorState.inputFrameID % ArrayLength(mFrameStartTime); + mFrameStartTime[historyIndex] = TimeStamp::Now(); + PushState(mSystemState); + } + + // Continue immersive mode + mServiceThread->Dispatch( + NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this, + &VRService::ServiceImmersiveMode)); +} + +void VRService::UpdateHaptics() { + MOZ_ASSERT(IsInServiceThread()); + MOZ_ASSERT(mSession); + + for (size_t i = 0; i < ArrayLength(mBrowserState.hapticState); i++) { + VRHapticState& state = mBrowserState.hapticState[i]; + VRHapticState& lastState = mLastHapticState[i]; + // Note that VRHapticState is asserted to be a POD type, thus memcmp is safe + if (memcmp(&state, &lastState, sizeof(VRHapticState)) == 0) { + // No change since the last update + continue; + } + if (state.inputFrameID == 0) { + // The haptic feedback was stopped + mSession->StopVibrateHaptic(state.controllerIndex); + } else { + TimeStamp now; + if (now.IsNull()) { + // TimeStamp::Now() is expensive, so we + // must call it only when needed and save the + // output for further loop iterations. + now = TimeStamp::Now(); + } + // This is a new haptic pulse, or we are overriding a prior one + size_t historyIndex = state.inputFrameID % ArrayLength(mFrameStartTime); + float startOffset = + (float)(now - mFrameStartTime[historyIndex]).ToSeconds(); + + // state.pulseStart is guaranteed never to be in the future + mSession->VibrateHaptic( + state.controllerIndex, state.hapticIndex, state.pulseIntensity, + state.pulseDuration + state.pulseStart - startOffset); + } + // Record the state for comparison in the next run + memcpy(&lastState, &state, sizeof(VRHapticState)); + } +} + +void VRService::PushState(const mozilla::gfx::VRSystemState& aState) { + if (mShmem != nullptr) { + mShmem->PushSystemState(aState); + } +} + +void VRService::PullState(mozilla::gfx::VRBrowserState& aState) { + if (mShmem != nullptr) { + mShmem->PullBrowserState(aState); + } +} |