diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/vr/VRManager.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/vr/VRManager.cpp')
-rw-r--r-- | gfx/vr/VRManager.cpp | 1545 |
1 files changed, 1545 insertions, 0 deletions
diff --git a/gfx/vr/VRManager.cpp b/gfx/vr/VRManager.cpp new file mode 100644 index 0000000000..2157439b92 --- /dev/null +++ b/gfx/vr/VRManager.cpp @@ -0,0 +1,1545 @@ +/* -*- 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 |