summaryrefslogtreecommitdiffstats
path: root/gfx/vr/service/VRService.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/vr/service/VRService.cpp
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/vr/service/VRService.cpp')
-rw-r--r--gfx/vr/service/VRService.cpp418
1 files changed, 418 insertions, 0 deletions
diff --git a/gfx/vr/service/VRService.cpp b/gfx/vr/service/VRService.cpp
new file mode 100644
index 0000000000..2b774c2531
--- /dev/null
+++ b/gfx/vr/service/VRService.cpp
@@ -0,0 +1,418 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VRService.h"
+
+#include <cstring> // for memcmp
+
+#include "../VRShMem.h"
+#include "../gfxVRMutex.h"
+#include "PuppetSession.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsThread.h"
+#include "nsXULAppAPI.h"
+
+#if defined(XP_WIN)
+# include "OculusSession.h"
+#endif
+
+#if defined(XP_WIN) || defined(XP_MACOSX) || \
+ (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
+# include "OpenVRSession.h"
+#endif
+#if !defined(MOZ_WIDGET_ANDROID)
+# include "OSVRSession.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+namespace {
+
+int64_t FrameIDFromBrowserState(const mozilla::gfx::VRBrowserState& aState) {
+ for (const auto& layer : aState.layerState) {
+ if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
+ return layer.layer_stereo_immersive.frameId;
+ }
+ }
+ return 0;
+}
+
+bool IsImmersiveContentActive(const mozilla::gfx::VRBrowserState& aState) {
+ for (const auto& layer : aState.layerState) {
+ if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // anonymous namespace
+
+/*static*/
+already_AddRefed<VRService> VRService::Create(
+ volatile VRExternalShmem* aShmem) {
+ RefPtr<VRService> service = new VRService(aShmem);
+ return service.forget();
+}
+
+VRService::VRService(volatile VRExternalShmem* aShmem)
+ : mSystemState{},
+ mBrowserState{},
+ mShutdownRequested(false),
+ mLastHapticState{},
+ mFrameStartTime{} {
+ // When we have the VR process, we map the memory
+ // of mAPIShmem from GPU process and pass it to the CTOR.
+ // If we don't have the VR process, we will instantiate
+ // mAPIShmem in VRService.
+ mShmem = new VRShMem(aShmem, aShmem == nullptr /*aRequiresMutex*/);
+}
+
+VRService::~VRService() {
+ // PSA: We must store the value of any staticPrefs preferences as this
+ // destructor will be called after staticPrefs has been shut down.
+ StopInternal(true /*aFromDtor*/);
+}
+
+void VRService::Refresh() {
+ if (mShmem != nullptr && mShmem->IsDisplayStateShutdown()) {
+ Stop();
+ }
+}
+
+void VRService::Start() {
+ if (!mServiceThread) {
+ /**
+ * We must ensure that any time the service is re-started, that
+ * the VRSystemState is reset, including mSystemState.enumerationCompleted
+ * This must happen before VRService::Start returns to the caller, in order
+ * to prevent the WebVR/WebXR promises from being resolved before the
+ * enumeration has been completed.
+ */
+ memset(&mSystemState, 0, sizeof(mSystemState));
+ PushState(mSystemState);
+ RefPtr<VRService> self = this;
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread(
+ "VRService", getter_AddRefs(thread),
+ NS_NewRunnableFunction("VRService::ServiceThreadStartup", [self]() {
+ self->mBackgroundHangMonitor =
+ MakeUnique<mozilla::BackgroundHangMonitor>(
+ "VRService",
+ /* Timeout values are powers-of-two to enable us get better
+ data. 128ms is chosen for transient hangs because 8Hz
+ should be the minimally acceptable goal for Compositor
+ responsiveness (normal goal is 60Hz). */
+ 128,
+ /* 2048ms is chosen for permanent hangs because it's longer
+ * than most Compositor hangs seen in the wild, but is short
+ * enough to not miss getting native hang stacks. */
+ 2048);
+ static_cast<nsThread*>(NS_GetCurrentThread())
+ ->SetUseHangMonitor(true);
+ }));
+
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ thread.swap(mServiceThread);
+ // ServiceInitialize needs mServiceThread to be set in order to be able to
+ // assert that it's running on the right thread as well as dispatching new
+ // tasks. It can't be run within the NS_NewRunnableFunction initial event.
+ MOZ_ALWAYS_SUCCEEDS(mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceInitialize", this,
+ &VRService::ServiceInitialize)));
+ }
+}
+
+void VRService::Stop() { StopInternal(false /*aFromDtor*/); }
+
+void VRService::StopInternal(bool aFromDtor) {
+ if (mServiceThread) {
+ // We must disable the background hang monitor before we can shutdown this
+ // thread. Dispatched a last task to do so. No task will be allowed to run
+ // on the service thread after this one.
+ mServiceThread->Dispatch(NS_NewRunnableFunction(
+ "VRService::StopInternal", [self = RefPtr<VRService>(this), this] {
+ static_cast<nsThread*>(NS_GetCurrentThread())
+ ->SetUseHangMonitor(false);
+ mBackgroundHangMonitor = nullptr;
+ }));
+ mShutdownRequested = true;
+ mServiceThread->Shutdown();
+ mServiceThread = nullptr;
+ }
+
+ if (mShmem != nullptr && (aFromDtor || !mShmem->IsSharedExternalShmem())) {
+ // Only leave the VRShMem and clean up the pointer when the struct
+ // was not passed in. Otherwise, VRService will no longer have a
+ // way to access that struct if VRService starts again.
+ mShmem->LeaveShMem();
+ delete mShmem;
+ mShmem = nullptr;
+ }
+
+ mSession = nullptr;
+}
+
+bool VRService::InitShmem() { return mShmem->JoinShMem(); }
+
+bool VRService::IsInServiceThread() {
+ return mServiceThread && mServiceThread->IsOnCurrentThread();
+}
+
+void VRService::ServiceInitialize() {
+ MOZ_ASSERT(IsInServiceThread());
+
+ if (!InitShmem()) {
+ return;
+ }
+
+ mShutdownRequested = false;
+ // Get initial state from the browser
+ PullState(mBrowserState);
+
+ // Try to start a VRSession
+ UniquePtr<VRSession> session;
+
+ if (StaticPrefs::dom_vr_puppet_enabled()) {
+ // When the VR Puppet is enabled, we don't want
+ // to enumerate any real devices
+ session = MakeUnique<PuppetSession>();
+ if (!session->Initialize(mSystemState, mBrowserState.detectRuntimesOnly)) {
+ session = nullptr;
+ }
+ } else {
+ // We try Oculus first to ensure we use Oculus
+ // devices trough the most native interface
+ // when possible.
+#if defined(XP_WIN)
+ // Try Oculus
+ if (!session) {
+ session = MakeUnique<OculusSession>();
+ if (!session->Initialize(mSystemState,
+ mBrowserState.detectRuntimesOnly)) {
+ session = nullptr;
+ }
+ }
+#endif
+
+#if defined(XP_WIN) || defined(XP_MACOSX) || \
+ (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID))
+ // Try OpenVR
+ if (!session) {
+ session = MakeUnique<OpenVRSession>();
+ if (!session->Initialize(mSystemState,
+ mBrowserState.detectRuntimesOnly)) {
+ session = nullptr;
+ }
+ }
+#endif
+#if !defined(MOZ_WIDGET_ANDROID)
+ // Try OSVR
+ if (!session) {
+ session = MakeUnique<OSVRSession>();
+ if (!session->Initialize(mSystemState,
+ mBrowserState.detectRuntimesOnly)) {
+ session = nullptr;
+ }
+ }
+#endif
+
+ } // if (staticPrefs:VRPuppetEnabled())
+
+ if (session) {
+ mSession = std::move(session);
+ // Setting enumerationCompleted to true indicates to the browser
+ // that it should resolve any promises in the WebVR/WebXR API
+ // waiting for hardware detection.
+ mSystemState.enumerationCompleted = true;
+ PushState(mSystemState);
+
+ mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
+ &VRService::ServiceWaitForImmersive));
+ } else {
+ // VR hardware was not detected.
+ // We must inform the browser of the failure so it may try again
+ // later and resolve WebVR promises. A failure or shutdown is
+ // indicated by enumerationCompleted being set to true, with all
+ // other fields remaining zeroed out.
+ VRDisplayCapabilityFlags capFlags =
+ mSystemState.displayState.capabilityFlags;
+ memset(&mSystemState, 0, sizeof(mSystemState));
+ mSystemState.enumerationCompleted = true;
+
+ if (mBrowserState.detectRuntimesOnly) {
+ mSystemState.displayState.capabilityFlags = capFlags;
+ } else {
+ mSystemState.displayState.minRestartInterval =
+ StaticPrefs::dom_vr_external_notdetected_timeout();
+ }
+ mSystemState.displayState.shutdown = true;
+ PushState(mSystemState);
+ }
+}
+
+void VRService::ServiceShutdown() {
+ MOZ_ASSERT(IsInServiceThread());
+
+ // Notify the browser that we have shut down.
+ // This is indicated by enumerationCompleted being set
+ // to true, with all other fields remaining zeroed out.
+ memset(&mSystemState, 0, sizeof(mSystemState));
+ mSystemState.enumerationCompleted = true;
+ mSystemState.displayState.shutdown = true;
+ if (mSession && mSession->ShouldQuit()) {
+ mSystemState.displayState.minRestartInterval =
+ StaticPrefs::dom_vr_external_quit_timeout();
+ }
+ PushState(mSystemState);
+ mSession = nullptr;
+}
+
+void VRService::ServiceWaitForImmersive() {
+ MOZ_ASSERT(IsInServiceThread());
+ MOZ_ASSERT(mSession);
+
+ mSession->ProcessEvents(mSystemState);
+ PushState(mSystemState);
+ PullState(mBrowserState);
+
+ if (mSession->ShouldQuit() || mShutdownRequested) {
+ // Shut down
+ mServiceThread->Dispatch(NewRunnableMethod(
+ "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
+ } else if (IsImmersiveContentActive(mBrowserState)) {
+ // Enter Immersive Mode
+ mSession->StartPresentation();
+ mSession->StartFrame(mSystemState);
+ PushState(mSystemState);
+
+ mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
+ &VRService::ServiceImmersiveMode));
+ } else {
+ // Continue waiting for immersive mode
+ mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
+ &VRService::ServiceWaitForImmersive));
+ }
+}
+
+void VRService::ServiceImmersiveMode() {
+ MOZ_ASSERT(IsInServiceThread());
+ MOZ_ASSERT(mSession);
+
+ mSession->ProcessEvents(mSystemState);
+ UpdateHaptics();
+ PushState(mSystemState);
+ PullState(mBrowserState);
+
+ if (mSession->ShouldQuit() || mShutdownRequested) {
+ // Shut down
+ mServiceThread->Dispatch(NewRunnableMethod(
+ "gfx::VRService::ServiceShutdown", this, &VRService::ServiceShutdown));
+ return;
+ }
+
+ if (!IsImmersiveContentActive(mBrowserState)) {
+ // Exit immersive mode
+ mSession->StopAllHaptics();
+ mSession->StopPresentation();
+ mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceWaitForImmersive", this,
+ &VRService::ServiceWaitForImmersive));
+ return;
+ }
+
+ uint64_t newFrameId = FrameIDFromBrowserState(mBrowserState);
+ if (newFrameId != mSystemState.displayState.lastSubmittedFrameId) {
+ // A new immersive frame has been received.
+ // Submit the textures to the VR system compositor.
+ bool success = false;
+ for (const auto& layer : mBrowserState.layerState) {
+ if (layer.type == VRLayerType::LayerType_Stereo_Immersive) {
+ // SubmitFrame may block in order to control the timing for
+ // the next frame start
+ success = mSession->SubmitFrame(layer.layer_stereo_immersive);
+ break;
+ }
+ }
+
+ // Changing mLastSubmittedFrameId triggers a new frame to start
+ // rendering. Changes to mLastSubmittedFrameId and the values
+ // used for rendering, such as headset pose, must be pushed
+ // atomically to the browser.
+ mSystemState.displayState.lastSubmittedFrameId = newFrameId;
+ mSystemState.displayState.lastSubmittedFrameSuccessful = success;
+
+ // StartFrame may block to control the timing for the next frame start
+ mSession->StartFrame(mSystemState);
+ mSystemState.sensorState.inputFrameID++;
+ size_t historyIndex =
+ mSystemState.sensorState.inputFrameID % ArrayLength(mFrameStartTime);
+ mFrameStartTime[historyIndex] = TimeStamp::Now();
+ PushState(mSystemState);
+ }
+
+ // Continue immersive mode
+ mServiceThread->Dispatch(
+ NewRunnableMethod("gfx::VRService::ServiceImmersiveMode", this,
+ &VRService::ServiceImmersiveMode));
+}
+
+void VRService::UpdateHaptics() {
+ MOZ_ASSERT(IsInServiceThread());
+ MOZ_ASSERT(mSession);
+
+ for (size_t i = 0; i < ArrayLength(mBrowserState.hapticState); i++) {
+ VRHapticState& state = mBrowserState.hapticState[i];
+ VRHapticState& lastState = mLastHapticState[i];
+ // Note that VRHapticState is asserted to be a POD type, thus memcmp is safe
+ if (memcmp(&state, &lastState, sizeof(VRHapticState)) == 0) {
+ // No change since the last update
+ continue;
+ }
+ if (state.inputFrameID == 0) {
+ // The haptic feedback was stopped
+ mSession->StopVibrateHaptic(state.controllerIndex);
+ } else {
+ TimeStamp now;
+ if (now.IsNull()) {
+ // TimeStamp::Now() is expensive, so we
+ // must call it only when needed and save the
+ // output for further loop iterations.
+ now = TimeStamp::Now();
+ }
+ // This is a new haptic pulse, or we are overriding a prior one
+ size_t historyIndex = state.inputFrameID % ArrayLength(mFrameStartTime);
+ float startOffset =
+ (float)(now - mFrameStartTime[historyIndex]).ToSeconds();
+
+ // state.pulseStart is guaranteed never to be in the future
+ mSession->VibrateHaptic(
+ state.controllerIndex, state.hapticIndex, state.pulseIntensity,
+ state.pulseDuration + state.pulseStart - startOffset);
+ }
+ // Record the state for comparison in the next run
+ memcpy(&lastState, &state, sizeof(VRHapticState));
+ }
+}
+
+void VRService::PushState(const mozilla::gfx::VRSystemState& aState) {
+ if (mShmem != nullptr) {
+ mShmem->PushSystemState(aState);
+ }
+}
+
+void VRService::PullState(mozilla::gfx::VRBrowserState& aState) {
+ if (mShmem != nullptr) {
+ mShmem->PullBrowserState(aState);
+ }
+}