tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

VRService.cpp (14167B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "VRService.h"
      8 
      9 #include <cstring>  // for memcmp
     10 
     11 #include "../VRShMem.h"
     12 #include "../gfxVRMutex.h"
     13 #include "PuppetSession.h"
     14 #include "mozilla/BackgroundHangMonitor.h"
     15 #include "mozilla/StaticPrefs_dom.h"
     16 #include "nsThread.h"
     17 #include "nsXULAppAPI.h"
     18 
     19 #if defined(XP_WIN)
     20 #  include "OculusSession.h"
     21 #endif
     22 
     23 #if defined(XP_WIN) || defined(XP_MACOSX) || \
     24    (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
     25 #  include "OpenVRSession.h"
     26 #endif
     27 #if !defined(MOZ_WIDGET_ANDROID)
     28 #  include "OSVRSession.h"
     29 #endif
     30 
     31 using namespace mozilla;
     32 using namespace mozilla::gfx;
     33 
     34 namespace {
     35 
     36 int64_t FrameIDFromBrowserState(const mozilla::gfx::VRBrowserState& aState) {
     37  for (const auto& layer : aState.layerState) {
     38    if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
     39      return layer.layer_stereo_immersive.frameId;
     40    }
     41  }
     42  return 0;
     43 }
     44 
     45 bool IsImmersiveContentActive(const mozilla::gfx::VRBrowserState& aState) {
     46  for (const auto& layer : aState.layerState) {
     47    if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
     48      return true;
     49    }
     50  }
     51  return false;
     52 }
     53 
     54 }  // anonymous namespace
     55 
     56 /*static*/
     57 already_AddRefed<VRService> VRService::Create(
     58    volatile VRExternalShmem* aShmem) {
     59  RefPtr<VRService> service = new VRService(aShmem);
     60  return service.forget();
     61 }
     62 
     63 VRService::VRService(volatile VRExternalShmem* aShmem)
     64    : mSystemState{},
     65      mBrowserState{},
     66      mShutdownRequested(false),
     67      mLastHapticState{},
     68      mFrameStartTime{} {
     69  // When we have the VR process, we map the memory
     70  // of mAPIShmem from GPU process and pass it to the CTOR.
     71  // If we don't have the VR process, we will instantiate
     72  // mAPIShmem in VRService.
     73  mShmem = new VRShMem(aShmem, aShmem == nullptr /*aRequiresMutex*/);
     74 }
     75 
     76 VRService::~VRService() {
     77  // PSA: We must store the value of any staticPrefs preferences as this
     78  // destructor will be called after staticPrefs has been shut down.
     79  StopInternal(true /*aFromDtor*/);
     80 }
     81 
     82 void VRService::Refresh() {
     83  if (mShmem != nullptr && mShmem->IsDisplayStateShutdown()) {
     84    Stop();
     85  }
     86 }
     87 
     88 void VRService::Start() {
     89  if (!mServiceThread) {
     90    /**
     91     * We must ensure that any time the service is re-started, that
     92     * the VRSystemState is reset, including mSystemState.enumerationCompleted
     93     * This must happen before VRService::Start returns to the caller, in order
     94     * to prevent the WebVR/WebXR promises from being resolved before the
     95     * enumeration has been completed.
     96     */
     97    memset(&mSystemState, 0, sizeof(mSystemState));
     98    PushState(mSystemState);
     99    RefPtr<VRService> self = this;
    100    nsCOMPtr<nsIThread> thread;
    101    nsresult rv = NS_NewNamedThread(
    102        "VRService", getter_AddRefs(thread),
    103        NS_NewRunnableFunction("VRService::ServiceThreadStartup", [self]() {
    104          self->mBackgroundHangMonitor =
    105              MakeUnique<mozilla::BackgroundHangMonitor>(
    106                  "VRService",
    107                  /* Timeout values are powers-of-two to enable us get better
    108                     data. 128ms is chosen for transient hangs because 8Hz
    109                     should be the minimally acceptable goal for Compositor
    110                     responsiveness (normal goal is 60Hz). */
    111                  128,
    112                  /* 2048ms is chosen for permanent hangs because it's longer
    113                   * than most Compositor hangs seen in the wild, but is short
    114                   * enough to not miss getting native hang stacks. */
    115                  2048);
    116          static_cast<nsThread*>(NS_GetCurrentThread())
    117              ->SetUseHangMonitor(true);
    118        }));
    119 
    120    if (NS_FAILED(rv)) {
    121      return;
    122    }
    123    thread.swap(mServiceThread);
    124    // ServiceInitialize needs mServiceThread to be set in order to be able to
    125    // assert that it's running on the right thread as well as dispatching new
    126    // tasks. It can't be run within the NS_NewRunnableFunction initial event.
    127    MOZ_ALWAYS_SUCCEEDS(mServiceThread->Dispatch(
    128        NewRunnableMethod("gfx::VRService::ServiceInitialize", this,
    129                          &VRService::ServiceInitialize)));
    130  }
    131 }
    132 
    133 void VRService::Stop() { StopInternal(false /*aFromDtor*/); }
    134 
    135 void VRService::StopInternal(bool aFromDtor) {
    136  if (mServiceThread) {
    137    // We must disable the background hang monitor before we can shutdown this
    138    // thread. Dispatched a last task to do so. No task will be allowed to run
    139    // on the service thread after this one.
    140    mServiceThread->Dispatch(NS_NewRunnableFunction(
    141        "VRService::StopInternal", [self = RefPtr<VRService>(this), this] {
    142          static_cast<nsThread*>(NS_GetCurrentThread())
    143              ->SetUseHangMonitor(false);
    144          mBackgroundHangMonitor = nullptr;
    145        }));
    146    mShutdownRequested = true;
    147    mServiceThread->Shutdown();
    148    mServiceThread = nullptr;
    149  }
    150 
    151  if (mShmem != nullptr && (aFromDtor || !mShmem->IsSharedExternalShmem())) {
    152    // Only leave the VRShMem and clean up the pointer when the struct
    153    // was not passed in. Otherwise, VRService will no longer have a
    154    // way to access that struct if VRService starts again.
    155    mShmem->LeaveShMem();
    156    delete mShmem;
    157    mShmem = nullptr;
    158  }
    159 
    160  mSession = nullptr;
    161 }
    162 
    163 bool VRService::InitShmem() { return mShmem->JoinShMem(); }
    164 
    165 bool VRService::IsInServiceThread() {
    166  return mServiceThread && mServiceThread->IsOnCurrentThread();
    167 }
    168 
    169 void VRService::ServiceInitialize() {
    170  MOZ_ASSERT(IsInServiceThread());
    171 
    172  if (!InitShmem()) {
    173    return;
    174  }
    175 
    176  mShutdownRequested = false;
    177  // Get initial state from the browser
    178  PullState(mBrowserState);
    179 
    180  // Try to start a VRSession
    181  UniquePtr<VRSession> session;
    182 
    183  if (StaticPrefs::dom_vr_puppet_enabled()) {
    184    // When the VR Puppet is enabled, we don't want
    185    // to enumerate any real devices
    186    session = MakeUnique<PuppetSession>();
    187    if (!session->Initialize(mSystemState, mBrowserState.detectRuntimesOnly)) {
    188      session = nullptr;
    189    }
    190  } else {
    191    // We try Oculus first to ensure we use Oculus
    192    // devices trough the most native interface
    193    // when possible.
    194 #if defined(XP_WIN)
    195    // Try Oculus
    196    if (!session) {
    197      session = MakeUnique<OculusSession>();
    198      if (!session->Initialize(mSystemState,
    199                               mBrowserState.detectRuntimesOnly)) {
    200        session = nullptr;
    201      }
    202    }
    203 #endif
    204 
    205 #if defined(XP_WIN) || defined(XP_MACOSX) || \
    206    (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
    207    // Try OpenVR
    208    if (!session) {
    209      session = MakeUnique<OpenVRSession>();
    210      if (!session->Initialize(mSystemState,
    211                               mBrowserState.detectRuntimesOnly)) {
    212        session = nullptr;
    213      }
    214    }
    215 #endif
    216 #if !defined(MOZ_WIDGET_ANDROID)
    217    // Try OSVR
    218    if (!session) {
    219      session = MakeUnique<OSVRSession>();
    220      if (!session->Initialize(mSystemState,
    221                               mBrowserState.detectRuntimesOnly)) {
    222        session = nullptr;
    223      }
    224    }
    225 #endif
    226 
    227  }  // if (staticPrefs:VRPuppetEnabled())
    228 
    229  if (session) {
    230    mSession = std::move(session);
    231    // Setting enumerationCompleted to true indicates to the browser
    232    // that it should resolve any promises in the WebVR/WebXR API
    233    // waiting for hardware detection.
    234    mSystemState.enumerationCompleted = true;
    235    PushState(mSystemState);
    236 
    237    mServiceThread->Dispatch(
    238        NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
    239                          &VRService::ServiceWaitForImmersive));
    240  } else {
    241    // VR hardware was not detected.
    242    // We must inform the browser of the failure so it may try again
    243    // later and resolve WebVR promises.  A failure or shutdown is
    244    // indicated by enumerationCompleted being set to true, with all
    245    // other fields remaining zeroed out.
    246    VRDisplayCapabilityFlags capFlags =
    247        mSystemState.displayState.capabilityFlags;
    248    memset(&mSystemState, 0, sizeof(mSystemState));
    249    mSystemState.enumerationCompleted = true;
    250 
    251    if (mBrowserState.detectRuntimesOnly) {
    252      mSystemState.displayState.capabilityFlags = capFlags;
    253    } else {
    254      mSystemState.displayState.minRestartInterval =
    255          StaticPrefs::dom_vr_external_notdetected_timeout();
    256    }
    257    mSystemState.displayState.shutdown = true;
    258    PushState(mSystemState);
    259  }
    260 }
    261 
    262 void VRService::ServiceShutdown() {
    263  MOZ_ASSERT(IsInServiceThread());
    264 
    265  // Notify the browser that we have shut down.
    266  // This is indicated by enumerationCompleted being set
    267  // to true, with all other fields remaining zeroed out.
    268  memset(&mSystemState, 0, sizeof(mSystemState));
    269  mSystemState.enumerationCompleted = true;
    270  mSystemState.displayState.shutdown = true;
    271  if (mSession && mSession->ShouldQuit()) {
    272    mSystemState.displayState.minRestartInterval =
    273        StaticPrefs::dom_vr_external_quit_timeout();
    274  }
    275  PushState(mSystemState);
    276  mSession = nullptr;
    277 }
    278 
    279 void VRService::ServiceWaitForImmersive() {
    280  MOZ_ASSERT(IsInServiceThread());
    281  MOZ_ASSERT(mSession);
    282 
    283  mSession->ProcessEvents(mSystemState);
    284  PushState(mSystemState);
    285  PullState(mBrowserState);
    286 
    287  if (mSession->ShouldQuit() || mShutdownRequested) {
    288    // Shut down
    289    mServiceThread->Dispatch(NewRunnableMethod(
    290        "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
    291  } else if (IsImmersiveContentActive(mBrowserState)) {
    292    // Enter Immersive Mode
    293    mSession->StartPresentation();
    294    mSession->StartFrame(mSystemState);
    295    PushState(mSystemState);
    296 
    297    mServiceThread->Dispatch(
    298        NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
    299                          &VRService::ServiceImmersiveMode));
    300  } else {
    301    // Continue waiting for immersive mode
    302    mServiceThread->Dispatch(
    303        NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
    304                          &VRService::ServiceWaitForImmersive));
    305  }
    306 }
    307 
    308 void VRService::ServiceImmersiveMode() {
    309  MOZ_ASSERT(IsInServiceThread());
    310  MOZ_ASSERT(mSession);
    311 
    312  mSession->ProcessEvents(mSystemState);
    313  UpdateHaptics();
    314  PushState(mSystemState);
    315  PullState(mBrowserState);
    316 
    317  if (mSession->ShouldQuit() || mShutdownRequested) {
    318    // Shut down
    319    mServiceThread->Dispatch(NewRunnableMethod(
    320        "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
    321    return;
    322  }
    323 
    324  if (!IsImmersiveContentActive(mBrowserState)) {
    325    // Exit immersive mode
    326    mSession->StopAllHaptics();
    327    mSession->StopPresentation();
    328    mServiceThread->Dispatch(
    329        NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
    330                          &VRService::ServiceWaitForImmersive));
    331    return;
    332  }
    333 
    334  uint64_t newFrameId = FrameIDFromBrowserState(mBrowserState);
    335  if (newFrameId != mSystemState.displayState.lastSubmittedFrameId) {
    336    // A new immersive frame has been received.
    337    // Submit the textures to the VR system compositor.
    338    bool success = false;
    339    for (const auto& layer : mBrowserState.layerState) {
    340      if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
    341        // SubmitFrame may block in order to control the timing for
    342        // the next frame start
    343        success = mSession->SubmitFrame(layer.layer_stereo_immersive);
    344        break;
    345      }
    346    }
    347 
    348    // Changing mLastSubmittedFrameId triggers a new frame to start
    349    // rendering.  Changes to mLastSubmittedFrameId and the values
    350    // used for rendering, such as headset pose, must be pushed
    351    // atomically to the browser.
    352    mSystemState.displayState.lastSubmittedFrameId = newFrameId;
    353    mSystemState.displayState.lastSubmittedFrameSuccessful = success;
    354 
    355    // StartFrame may block to control the timing for the next frame start
    356    mSession->StartFrame(mSystemState);
    357    mSystemState.sensorState.inputFrameID++;
    358    size_t historyIndex =
    359        mSystemState.sensorState.inputFrameID % std::size(mFrameStartTime);
    360    mFrameStartTime[historyIndex] = TimeStamp::Now();
    361    PushState(mSystemState);
    362  }
    363 
    364  // Continue immersive mode
    365  mServiceThread->Dispatch(
    366      NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
    367                        &VRService::ServiceImmersiveMode));
    368 }
    369 
    370 void VRService::UpdateHaptics() {
    371  MOZ_ASSERT(IsInServiceThread());
    372  MOZ_ASSERT(mSession);
    373 
    374  for (size_t i = 0; i < std::size(mBrowserState.hapticState); i++) {
    375    VRHapticState& state = mBrowserState.hapticState[i];
    376    VRHapticState& lastState = mLastHapticState[i];
    377    // Note that VRHapticState is asserted to be a POD type, thus memcmp is safe
    378    if (memcmp(&state, &lastState, sizeof(VRHapticState)) == 0) {
    379      // No change since the last update
    380      continue;
    381    }
    382    if (state.inputFrameID == 0) {
    383      // The haptic feedback was stopped
    384      mSession->StopVibrateHaptic(state.controllerIndex);
    385    } else {
    386      TimeStamp now;
    387      if (now.IsNull()) {
    388        // TimeStamp::Now() is expensive, so we
    389        // must call it only when needed and save the
    390        // output for further loop iterations.
    391        now = TimeStamp::Now();
    392      }
    393      // This is a new haptic pulse, or we are overriding a prior one
    394      size_t historyIndex = state.inputFrameID % std::size(mFrameStartTime);
    395      float startOffset =
    396          (float)(now - mFrameStartTime[historyIndex]).ToSeconds();
    397 
    398      // state.pulseStart is guaranteed never to be in the future
    399      mSession->VibrateHaptic(
    400          state.controllerIndex, state.hapticIndex, state.pulseIntensity,
    401          state.pulseDuration + state.pulseStart - startOffset);
    402    }
    403    // Record the state for comparison in the next run
    404    memcpy(&lastState, &state, sizeof(VRHapticState));
    405  }
    406 }
    407 
    408 void VRService::PushState(const mozilla::gfx::VRSystemState& aState) {
    409  if (mShmem != nullptr) {
    410    mShmem->PushSystemState(aState);
    411  }
    412 }
    413 
    414 void VRService::PullState(mozilla::gfx::VRBrowserState& aState) {
    415  if (mShmem != nullptr) {
    416    mShmem->PullBrowserState(aState);
    417  }
    418 }