blob: ddd42b0089d946018ee767b301cb2ba6f29dc0f4 [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 "WebXRSystem.h"
#if ENABLE(WEBXR)
#include "Chrome.h"
#include "ChromeClient.h"
#include "DOMWindow.h"
#include "Document.h"
#include "FeaturePolicy.h"
#include "IDLTypes.h"
#include "JSWebXRSession.h"
#include "JSXRReferenceSpaceType.h"
#include "Navigator.h"
#include "Page.h"
#include "PlatformXR.h"
#include "RequestAnimationFrameCallback.h"
#include "RuntimeEnabledFeatures.h"
#include "SecurityOrigin.h"
#include "UserGestureIndicator.h"
#include "WebXRSession.h"
#include "XRReferenceSpaceType.h"
#include "XRSessionInit.h"
#include <JavaScriptCore/JSGlobalObject.h>
#include <wtf/IsoMallocInlines.h>
#include <wtf/Scope.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(WebXRSystem);
Ref<WebXRSystem> WebXRSystem::create(Navigator& navigator)
{
auto system = adoptRef(*new WebXRSystem(navigator));
system->suspendIfNeeded();
return system;
}
WebXRSystem::WebXRSystem(Navigator& navigator)
: ActiveDOMObject(navigator.scriptExecutionContext())
, m_navigator(navigator)
, m_defaultInlineDevice(*navigator.scriptExecutionContext())
{
m_inlineXRDevice = m_defaultInlineDevice;
}
WebXRSystem::~WebXRSystem() = default;
Navigator* WebXRSystem::navigator()
{
return m_navigator.get();
}
// https://immersive-web.github.io/webxr/#ensures-an-immersive-xr-device-is-selected
void WebXRSystem::ensureImmersiveXRDeviceIsSelected(CompletionHandler<void()>&& callback)
{
// Don't ask platform code for XR devices, we're using simulated ones.
if (UNLIKELY(m_testingDevices)) {
callback();
return;
}
if (m_activeImmersiveDevice) {
callback();
return;
}
// https://immersive-web.github.io/webxr/#enumerate-immersive-xr-devices
auto document = downcast<Document>(scriptExecutionContext());
if (!document || !document->page()) {
callback();
return;
}
bool isFirstXRDevicesEnumeration = !m_immersiveXRDevicesHaveBeenEnumerated;
document->page()->chrome().client().enumerateImmersiveXRDevices([this, protectedThis = Ref { *this }, isFirstXRDevicesEnumeration, callback = WTFMove(callback)](auto& immersiveXRDevices) mutable {
m_immersiveXRDevicesHaveBeenEnumerated = true;
auto callbackOnExit = makeScopeExit([&]() {
callOnMainThread(WTFMove(callback));
});
// https://immersive-web.github.io/webxr/#select-an-immersive-xr-device
auto* oldDevice = m_activeImmersiveDevice.get();
if (immersiveXRDevices.isEmpty()) {
m_activeImmersiveDevice = nullptr;
return;
}
if (immersiveXRDevices.size() == 1) {
m_activeImmersiveDevice = immersiveXRDevices.first().get();
return;
}
if (m_activeImmersiveSession && oldDevice && immersiveXRDevices.findIf([&](auto& entry) { return entry.ptr() == oldDevice; }) != notFound)
ASSERT(m_activeImmersiveDevice.get() == oldDevice);
else {
// FIXME: implement a better UA selection mechanism if required.
m_activeImmersiveDevice = immersiveXRDevices.first().get();
}
if (isFirstXRDevicesEnumeration || m_activeImmersiveDevice.get() == oldDevice) {
return;
}
// FIXME: 7. Shut down any active XRSessions.
// FIXME: 8. Set the XR compatible boolean of all WebGLRenderingContextBase instances to false.
// FIXME: 9. Queue a task to fire an event named devicechange on the context object.
});
}
void WebXRSystem::obtainCurrentDevice(XRSessionMode mode, const JSFeatureList& requiredFeatures, const JSFeatureList& optionalFeatures, CompletionHandler<void(PlatformXR::Device*)>&& callback)
{
if (mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr) {
ensureImmersiveXRDeviceIsSelected([this, callback = WTFMove(callback)]() mutable {
callback(m_activeImmersiveDevice.get());
});
return;
}
if (!requiredFeatures.isEmpty() || !optionalFeatures.isEmpty()) {
callback(m_inlineXRDevice.get());
return;
}
callback(&m_defaultInlineDevice);
}
// https://immersive-web.github.io/webxr/#dom-xrsystem-issessionsupported
void WebXRSystem::isSessionSupported(XRSessionMode mode, IsSessionSupportedPromise&& promise)
{
// 1. Let promise be a new Promise.
// 2. If mode is "inline", resolve promise with true and return it.
if (mode == XRSessionMode::Inline) {
promise.resolve(true);
return;
}
// 3. If the requesting document's origin is not allowed to use the "xr-spatial-tracking" feature policy,
// reject promise with a "SecurityError" DOMException and return it.
auto document = downcast<Document>(scriptExecutionContext());
if (!isFeaturePolicyAllowedByDocumentAndAllOwners(FeaturePolicy::Type::XRSpatialTracking, *document, LogFeaturePolicyFailure::Yes)) {
promise.reject(Exception { SecurityError });
return;
}
// 4. Run the following steps in parallel:
// 4.1 Ensure an immersive XR device is selected.
ensureImmersiveXRDeviceIsSelected([this, promise = WTFMove(promise), mode]() mutable {
// 4.2 If the immersive XR device is null, resolve promise with false and abort these steps.
if (!m_activeImmersiveDevice) {
promise.resolve(false);
return;
}
// 4.3 If the immersive XR device's list of supported modes does not contain mode, resolve promise with false and abort these steps.
if (!m_activeImmersiveDevice->supports(mode)) {
promise.resolve(false);
return;
}
// 4.4 Resolve promise with true.
promise.resolve(true);
});
}
// https://immersive-web.github.io/webxr/#immersive-session-request-is-allowed
bool WebXRSystem::immersiveSessionRequestIsAllowedForGlobalObject(DOMWindow& globalObject, Document& document) const
{
// 1. If the request was not made while the global object has transient
// activation or when launching a web application, return false
// TODO: add a check for "not launching a web application".
if (!globalObject.hasTransientActivation())
return false;
// 2. If the requesting document is not considered trustworthy, return false.
// https://immersive-web.github.io/webxr/#trustworthy.
if (&document != globalObject.document())
return false;
// https://immersive-web.github.io/webxr/#active-and-focused
if (!document.hasFocus() || !document.securityOrigin().isSameOriginAs(globalObject.document()->securityOrigin()))
return false;
// 3. If user intent to begin an immersive session is not well understood,
// either via explicit consent or implicit consent, return false
if (!UserGestureIndicator::processingUserGesture())
return false;
return true;
}
// https://immersive-web.github.io/webxr/#inline-session-request-is-allowed
bool WebXRSystem::inlineSessionRequestIsAllowedForGlobalObject(DOMWindow& globalObject, Document& document, const XRSessionInit& init) const
{
auto isEmptyOrViewer = [&document](const JSFeatureList& features) {
if (features.isEmpty())
return true;
if (features.size() == 1) {
auto feature = parseEnumeration<XRReferenceSpaceType>(*document.globalObject(), features.first());
if (feature == XRReferenceSpaceType::Viewer)
return true;
}
return false;
};
// 1. If the session request contained any required features or optional features (besides 'viewer') and the request was not made
// while the global object has transient activation or when launching a web application, return false.
bool sessionRequestContainedAnyFeature = !isEmptyOrViewer(init.optionalFeatures) || !isEmptyOrViewer(init.requiredFeatures);
if (sessionRequestContainedAnyFeature && !globalObject.hasTransientActivation() /* TODO: || !launching a web app */)
return false;
// 2. If the requesting document is not responsible, return false.
if (&document != globalObject.document())
return false;
return true;
}
struct WebXRSystem::ResolvedRequestedFeatures {
FeatureList granted;
FeatureList consentRequired;
FeatureList consentOptional;
FeatureList requested;
};
#define RETURN_FALSE_OR_CONTINUE(mustReturn) { \
if (mustReturn) {\
return false; \
} \
continue; \
}
// https://immersive-web.github.io/webxr/#resolve-the-requested-features
std::optional<WebXRSystem::ResolvedRequestedFeatures> WebXRSystem::resolveRequestedFeatures(XRSessionMode mode, const XRSessionInit& init, PlatformXR::Device* device, JSC::JSGlobalObject& globalObject) const
{
// 1. Let consentRequired be an empty list of DOMString.
// 2. Let consentOptional be an empty list of DOMString.
ResolvedRequestedFeatures resolvedFeatures;
// 3. Let device be the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.
// 4. Let granted be a list of DOMString initialized to device's list of enabled features for mode.
// 5. If device is null or device's list of supported modes does not contain mode, run the following steps:
// 5.1 Return the tuple (consentRequired, consentOptional, granted)
if (!device || !device->supports(mode))
return resolvedFeatures;
resolvedFeatures.granted = device->enabledFeatures(mode);
// 6. Add every feature descriptor in the default features table associated
// with mode to the indicated feature list if it is not already present.
// https://immersive-web.github.io/webxr/#default-features
auto requiredFeaturesWithDefaultFeatures = init.requiredFeatures;
requiredFeaturesWithDefaultFeatures.append(convertEnumerationToJS(globalObject, XRReferenceSpaceType::Viewer));
if (mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr)
requiredFeaturesWithDefaultFeatures.append(convertEnumerationToJS(globalObject, XRReferenceSpaceType::Local));
// 7. For each feature in requiredFeatures|optionalFeatures perform the following steps:
// 8. For each feature in optionalFeatures perform the following steps:
// We're merging both loops in a single lambda. The only difference is that a failure on any required features
// implies cancelling the whole process while failures in optional features are just skipped.
enum class ParsingMode { Strict, Loose };
auto parseFeatures = [&device, &globalObject, mode, &resolvedFeatures] (const JSFeatureList& sessionFeatures, ParsingMode parsingMode) -> bool {
bool returnOnFailure = parsingMode == ParsingMode::Strict;
for (const auto& sessionFeature : sessionFeatures) {
// 1. If the feature is null, continue to the next entry.
if (sessionFeature.isNull())
continue;
// FIXME: This is only testing for XRReferenceSpaceType features. It
// needs to handle optional features like "hand-tracking".
// 2. If feature is not a valid feature descriptor, perform the following steps
// 2.1. Let s be the result of calling ? ToString(feature).
// 2.2. If s is not a valid feature descriptor or is undefined, (return null|continue to next entry).
// 2.3. Set feature to s.
auto feature = parseEnumeration<XRReferenceSpaceType>(globalObject, sessionFeature);
if (!feature)
RETURN_FALSE_OR_CONTINUE(returnOnFailure);
// 3. If feature is already in granted, continue to the next entry.
if (resolvedFeatures.granted.contains(feature.value())) {
resolvedFeatures.requested.append(feature.value());
continue;
}
// 4. If the requesting document's origin is not allowed to use any feature policy required by feature
// as indicated by the feature requirements table, (return null|continue to next entry).
// 5. If session's XR device is not capable of supporting the functionality described by feature or the
// user agent has otherwise determined to reject the feature, (return null|continue to next entry).
if (!device->supportedFeatures(mode).contains(feature.value()))
RETURN_FALSE_OR_CONTINUE(returnOnFailure);
// 6. If the functionality described by feature requires explicit consent, append it to (consentRequired|consentOptional).
// 7. Else append feature to granted.
resolvedFeatures.granted.append(feature.value());
// https://immersive-web.github.io/webxr/#requested-features
// The combined list of feature descriptors given by the requiredFeatures and optionalFeatures are collectively considered the requested features for an XRSession.
resolvedFeatures.requested.append(feature.value());
}
return true;
};
if (!parseFeatures(requiredFeaturesWithDefaultFeatures, ParsingMode::Strict))
return std::nullopt;
parseFeatures(init.optionalFeatures, ParsingMode::Loose);
return resolvedFeatures;
}
// https://immersive-web.github.io/webxr/#request-the-xr-permission
void WebXRSystem::resolveFeaturePermissions(XRSessionMode mode, const XRSessionInit& init, PlatformXR::Device* device, JSC::JSGlobalObject& globalObject, CompletionHandler<void(std::optional<FeatureList>&&)>&& completionHandler) const
{
// 1. Set status's granted to an empty FrozenArray.
// 2. Let requiredFeatures be descriptor's requiredFeatures.
// 3. Let optionalFeatures be descriptor's optionalFeatures.
// 4. Let device be the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.
// 5. Let result be the result of resolving the requested features given requiredFeatures,optionalFeatures, and mode.
auto resolvedFeatures = resolveRequestedFeatures(mode, init, device, globalObject);
// 6. If result is null, run the following steps:
// 6.1. Set status's state to "denied".
// 6.2. Abort these steps.
if (!resolvedFeatures) {
completionHandler(std::nullopt);
return;
}
// 7. Let (consentRequired, consentOptional, granted) be the fields of result.
// 8. The user agent MAY at this point ask the user's permission for the calling algorithm to use any of the features
// in consentRequired and consentOptional. The results of these prompts should be included when determining if there
// is a clear signal of user intent for enabling these features.
// 9. For each feature in consentRequired perform the following steps:
// 9.1. The user agent MAY at this point ask the user's permission for the calling algorithm to use feature. The results
// of these prompts should be included when determining if there is a clear signal of user intent to enable feature.
// 9.2. If a clear signal of user intent to enable feature has not been determined, set status's state to "denied" and
// abort these steps.
// 9.3. If feature is not in granted, append feature to granted.
// 10. For each feature in consentOptional perform the following steps:
// 10.1. The user agent MAY at this point ask the user's permission for the calling algorithm to use feature. The results
// of these prompts should be included when determining if there is a clear signal of user intent to enable feature.
// 10.2. If a clear signal of user intent to enable feature has not been determined, continue to the next entry.
// 10.3. If feature is not in granted, append feature to granted.
auto document = downcast<Document>(scriptExecutionContext());
if (!document || !document->page()) {
completionHandler(std::nullopt);
return;
}
// FIXME: Replace with Permissions API implementation.
document->page()->chrome().client().requestPermissionOnXRSessionFeatures(document->securityOrigin().data(), mode, resolvedFeatures->granted, resolvedFeatures->consentRequired, resolvedFeatures->consentOptional, [device, mode, completionHandler = WTFMove(completionHandler)](std::optional<PlatformXR::Device::FeatureList>&& userGranted) mutable {
if (!userGranted) {
completionHandler(std::nullopt);
return;
}
// 11. Set status's granted to granted.
// 12. Set device's list of enabled features for mode to granted.
// 13. Set status's state to "granted".
device->setEnabledFeatures(mode, *userGranted);
completionHandler(*userGranted);
});
}
// https://immersive-web.github.io/webxr/#dom-xrsystem-requestsession
void WebXRSystem::requestSession(Document& document, XRSessionMode mode, const XRSessionInit& init, RequestSessionPromise&& promise)
{
// 1. Let promise be a new Promise.
// 2. Let immersive be true if mode is an immersive session mode, and false otherwise.
// 3. Let global object be the relevant Global object for the XRSystem on which this method was invoked.
bool immersive = mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr;
auto* globalObject = document.domWindow();
if (!globalObject) {
promise.reject(Exception { InvalidAccessError});
return;
}
// 4. Check whether the session request is allowed as follows:
if (immersive) {
if (!immersiveSessionRequestIsAllowedForGlobalObject(*globalObject, document)) {
promise.reject(Exception { SecurityError });
return;
}
if (m_pendingImmersiveSession || m_activeImmersiveSession) {
promise.reject(Exception { InvalidStateError });
return;
}
m_pendingImmersiveSession = true;
} else if (!inlineSessionRequestIsAllowedForGlobalObject(*globalObject, document, init)) {
promise.reject(Exception { SecurityError });
return;
}
// 5. Run the following steps in parallel:
// 5.1 Let requiredFeatures be options' requiredFeatures.
// 5.2 Let optionalFeatures be options' optionalFeatures.
// 5.3 Set device to the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.
// 5.4 Queue a task to perform the following steps:
obtainCurrentDevice(mode, init.requiredFeatures, init.optionalFeatures, [this, protectedDocument = Ref { document }, immersive, init, mode, promise = WTFMove(promise)](auto* device) mutable {
auto rejectPromiseWithNotSupportedError = makeScopeExit([&]() {
promise.reject(Exception { NotSupportedError });
m_pendingImmersiveSession = false;
});
// 5.4.1 If device is null or device's list of supported modes does not contain mode, run the following steps:
// - Reject promise with a "NotSupportedError" DOMException.
// - If immersive is true, set pending immersive session to false.
// - Abort these steps.
if (!device || !device->supports(mode))
return;
auto* globalObject = protectedDocument->globalObject();
if (!globalObject)
return;
rejectPromiseWithNotSupportedError.release();
// WebKit does not currently support the Permissions API. https://w3c.github.io/permissions/
// However we do implement here the permission request algorithm without the
// Permissions API bits as it handles, among others, the session features parsing. We also
// do it here before creating the session as there is no need to do it on advance.
// 5.4.4 Let descriptor be an XRPermissionDescriptor initialized with session, requiredFeatures, and optionalFeatures
// 5.4.5 Let status be an XRPermissionStatus, initially null
// 5.4.6 Request the xr permission with descriptor and status.
// 5.4.7 If status' state is "denied" run the following steps: (same as above in 5.4.1)
resolveFeaturePermissions(mode, init, device, *globalObject, [this, weakThis = WeakPtr { *this }, protectedDocument, device, immersive, mode, promise](std::optional<FeatureList>&& requestedFeatures) mutable {
if (!weakThis || !requestedFeatures) {
promise.reject(Exception { NotSupportedError });
m_pendingImmersiveSession = false;
return;
}
// 5.4.2 Let session be a new XRSession object.
// 5.4.3 Initialize the session with session, mode, and device.
auto session = WebXRSession::create(protectedDocument.get(), *this, mode, *device, WTFMove(*requestedFeatures));
// 5.4.8 Potentially set the active immersive session as follows:
if (immersive) {
m_activeImmersiveSession = session.copyRef();
m_pendingImmersiveSession = false;
} else
m_inlineSessions.add(session.copyRef());
// 5.4.9 Resolve promise with session.
promise.resolve(WTFMove(session));
// 5.4.10 is handled in WebXRSession::sessionDidInitializeInputSources.
});
});
}
const char* WebXRSystem::activeDOMObjectName() const
{
return "XRSystem";
}
void WebXRSystem::stop()
{
}
void WebXRSystem::registerSimulatedXRDeviceForTesting(PlatformXR::Device& device)
{
auto scriptExecutionContext = this->scriptExecutionContext();
if (!scriptExecutionContext || !scriptExecutionContext->settingsValues().webXREnabled)
return;
m_testingDevices++;
if (device.supports(XRSessionMode::ImmersiveVr) || device.supports(XRSessionMode::ImmersiveAr)) {
m_immersiveDevices.add(device);
m_activeImmersiveDevice = device;
}
if (device.supports(XRSessionMode::Inline))
m_inlineXRDevice = device;
}
void WebXRSystem::unregisterSimulatedXRDeviceForTesting(PlatformXR::Device& device)
{
auto scriptExecutionContext = this->scriptExecutionContext();
if (!scriptExecutionContext || !scriptExecutionContext->settingsValues().webXREnabled)
return;
ASSERT(m_testingDevices);
bool removed = m_immersiveDevices.remove(device);
ASSERT_UNUSED(removed, removed || m_inlineXRDevice == &device);
if (m_activeImmersiveDevice == &device)
m_activeImmersiveDevice = nullptr;
if (m_inlineXRDevice == &device)
m_inlineXRDevice = m_defaultInlineDevice;
m_testingDevices--;
}
void WebXRSystem::sessionEnded(WebXRSession& session)
{
if (m_activeImmersiveSession == &session)
m_activeImmersiveSession = nullptr;
m_inlineSessions.remove(session);
}
class InlineRequestAnimationFrameCallback final: public RequestAnimationFrameCallback {
public:
static Ref<InlineRequestAnimationFrameCallback> create(ScriptExecutionContext& scriptExecutionContext, Function<void()>&& callback)
{
return adoptRef(*new InlineRequestAnimationFrameCallback(scriptExecutionContext, WTFMove(callback)));
}
private:
InlineRequestAnimationFrameCallback(ScriptExecutionContext& scriptExecutionContext, Function<void()>&& callback)
: RequestAnimationFrameCallback(&scriptExecutionContext), m_callback(WTFMove(callback))
{
}
CallbackResult<void> handleEvent(double) final
{
m_callback();
return { };
}
Function<void()> m_callback;
};
WebXRSystem::DummyInlineDevice::DummyInlineDevice(ScriptExecutionContext& scriptExecutionContext)
: ContextDestructionObserver(&scriptExecutionContext)
{
setSupportedFeatures(XRSessionMode::Inline, { XRReferenceSpaceType::Viewer });
}
void WebXRSystem::DummyInlineDevice::requestFrame(PlatformXR::Device::RequestFrameCallback&& callback)
{
if (!scriptExecutionContext())
return;
// Inline XR sessions rely on document.requestAnimationFrame to perform the render loop.
auto document = downcast<Document>(scriptExecutionContext());
if (!document)
return;
auto raf = InlineRequestAnimationFrameCallback::create(*scriptExecutionContext(), [callback = WTFMove(callback)]() mutable {
PlatformXR::Device::FrameData data;
data.isTrackingValid = true;
data.isPositionValid = true;
data.shouldRender = true;
data.views.append({ });
callback(WTFMove(data));
});
document->requestAnimationFrame(raf);
}
Vector<PlatformXR::Device::ViewData> WebXRSystem::DummyInlineDevice::views(XRSessionMode) const
{
return { { .active = true, .eye = PlatformXR::Eye::None } };
}
} // namespace WebCore
#endif // ENABLE(WEBXR)