Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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 % std::size(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 < std::size(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 % std::size(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);
}
}