blob: e567fc101d59c74b57a28a6ff90e732f9bfe0e9d [file] [log] [blame]
/*
* Copyright (C) 2020 Igalia S.L. All rights reserved.
* Copyright (C) 2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebXRSession.h"
#if ENABLE(WEBXR)
#include "Document.h"
#include "EventNames.h"
#include "JSWebXRReferenceSpace.h"
#include "WebXRBoundedReferenceSpace.h"
#include "WebXRFrame.h"
#include "WebXRSystem.h"
#include "WebXRView.h"
#include "XRFrameRequestCallback.h"
#include "XRRenderStateInit.h"
#include "XRSessionEvent.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/RefPtr.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(WebXRSession);
Ref<WebXRSession> WebXRSession::create(Document& document, WebXRSystem& system, XRSessionMode mode, PlatformXR::Device& device, FeatureList&& requestedFeatures)
{
auto session = adoptRef(*new WebXRSession(document, system, mode, device, WTFMove(requestedFeatures)));
session->suspendIfNeeded();
return session;
}
WebXRSession::WebXRSession(Document& document, WebXRSystem& system, XRSessionMode mode, PlatformXR::Device& device, FeatureList&& requestedFeatures)
: ActiveDOMObject(&document)
, m_inputSources(WebXRInputSourceArray::create(*this))
, m_xrSystem(system)
, m_mode(mode)
, m_device(device)
, m_requestedFeatures(WTFMove(requestedFeatures))
, m_activeRenderState(WebXRRenderState::create(mode))
, m_viewerReferenceSpace(makeUnique<WebXRViewerSpace>(document, *this))
, m_timeOrigin(MonotonicTime::now())
, m_views(device.views(mode))
{
m_device->setTrackingAndRenderingClient(*this);
m_device->initializeTrackingAndRendering(mode);
// https://immersive-web.github.io/webxr/#ref-for-dom-xrreferencespacetype-viewer%E2%91%A2
// Every session MUST support viewer XRReferenceSpaces.
m_device->initializeReferenceSpace(XRReferenceSpaceType::Viewer);
}
WebXRSession::~WebXRSession()
{
if (!m_ended && m_device)
m_device->shutDownTrackingAndRendering();
}
XREnvironmentBlendMode WebXRSession::environmentBlendMode() const
{
return m_environmentBlendMode;
}
XRInteractionMode WebXRSession::interactionMode() const
{
return m_interactionMode;
}
XRVisibilityState WebXRSession::visibilityState() const
{
return m_visibilityState;
}
const WebXRRenderState& WebXRSession::renderState() const
{
return *m_activeRenderState;
}
const WebXRInputSourceArray& WebXRSession::inputSources() const
{
return m_inputSources;
}
static bool isImmersive(XRSessionMode mode)
{
return mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr;
}
// https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate
ExceptionOr<void> WebXRSession::updateRenderState(const XRRenderStateInit& newState)
{
// 1. Let session be this.
// 2. If session's ended value is true, throw an InvalidStateError and abort these steps.
if (m_ended)
return Exception { InvalidStateError };
// 3. If newState's baseLayer was created with an XRSession other than session,
// throw an InvalidStateError and abort these steps.
if (newState.baseLayer && &newState.baseLayer->session() != this)
return Exception { InvalidStateError };
// 4. If newState's inlineVerticalFieldOfView is set and session is an immersive session,
// throw an InvalidStateError and abort these steps.
if (newState.inlineVerticalFieldOfView && isImmersive(m_mode))
return Exception { InvalidStateError };
// 5. If none of newState's depthNear, depthFar, inlineVerticalFieldOfView, baseLayer,
// layers are set, abort these steps.
if (!newState.depthNear && !newState.depthFar && !newState.inlineVerticalFieldOfView && !newState.baseLayer && !newState.layers)
return { };
// 6. Run update the pending layers state with session and newState.
// https://immersive-web.github.io/webxr/#update-the-pending-layers-state
if (newState.layers)
return Exception { NotSupportedError };
// 7. Let activeState be session's active render state.
// 8. If session's pending render state is null, set it to a copy of activeState.
if (!m_pendingRenderState)
m_pendingRenderState = m_activeRenderState->clone();
// 9. If newState's depthNear value is set, set session's pending render state's depthNear to newState's depthNear.
if (newState.depthNear)
m_pendingRenderState->setDepthNear(newState.depthNear.value());
// 10. If newState's depthFar value is set, set session's pending render state's depthFar to newState's depthFar.
if (newState.depthFar)
m_pendingRenderState->setDepthFar(newState.depthFar.value());
// 11. If newState's inlineVerticalFieldOfView is set, set session's pending render state's inlineVerticalFieldOfView
// to newState's inlineVerticalFieldOfView.
if (newState.inlineVerticalFieldOfView)
m_pendingRenderState->setInlineVerticalFieldOfView(newState.inlineVerticalFieldOfView.value());
// 12. If newState's baseLayer is set, set session's pending render state's baseLayer to newState's baseLayer.
if (newState.baseLayer)
m_pendingRenderState->setBaseLayer(newState.baseLayer.get());
return { };
}
// https://immersive-web.github.io/webxr/#reference-space-is-supported
bool WebXRSession::referenceSpaceIsSupported(XRReferenceSpaceType type) const
{
// 1. If type is not contained in session’s XR device's list of enabled features for mode return false.
if (!m_requestedFeatures.contains(type))
return false;
// 2. If type is viewer, return true.
if (type == XRReferenceSpaceType::Viewer)
return true;
bool isImmersiveSession = isImmersive(m_mode);
if (type == XRReferenceSpaceType::Local || type == XRReferenceSpaceType::LocalFloor) {
// 3. If type is local or local-floor, and session is an immersive session, return true.
if (isImmersiveSession)
return true;
// 4. If type is local or local-floor, and the XR device supports reporting orientation data, return true.
if (m_device->supportsOrientationTracking())
return true;
}
// 5. If type is bounded-floor and session is an immersive session, return the result of whether bounded
// reference spaces are supported by the XR device.
// https://immersive-web.github.io/webxr/#bounded-reference-spaces-are-supported
// TODO: add API to PlatformXR::Device
if (type == XRReferenceSpaceType::BoundedFloor && isImmersiveSession)
return true;
// 6. If type is unbounded, session is an immersive session, and the XR device supports stable tracking
// near the user over an unlimited distance, return true.
// TODO: add API to PlatformXR::Device to check stable tracking over unlimited distance.
if (type == XRReferenceSpaceType::Unbounded && isImmersiveSession)
return true;
// 7. Return false.
return false;
}
// https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace
void WebXRSession::requestReferenceSpace(XRReferenceSpaceType type, RequestReferenceSpacePromise&& promise)
{
if (!scriptExecutionContext()) {
promise.reject(Exception { InvalidStateError });
return;
}
// 1. Let promise be a new Promise.
// 2. Run the following steps in parallel:
scriptExecutionContext()->postTask([this, weakThis = WeakPtr { *this }, promise = WTFMove(promise), type](auto&) mutable {
if (!weakThis)
return;
// 2.1. If the result of running reference space is supported for type and session is false, queue a task to reject promise
// with a NotSupportedError and abort these steps.
if (!referenceSpaceIsSupported(type)) {
queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [promise = WTFMove(promise)]() mutable {
promise.reject(Exception { NotSupportedError });
});
return;
}
// 2.2. Set up any platform resources required to track reference spaces of type type.
m_device->initializeReferenceSpace(type);
// 2.3. Queue a task to run the following steps:
queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, type, promise = WTFMove(promise)]() mutable {
if (!scriptExecutionContext()) {
promise.reject(Exception { InvalidStateError });
return;
}
auto& document = downcast<Document>(*scriptExecutionContext());
// 2.4. Create a reference space, referenceSpace, with type and session.
// https://immersive-web.github.io/webxr/#create-a-reference-space
RefPtr<WebXRReferenceSpace> referenceSpace;
if (type == XRReferenceSpaceType::BoundedFloor)
referenceSpace = WebXRBoundedReferenceSpace::create(document, Ref { *this }, type);
else
referenceSpace = WebXRReferenceSpace::create(document, Ref { *this }, type);
// 2.5. Resolve promise with referenceSpace.
promise.resolve(referenceSpace.releaseNonNull());
});
});
}
// https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe
unsigned WebXRSession::requestAnimationFrame(Ref<XRFrameRequestCallback>&& callback)
{
// Ignore any new frame requests once the session is ended.
if (m_ended)
return 0;
// 1. Let session be the target XRSession object.
// 2. Increment session's animation frame callback identifier by one.
unsigned newId = m_nextCallbackId++;
// 3. Append callback to session's list of animation frame callbacks, associated with session's
// animation frame callback identifier's current value.
callback->setCallbackId(newId);
m_callbacks.append(WTFMove(callback));
// Script can add multiple requestAnimationFrame callbacks but we should only request a device frame once.
// When requestAnimationFrame is called during processing RAF callbacks the next requestFrame is scheduled
// at the end of WebXRSession::onFrame() to prevent requesting a new frame before the current one has ended.
if (m_callbacks.size() == 1)
requestFrame();
// 4. Return session's animation frame callback identifier's current value.
return newId;
}
// https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe
void WebXRSession::cancelAnimationFrame(unsigned callbackId)
{
// 1. Let session be the target XRSession object.
// 2. Find the entry in session's list of animation frame callbacks or session's list of
// currently running animation frame callbacks that is associated with the value handle.
// 3. If there is such an entry, set its cancelled boolean to true and remove it from
// session's list of animation frame callbacks.
size_t position = m_callbacks.findIf([callbackId] (auto& item) {
return item->callbackId() == callbackId;
});
if (position != notFound) {
m_callbacks[position]->setFiredOrCancelled();
return;
}
}
// https://immersive-web.github.io/webxr/#native-webgl-framebuffer-resolution
IntSize WebXRSession::nativeWebGLFramebufferResolution() const
{
if (m_mode == XRSessionMode::Inline) {
// FIXME: replace the conditional by ASSERTs once we properly initialize the outputCanvas.
return m_activeRenderState && m_activeRenderState->outputCanvas() ? m_activeRenderState->outputCanvas()->size() : IntSize(1, 1);
}
return recommendedWebGLFramebufferResolution();
}
// https://immersive-web.github.io/webxr/#recommended-webgl-framebuffer-resolution
IntSize WebXRSession::recommendedWebGLFramebufferResolution() const
{
ASSERT(m_device);
return m_device->recommendedResolution(m_mode);
}
// https://immersive-web.github.io/webxr/#view-viewport-modifiable
bool WebXRSession::supportsViewportScaling() const
{
ASSERT(m_device);
// Only immersive sessions support viewport scaling.
return m_mode == XRSessionMode::ImmersiveVr && m_device->supportsViewportScaling();
}
bool WebXRSession::isPositionEmulated() const
{
return m_frameData.isPositionEmulated || !m_frameData.isPositionValid;
}
// https://immersive-web.github.io/webxr/#shut-down-the-session
void WebXRSession::shutdown(InitiatedBySystem initiatedBySystem)
{
Ref protectedThis { *this };
if (m_ended) {
// This method was called earlier with initiatedBySystem=No when the
// session termination was requested manually via XRSession.end(). When
// the system has completed the shutdown, this method is now called again
// with initiatedBySystem=Yes to do the final cleanup.
if (initiatedBySystem == InitiatedBySystem::Yes)
didCompleteShutdown();
return;
}
// 1. Let session be the target XRSession object.
// 2. Set session's ended value to true.
m_ended = true;
// 3. If the active immersive session is equal to session, set the active immersive session to null.
// 4. Remove session from the list of inline sessions.
m_xrSystem.sessionEnded(*this);
m_inputSources->clear();
if (initiatedBySystem == InitiatedBySystem::Yes) {
// If we get here, the session termination was triggered by the system rather than
// via XRSession.end(). Since the system has completed the session shutdown, we can
// immediately do the final cleanup.
didCompleteShutdown();
return;
}
// TODO: complete the implementation
// 5. Reject any outstanding promises returned by session with an InvalidStateError, except for any promises returned by end().
// 6. If no other features of the user agent are actively using them, perform the necessary platform-specific steps to shut down the device's tracking and rendering capabilities. This MUST include:
// 6.1. Releasing exclusive access to the XR device if session is an immersive session.
// 6.2. Deallocating any graphics resources acquired by session for presentation to the XR device.
// 6.3. Putting the XR device in a state such that a different source may be able to initiate a session with the same device if session is an immersive session.
if (m_device)
m_device->shutDownTrackingAndRendering();
// If device will not report shutdown completion via the TrackingAndRenderingClient,
// complete the shutdown cleanup here.
if (!m_device || !m_device->supportsSessionShutdownNotification())
didCompleteShutdown();
}
void WebXRSession::didCompleteShutdown()
{
if (m_device)
m_device->setTrackingAndRenderingClient(nullptr);
// Resolve end promise from XRSession::end()
if (m_endPromise) {
m_endPromise->resolve();
m_endPromise = std::nullopt;
}
// From https://immersive-web.github.io/webxr/#shut-down-the-session
// 7. Queue a task that fires an XRSessionEvent named end on session.
auto event = XRSessionEvent::create(eventNames().endEvent, { RefPtr { this } });
queueTaskToDispatchEvent(*this, TaskSource::WebXR, WTFMove(event));
}
// https://immersive-web.github.io/webxr/#dom-xrsession-end
ExceptionOr<void> WebXRSession::end(EndPromise&& promise)
{
// The shutdown() call below might remove the sole reference to session
// that could exist (the XRSystem owns the sessions) so let's protect this.
Ref protectedThis { *this };
if (m_ended)
return Exception { InvalidStateError, "Cannot end a session more than once"_s };
ASSERT(!m_endPromise);
m_endPromise = WTFMove(promise);
// 1. Let promise be a new Promise.
// 2. Shut down the target XRSession object.
shutdown(InitiatedBySystem::No);
// 3. Queue a task to perform the following steps:
// 3.1 Wait until any platform-specific steps related to shutting down the session have completed.
// 3.2 Resolve promise.
// 4. Return promise.
return { };
}
const char* WebXRSession::activeDOMObjectName() const
{
return "XRSession";
}
void WebXRSession::stop()
{
}
void WebXRSession::sessionDidInitializeInputSources(Vector<PlatformXR::Device::FrameData::InputSource>&& inputSources)
{
// https://immersive-web.github.io/webxr/#dom-xrsystem-requestsession
// 5.4.11 Queue a task to perform the following steps: NOTE: These steps ensure that initial inputsourceschange
// events occur after the initial session is resolved.
queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, inputSources = WTFMove(inputSources)]() mutable {
// 1. Set session's promise resolved flag to true.
m_inputInitialized = true;
// 2. Let sources be any existing input sources attached to session.
// 3. If sources is non-empty, perform the following steps:
if (!inputSources.isEmpty()) {
auto timestamp = (MonotonicTime::now() - m_timeOrigin).milliseconds();
// 3.1. Set session's list of active XR input sources to sources.
// 3.2. Fire an XRInputSourcesChangeEvent named inputsourceschange on session with added set to sources.
// Note: 3.1 and 3.2 steps are handled inside the update() call.
m_inputSources->update(timestamp, inputSources);
}
});
}
void WebXRSession::sessionDidEnd()
{
// This can be called as a result of finishing the shutdown initiated
// from XRSession::end(), or session termination triggered by the system.
shutdown(InitiatedBySystem::Yes);
}
void WebXRSession::applyPendingRenderState()
{
// https: //immersive-web.github.io/webxr/#apply-the-pending-render-state
// 1. Let activeState be session’s active render state.
// 2. Let newState be session’s pending render state.
// 3. Set session’s pending render state to null.
auto newState = m_pendingRenderState;
ASSERT(newState);
// 4. Let oldBaseLayer be activeState’s baseLayer.
// 5. Let oldLayers be activeState’s layers.
// FIXME: those are only needed for step 6.2.
// 6.1 Set activeState to newState.
m_activeRenderState = newState;
// 6.2 If oldBaseLayer is not equal to activeState’s baseLayer, oldLayers is not equal to activeState’s layers, or the dimensions of any of the layers have changed, update the viewports for session.
// FIXME: implement this.
// 6.3 If activeState’s inlineVerticalFieldOfView is less than session’s minimum inline field of view set activeState’s inlineVerticalFieldOfView to session’s minimum inline field of view.
if (m_activeRenderState->inlineVerticalFieldOfView() < m_minimumInlineFOV)
m_activeRenderState->setInlineVerticalFieldOfView(m_minimumInlineFOV);
// 6.4 If activeState’s inlineVerticalFieldOfView is greater than session’s maximum inline field of view set activeState’s inlineVerticalFieldOfView to session’s maximum inline field of view.
if (m_activeRenderState->inlineVerticalFieldOfView() > m_maximumInlineFOV)
m_activeRenderState->setInlineVerticalFieldOfView(m_maximumInlineFOV);
// 6.5 If activeState’s depthNear is less than session’s minimum near clip plane set activeState’s depthNear to session’s minimum near clip plane.
if (m_activeRenderState->depthNear() < m_minimumNearClipPlane)
m_activeRenderState->setDepthNear(m_minimumNearClipPlane);
// 6.6 If activeState’s depthFar is greater than session’s maximum far clip plane set activeState’s depthFar to session’s maximum far clip plane.
if (m_activeRenderState->depthFar() > m_maximumFarClipPlane)
m_activeRenderState->setDepthFar(m_maximumFarClipPlane);
// 6.7 Let baseLayer be activeState’s baseLayer.
auto baseLayer = m_activeRenderState->baseLayer();
// 6.8 Set activeState’s composition enabled and output canvas as follows:
if (m_mode == XRSessionMode::Inline && is<WebXRWebGLLayer>(baseLayer) && !baseLayer->isCompositionEnabled()) {
m_activeRenderState->setCompositionEnabled(false);
m_activeRenderState->setOutputCanvas(baseLayer->canvas());
} else {
m_activeRenderState->setCompositionEnabled(true);
m_activeRenderState->setOutputCanvas(nullptr);
}
}
// https://immersive-web.github.io/webxr/#should-be-rendered
bool WebXRSession::frameShouldBeRendered() const
{
if (!m_activeRenderState->baseLayer())
return false;
if (m_mode == XRSessionMode::Inline && !m_activeRenderState->outputCanvas())
return false;
return true;
}
void WebXRSession::requestFrame()
{
m_device->requestFrame([this, protectedThis = Ref { *this }](auto&& frameData) {
onFrame(WTFMove(frameData));
});
}
void WebXRSession::onFrame(PlatformXR::Device::FrameData&& frameData)
{
ASSERT(isMainThread());
if (m_ended)
return;
// Queue a task to perform the following steps.
queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, frameData = WTFMove(frameData)]() mutable {
if (m_ended)
return;
m_frameData = WTFMove(frameData);
// 1.Let now be the current high resolution time.
auto now = (MonotonicTime::now() - m_timeOrigin).milliseconds();
auto frame = WebXRFrame::create(*this, WebXRFrame::IsAnimationFrame::Yes);
// 2.Let frame be session’s animation frame.
// 3.Set frame’s time to frameTime.
frame->setTime(static_cast<DOMHighResTimeStamp>(m_frameData.predictedDisplayTime));
// 4. For each view in list of views, set view’s viewport modifiable flag to true.
// 5. If the active flag of any view in the list of views has changed since the last XR animation frame, update the viewports.
// FIXME: implement.
// FIXME: I moved step 7 before 6 because of https://github.com/immersive-web/webxr/issues/1164
// 7.If session’s pending render state is not null, apply the pending render state.
if (m_pendingRenderState)
applyPendingRenderState();
// 6. If the frame should be rendered for session:
if (frameShouldBeRendered() && m_frameData.shouldRender) {
// Prepare all layers for render
if (m_mode == XRSessionMode::ImmersiveVr && m_activeRenderState->baseLayer())
m_activeRenderState->baseLayer()->startFrame(m_frameData);
// 6.1.Set session’s list of currently running animation frame callbacks to be session’s list of animation frame callbacks.
// 6.2.Set session’s list of animation frame callbacks to the empty list.
auto callbacks = m_callbacks;
// 6.3.Set frame’s active boolean to true.
frame->setActive(true);
// 6.4.Apply frame updates for frame.
if (m_inputInitialized)
m_inputSources->update(now, m_frameData.inputSources);
// 6.5.For each entry in session’s list of currently running animation frame callbacks, in order:
for (auto& callback : callbacks) {
// 6.6.If the entry’s cancelled boolean is true, continue to the next entry.
if (callback->isFiredOrCancelled())
continue;
callback->setFiredOrCancelled();
// 6.7.Invoke the Web IDL callback function for entry, passing now and frame as the arguments
callback->handleEvent(now, frame.get());
// 6.8.If an exception is thrown, report the exception.
}
// 6.9.Set session’s list of currently running animation frame callbacks to the empty list.
m_callbacks.removeAllMatching([](auto& callback) {
return callback->isFiredOrCancelled();
});
// 6.10.Set frame’s active boolean to false.
// If the session is ended, m_animationFrame->setActive false is set in shutdown().
frame->setActive(false);
// Submit current frame layers to the device.
Vector<PlatformXR::Device::Layer> frameLayers;
if (m_mode == XRSessionMode::ImmersiveVr && m_activeRenderState->baseLayer())
frameLayers.append(m_activeRenderState->baseLayer()->endFrame());
m_device->submitFrame(WTFMove(frameLayers));
}
if (!m_callbacks.isEmpty())
requestFrame();
});
}
// https://immersive-web.github.io/webxr/#poses-may-be-reported
bool WebXRSession::posesCanBeReported(const Document& document) const
{
// 1. If session’s relevant global object is not the current global object, return false.
auto* sessionDocument = downcast<Document>(scriptExecutionContext());
if (!sessionDocument || sessionDocument->domWindow() != document.domWindow())
return false;
// 2. If session's visibilityState in not "visible", return false.
if (m_visibilityState != XRVisibilityState::Visible)
return false;
// 5. Determine if the pose data can be returned as follows:
// The procedure in the specs tries to ensure that we apply measures to
// prevent fingerprintint in pose data and return false in case we don't.
// We're going to apply them so let's just return true.
return true;
}
} // namespace WebCore
#endif // ENABLE(WEBXR)