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/. */
9 #include <cstring> // for memcmp
11 #include "../VRShMem.h"
12 #include "../gfxVRMutex.h"
13 #include "PuppetSession.h"
14 #include "mozilla/BackgroundHangMonitor.h"
15 #include "mozilla/StaticPrefs_dom.h"
17 #include "nsXULAppAPI.h"
20 # include "OculusSession.h"
23 #if defined(XP_WIN) || defined(XP_MACOSX) || \
24 (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
25 # include "OpenVRSession.h"
27 #if !defined(MOZ_WIDGET_ANDROID)
28 # include "OSVRSession.h"
31 using namespace mozilla
;
32 using namespace mozilla::gfx
;
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
;
45 bool IsImmersiveContentActive(const mozilla::gfx::VRBrowserState
& aState
) {
46 for (const auto& layer
: aState
.layerState
) {
47 if (layer
.type
== VRLayerType::LayerType_Stereo_Immersive
) {
54 } // anonymous namespace
57 already_AddRefed
<VRService
> VRService::Create(
58 volatile VRExternalShmem
* aShmem
) {
59 RefPtr
<VRService
> service
= new VRService(aShmem
);
60 return service
.forget();
63 VRService::VRService(volatile VRExternalShmem
* aShmem
)
66 mShutdownRequested(false),
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*/);
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*/);
82 void VRService::Refresh() {
83 if (mShmem
!= nullptr && mShmem
->IsDisplayStateShutdown()) {
88 void VRService::Start() {
89 if (!mServiceThread
) {
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.
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
>(
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). */
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. */
116 static_cast<nsThread
*>(NS_GetCurrentThread())
117 ->SetUseHangMonitor(true);
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
)));
133 void VRService::Stop() { StopInternal(false /*aFromDtor*/); }
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;
146 mShutdownRequested
= true;
147 mServiceThread
->Shutdown();
148 mServiceThread
= nullptr;
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();
163 bool VRService::InitShmem() { return mShmem
->JoinShMem(); }
165 bool VRService::IsInServiceThread() {
166 return mServiceThread
&& mServiceThread
->IsOnCurrentThread();
169 void VRService::ServiceInitialize() {
170 MOZ_ASSERT(IsInServiceThread());
176 mShutdownRequested
= false;
177 // Get initial state from the browser
178 PullState(mBrowserState
);
180 // Try to start a VRSession
181 UniquePtr
<VRSession
> session
;
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
)) {
191 // We try Oculus first to ensure we use Oculus
192 // devices trough the most native interface
197 session
= MakeUnique
<OculusSession
>();
198 if (!session
->Initialize(mSystemState
,
199 mBrowserState
.detectRuntimesOnly
)) {
205 #if defined(XP_WIN) || defined(XP_MACOSX) || \
206 (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
209 session
= MakeUnique
<OpenVRSession
>();
210 if (!session
->Initialize(mSystemState
,
211 mBrowserState
.detectRuntimesOnly
)) {
216 #if !defined(MOZ_WIDGET_ANDROID)
219 session
= MakeUnique
<OSVRSession
>();
220 if (!session
->Initialize(mSystemState
,
221 mBrowserState
.detectRuntimesOnly
)) {
227 } // if (staticPrefs:VRPuppetEnabled())
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
);
237 mServiceThread
->Dispatch(
238 NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
239 &VRService::ServiceWaitForImmersive
));
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;
251 if (mBrowserState
.detectRuntimesOnly
) {
252 mSystemState
.displayState
.capabilityFlags
= capFlags
;
254 mSystemState
.displayState
.minRestartInterval
=
255 StaticPrefs::dom_vr_external_notdetected_timeout();
257 mSystemState
.displayState
.shutdown
= true;
258 PushState(mSystemState
);
262 void VRService::ServiceShutdown() {
263 MOZ_ASSERT(IsInServiceThread());
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();
275 PushState(mSystemState
);
279 void VRService::ServiceWaitForImmersive() {
280 MOZ_ASSERT(IsInServiceThread());
281 MOZ_ASSERT(mSession
);
283 mSession
->ProcessEvents(mSystemState
);
284 PushState(mSystemState
);
285 PullState(mBrowserState
);
287 if (mSession
->ShouldQuit() || mShutdownRequested
) {
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
);
297 mServiceThread
->Dispatch(
298 NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
299 &VRService::ServiceImmersiveMode
));
301 // Continue waiting for immersive mode
302 mServiceThread
->Dispatch(
303 NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
304 &VRService::ServiceWaitForImmersive
));
308 void VRService::ServiceImmersiveMode() {
309 MOZ_ASSERT(IsInServiceThread());
310 MOZ_ASSERT(mSession
);
312 mSession
->ProcessEvents(mSystemState
);
314 PushState(mSystemState
);
315 PullState(mBrowserState
);
317 if (mSession
->ShouldQuit() || mShutdownRequested
) {
319 mServiceThread
->Dispatch(NewRunnableMethod(
320 "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown
));
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
));
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
);
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
;
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
% ArrayLength(mFrameStartTime
);
360 mFrameStartTime
[historyIndex
] = TimeStamp::Now();
361 PushState(mSystemState
);
364 // Continue immersive mode
365 mServiceThread
->Dispatch(
366 NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
367 &VRService::ServiceImmersiveMode
));
370 void VRService::UpdateHaptics() {
371 MOZ_ASSERT(IsInServiceThread());
372 MOZ_ASSERT(mSession
);
374 for (size_t i
= 0; i
< ArrayLength(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
382 if (state
.inputFrameID
== 0) {
383 // The haptic feedback was stopped
384 mSession
->StopVibrateHaptic(state
.controllerIndex
);
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();
393 // This is a new haptic pulse, or we are overriding a prior one
394 size_t historyIndex
= state
.inputFrameID
% ArrayLength(mFrameStartTime
);
396 (float)(now
- mFrameStartTime
[historyIndex
]).ToSeconds();
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
);
403 // Record the state for comparison in the next run
404 memcpy(&lastState
, &state
, sizeof(VRHapticState
));
408 void VRService::PushState(const mozilla::gfx::VRSystemState
& aState
) {
409 if (mShmem
!= nullptr) {
410 mShmem
->PushSystemState(aState
);
414 void VRService::PullState(mozilla::gfx::VRBrowserState
& aState
) {
415 if (mShmem
!= nullptr) {
416 mShmem
->PullBrowserState(aState
);