mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
3f169d905a
Differential Revision: https://phabricator.services.mozilla.com/D140785
1546 lines
49 KiB
C++
1546 lines
49 KiB
C++
/* -*- 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 "VRManager.h"
|
|
|
|
#include "GeckoProfiler.h"
|
|
#include "VRManagerParent.h"
|
|
#include "VRShMem.h"
|
|
#include "VRThread.h"
|
|
#include "gfxVR.h"
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/dom/VRDisplay.h"
|
|
#include "mozilla/dom/GamepadEventTypes.h"
|
|
#include "mozilla/layers/TextureHost.h"
|
|
#include "mozilla/layers/CompositorThread.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/Services.h"
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "mozilla/Unused.h"
|
|
#include "nsIObserverService.h"
|
|
|
|
#include "gfxVR.h"
|
|
#include <cstring>
|
|
|
|
#include "ipc/VRLayerParent.h"
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
# include "VRServiceHost.h"
|
|
#endif
|
|
|
|
#ifdef XP_WIN
|
|
# include "CompositorD3D11.h"
|
|
# include "TextureD3D11.h"
|
|
# include <d3d11.h>
|
|
# include "gfxWindowsPlatform.h"
|
|
# include "mozilla/gfx/DeviceManagerDx.h"
|
|
#elif defined(XP_MACOSX)
|
|
# include "mozilla/gfx/MacIOSurface.h"
|
|
# include <errno.h>
|
|
#elif defined(MOZ_WIDGET_ANDROID)
|
|
# include <string.h>
|
|
# include <pthread.h>
|
|
# include "GeckoVRManager.h"
|
|
# include "mozilla/java/GeckoSurfaceTextureWrappers.h"
|
|
# include "mozilla/layers/CompositorThread.h"
|
|
#endif // defined(MOZ_WIDGET_ANDROID)
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::gfx;
|
|
using namespace mozilla::layers;
|
|
using namespace mozilla::gl;
|
|
|
|
using mozilla::dom::GamepadHandle;
|
|
|
|
namespace mozilla::gfx {
|
|
|
|
/**
|
|
* When VR content is active, we run the tasks at 1ms
|
|
* intervals, enabling multiple events to be processed
|
|
* per frame, such as haptic feedback pulses.
|
|
*/
|
|
const uint32_t kVRActiveTaskInterval = 1; // milliseconds
|
|
|
|
/**
|
|
* When VR content is inactive, we run the tasks at 100ms
|
|
* intervals, enabling VR display enumeration and
|
|
* presentation startup to be relatively responsive
|
|
* while not consuming unnecessary resources.
|
|
*/
|
|
const uint32_t kVRIdleTaskInterval = 100; // milliseconds
|
|
|
|
/**
|
|
* Max frame duration before the watchdog submits a new one.
|
|
* Probably we can get rid of this when we enforce that SubmitFrame can only be
|
|
* called in a VRDisplay loop.
|
|
*/
|
|
const double kVRMaxFrameSubmitDuration = 4000.0f; // milliseconds
|
|
|
|
static StaticRefPtr<VRManager> sVRManagerSingleton;
|
|
|
|
static bool ValidVRManagerProcess() {
|
|
return XRE_IsParentProcess() || XRE_IsGPUProcess();
|
|
}
|
|
|
|
/* static */
|
|
VRManager* VRManager::Get() {
|
|
MOZ_ASSERT(sVRManagerSingleton != nullptr);
|
|
MOZ_ASSERT(ValidVRManagerProcess());
|
|
|
|
return sVRManagerSingleton;
|
|
}
|
|
|
|
/* static */
|
|
VRManager* VRManager::MaybeGet() {
|
|
MOZ_ASSERT(ValidVRManagerProcess());
|
|
|
|
return sVRManagerSingleton;
|
|
}
|
|
|
|
Atomic<uint32_t> VRManager::sDisplayBase(0);
|
|
|
|
/* static */
|
|
uint32_t VRManager::AllocateDisplayID() { return ++sDisplayBase; }
|
|
|
|
/*static*/
|
|
void VRManager::ManagerInit() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
if (!ValidVRManagerProcess()) {
|
|
return;
|
|
}
|
|
|
|
// Enable gamepad extensions while VR is enabled.
|
|
// Preference only can be set at the Parent process.
|
|
if (StaticPrefs::dom_vr_enabled() && XRE_IsParentProcess()) {
|
|
Preferences::SetBool("dom.gamepad.extensions.enabled", true);
|
|
}
|
|
|
|
if (sVRManagerSingleton == nullptr) {
|
|
sVRManagerSingleton = new VRManager();
|
|
ClearOnShutdown(&sVRManagerSingleton);
|
|
}
|
|
}
|
|
|
|
VRManager::VRManager()
|
|
: mState(VRManagerState::Disabled),
|
|
mAccumulator100ms(0.0f),
|
|
mRuntimeDetectionRequested(false),
|
|
mRuntimeDetectionCompleted(false),
|
|
mEnumerationRequested(false),
|
|
mEnumerationCompleted(false),
|
|
mVRDisplaysRequested(false),
|
|
mVRDisplaysRequestedNonFocus(false),
|
|
mVRControllersRequested(false),
|
|
mFrameStarted(false),
|
|
mTaskInterval(0),
|
|
mCurrentSubmitTaskMonitor("CurrentSubmitTaskMonitor"),
|
|
mCurrentSubmitTask(nullptr),
|
|
mLastSubmittedFrameId(0),
|
|
mLastStartedFrame(0),
|
|
mRuntimeSupportFlags(VRDisplayCapabilityFlags::Cap_None),
|
|
mAppPaused(false),
|
|
mShmem(nullptr),
|
|
mHapticPulseRemaining{},
|
|
mDisplayInfo{},
|
|
mLastUpdateDisplayInfo{},
|
|
mBrowserState{},
|
|
mLastSensorState{} {
|
|
MOZ_ASSERT(sVRManagerSingleton == nullptr);
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(ValidVRManagerProcess());
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
// XRE_IsGPUProcess() is helping us to check some platforms like
|
|
// Win 7 try which are not using GPU process but VR process is enabled.
|
|
mVRProcessEnabled =
|
|
StaticPrefs::dom_vr_process_enabled_AtStartup() && XRE_IsGPUProcess();
|
|
VRServiceHost::Init(mVRProcessEnabled);
|
|
mServiceHost = VRServiceHost::Get();
|
|
// We must shutdown before VRServiceHost, which is cleared
|
|
// on ShutdownPhase::XPCOMShutdownFinal, potentially before VRManager.
|
|
// We hold a reference to VRServiceHost to ensure it stays
|
|
// alive until we have shut down.
|
|
#else
|
|
// For Android, there is no VRProcess available and no VR service is
|
|
// created, so default to false.
|
|
mVRProcessEnabled = false;
|
|
#endif // !defined(MOZ_WIDGET_ANDROID)
|
|
|
|
nsCOMPtr<nsIObserverService> service = services::GetObserverService();
|
|
if (service) {
|
|
service->AddObserver(this, "application-background", false);
|
|
service->AddObserver(this, "application-foreground", false);
|
|
}
|
|
}
|
|
|
|
void VRManager::OpenShmem() {
|
|
if (mShmem == nullptr) {
|
|
mShmem = new VRShMem(nullptr, true /*aRequiresMutex*/);
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
mShmem->CreateShMem(mVRProcessEnabled /*aCreateOnSharedMemory*/);
|
|
// The VR Service accesses all hardware from a separate process
|
|
// and replaces the other VRManager when enabled.
|
|
// If the VR process is not enabled, create an in-process VRService.
|
|
if (!mVRProcessEnabled) {
|
|
// If the VR process is disabled, attempt to create a
|
|
// VR service within the current process
|
|
mServiceHost->CreateService(mShmem->GetExternalShmem());
|
|
return;
|
|
}
|
|
#else
|
|
mShmem->CreateShMemForAndroid();
|
|
#endif
|
|
} else {
|
|
mShmem->ClearShMem();
|
|
}
|
|
|
|
// Reset local information for new connection
|
|
mDisplayInfo.Clear();
|
|
mLastUpdateDisplayInfo.Clear();
|
|
mFrameStarted = false;
|
|
mBrowserState.Clear();
|
|
mLastSensorState.Clear();
|
|
mEnumerationCompleted = false;
|
|
mDisplayInfo.mGroupMask = kVRGroupContent;
|
|
}
|
|
|
|
void VRManager::CloseShmem() {
|
|
if (mShmem != nullptr) {
|
|
mShmem->CloseShMem();
|
|
delete mShmem;
|
|
mShmem = nullptr;
|
|
}
|
|
}
|
|
|
|
VRManager::~VRManager() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(mState == VRManagerState::Disabled);
|
|
|
|
nsCOMPtr<nsIObserverService> service = services::GetObserverService();
|
|
if (service) {
|
|
service->RemoveObserver(this, "application-background");
|
|
service->RemoveObserver(this, "application-foreground");
|
|
}
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
mServiceHost->Shutdown();
|
|
#endif
|
|
CloseShmem();
|
|
}
|
|
|
|
void VRManager::AddLayer(VRLayerParent* aLayer) {
|
|
mLayers.AppendElement(aLayer);
|
|
mDisplayInfo.mPresentingGroups |= aLayer->GetGroup();
|
|
if (mLayers.Length() == 1) {
|
|
StartPresentation();
|
|
}
|
|
|
|
// Ensure that the content process receives the change immediately
|
|
if (mState != VRManagerState::Enumeration &&
|
|
mState != VRManagerState::RuntimeDetection) {
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
}
|
|
|
|
void VRManager::RemoveLayer(VRLayerParent* aLayer) {
|
|
mLayers.RemoveElement(aLayer);
|
|
if (mLayers.Length() == 0) {
|
|
StopPresentation();
|
|
}
|
|
mDisplayInfo.mPresentingGroups = 0;
|
|
for (auto layer : mLayers) {
|
|
mDisplayInfo.mPresentingGroups |= layer->GetGroup();
|
|
}
|
|
|
|
// Ensure that the content process receives the change immediately
|
|
if (mState != VRManagerState::Enumeration &&
|
|
mState != VRManagerState::RuntimeDetection) {
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
}
|
|
|
|
void VRManager::AddVRManagerParent(VRManagerParent* aVRManagerParent) {
|
|
mVRManagerParents.Insert(aVRManagerParent);
|
|
}
|
|
|
|
void VRManager::RemoveVRManagerParent(VRManagerParent* aVRManagerParent) {
|
|
mVRManagerParents.Remove(aVRManagerParent);
|
|
if (mVRManagerParents.IsEmpty()) {
|
|
Destroy();
|
|
}
|
|
}
|
|
|
|
void VRManager::UpdateRequestedDevices() {
|
|
bool bHaveEventListener = false;
|
|
bool bHaveEventListenerNonFocus = false;
|
|
bool bHaveControllerListener = false;
|
|
|
|
for (VRManagerParent* vmp : mVRManagerParents) {
|
|
bHaveEventListener |= vmp->HaveEventListener() && vmp->GetVRActiveStatus();
|
|
bHaveEventListenerNonFocus |=
|
|
vmp->HaveEventListener() && !vmp->GetVRActiveStatus();
|
|
bHaveControllerListener |= vmp->HaveControllerListener();
|
|
}
|
|
|
|
mVRDisplaysRequested = bHaveEventListener;
|
|
mVRDisplaysRequestedNonFocus = bHaveEventListenerNonFocus;
|
|
// We only currently allow controllers to be used when
|
|
// also activating a VR display
|
|
mVRControllersRequested = mVRDisplaysRequested && bHaveControllerListener;
|
|
}
|
|
|
|
/**
|
|
* VRManager::NotifyVsync must be called on every 2d vsync (usually at 60hz).
|
|
* This must be called even when no WebVR site is active.
|
|
* If we don't have a 2d display attached to the system, we can call this
|
|
* at the VR display's native refresh rate.
|
|
**/
|
|
void VRManager::NotifyVsync(const TimeStamp& aVsyncTimestamp) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
/**
|
|
* If the display isn't presenting, refresh the sensors and trigger
|
|
* VRDisplay.requestAnimationFrame at the normal 2d display refresh rate.
|
|
*/
|
|
if (mDisplayInfo.mPresentingGroups == 0) {
|
|
StartFrame();
|
|
}
|
|
}
|
|
|
|
void VRManager::StartTasks() {
|
|
if (!mTaskTimer) {
|
|
mTaskInterval = GetOptimalTaskInterval();
|
|
mTaskTimer = NS_NewTimer();
|
|
mTaskTimer->SetTarget(CompositorThread());
|
|
mTaskTimer->InitWithNamedFuncCallback(
|
|
TaskTimerCallback, this, mTaskInterval,
|
|
nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP,
|
|
"VRManager::TaskTimerCallback");
|
|
}
|
|
}
|
|
|
|
void VRManager::StopTasks() {
|
|
if (mTaskTimer) {
|
|
mTaskTimer->Cancel();
|
|
mTaskTimer = nullptr;
|
|
}
|
|
}
|
|
|
|
/*static*/
|
|
void VRManager::TaskTimerCallback(nsITimer* aTimer, void* aClosure) {
|
|
/**
|
|
* It is safe to use the pointer passed in aClosure to reference the
|
|
* VRManager object as the timer is canceled in VRManager::Destroy.
|
|
* VRManager::Destroy set mState to VRManagerState::Disabled, which
|
|
* is asserted in the VRManager destructor, guaranteeing that this
|
|
* functions runs if and only if the VRManager object is valid.
|
|
*/
|
|
VRManager* self = static_cast<VRManager*>(aClosure);
|
|
self->RunTasks();
|
|
|
|
if (self->mAppPaused) {
|
|
// When the apps goes the background (e.g. Android) we should stop the
|
|
// tasks.
|
|
self->StopTasks();
|
|
self->mState = VRManagerState::Idle;
|
|
}
|
|
}
|
|
|
|
void VRManager::RunTasks() {
|
|
// Will be called once every 1ms when a VR presentation
|
|
// is active or once per vsync when a VR presentation is
|
|
// not active.
|
|
|
|
if (mState == VRManagerState::Disabled) {
|
|
// We may have been destroyed but still have messages
|
|
// in the queue from mTaskTimer. Bail out to avoid
|
|
// running them.
|
|
return;
|
|
}
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
double lastTickMs = mAccumulator100ms;
|
|
double deltaTime = 0.0f;
|
|
if (!mLastTickTime.IsNull()) {
|
|
deltaTime = (now - mLastTickTime).ToMilliseconds();
|
|
}
|
|
mAccumulator100ms += deltaTime;
|
|
mLastTickTime = now;
|
|
|
|
if (deltaTime > 0.0f && floor(mAccumulator100ms) != floor(lastTickMs)) {
|
|
// Even if more than 1 ms has passed, we will only
|
|
// execute Run1msTasks() once.
|
|
Run1msTasks(deltaTime);
|
|
}
|
|
|
|
if (floor(mAccumulator100ms * 0.1f) != floor(lastTickMs * 0.1f)) {
|
|
// Even if more than 10 ms has passed, we will only
|
|
// execute Run10msTasks() once.
|
|
Run10msTasks();
|
|
}
|
|
|
|
if (mAccumulator100ms >= 100.0f) {
|
|
// Even if more than 100 ms has passed, we will only
|
|
// execute Run100msTasks() once.
|
|
Run100msTasks();
|
|
mAccumulator100ms = fmod(mAccumulator100ms, 100.0f);
|
|
}
|
|
|
|
uint32_t optimalTaskInterval = GetOptimalTaskInterval();
|
|
if (mTaskTimer && optimalTaskInterval != mTaskInterval) {
|
|
mTaskTimer->SetDelay(optimalTaskInterval);
|
|
mTaskInterval = optimalTaskInterval;
|
|
}
|
|
}
|
|
|
|
uint32_t VRManager::GetOptimalTaskInterval() {
|
|
/**
|
|
* When either VR content is detected or VR hardware
|
|
* has already been activated, we schedule tasks more
|
|
* frequently.
|
|
*/
|
|
bool wantGranularTasks = mVRDisplaysRequested || mVRControllersRequested ||
|
|
mDisplayInfo.mDisplayID != 0;
|
|
if (wantGranularTasks) {
|
|
return kVRActiveTaskInterval;
|
|
}
|
|
|
|
return kVRIdleTaskInterval;
|
|
}
|
|
|
|
/**
|
|
* Run1msTasks() is guaranteed not to be
|
|
* called more than once within 1ms.
|
|
* When VR is not active, this will be
|
|
* called once per VSync if it wasn't
|
|
* called within the last 1ms.
|
|
*/
|
|
void VRManager::Run1msTasks(double aDeltaTime) { UpdateHaptics(aDeltaTime); }
|
|
|
|
/**
|
|
* Run10msTasks() is guaranteed not to be
|
|
* called more than once within 10ms.
|
|
* When VR is not active, this will be
|
|
* called once per VSync if it wasn't
|
|
* called within the last 10ms.
|
|
*/
|
|
void VRManager::Run10msTasks() {
|
|
UpdateRequestedDevices();
|
|
CheckWatchDog();
|
|
ExpireNavigationTransition();
|
|
PullState();
|
|
PushState();
|
|
}
|
|
|
|
/**
|
|
* Run100msTasks() is guaranteed not to be
|
|
* called more than once within 100ms.
|
|
* When VR is not active, this will be
|
|
* called once per VSync if it wasn't
|
|
* called within the last 100ms.
|
|
*/
|
|
void VRManager::Run100msTasks() {
|
|
// We must continually refresh the VR display enumeration to check
|
|
// for events that we must fire such as Window.onvrdisplayconnect
|
|
// Note that enumeration itself may activate display hardware, such
|
|
// as Oculus, so we only do this when we know we are displaying content
|
|
// that is looking for VR displays.
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
mServiceHost->Refresh();
|
|
CheckForPuppetCompletion();
|
|
#endif
|
|
ProcessManagerState();
|
|
}
|
|
|
|
void VRManager::CheckForInactiveTimeout() {
|
|
// Shut down the VR devices when not in use
|
|
if (mVRDisplaysRequested || mVRDisplaysRequestedNonFocus ||
|
|
mVRControllersRequested || mEnumerationRequested ||
|
|
mRuntimeDetectionRequested || mState == VRManagerState::Enumeration ||
|
|
mState == VRManagerState::RuntimeDetection) {
|
|
// We are using a VR device, keep it alive
|
|
mLastActiveTime = TimeStamp::Now();
|
|
} else if (mLastActiveTime.IsNull()) {
|
|
Shutdown();
|
|
} else {
|
|
TimeDuration duration = TimeStamp::Now() - mLastActiveTime;
|
|
if (duration.ToMilliseconds() > StaticPrefs::dom_vr_inactive_timeout()) {
|
|
Shutdown();
|
|
// We must not throttle the next enumeration request
|
|
// after an idle timeout, as it may result in the
|
|
// user needing to refresh the browser to detect
|
|
// VR hardware when leaving and returning to a VR
|
|
// site.
|
|
mLastDisplayEnumerationTime = TimeStamp();
|
|
}
|
|
}
|
|
}
|
|
|
|
void VRManager::CheckForShutdown() {
|
|
// Check for remote end shutdown
|
|
if (mDisplayInfo.mDisplayState.shutdown) {
|
|
Shutdown();
|
|
}
|
|
}
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
void VRManager::CheckForPuppetCompletion() {
|
|
// Notify content process about completion of puppet test resets
|
|
if (mState != VRManagerState::Active) {
|
|
for (const auto& key : mManagerParentsWaitingForPuppetReset) {
|
|
Unused << key->SendNotifyPuppetResetComplete();
|
|
}
|
|
mManagerParentsWaitingForPuppetReset.Clear();
|
|
}
|
|
// Notify content process about completion of puppet test scripts
|
|
if (mManagerParentRunningPuppet) {
|
|
mServiceHost->CheckForPuppetCompletion();
|
|
}
|
|
}
|
|
|
|
void VRManager::NotifyPuppetComplete() {
|
|
// Notify content process about completion of puppet test scripts
|
|
if (mManagerParentRunningPuppet) {
|
|
Unused << mManagerParentRunningPuppet
|
|
->SendNotifyPuppetCommandBufferCompleted(true);
|
|
mManagerParentRunningPuppet = nullptr;
|
|
}
|
|
}
|
|
|
|
#endif // !defined(MOZ_WIDGET_ANDROID)
|
|
|
|
void VRManager::StartFrame() {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
AUTO_PROFILER_TRACING_MARKER("VR", "GetSensorState", OTHER);
|
|
|
|
/**
|
|
* Do not start more VR frames until the last submitted frame is already
|
|
* processed, or the last has stalled for more than
|
|
* kVRMaxFrameSubmitDuration milliseconds.
|
|
*/
|
|
TimeStamp now = TimeStamp::Now();
|
|
const TimeStamp lastFrameStart =
|
|
mLastFrameStart[mDisplayInfo.mFrameId % kVRMaxLatencyFrames];
|
|
const bool isPresenting = mLastUpdateDisplayInfo.GetPresentingGroups() != 0;
|
|
double duration =
|
|
lastFrameStart.IsNull() ? 0.0 : (now - lastFrameStart).ToMilliseconds();
|
|
if (isPresenting && mLastStartedFrame > 0 &&
|
|
mDisplayInfo.mDisplayState.lastSubmittedFrameId < mLastStartedFrame &&
|
|
duration < kVRMaxFrameSubmitDuration) {
|
|
return;
|
|
}
|
|
|
|
mDisplayInfo.mFrameId++;
|
|
size_t bufferIndex = mDisplayInfo.mFrameId % kVRMaxLatencyFrames;
|
|
mDisplayInfo.mLastSensorState[bufferIndex] = mLastSensorState;
|
|
mLastFrameStart[bufferIndex] = now;
|
|
mFrameStarted = true;
|
|
mLastStartedFrame = mDisplayInfo.mFrameId;
|
|
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
|
|
void VRManager::DetectRuntimes() {
|
|
if (mState == VRManagerState::RuntimeDetection) {
|
|
// Runtime detection has already been started.
|
|
// This additional request will also receive the
|
|
// result from the first request.
|
|
return;
|
|
}
|
|
|
|
// Detect XR runtimes to determine if they are
|
|
// capable of supporting VR or AR sessions, while
|
|
// avoiding activating any XR devices or persistent
|
|
// background software.
|
|
if (mRuntimeDetectionCompleted) {
|
|
// We have already detected runtimes, so we can
|
|
// immediately respond with the same results.
|
|
// This will require the user to restart the browser
|
|
// after installing or removing an XR device
|
|
// runtime.
|
|
DispatchRuntimeCapabilitiesUpdate();
|
|
return;
|
|
}
|
|
mRuntimeDetectionRequested = true;
|
|
ProcessManagerState();
|
|
}
|
|
|
|
void VRManager::EnumerateDevices() {
|
|
if (mState == VRManagerState::Enumeration ||
|
|
(mRuntimeDetectionCompleted &&
|
|
(mVRDisplaysRequested || mEnumerationRequested))) {
|
|
// Enumeration has already been started.
|
|
// This additional request will also receive the
|
|
// result from the first request.
|
|
return;
|
|
}
|
|
// Activate XR runtimes and enumerate XR devices.
|
|
mEnumerationRequested = true;
|
|
ProcessManagerState();
|
|
}
|
|
|
|
void VRManager::ProcessManagerState() {
|
|
switch (mState) {
|
|
case VRManagerState::Disabled:
|
|
ProcessManagerState_Disabled();
|
|
break;
|
|
case VRManagerState::Idle:
|
|
ProcessManagerState_Idle();
|
|
break;
|
|
case VRManagerState::RuntimeDetection:
|
|
ProcessManagerState_DetectRuntimes();
|
|
break;
|
|
case VRManagerState::Enumeration:
|
|
ProcessManagerState_Enumeration();
|
|
break;
|
|
case VRManagerState::Active:
|
|
ProcessManagerState_Active();
|
|
break;
|
|
case VRManagerState::Stopping:
|
|
ProcessManagerState_Stopping();
|
|
break;
|
|
}
|
|
CheckForInactiveTimeout();
|
|
CheckForShutdown();
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Disabled() {
|
|
MOZ_ASSERT(mState == VRManagerState::Disabled);
|
|
|
|
if (!StaticPrefs::dom_vr_enabled() && !StaticPrefs::dom_vr_webxr_enabled()) {
|
|
return;
|
|
}
|
|
|
|
if (mRuntimeDetectionRequested || mEnumerationRequested ||
|
|
mVRDisplaysRequested) {
|
|
StartTasks();
|
|
mState = VRManagerState::Idle;
|
|
}
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Stopping() {
|
|
MOZ_ASSERT(mState == VRManagerState::Stopping);
|
|
PullState();
|
|
/**
|
|
* In the case of Desktop, the VRService shuts itself down.
|
|
* Before it's finished stopping, it sets a flag in the ShMem
|
|
* to let VRManager know that it's done. VRManager watches for
|
|
* this flag and transitions out of the VRManagerState::Stopping
|
|
* state to VRManagerState::Idle.
|
|
*/
|
|
#if defined(MOZ_WIDGET_ANDROID)
|
|
// On Android, the VR service never actually shuts
|
|
// down or requests VRManager to stop.
|
|
Shutdown();
|
|
#endif // defined(MOZ_WIDGET_ANDROID)
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Idle_StartEnumeration() {
|
|
MOZ_ASSERT(mState == VRManagerState::Idle);
|
|
|
|
if (!mEarliestRestartTime.IsNull() &&
|
|
mEarliestRestartTime > TimeStamp::Now()) {
|
|
// When the VR Service shuts down it informs us of how long we
|
|
// must wait until we can re-start it.
|
|
// We must wait until mEarliestRestartTime before attempting
|
|
// to enumerate again.
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Throttle the rate of enumeration to the interval set in
|
|
* VRDisplayEnumerateInterval
|
|
*/
|
|
if (!mLastDisplayEnumerationTime.IsNull()) {
|
|
TimeDuration duration = TimeStamp::Now() - mLastDisplayEnumerationTime;
|
|
if (duration.ToMilliseconds() <
|
|
StaticPrefs::dom_vr_display_enumerate_interval()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If we get this far, don't try again until
|
|
* the VRDisplayEnumerateInterval elapses
|
|
*/
|
|
mLastDisplayEnumerationTime = TimeStamp::Now();
|
|
|
|
OpenShmem();
|
|
|
|
mEnumerationRequested = false;
|
|
// We must block until enumeration has completed in order
|
|
// to signal that the WebVR promise should be resolved at the
|
|
// right time.
|
|
#if defined(MOZ_WIDGET_ANDROID)
|
|
// In Android, we need to make sure calling
|
|
// GeckoVRManager::SetExternalContext() from an external VR service
|
|
// before doing enumeration.
|
|
if (!mShmem->GetExternalShmem()) {
|
|
mShmem->CreateShMemForAndroid();
|
|
}
|
|
if (mShmem->GetExternalShmem()) {
|
|
mState = VRManagerState::Enumeration;
|
|
} else {
|
|
// Not connected to shmem, so no devices to enumerate.
|
|
mDisplayInfo.Clear();
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
#else
|
|
|
|
PushState();
|
|
|
|
/**
|
|
* We must start the VR Service thread
|
|
* and VR Process before enumeration.
|
|
* We don't want to start this until we will
|
|
* actualy enumerate, to avoid continuously
|
|
* re-launching the thread/process when
|
|
* no hardware is found or a VR software update
|
|
* is in progress
|
|
*/
|
|
mServiceHost->StartService();
|
|
mState = VRManagerState::Enumeration;
|
|
#endif // MOZ_WIDGET_ANDROID
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Idle_StartRuntimeDetection() {
|
|
MOZ_ASSERT(mState == VRManagerState::Idle);
|
|
|
|
OpenShmem();
|
|
mBrowserState.detectRuntimesOnly = true;
|
|
mRuntimeDetectionRequested = false;
|
|
|
|
// We must block until enumeration has completed in order
|
|
// to signal that the WebVR promise should be resolved at the
|
|
// right time.
|
|
#if defined(MOZ_WIDGET_ANDROID)
|
|
// In Android, we need to make sure calling
|
|
// GeckoVRManager::SetExternalContext() from an external VR service
|
|
// before doing enumeration.
|
|
if (!mShmem->GetExternalShmem()) {
|
|
mShmem->CreateShMemForAndroid();
|
|
}
|
|
if (mShmem->GetExternalShmem()) {
|
|
mState = VRManagerState::RuntimeDetection;
|
|
} else {
|
|
// Not connected to shmem, so no runtimes to detect.
|
|
mRuntimeSupportFlags = VRDisplayCapabilityFlags::Cap_None;
|
|
mRuntimeDetectionCompleted = true;
|
|
DispatchRuntimeCapabilitiesUpdate();
|
|
}
|
|
#else
|
|
|
|
PushState();
|
|
|
|
/**
|
|
* We must start the VR Service thread
|
|
* and VR Process before enumeration.
|
|
* We don't want to start this until we will
|
|
* actualy enumerate, to avoid continuously
|
|
* re-launching the thread/process when
|
|
* no hardware is found or a VR software update
|
|
* is in progress
|
|
*/
|
|
mServiceHost->StartService();
|
|
mState = VRManagerState::RuntimeDetection;
|
|
#endif // MOZ_WIDGET_ANDROID
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Idle() {
|
|
MOZ_ASSERT(mState == VRManagerState::Idle);
|
|
|
|
if (!mRuntimeDetectionCompleted) {
|
|
// Check if we should start detecting runtimes
|
|
// We must alwasy detect runtimes before doing anything
|
|
// else with the VR process.
|
|
// This will happen only once per browser startup.
|
|
if (mRuntimeDetectionRequested || mEnumerationRequested) {
|
|
ProcessManagerState_Idle_StartRuntimeDetection();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if we should start activating enumerating XR hardware
|
|
if (mRuntimeDetectionCompleted &&
|
|
(mVRDisplaysRequested || mEnumerationRequested)) {
|
|
ProcessManagerState_Idle_StartEnumeration();
|
|
}
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_DetectRuntimes() {
|
|
MOZ_ASSERT(mState == VRManagerState::RuntimeDetection);
|
|
MOZ_ASSERT(mShmem != nullptr);
|
|
|
|
PullState();
|
|
if (mEnumerationCompleted) {
|
|
/**
|
|
* When mBrowserState.detectRuntimesOnly is set, the
|
|
* VRService and VR process will shut themselves down
|
|
* automatically after detecting runtimes.
|
|
* mEnumerationCompleted is also used in this case,
|
|
* but to mean "enumeration of runtimes" not
|
|
* "enumeration of VR devices".
|
|
*
|
|
* We set mState to `VRManagerState::Stopping`
|
|
* to make sure that we don't try to do anything
|
|
* else with the active VRService until it has stopped.
|
|
* We must start another one when an XR session will be
|
|
* requested.
|
|
*
|
|
* This logic is optimized for the WebXR design, but still
|
|
* works for WebVR so it can continue to function until
|
|
* deprecated and removed.
|
|
*/
|
|
mState = VRManagerState::Stopping;
|
|
mRuntimeSupportFlags = mDisplayInfo.mDisplayState.capabilityFlags &
|
|
(VRDisplayCapabilityFlags::Cap_ImmersiveVR |
|
|
VRDisplayCapabilityFlags::Cap_ImmersiveAR |
|
|
VRDisplayCapabilityFlags::Cap_Inline);
|
|
mRuntimeDetectionCompleted = true;
|
|
DispatchRuntimeCapabilitiesUpdate();
|
|
}
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Enumeration() {
|
|
MOZ_ASSERT(mState == VRManagerState::Enumeration);
|
|
MOZ_ASSERT(mShmem != nullptr);
|
|
|
|
PullState();
|
|
if (mEnumerationCompleted) {
|
|
if (mDisplayInfo.mDisplayState.isConnected) {
|
|
mDisplayInfo.mDisplayID = VRManager::AllocateDisplayID();
|
|
mState = VRManagerState::Active;
|
|
} else {
|
|
mDisplayInfo.Clear();
|
|
mState = VRManagerState::Stopping;
|
|
}
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
}
|
|
|
|
void VRManager::ProcessManagerState_Active() {
|
|
MOZ_ASSERT(mState == VRManagerState::Active);
|
|
|
|
if (mDisplayInfo != mLastUpdateDisplayInfo) {
|
|
// While the display is active, send continuous updates
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
}
|
|
|
|
void VRManager::DispatchVRDisplayInfoUpdate() {
|
|
for (VRManagerParent* vmp : mVRManagerParents) {
|
|
Unused << vmp->SendUpdateDisplayInfo(mDisplayInfo);
|
|
}
|
|
mLastUpdateDisplayInfo = mDisplayInfo;
|
|
}
|
|
|
|
void VRManager::DispatchRuntimeCapabilitiesUpdate() {
|
|
VRDisplayCapabilityFlags flags = mRuntimeSupportFlags;
|
|
if (StaticPrefs::dom_vr_always_support_vr()) {
|
|
flags |= VRDisplayCapabilityFlags::Cap_ImmersiveVR;
|
|
}
|
|
|
|
if (StaticPrefs::dom_vr_always_support_ar()) {
|
|
flags |= VRDisplayCapabilityFlags::Cap_ImmersiveAR;
|
|
}
|
|
|
|
for (VRManagerParent* vmp : mVRManagerParents) {
|
|
Unused << vmp->SendUpdateRuntimeCapabilities(flags);
|
|
}
|
|
}
|
|
|
|
void VRManager::StopAllHaptics() {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
|
|
ClearHapticSlot(i);
|
|
}
|
|
PushState();
|
|
}
|
|
|
|
void VRManager::VibrateHaptic(GamepadHandle aGamepadHandle,
|
|
uint32_t aHapticIndex, double aIntensity,
|
|
double aDuration,
|
|
const VRManagerPromise& aPromise)
|
|
|
|
{
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
// VRDisplayClient::FireGamepadEvents() assigns a controller ID with
|
|
// ranges based on displayID. We must translate this to the indexes
|
|
// understood by VRDisplayExternal.
|
|
uint32_t controllerBaseIndex =
|
|
kVRControllerMaxCount * mDisplayInfo.mDisplayID;
|
|
uint32_t controllerIndex = aGamepadHandle.GetValue() - controllerBaseIndex;
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
size_t bestSlotIndex = 0;
|
|
// Default to an empty slot, or the slot holding the oldest haptic pulse
|
|
for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
|
|
const VRHapticState& state = mBrowserState.hapticState[i];
|
|
if (state.inputFrameID == 0) {
|
|
// Unused slot, use it
|
|
bestSlotIndex = i;
|
|
break;
|
|
}
|
|
if (mHapticPulseRemaining[i] < mHapticPulseRemaining[bestSlotIndex]) {
|
|
// If no empty slots are available, fall back to overriding
|
|
// the pulse which is ending soonest.
|
|
bestSlotIndex = i;
|
|
}
|
|
}
|
|
// Override the last pulse on the same actuator if present.
|
|
for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
|
|
const VRHapticState& state = mBrowserState.hapticState[i];
|
|
if (state.inputFrameID == 0) {
|
|
// This is an empty slot -- no match
|
|
continue;
|
|
}
|
|
if (state.controllerIndex == controllerIndex &&
|
|
state.hapticIndex == aHapticIndex) {
|
|
// Found pulse on same actuator -- let's override it.
|
|
bestSlotIndex = i;
|
|
}
|
|
}
|
|
ClearHapticSlot(bestSlotIndex);
|
|
|
|
// Populate the selected slot with new haptic state
|
|
size_t bufferIndex = mDisplayInfo.mFrameId % kVRMaxLatencyFrames;
|
|
VRHapticState& bestSlot = mBrowserState.hapticState[bestSlotIndex];
|
|
bestSlot.inputFrameID =
|
|
mDisplayInfo.mLastSensorState[bufferIndex].inputFrameID;
|
|
bestSlot.controllerIndex = controllerIndex;
|
|
bestSlot.hapticIndex = aHapticIndex;
|
|
bestSlot.pulseStart = (float)(now - mLastFrameStart[bufferIndex]).ToSeconds();
|
|
bestSlot.pulseDuration =
|
|
(float)aDuration * 0.001f; // Convert from ms to seconds
|
|
bestSlot.pulseIntensity = (float)aIntensity;
|
|
|
|
mHapticPulseRemaining[bestSlotIndex] = aDuration;
|
|
MOZ_ASSERT(bestSlotIndex <= mHapticPromises.Length());
|
|
if (bestSlotIndex == mHapticPromises.Length()) {
|
|
mHapticPromises.AppendElement(
|
|
UniquePtr<VRManagerPromise>(new VRManagerPromise(aPromise)));
|
|
} else {
|
|
mHapticPromises[bestSlotIndex] =
|
|
UniquePtr<VRManagerPromise>(new VRManagerPromise(aPromise));
|
|
}
|
|
PushState();
|
|
}
|
|
|
|
void VRManager::StopVibrateHaptic(GamepadHandle aGamepadHandle) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
// VRDisplayClient::FireGamepadEvents() assigns a controller ID with
|
|
// ranges based on displayID. We must translate this to the indexes
|
|
// understood by VRDisplayExternal.
|
|
uint32_t controllerBaseIndex =
|
|
kVRControllerMaxCount * mDisplayInfo.mDisplayID;
|
|
uint32_t controllerIndex = aGamepadHandle.GetValue() - controllerBaseIndex;
|
|
|
|
for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
|
|
VRHapticState& state = mBrowserState.hapticState[i];
|
|
if (state.controllerIndex == controllerIndex) {
|
|
memset(&state, 0, sizeof(VRHapticState));
|
|
}
|
|
}
|
|
PushState();
|
|
}
|
|
|
|
void VRManager::NotifyVibrateHapticCompleted(const VRManagerPromise& aPromise) {
|
|
aPromise.mParent->SendReplyGamepadVibrateHaptic(aPromise.mPromiseID);
|
|
}
|
|
|
|
void VRManager::StartVRNavigation(const uint32_t& aDisplayID) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
/**
|
|
* We only support a single VRSession with a single VR display at a
|
|
* time; however, due to the asynchronous nature of the API, it's possible
|
|
* that the previously used VR display was a different one than the one now
|
|
* allocated. We catch these cases to avoid automatically activating the new
|
|
* VR displays. This situation is expected to be very rare and possibly never
|
|
* seen. Perhaps further simplification could be made in the content process
|
|
* code which passes around displayID's that may no longer be needed.
|
|
**/
|
|
if (mDisplayInfo.GetDisplayID() != aDisplayID) {
|
|
return;
|
|
}
|
|
mBrowserState.navigationTransitionActive = true;
|
|
mVRNavigationTransitionEnd = TimeStamp();
|
|
PushState();
|
|
}
|
|
|
|
void VRManager::StopVRNavigation(const uint32_t& aDisplayID,
|
|
const TimeDuration& aTimeout) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
if (mDisplayInfo.GetDisplayID() != aDisplayID) {
|
|
return;
|
|
}
|
|
if (aTimeout.ToMilliseconds() <= 0) {
|
|
mBrowserState.navigationTransitionActive = false;
|
|
mVRNavigationTransitionEnd = TimeStamp();
|
|
PushState();
|
|
}
|
|
mVRNavigationTransitionEnd = TimeStamp::Now() + aTimeout;
|
|
}
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
|
|
bool VRManager::RunPuppet(const nsTArray<uint64_t>& aBuffer,
|
|
VRManagerParent* aManagerParent) {
|
|
if (!StaticPrefs::dom_vr_puppet_enabled()) {
|
|
// Sanity check to ensure that a compromised content process
|
|
// can't use this to escalate permissions.
|
|
return false;
|
|
}
|
|
if (mManagerParentRunningPuppet != nullptr) {
|
|
// Only one parent may run a puppet at a time
|
|
return false;
|
|
}
|
|
mManagerParentRunningPuppet = aManagerParent;
|
|
mServiceHost->PuppetSubmit(aBuffer);
|
|
return true;
|
|
}
|
|
|
|
void VRManager::ResetPuppet(VRManagerParent* aManagerParent) {
|
|
if (!StaticPrefs::dom_vr_puppet_enabled()) {
|
|
return;
|
|
}
|
|
|
|
mManagerParentsWaitingForPuppetReset.Insert(aManagerParent);
|
|
if (mManagerParentRunningPuppet != nullptr) {
|
|
Unused << mManagerParentRunningPuppet
|
|
->SendNotifyPuppetCommandBufferCompleted(false);
|
|
mManagerParentRunningPuppet = nullptr;
|
|
}
|
|
mServiceHost->PuppetReset();
|
|
// In the event that we are shut down, the task timer won't be running
|
|
// to trigger CheckForPuppetCompletion.
|
|
// In this case, CheckForPuppetCompletion() would immediately resolve
|
|
// the promises for mManagerParentsWaitingForPuppetReset.
|
|
// We can simply call it once here to handle that case.
|
|
CheckForPuppetCompletion();
|
|
}
|
|
|
|
#endif // !defined(MOZ_WIDGET_ANDROID)
|
|
|
|
void VRManager::PullState(
|
|
const std::function<bool()>& aWaitCondition /* = nullptr */) {
|
|
if (mShmem != nullptr) {
|
|
mShmem->PullSystemState(mDisplayInfo.mDisplayState, mLastSensorState,
|
|
mDisplayInfo.mControllerState,
|
|
mEnumerationCompleted, aWaitCondition);
|
|
}
|
|
}
|
|
|
|
void VRManager::PushState(bool aNotifyCond) {
|
|
if (mShmem != nullptr) {
|
|
mShmem->PushBrowserState(mBrowserState, aNotifyCond);
|
|
}
|
|
}
|
|
|
|
void VRManager::Destroy() {
|
|
if (mState == VRManagerState::Disabled) {
|
|
return;
|
|
}
|
|
Shutdown();
|
|
StopTasks();
|
|
mState = VRManagerState::Disabled;
|
|
}
|
|
|
|
void VRManager::Shutdown() {
|
|
if (mState == VRManagerState::Disabled || mState == VRManagerState::Idle) {
|
|
return;
|
|
}
|
|
|
|
if (mDisplayInfo.mDisplayState.shutdown) {
|
|
// Shutdown was requested by VR Service, so we must throttle
|
|
// as requested by the VR Service
|
|
TimeStamp now = TimeStamp::Now();
|
|
mEarliestRestartTime =
|
|
now + TimeDuration::FromMilliseconds(
|
|
(double)mDisplayInfo.mDisplayState.minRestartInterval);
|
|
}
|
|
|
|
StopAllHaptics();
|
|
StopPresentation();
|
|
CancelCurrentSubmitTask();
|
|
ShutdownSubmitThread();
|
|
|
|
mDisplayInfo.Clear();
|
|
mEnumerationCompleted = false;
|
|
|
|
if (mState == VRManagerState::RuntimeDetection) {
|
|
/**
|
|
* We have failed to detect runtimes before shutting down.
|
|
* Ensure that promises are resolved
|
|
*
|
|
* This call to DispatchRuntimeCapabilitiesUpdate will only
|
|
* happen when we have failed to detect runtimes. In that case,
|
|
* mRuntimeSupportFlags will be 0 and send the correct message
|
|
* to the content process.
|
|
*
|
|
* When we are successful, we store the result in mRuntimeSupportFlags
|
|
* and never try again unless the browser is restarted. mRuntimeSupportFlags
|
|
* is never reset back to 0 in that case but we will never re-enter the
|
|
* VRManagerState::RuntimeDetection state and hit this code path again.
|
|
*/
|
|
DispatchRuntimeCapabilitiesUpdate();
|
|
}
|
|
|
|
if (mState == VRManagerState::Enumeration) {
|
|
// We have failed to enumerate VR devices before shutting down.
|
|
// Ensure that promises are resolved
|
|
DispatchVRDisplayInfoUpdate();
|
|
}
|
|
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
mServiceHost->StopService();
|
|
#endif
|
|
mState = VRManagerState::Idle;
|
|
|
|
// We will close Shmem in the DTOR to avoid
|
|
// mSubmitThread is still running but its shmem
|
|
// has been released.
|
|
}
|
|
|
|
void VRManager::ShutdownVRManagerParents() {
|
|
// Close removes the CanvasParent from the set so take a copy first.
|
|
const auto parents = ToTArray<nsTArray<VRManagerParent*>>(mVRManagerParents);
|
|
for (RefPtr<VRManagerParent> vrManagerParent : parents) {
|
|
vrManagerParent->Close();
|
|
}
|
|
|
|
MOZ_DIAGNOSTIC_ASSERT(mVRManagerParents.IsEmpty(),
|
|
"Closing should have cleared all entries.");
|
|
}
|
|
|
|
void VRManager::CheckWatchDog() {
|
|
/**
|
|
* We will trigger a new frame immediately after a successful frame
|
|
* texture submission. If content fails to call VRDisplay.submitFrame
|
|
* after dom.vr.display.rafMaxDuration milliseconds has elapsed since the
|
|
* last VRDisplay.requestAnimationFrame, we act as a "watchdog" and
|
|
* kick-off a new VRDisplay.requestAnimationFrame to avoid a render loop
|
|
* stall and to give content a chance to recover.
|
|
*
|
|
* If the lower level VR platform API's are rejecting submitted frames,
|
|
* such as when the Oculus "Health and Safety Warning" is displayed,
|
|
* we will not kick off the next frame immediately after
|
|
* VRDisplay.submitFrame as it would result in an unthrottled render loop
|
|
* that would free run at potentially extreme frame rates. To ensure that
|
|
* content has a chance to resume its presentation when the frames are
|
|
* accepted once again, we rely on this "watchdog" to act as a VR refresh
|
|
* driver cycling at a rate defined by dom.vr.display.rafMaxDuration.
|
|
*
|
|
* This number must be larger than the slowest expected frame time during
|
|
* normal VR presentation, but small enough not to break content that
|
|
* makes assumptions of reasonably minimal VSync rate.
|
|
*
|
|
* The slowest expected refresh rate for a VR display currently is an
|
|
* Oculus CV1 when ASW (Asynchronous Space Warp) is enabled, at 45hz.
|
|
* A dom.vr.display.rafMaxDuration value of 50 milliseconds results in a
|
|
* 20hz rate, which avoids inadvertent triggering of the watchdog during
|
|
* Oculus ASW even if every second frame is dropped.
|
|
*/
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
bool bShouldStartFrame = false;
|
|
|
|
// If content fails to call VRDisplay.submitFrame, we must eventually
|
|
// time-out and trigger a new frame.
|
|
TimeStamp lastFrameStart =
|
|
mLastFrameStart[mDisplayInfo.mFrameId % kVRMaxLatencyFrames];
|
|
if (lastFrameStart.IsNull()) {
|
|
bShouldStartFrame = true;
|
|
} else {
|
|
TimeDuration duration = TimeStamp::Now() - lastFrameStart;
|
|
if (duration.ToMilliseconds() >
|
|
StaticPrefs::dom_vr_display_rafMaxDuration()) {
|
|
bShouldStartFrame = true;
|
|
}
|
|
}
|
|
|
|
if (bShouldStartFrame) {
|
|
StartFrame();
|
|
}
|
|
}
|
|
|
|
void VRManager::ExpireNavigationTransition() {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
if (!mVRNavigationTransitionEnd.IsNull() &&
|
|
TimeStamp::Now() > mVRNavigationTransitionEnd) {
|
|
mBrowserState.navigationTransitionActive = false;
|
|
}
|
|
}
|
|
|
|
void VRManager::UpdateHaptics(double aDeltaTime) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
bool bNeedPush = false;
|
|
// Check for any haptic pulses that have ended and clear them
|
|
for (size_t i = 0; i < mozilla::ArrayLength(mBrowserState.hapticState); i++) {
|
|
const VRHapticState& state = mBrowserState.hapticState[i];
|
|
if (state.inputFrameID == 0) {
|
|
// Nothing in this slot
|
|
continue;
|
|
}
|
|
mHapticPulseRemaining[i] -= aDeltaTime;
|
|
if (mHapticPulseRemaining[i] <= 0.0f) {
|
|
// The pulse has finished
|
|
ClearHapticSlot(i);
|
|
bNeedPush = true;
|
|
}
|
|
}
|
|
if (bNeedPush) {
|
|
PushState();
|
|
}
|
|
}
|
|
|
|
void VRManager::ClearHapticSlot(size_t aSlot) {
|
|
MOZ_ASSERT(aSlot < mozilla::ArrayLength(mBrowserState.hapticState));
|
|
memset(&mBrowserState.hapticState[aSlot], 0, sizeof(VRHapticState));
|
|
mHapticPulseRemaining[aSlot] = 0.0f;
|
|
if (aSlot < mHapticPromises.Length() && mHapticPromises[aSlot]) {
|
|
NotifyVibrateHapticCompleted(*(mHapticPromises[aSlot]));
|
|
mHapticPromises[aSlot] = nullptr;
|
|
}
|
|
}
|
|
|
|
void VRManager::ShutdownSubmitThread() {
|
|
if (mSubmitThread) {
|
|
mSubmitThread->Shutdown();
|
|
mSubmitThread = nullptr;
|
|
}
|
|
}
|
|
|
|
void VRManager::StartPresentation() {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
if (mBrowserState.presentationActive) {
|
|
return;
|
|
}
|
|
mTelemetry.Clear();
|
|
mTelemetry.mPresentationStart = TimeStamp::Now();
|
|
|
|
// Indicate that we are ready to start immersive mode
|
|
mBrowserState.presentationActive = true;
|
|
mBrowserState.layerState[0].type = VRLayerType::LayerType_Stereo_Immersive;
|
|
PushState();
|
|
|
|
mDisplayInfo.mDisplayState.lastSubmittedFrameId = 0;
|
|
if (mDisplayInfo.mDisplayState.reportsDroppedFrames) {
|
|
mTelemetry.mLastDroppedFrameCount =
|
|
mDisplayInfo.mDisplayState.droppedFrameCount;
|
|
}
|
|
|
|
mLastSubmittedFrameId = 0;
|
|
mLastStartedFrame = 0;
|
|
}
|
|
|
|
void VRManager::StopPresentation() {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
if (!mBrowserState.presentationActive) {
|
|
return;
|
|
}
|
|
|
|
// Indicate that we have stopped immersive mode
|
|
mBrowserState.presentationActive = false;
|
|
memset(mBrowserState.layerState, 0,
|
|
sizeof(VRLayerState) * mozilla::ArrayLength(mBrowserState.layerState));
|
|
|
|
PushState(true);
|
|
|
|
Telemetry::HistogramID timeSpentID = Telemetry::HistogramCount;
|
|
Telemetry::HistogramID droppedFramesID = Telemetry::HistogramCount;
|
|
int viewIn = 0;
|
|
|
|
if (mDisplayInfo.mDisplayState.eightCC ==
|
|
GFX_VR_EIGHTCC('O', 'c', 'u', 'l', 'u', 's', ' ', 'D')) {
|
|
// Oculus Desktop API
|
|
timeSpentID = Telemetry::WEBVR_TIME_SPENT_VIEWING_IN_OCULUS;
|
|
droppedFramesID = Telemetry::WEBVR_DROPPED_FRAMES_IN_OCULUS;
|
|
viewIn = 1;
|
|
} else if (mDisplayInfo.mDisplayState.eightCC ==
|
|
GFX_VR_EIGHTCC('O', 'p', 'e', 'n', 'V', 'R', ' ', ' ')) {
|
|
// OpenVR API
|
|
timeSpentID = Telemetry::WEBVR_TIME_SPENT_VIEWING_IN_OPENVR;
|
|
droppedFramesID = Telemetry::WEBVR_DROPPED_FRAMES_IN_OPENVR;
|
|
viewIn = 2;
|
|
}
|
|
|
|
if (viewIn) {
|
|
const TimeDuration duration =
|
|
TimeStamp::Now() - mTelemetry.mPresentationStart;
|
|
Telemetry::Accumulate(Telemetry::WEBVR_USERS_VIEW_IN, viewIn);
|
|
Telemetry::Accumulate(timeSpentID, duration.ToMilliseconds());
|
|
const uint32_t droppedFramesPerSec =
|
|
(uint32_t)((double)(mDisplayInfo.mDisplayState.droppedFrameCount -
|
|
mTelemetry.mLastDroppedFrameCount) /
|
|
duration.ToSeconds());
|
|
Telemetry::Accumulate(droppedFramesID, droppedFramesPerSec);
|
|
}
|
|
}
|
|
|
|
bool VRManager::IsPresenting() {
|
|
if (mShmem) {
|
|
return mDisplayInfo.mPresentingGroups != 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void VRManager::SetGroupMask(uint32_t aGroupMask) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
mDisplayInfo.mGroupMask = aGroupMask;
|
|
}
|
|
|
|
void VRManager::SubmitFrame(VRLayerParent* aLayer,
|
|
const layers::SurfaceDescriptor& aTexture,
|
|
uint64_t aFrameId, const gfx::Rect& aLeftEyeRect,
|
|
const gfx::Rect& aRightEyeRect) {
|
|
if (mState != VRManagerState::Active) {
|
|
return;
|
|
}
|
|
MonitorAutoLock lock(mCurrentSubmitTaskMonitor);
|
|
if ((mDisplayInfo.mGroupMask & aLayer->GetGroup()) == 0) {
|
|
// Suppress layers hidden by the group mask
|
|
return;
|
|
}
|
|
|
|
// Ensure that we only accept the first SubmitFrame call per RAF cycle.
|
|
if (!mFrameStarted || aFrameId != mDisplayInfo.mFrameId) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Do not queue more submit frames until the last submitted frame is
|
|
* already processed and the new WebGL texture is ready.
|
|
*/
|
|
if (mLastSubmittedFrameId > 0 &&
|
|
mLastSubmittedFrameId !=
|
|
mDisplayInfo.mDisplayState.lastSubmittedFrameId) {
|
|
mLastStartedFrame = 0;
|
|
return;
|
|
}
|
|
|
|
mLastSubmittedFrameId = aFrameId;
|
|
|
|
mFrameStarted = false;
|
|
|
|
RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod<
|
|
StoreCopyPassByConstLRef<layers::SurfaceDescriptor>, uint64_t,
|
|
StoreCopyPassByConstLRef<gfx::Rect>, StoreCopyPassByConstLRef<gfx::Rect>>(
|
|
"gfx::VRManager::SubmitFrameInternal", this,
|
|
&VRManager::SubmitFrameInternal, aTexture, aFrameId, aLeftEyeRect,
|
|
aRightEyeRect);
|
|
|
|
if (!mCurrentSubmitTask) {
|
|
mCurrentSubmitTask = task;
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
if (!mSubmitThread) {
|
|
mSubmitThread = new VRThread("VR_SubmitFrame"_ns);
|
|
}
|
|
mSubmitThread->Start();
|
|
mSubmitThread->PostTask(task.forget());
|
|
#else
|
|
CompositorThread()->Dispatch(task.forget());
|
|
#endif // defined(MOZ_WIDGET_ANDROID)
|
|
}
|
|
}
|
|
|
|
bool VRManager::SubmitFrame(const layers::SurfaceDescriptor& aTexture,
|
|
uint64_t aFrameId, const gfx::Rect& aLeftEyeRect,
|
|
const gfx::Rect& aRightEyeRect) {
|
|
if (mState != VRManagerState::Active) {
|
|
return false;
|
|
}
|
|
#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID)
|
|
MOZ_ASSERT(mBrowserState.layerState[0].type ==
|
|
VRLayerType::LayerType_Stereo_Immersive);
|
|
VRLayer_Stereo_Immersive& layer =
|
|
mBrowserState.layerState[0].layer_stereo_immersive;
|
|
|
|
switch (aTexture.type()) {
|
|
# if defined(XP_WIN)
|
|
case SurfaceDescriptor::TSurfaceDescriptorD3D10: {
|
|
const SurfaceDescriptorD3D10& surf =
|
|
aTexture.get_SurfaceDescriptorD3D10();
|
|
layer.textureType =
|
|
VRLayerTextureType::LayerTextureType_D3D10SurfaceDescriptor;
|
|
layer.textureHandle = (void*)surf.handle();
|
|
layer.textureSize.width = surf.size().width;
|
|
layer.textureSize.height = surf.size().height;
|
|
} break;
|
|
# elif defined(XP_MACOSX)
|
|
case SurfaceDescriptor::TSurfaceDescriptorMacIOSurface: {
|
|
// MacIOSurface ptr can't be fetched or used at different threads.
|
|
// Both of fetching and using this MacIOSurface are at the VRService
|
|
// thread.
|
|
const auto& desc = aTexture.get_SurfaceDescriptorMacIOSurface();
|
|
layer.textureType = VRLayerTextureType::LayerTextureType_MacIOSurface;
|
|
layer.textureHandle = desc.surfaceId();
|
|
RefPtr<MacIOSurface> surf = MacIOSurface::LookupSurface(
|
|
desc.surfaceId(), !desc.isOpaque(), desc.yUVColorSpace());
|
|
if (surf) {
|
|
layer.textureSize.width = surf->GetDevicePixelWidth();
|
|
layer.textureSize.height = surf->GetDevicePixelHeight();
|
|
}
|
|
} break;
|
|
# elif defined(MOZ_WIDGET_ANDROID)
|
|
case SurfaceDescriptor::TSurfaceTextureDescriptor: {
|
|
const SurfaceTextureDescriptor& desc =
|
|
aTexture.get_SurfaceTextureDescriptor();
|
|
java::GeckoSurfaceTexture::LocalRef surfaceTexture =
|
|
java::GeckoSurfaceTexture::Lookup(desc.handle());
|
|
if (!surfaceTexture) {
|
|
NS_WARNING("VRManager::SubmitFrame failed to get a SurfaceTexture");
|
|
return false;
|
|
}
|
|
layer.textureType =
|
|
VRLayerTextureType::LayerTextureType_GeckoSurfaceTexture;
|
|
layer.textureHandle = desc.handle();
|
|
layer.textureSize.width = desc.size().width;
|
|
layer.textureSize.height = desc.size().height;
|
|
} break;
|
|
# endif
|
|
default: {
|
|
MOZ_ASSERT(false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
layer.frameId = aFrameId;
|
|
layer.inputFrameId =
|
|
mDisplayInfo.mLastSensorState[mDisplayInfo.mFrameId % kVRMaxLatencyFrames]
|
|
.inputFrameID;
|
|
|
|
layer.leftEyeRect.x = aLeftEyeRect.x;
|
|
layer.leftEyeRect.y = aLeftEyeRect.y;
|
|
layer.leftEyeRect.width = aLeftEyeRect.width;
|
|
layer.leftEyeRect.height = aLeftEyeRect.height;
|
|
layer.rightEyeRect.x = aRightEyeRect.x;
|
|
layer.rightEyeRect.y = aRightEyeRect.y;
|
|
layer.rightEyeRect.width = aRightEyeRect.width;
|
|
layer.rightEyeRect.height = aRightEyeRect.height;
|
|
|
|
PushState(true);
|
|
|
|
PullState([&]() {
|
|
return (mDisplayInfo.mDisplayState.lastSubmittedFrameId >= aFrameId) ||
|
|
mDisplayInfo.mDisplayState.suppressFrames ||
|
|
!mDisplayInfo.mDisplayState.isConnected;
|
|
});
|
|
|
|
if (mDisplayInfo.mDisplayState.suppressFrames ||
|
|
!mDisplayInfo.mDisplayState.isConnected) {
|
|
// External implementation wants to supress frames, service has shut
|
|
// down or hardware has been disconnected.
|
|
return false;
|
|
}
|
|
|
|
return mDisplayInfo.mDisplayState.lastSubmittedFrameSuccessful;
|
|
#else
|
|
MOZ_ASSERT(false); // Not implmented for this platform
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void VRManager::SubmitFrameInternal(const layers::SurfaceDescriptor& aTexture,
|
|
uint64_t aFrameId,
|
|
const gfx::Rect& aLeftEyeRect,
|
|
const gfx::Rect& aRightEyeRect) {
|
|
#if !defined(MOZ_WIDGET_ANDROID)
|
|
MOZ_ASSERT(mSubmitThread->GetThread() == NS_GetCurrentThread());
|
|
#endif // !defined(MOZ_WIDGET_ANDROID)
|
|
AUTO_PROFILER_TRACING_MARKER("VR", "SubmitFrameAtVRDisplayExternal", OTHER);
|
|
|
|
{ // scope lock
|
|
MonitorAutoLock lock(mCurrentSubmitTaskMonitor);
|
|
|
|
if (!SubmitFrame(aTexture, aFrameId, aLeftEyeRect, aRightEyeRect)) {
|
|
mCurrentSubmitTask = nullptr;
|
|
return;
|
|
}
|
|
mCurrentSubmitTask = nullptr;
|
|
}
|
|
|
|
#if defined(XP_WIN) || defined(XP_MACOSX)
|
|
|
|
/**
|
|
* Trigger the next VSync immediately after we are successfully
|
|
* submitting frames. As SubmitFrame is responsible for throttling
|
|
* the render loop, if we don't successfully call it, we shouldn't trigger
|
|
* StartFrame immediately, as it will run unbounded.
|
|
* If StartFrame is not called here due to SubmitFrame failing, the
|
|
* fallback "watchdog" code in VRManager::NotifyVSync() will cause
|
|
* frames to continue at a lower refresh rate until frame submission
|
|
* succeeds again.
|
|
*/
|
|
CompositorThread()->Dispatch(NewRunnableMethod("gfx::VRManager::StartFrame",
|
|
this, &VRManager::StartFrame));
|
|
#elif defined(MOZ_WIDGET_ANDROID)
|
|
// We are already in the CompositorThreadHolder event loop on Android.
|
|
StartFrame();
|
|
#endif
|
|
}
|
|
|
|
void VRManager::CancelCurrentSubmitTask() {
|
|
MonitorAutoLock lock(mCurrentSubmitTaskMonitor);
|
|
if (mCurrentSubmitTask) {
|
|
mCurrentSubmitTask->Cancel();
|
|
mCurrentSubmitTask = nullptr;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// VRManager::nsIObserver
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMETHODIMP
|
|
VRManager::Observe(nsISupports* subject, const char* topic,
|
|
const char16_t* data) {
|
|
if (!StaticPrefs::dom_vr_enabled() && !StaticPrefs::dom_vr_webxr_enabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(topic, "application-background")) {
|
|
// StopTasks() is called later in the timer thread based on this flag to
|
|
// avoid threading issues.
|
|
mAppPaused = true;
|
|
} else if (!strcmp(topic, "application-foreground") && mAppPaused) {
|
|
mAppPaused = false;
|
|
// When the apps goes the foreground (e.g. Android) we should restart the
|
|
// tasks.
|
|
StartTasks();
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(VRManager, nsIObserver)
|
|
|
|
} // namespace mozilla::gfx
|