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