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