/* -*- 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
