XRSession.cpp (18562B)
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 file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/XRSession.h" 8 9 #include "VRDisplayClient.h" 10 #include "VRDisplayPresentation.h" 11 #include "VRLayerChild.h" 12 #include "XRBoundedReferenceSpace.h" 13 #include "XRFrame.h" 14 #include "XRInputSourceArray.h" 15 #include "XRNativeOrigin.h" 16 #include "XRNativeOriginFixed.h" 17 #include "XRNativeOriginLocal.h" 18 #include "XRNativeOriginLocalFloor.h" 19 #include "XRNativeOriginViewer.h" 20 #include "XRRenderState.h" 21 #include "XRSystem.h" 22 #include "XRView.h" 23 #include "XRViewerPose.h" 24 #include "mozilla/EventDispatcher.h" 25 #include "mozilla/dom/DocumentInlines.h" 26 #include "mozilla/dom/Promise.h" 27 #include "mozilla/dom/XRInputSourceEvent.h" 28 #include "mozilla/dom/XRSessionEvent.h" 29 #include "nsGlobalWindowInner.h" 30 #include "nsIObserverService.h" 31 #include "nsISupportsPrimitives.h" 32 #include "nsRefreshDriver.h" 33 34 /** 35 * Maximum instances of XRFrame and XRViewerPose objects 36 * created in the pool. 37 */ 38 const uint32_t kMaxPoolSize = 16; 39 40 namespace mozilla::dom { 41 42 NS_IMPL_CYCLE_COLLECTION_CLASS(XRSession) 43 44 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRSession, 45 DOMEventTargetHelper) 46 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mXRSystem) 47 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveRenderState) 48 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingRenderState) 49 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputSources) 50 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mViewerPosePool) 51 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFramePool) 52 53 for (uint32_t i = 0; i < tmp->mFrameRequestCallbacks.Length(); ++i) { 54 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mFrameRequestCallbacks[i]"); 55 cb.NoteXPCOMChild(tmp->mFrameRequestCallbacks[i].mCallback); 56 } 57 58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 59 60 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRSession, DOMEventTargetHelper) 61 NS_IMPL_CYCLE_COLLECTION_UNLINK(mXRSystem) 62 NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveRenderState) 63 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingRenderState) 64 NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputSources) 65 NS_IMPL_CYCLE_COLLECTION_UNLINK(mViewerPosePool) 66 NS_IMPL_CYCLE_COLLECTION_UNLINK(mFramePool) 67 68 tmp->mFrameRequestCallbacks.Clear(); 69 70 // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because 71 // DOMEventTargetHelper does it for us. 72 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 73 74 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRSession, DOMEventTargetHelper) 75 76 already_AddRefed<XRSession> XRSession::CreateInlineSession( 77 nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, 78 const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) { 79 nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(aWindow); 80 if (!win) { 81 return nullptr; 82 } 83 Document* doc = aWindow->GetExtantDoc(); 84 if (!doc) { 85 return nullptr; 86 } 87 nsPresContext* context = doc->GetPresContext(); 88 if (!context) { 89 return nullptr; 90 } 91 nsRefreshDriver* driver = context->RefreshDriver(); 92 if (!driver) { 93 return nullptr; 94 } 95 96 RefPtr<XRSession> session = 97 new XRSession(aWindow, aXRSystem, driver, nullptr, gfx::kVRGroupContent, 98 aEnabledReferenceSpaceTypes); 99 driver->AddRefreshObserver(session, FlushType::Display, "XR Session"); 100 return session.forget(); 101 } 102 103 already_AddRefed<XRSession> XRSession::CreateImmersiveSession( 104 nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, 105 gfx::VRDisplayClient* aClient, uint32_t aPresentationGroup, 106 const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) { 107 RefPtr<XRSession> session = 108 new XRSession(aWindow, aXRSystem, nullptr, aClient, aPresentationGroup, 109 aEnabledReferenceSpaceTypes); 110 return session.forget(); 111 } 112 113 XRSession::XRSession( 114 nsPIDOMWindowInner* aWindow, XRSystem* aXRSystem, 115 nsRefreshDriver* aRefreshDriver, gfx::VRDisplayClient* aClient, 116 uint32_t aPresentationGroup, 117 const nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) 118 : DOMEventTargetHelper(aWindow), 119 mXRSystem(aXRSystem), 120 mShutdown(false), 121 mEnded(false), 122 mRefreshDriver(aRefreshDriver), 123 mDisplayClient(aClient), 124 mFrameRequestCallbackCounter(0), 125 mEnabledReferenceSpaceTypes(aEnabledReferenceSpaceTypes.Clone()), 126 mViewerPosePoolIndex(0), 127 mFramePoolIndex(0) { 128 if (aClient) { 129 aClient->SessionStarted(this); 130 } 131 mActiveRenderState = new XRRenderState(aWindow, this); 132 mStartTimeStamp = TimeStamp::Now(); 133 if (IsImmersive()) { 134 mDisplayPresentation = 135 mDisplayClient->BeginPresentation({}, aPresentationGroup); 136 } 137 if (mDisplayClient) { 138 mDisplayClient->SetXRAPIMode(gfx::VRAPIMode::WebXR); 139 } 140 // TODO: Handle XR input sources are no longer available cases. 141 // https://immersive-web.github.io/webxr/#dom-xrsession-inputsources 142 mInputSources = new XRInputSourceArray(aWindow); 143 } 144 145 XRSession::~XRSession() { MOZ_ASSERT(mShutdown); } 146 147 gfx::VRDisplayClient* XRSession::GetDisplayClient() const { 148 return mDisplayClient; 149 } 150 151 JSObject* XRSession::WrapObject(JSContext* aCx, 152 JS::Handle<JSObject*> aGivenProto) { 153 return XRSession_Binding::Wrap(aCx, this, aGivenProto); 154 } 155 156 bool XRSession::IsEnded() const { return mEnded; } 157 158 already_AddRefed<Promise> XRSession::End(ErrorResult& aRv) { 159 nsCOMPtr<nsIGlobalObject> global = GetParentObject(); 160 NS_ENSURE_TRUE(global, nullptr); 161 162 ExitPresentInternal(); 163 164 RefPtr<Promise> promise = Promise::Create(global, aRv); 165 NS_ENSURE_TRUE(!aRv.Failed(), nullptr); 166 167 promise->MaybeResolve(JS::UndefinedHandleValue); 168 169 return promise.forget(); 170 } 171 172 bool XRSession::IsImmersive() const { 173 // Only immersive sessions have a VRDisplayClient 174 return mDisplayClient != nullptr; 175 } 176 177 XRVisibilityState XRSession::VisibilityState() const { 178 return XRVisibilityState::Visible; 179 // TODO (Bug 1609771): Implement changing visibility state 180 } 181 182 // https://immersive-web.github.io/webxr/#poses-may-be-reported 183 // Given that an XRSession cannot be requested without explicit consent 184 // by the user, the only necessary check is whether the XRSession's 185 // visiblityState is 'visible'. 186 bool XRSession::CanReportPoses() const { 187 return VisibilityState() == XRVisibilityState::Visible; 188 } 189 190 // https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate 191 void XRSession::UpdateRenderState(const XRRenderStateInit& aNewState, 192 ErrorResult& aRv) { 193 if (mEnded) { 194 aRv.ThrowInvalidStateError( 195 "UpdateRenderState can not be called on an XRSession that has ended."); 196 return; 197 } 198 if (aNewState.mBaseLayer.WasPassed() && 199 aNewState.mBaseLayer.Value()->mSession != this) { 200 aRv.ThrowInvalidStateError( 201 "The baseLayer passed in to UpdateRenderState must " 202 "belong to the XRSession that UpdateRenderState is " 203 "being called on."); 204 return; 205 } 206 if (aNewState.mInlineVerticalFieldOfView.WasPassed() && IsImmersive()) { 207 aRv.ThrowInvalidStateError( 208 "The inlineVerticalFieldOfView can not be set on an " 209 "XRRenderState for an immersive XRSession."); 210 return; 211 } 212 if (mPendingRenderState == nullptr) { 213 mPendingRenderState = new XRRenderState(*mActiveRenderState); 214 } 215 if (aNewState.mDepthNear.WasPassed()) { 216 mPendingRenderState->SetDepthNear(aNewState.mDepthNear.Value()); 217 } 218 if (aNewState.mDepthFar.WasPassed()) { 219 mPendingRenderState->SetDepthFar(aNewState.mDepthFar.Value()); 220 } 221 if (aNewState.mInlineVerticalFieldOfView.WasPassed()) { 222 mPendingRenderState->SetInlineVerticalFieldOfView( 223 aNewState.mInlineVerticalFieldOfView.Value()); 224 } 225 if (aNewState.mBaseLayer.WasPassed()) { 226 mPendingRenderState->SetBaseLayer(aNewState.mBaseLayer.Value()); 227 } 228 } 229 230 XRRenderState* XRSession::RenderState() { return mActiveRenderState; } 231 232 XRInputSourceArray* XRSession::InputSources() { return mInputSources; } 233 234 Nullable<float> XRSession::GetFrameRate() { return {}; } 235 236 void XRSession::GetSupportedFrameRates(JSContext*, 237 JS::MutableHandle<JSObject*> aRetVal) { 238 aRetVal.set(nullptr); 239 } 240 241 // https://immersive-web.github.io/webxr/#apply-the-pending-render-state 242 void XRSession::ApplyPendingRenderState() { 243 if (mPendingRenderState == nullptr) { 244 return; 245 } 246 mActiveRenderState = mPendingRenderState; 247 mPendingRenderState = nullptr; 248 249 // https://immersive-web.github.io/webxr/#minimum-inline-field-of-view 250 const double kMinimumInlineVerticalFieldOfView = 0.0f; 251 252 // https://immersive-web.github.io/webxr/#maximum-inline-field-of-view 253 const double kMaximumInlineVerticalFieldOfView = M_PI; 254 255 if (!mActiveRenderState->GetInlineVerticalFieldOfView().IsNull()) { 256 double verticalFOV = 257 mActiveRenderState->GetInlineVerticalFieldOfView().Value(); 258 if (verticalFOV < kMinimumInlineVerticalFieldOfView) { 259 verticalFOV = kMinimumInlineVerticalFieldOfView; 260 } 261 if (verticalFOV > kMaximumInlineVerticalFieldOfView) { 262 verticalFOV = kMaximumInlineVerticalFieldOfView; 263 } 264 mActiveRenderState->SetInlineVerticalFieldOfView(verticalFOV); 265 } 266 267 // Our minimum near plane value is set to a small value close but not equal to 268 // zero (kEpsilon) The maximum far plane is infinite. 269 const float kEpsilon = 0.00001f; 270 double depthNear = mActiveRenderState->DepthNear(); 271 double depthFar = mActiveRenderState->DepthFar(); 272 if (depthNear < 0.0f) { 273 depthNear = 0.0f; 274 } 275 if (depthFar < 0.0f) { 276 depthFar = 0.0f; 277 } 278 // Ensure at least a small distance between the near and far planes 279 if (fabs(depthFar - depthNear) < kEpsilon) { 280 depthFar = depthNear + kEpsilon; 281 } 282 mActiveRenderState->SetDepthNear(depthNear); 283 mActiveRenderState->SetDepthFar(depthFar); 284 285 XRWebGLLayer* baseLayer = mActiveRenderState->GetBaseLayer(); 286 if (baseLayer) { 287 if (!IsImmersive() && baseLayer->mCompositionDisabled) { 288 mActiveRenderState->SetCompositionDisabled(true); 289 mActiveRenderState->SetOutputCanvas(baseLayer->GetCanvas()); 290 } else { 291 mActiveRenderState->SetCompositionDisabled(false); 292 mActiveRenderState->SetOutputCanvas(nullptr); 293 mDisplayPresentation->UpdateXRWebGLLayer(baseLayer); 294 } 295 } // if (baseLayer) 296 } 297 298 void XRSession::WillRefresh(mozilla::TimeStamp aTime) { 299 // Inline sessions are driven by nsRefreshDriver directly, 300 // unlike immersive sessions, which are driven VRDisplayClient. 301 if (!IsImmersive() && !mXRSystem->HasActiveImmersiveSession()) { 302 if (nsIGlobalObject* global = GetOwnerGlobal()) { 303 if (JSObject* obj = global->GetGlobalJSObject()) { 304 js::NotifyAnimationActivity(obj); 305 } 306 } 307 StartFrame(); 308 } 309 } 310 311 void XRSession::StartFrame() { 312 if (mShutdown || mEnded) { 313 return; 314 } 315 ApplyPendingRenderState(); 316 317 XRWebGLLayer* baseLayer = mActiveRenderState->GetBaseLayer(); 318 if (!baseLayer) { 319 return; 320 } 321 322 if (!IsImmersive() && mActiveRenderState->GetOutputCanvas() == nullptr) { 323 return; 324 } 325 326 // Determine timestamp for the callbacks 327 TimeStamp nowTime = TimeStamp::Now(); 328 mozilla::TimeDuration duration = nowTime - mStartTimeStamp; 329 DOMHighResTimeStamp timeStamp = duration.ToMilliseconds(); 330 331 // Create an XRFrame for the callbacks 332 RefPtr<XRFrame> frame = PooledFrame(); 333 frame->StartAnimationFrame(); 334 335 baseLayer->StartAnimationFrame(); 336 nsTArray<XRFrameRequest> callbacks; 337 callbacks.AppendElements(mFrameRequestCallbacks); 338 mFrameRequestCallbacks.Clear(); 339 for (auto& callback : callbacks) { 340 callback.Call(timeStamp, *frame); 341 } 342 343 baseLayer->EndAnimationFrame(); 344 frame->EndAnimationFrame(); 345 if (mDisplayPresentation) { 346 mDisplayPresentation->SubmitFrame(); 347 } 348 } 349 350 void XRSession::ExitPresent() { ExitPresentInternal(); } 351 352 already_AddRefed<Promise> XRSession::RequestReferenceSpace( 353 const XRReferenceSpaceType& aReferenceSpaceType, ErrorResult& aRv) { 354 nsCOMPtr<nsIGlobalObject> global = GetParentObject(); 355 NS_ENSURE_TRUE(global, nullptr); 356 357 RefPtr<Promise> promise = Promise::Create(global, aRv); 358 NS_ENSURE_TRUE(!aRv.Failed(), nullptr); 359 360 if (!mEnabledReferenceSpaceTypes.Contains(aReferenceSpaceType)) { 361 promise->MaybeRejectWithNotSupportedError(nsLiteralCString( 362 "Requested XRReferenceSpaceType not available for the XRSession.")); 363 return promise.forget(); 364 } 365 RefPtr<XRReferenceSpace> space; 366 RefPtr<XRNativeOrigin> nativeOrigin; 367 if (mDisplayClient) { 368 switch (aReferenceSpaceType) { 369 case XRReferenceSpaceType::Viewer: 370 nativeOrigin = new XRNativeOriginViewer(mDisplayClient); 371 break; 372 case XRReferenceSpaceType::Local: 373 nativeOrigin = new XRNativeOriginLocal(mDisplayClient); 374 break; 375 case XRReferenceSpaceType::Local_floor: 376 case XRReferenceSpaceType::Bounded_floor: 377 nativeOrigin = new XRNativeOriginLocalFloor(mDisplayClient); 378 break; 379 default: 380 nativeOrigin = new XRNativeOriginFixed(gfx::PointDouble3D()); 381 break; 382 } 383 } else { 384 // We currently only support XRReferenceSpaceType::Viewer when 385 // there is no XR hardware. In this case, the native origin 386 // will always be at {0, 0, 0} which will always be the same 387 // as the 'tracked' position of the non-existant pose. 388 MOZ_ASSERT(aReferenceSpaceType == XRReferenceSpaceType::Viewer); 389 nativeOrigin = new XRNativeOriginFixed(gfx::PointDouble3D()); 390 } 391 if (aReferenceSpaceType == XRReferenceSpaceType::Bounded_floor) { 392 space = new XRBoundedReferenceSpace(GetParentObject(), this, nativeOrigin); 393 } else { 394 space = new XRReferenceSpace(GetParentObject(), this, nativeOrigin, 395 aReferenceSpaceType); 396 } 397 398 promise->MaybeResolve(space); 399 return promise.forget(); 400 } 401 402 already_AddRefed<Promise> XRSession::UpdateTargetFrameRate(float aRate, 403 ErrorResult& aRv) { 404 nsCOMPtr<nsIGlobalObject> global = GetParentObject(); 405 NS_ENSURE_TRUE(global, nullptr); 406 407 RefPtr<Promise> promise = Promise::Create(global, aRv); 408 NS_ENSURE_TRUE(!aRv.Failed(), nullptr); 409 410 if (mEnded) { 411 promise->MaybeRejectWithInvalidStateError( 412 "UpdateTargetFrameRate can not be called on an XRSession that has " 413 "ended."); 414 return promise.forget(); 415 } 416 417 // https://immersive-web.github.io/webxr/#dom-xrsession-updatetargetframerate 418 // TODO: Validate the rate with the frame rates supported from the device. 419 // We add a no op for now to avoid JS exceptions related to undefined method. 420 // The spec states that user agent MAY use rate to calculate a new display 421 // frame rate, so it's fine to let the default frame rate for now. 422 423 promise->MaybeResolve(JS::UndefinedHandleValue); 424 return promise.forget(); 425 } 426 427 XRRenderState* XRSession::GetActiveRenderState() const { 428 return mActiveRenderState; 429 } 430 431 void XRSession::XRFrameRequest::Call(const DOMHighResTimeStamp& aTimeStamp, 432 XRFrame& aFrame) { 433 RefPtr<mozilla::dom::XRFrameRequestCallback> callback = mCallback; 434 callback->Call(aTimeStamp, aFrame); 435 } 436 437 int32_t XRSession::RequestAnimationFrame(XRFrameRequestCallback& aCallback, 438 ErrorResult& aError) { 439 if (mShutdown) { 440 return 0; 441 } 442 443 int32_t handle = ++mFrameRequestCallbackCounter; 444 445 mFrameRequestCallbacks.AppendElement(XRFrameRequest(aCallback, handle)); 446 447 return handle; 448 } 449 450 void XRSession::CancelAnimationFrame(int32_t aHandle, ErrorResult& aError) { 451 mFrameRequestCallbacks.RemoveElementSorted(aHandle); 452 } 453 454 void XRSession::Shutdown() { 455 mShutdown = true; 456 ExitPresentInternal(); 457 mViewerPosePool.Clear(); 458 mViewerPosePoolIndex = 0; 459 mFramePool.Clear(); 460 mFramePoolIndex = 0; 461 mActiveRenderState = nullptr; 462 mPendingRenderState = nullptr; 463 mFrameRequestCallbacks.Clear(); 464 465 // Unregister from nsRefreshObserver 466 if (mRefreshDriver) { 467 mRefreshDriver->RemoveRefreshObserver(this, FlushType::Display); 468 mRefreshDriver = nullptr; 469 } 470 } 471 472 void XRSession::ExitPresentInternal() { 473 if (mInputSources) { 474 mInputSources->Clear(this); 475 } 476 if (mDisplayClient) { 477 mDisplayClient->SessionEnded(this); 478 } 479 480 if (mXRSystem) { 481 mXRSystem->SessionEnded(this); 482 } 483 484 if (mActiveRenderState) { 485 mActiveRenderState->SessionEnded(); 486 } 487 488 if (mPendingRenderState) { 489 mPendingRenderState->SessionEnded(); 490 } 491 492 mDisplayPresentation = nullptr; 493 if (!mEnded) { 494 mEnded = true; 495 496 XRSessionEventInit init; 497 init.mBubbles = false; 498 init.mCancelable = false; 499 init.mSession = this; 500 RefPtr<XRSessionEvent> event = 501 XRSessionEvent::Constructor(this, u"end"_ns, init); 502 503 event->SetTrusted(true); 504 this->DispatchEvent(*event); 505 } 506 } 507 508 void XRSession::DisconnectFromOwner() { 509 MOZ_ASSERT(NS_IsMainThread()); 510 Shutdown(); 511 DOMEventTargetHelper::DisconnectFromOwner(); 512 } 513 514 void XRSession::LastRelease() { 515 // We don't want to wait for the GC to free up the presentation 516 // for use in other documents, so we do this in LastRelease(). 517 Shutdown(); 518 } 519 520 RefPtr<XRViewerPose> XRSession::PooledViewerPose( 521 const gfx::Matrix4x4Double& aTransform, bool aEmulatedPosition) { 522 RefPtr<XRViewerPose> pose; 523 if (mViewerPosePool.Length() > mViewerPosePoolIndex) { 524 pose = mViewerPosePool.ElementAt(mViewerPosePoolIndex); 525 pose->Transform()->Update(aTransform); 526 pose->SetEmulatedPosition(aEmulatedPosition); 527 } else { 528 RefPtr<XRRigidTransform> transform = 529 new XRRigidTransform(static_cast<EventTarget*>(this), aTransform); 530 nsTArray<RefPtr<XRView>> views; 531 if (IsImmersive()) { 532 views.AppendElement(new XRView(GetParentObject(), XREye::Left)); 533 views.AppendElement(new XRView(GetParentObject(), XREye::Right)); 534 } else { 535 views.AppendElement(new XRView(GetParentObject(), XREye::None)); 536 } 537 pose = new XRViewerPose(static_cast<EventTarget*>(this), transform, 538 aEmulatedPosition, views); 539 mViewerPosePool.AppendElement(pose); 540 } 541 542 mViewerPosePoolIndex++; 543 if (mViewerPosePoolIndex >= kMaxPoolSize) { 544 mViewerPosePoolIndex = 0; 545 } 546 547 return pose; 548 } 549 550 RefPtr<XRFrame> XRSession::PooledFrame() { 551 RefPtr<XRFrame> frame; 552 if (mFramePool.Length() > mFramePoolIndex) { 553 frame = mFramePool.ElementAt(mFramePoolIndex); 554 } else { 555 frame = new XRFrame(GetParentObject(), this); 556 mFramePool.AppendElement(frame); 557 } 558 559 return frame; 560 } 561 562 } // namespace mozilla::dom