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 }