/* -*- 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 "VRShMem.h" #ifdef MOZILLA_INTERNAL_API # include "nsString.h" # include "nsXULAppAPI.h" #endif #include "gfxVRMutex.h" #if defined(XP_MACOSX) # include # include /* For mode constants */ # include /* For O_* constants */ #elif defined(MOZ_WIDGET_ANDROID) # include "GeckoVRManager.h" #endif #if !defined(XP_WIN) # include // for ::sleep #endif using namespace mozilla::gfx; #ifdef XP_WIN static const char* kShmemName = "moz.gecko.vr_ext." SHMEM_VERSION; static LPCTSTR kMutexName = TEXT("mozilla::vr::ShmemMutex" SHMEM_VERSION); #elif defined(XP_MACOSX) static const char* kShmemName = "/moz.gecko.vr_ext." SHMEM_VERSION; #endif // XP_WIN #if !defined(MOZ_WIDGET_ANDROID) namespace { void YieldThread() { # if defined(XP_WIN) ::Sleep(0); # else ::sleep(0); # endif } } // anonymous namespace #endif // !defined(MOZ_WIDGET_ANDROID) VRShMem::VRShMem(volatile VRExternalShmem* aShmem, bool aRequiresMutex) : mExternalShmem(aShmem), mIsSharedExternalShmem(aShmem != nullptr) #if defined(XP_WIN) , mRequiresMutex(aRequiresMutex) #endif #if defined(XP_MACOSX) , mShmemFD(0) #elif defined(XP_WIN) , mShmemFile(nullptr), mMutex(nullptr) #endif { // Regarding input parameters, // - aShmem is null for VRManager or for VRService in multi-proc // - aShmem is !null for VRService in-proc (i.e., no VR proc) } // Note: This function should only be called for in-proc scenarios, where the // shared memory is only shared within the same proc (rather than across // processes). Also, this local heap memory's lifetime is tied to the class. // Callers to this must ensure that its reference doesn't outlive the owning // VRShMem instance. volatile VRExternalShmem* VRShMem::GetExternalShmem() const { #if defined(XP_MACOSX) MOZ_ASSERT(mShmemFD == 0); #elif defined(XP_WIN) MOZ_ASSERT(mShmemFile == nullptr); #endif return mExternalShmem; } bool VRShMem::IsDisplayStateShutdown() const { // adapted from VRService::Refresh // Does this need the mutex for getting .shutdown? return mExternalShmem != nullptr && mExternalShmem->state.displayState.shutdown; } // This method returns true when there is a Shmem struct allocated and // when there is a shmem handle from the OS. This implies that the struct // is mapped to shared memory rather than being allocated on the heap by // this process. bool VRShMem::IsCreatedOnSharedMemory() const { #if defined(XP_MACOSX) return HasExternalShmem() && (mShmemFD != 0); #elif defined(XP_WIN) return HasExternalShmem() && (mShmemFile != nullptr); #else // VRShMem does not support system shared memory on remaining platformss return false; #endif } // CreateShMem allocates system shared memory for mExternalShmem and // synchronization primitives to protect it. // Callers/Processes to CreateShMem should followup with CloseShMem void VRShMem::CreateShMem(bool aCreateOnSharedMemory) { if (HasExternalShmem()) { MOZ_ASSERT(mIsSharedExternalShmem && !IsCreatedOnSharedMemory()); return; } #if defined(XP_WIN) if (mMutex == nullptr) { mMutex = CreateMutex(nullptr, // default security descriptor false, // mutex not owned kMutexName); // object name if (mMutex == nullptr) { # ifdef MOZILLA_INTERNAL_API nsAutoCString msg; msg.AppendPrintf("VRManager CreateMutex error \"%lu\".", GetLastError()); NS_WARNING(msg.get()); # endif MOZ_ASSERT(false); return; } // At xpcshell extension tests, it creates multiple VRManager // instances in plug-contrainer.exe. It causes GetLastError() return // `ERROR_ALREADY_EXISTS`. However, even though `ERROR_ALREADY_EXISTS`, it // still returns the same mutex handle. // // https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-createmutexa MOZ_ASSERT(GetLastError() == 0 || GetLastError() == ERROR_ALREADY_EXISTS); } #endif // XP_WIN #if !defined(MOZ_WIDGET_ANDROID) // The VR Service accesses all hardware from a separate process // and replaces the other VRManager when enabled. // If the VR process is not enabled, create an in-process VRService. if (!aCreateOnSharedMemory) { MOZ_ASSERT(mExternalShmem == nullptr); // If the VR process is disabled, attempt to create a // VR service within the current process on the heap mExternalShmem = new VRExternalShmem(); ClearShMem(); return; } #endif MOZ_ASSERT(aCreateOnSharedMemory); #if defined(XP_MACOSX) if (mShmemFD == 0) { mShmemFD = shm_open(kShmemName, O_RDWR, S_IRUSR | S_IWUSR | S_IROTH | S_IWOTH); } if (mShmemFD <= 0) { mShmemFD = 0; return; } struct stat sb; fstat(mShmemFD, &sb); off_t length = sb.st_size; if (length < (off_t)sizeof(VRExternalShmem)) { // TODO - Implement logging (Bug 1558912) CloseShMem(); return; } mExternalShmem = (VRExternalShmem*)mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, mShmemFD, 0); if (mExternalShmem == MAP_FAILED) { // TODO - Implement logging (Bug 1558912) mExternalShmem = NULL; CloseShMem(); return; } #elif defined(XP_WIN) if (mShmemFile == nullptr) { mShmemFile = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(VRExternalShmem), kShmemName); MOZ_ASSERT(GetLastError() == 0 || GetLastError() == ERROR_ALREADY_EXISTS); MOZ_ASSERT(mShmemFile); if (mShmemFile == nullptr) { // TODO - Implement logging (Bug 1558912) CloseShMem(); return; } } LARGE_INTEGER length; length.QuadPart = sizeof(VRExternalShmem); mExternalShmem = (VRExternalShmem*)MapViewOfFile( mShmemFile, // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, 0, length.QuadPart); if (mExternalShmem == nullptr) { // TODO - Implement logging (Bug 1558912) CloseShMem(); return; } #elif defined(MOZ_WIDGET_ANDROID) MOZ_ASSERT(false, "CreateShMem should not be called for Android. Use " "CreateShMemForAndroid instead"); #endif } // This function sets mExternalShmem in the Android/GeckoView // scenarios where the host creates it in-memory and VRShMem // accesses it via GeckVRManager. void VRShMem::CreateShMemForAndroid() { #if defined(MOZ_WIDGET_ANDROID) && defined(MOZILLA_INTERNAL_API) mExternalShmem = (VRExternalShmem*)mozilla::GeckoVRManager::GetExternalContext(); if (!mExternalShmem) { return; } else { mIsSharedExternalShmem = true; } int32_t version = -1; int32_t size = 0; if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->systemMutex)) == 0) { version = mExternalShmem->version; size = mExternalShmem->size; pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex)); } else { return; } if (version != kVRExternalVersion) { mExternalShmem = nullptr; return; } if (size != sizeof(VRExternalShmem)) { mExternalShmem = nullptr; return; } #endif } void VRShMem::ClearShMem() { if (mExternalShmem != nullptr) { #ifdef MOZILLA_INTERNAL_API // VRExternalShmem is asserted to be POD mExternalShmem->Clear(); #else memset((void*)mExternalShmem, 0, sizeof(VRExternalShmem)); #endif } } // The cleanup corresponding to CreateShMem void VRShMem::CloseShMem() { #if !defined(MOZ_WIDGET_ANDROID) if (!IsCreatedOnSharedMemory()) { MOZ_ASSERT(!mIsSharedExternalShmem); if (mExternalShmem) { delete mExternalShmem; mExternalShmem = nullptr; } return; } #endif #if defined(XP_MACOSX) if (mExternalShmem) { munmap((void*)mExternalShmem, sizeof(VRExternalShmem)); mExternalShmem = NULL; } if (mShmemFD) { close(mShmemFD); mShmemFD = 0; } #elif defined(XP_WIN) if (mExternalShmem) { UnmapViewOfFile((void*)mExternalShmem); mExternalShmem = nullptr; } if (mShmemFile) { CloseHandle(mShmemFile); mShmemFile = nullptr; } #elif defined(MOZ_WIDGET_ANDROID) mExternalShmem = NULL; #endif #if defined(XP_WIN) if (mMutex) { MOZ_ASSERT(mRequiresMutex); CloseHandle(mMutex); mMutex = nullptr; } #endif } // Called to use an existing shmem instance created by another process // Callers to JoinShMem should call LeaveShMem for cleanup bool VRShMem::JoinShMem() { #if defined(XP_WIN) if (!mMutex && mRequiresMutex) { // Check that there are no errors before making system calls MOZ_ASSERT(GetLastError() == 0); mMutex = OpenMutex(MUTEX_ALL_ACCESS, // request full access false, // handle not inheritable kMutexName); // object name if (mMutex == nullptr) { # ifdef MOZILLA_INTERNAL_API nsAutoCString msg; msg.AppendPrintf("VRService OpenMutex error \"%lu\".", GetLastError()); NS_WARNING(msg.get()); # endif return false; } MOZ_ASSERT(GetLastError() == 0); } #endif if (HasExternalShmem()) { // An ExternalShmem is already set. No need to override and rejoin return true; } #if defined(XP_WIN) // Opening a file-mapping object by name base::ProcessHandle targetHandle = OpenFileMappingA(FILE_MAP_ALL_ACCESS, // read/write access FALSE, // do not inherit the name kShmemName); // name of mapping object MOZ_ASSERT(GetLastError() == 0); LARGE_INTEGER length; length.QuadPart = sizeof(VRExternalShmem); mExternalShmem = (VRExternalShmem*)MapViewOfFile( reinterpret_cast( targetHandle), // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, 0, length.QuadPart); MOZ_ASSERT(GetLastError() == 0); // TODO - Implement logging (Bug 1558912) mShmemFile = targetHandle; if (!mExternalShmem) { MOZ_ASSERT(mExternalShmem); return false; } #else // TODO: Implement shmem for other platforms. // // TODO: ** Does this mean that ShMem only works in Windows for now? If so, // let's delete the code from other platforms (Bug 1563234) MOZ_ASSERT(false, "JoinShMem not implemented"); #endif return true; } // The cleanup corresponding to JoinShMem void VRShMem::LeaveShMem() { #if defined(XP_WIN) // Check that there are no errors before making system calls MOZ_ASSERT(GetLastError() == 0); if (mShmemFile) { ::CloseHandle(mShmemFile); mShmemFile = nullptr; } #endif if (mExternalShmem != nullptr) { #if defined(XP_WIN) if (IsCreatedOnSharedMemory()) { UnmapViewOfFile((void*)mExternalShmem); MOZ_ASSERT(GetLastError() == 0); } // Otherwise, if not created on shared memory, simply null the shared // reference to the heap object. The call to CloseShMem will appropriately // free the allocation. #endif mExternalShmem = nullptr; } #if defined(XP_WIN) if (mMutex) { MOZ_ASSERT(mRequiresMutex); CloseHandle(mMutex); mMutex = nullptr; } #endif } void VRShMem::PushBrowserState(VRBrowserState& aBrowserState, bool aNotifyCond) { if (!mExternalShmem) { return; } #if defined(MOZ_WIDGET_ANDROID) if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->geckoMutex)) == 0) { memcpy((void*)&(mExternalShmem->geckoState), (void*)&aBrowserState, sizeof(VRBrowserState)); if (aNotifyCond) { pthread_cond_signal((pthread_cond_t*)&(mExternalShmem->geckoCond)); } pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->geckoMutex)); } #else bool status = true; # if defined(XP_WIN) WaitForMutex lock(mMutex); status = lock.GetStatus(); # endif // defined(XP_WIN) if (status) { mExternalShmem->geckoGenerationA = mExternalShmem->geckoGenerationA + 1; memcpy((void*)&(mExternalShmem->geckoState), (void*)&aBrowserState, sizeof(VRBrowserState)); mExternalShmem->geckoGenerationB = mExternalShmem->geckoGenerationB + 1; } #endif // defined(MOZ_WIDGET_ANDROID) } void VRShMem::PullBrowserState(mozilla::gfx::VRBrowserState& aState) { if (!mExternalShmem) { return; } // Copying the browser state from the shmem is non-blocking // on x86/x64 architectures. Arm requires a mutex that is // locked for the duration of the memcpy to and from shmem on // both sides. // On x86/x64 It is fallable -- If a dirty copy is detected by // a mismatch of geckoGenerationA and geckoGenerationB, // the copy is discarded and will not replace the last known // browser state. #if defined(MOZ_WIDGET_ANDROID) // TODO: This code is out-of-date and fails to compile, as // VRService isn't compiled for Android (Bug 1563234) /* if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->geckoMutex)) == 0) { memcpy(&aState, &tmp.geckoState, sizeof(VRBrowserState)); pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->geckoMutex)); } */ MOZ_ASSERT(false, "PullBrowserState not implemented"); #else bool status = true; # if defined(XP_WIN) if (mRequiresMutex) { // TODO: Is this scoped lock okay? Seems like it should allow some // race condition (Bug 1563234) WaitForMutex lock(mMutex); status = lock.GetStatus(); } # endif // defined(XP_WIN) if (status) { VRExternalShmem tmp; if (mExternalShmem->geckoGenerationA != mBrowserGeneration) { // TODO - (void *) cast removes volatile semantics. // The memcpy is not likely to be optimized out, but is theoretically // possible. Suggest refactoring to either explicitly enforce memory // order or to use locks. memcpy(&tmp, (void*)mExternalShmem, sizeof(VRExternalShmem)); if (tmp.geckoGenerationA == tmp.geckoGenerationB && tmp.geckoGenerationA != 0) { memcpy(&aState, &tmp.geckoState, sizeof(VRBrowserState)); mBrowserGeneration = tmp.geckoGenerationA; } } } #endif // defined(MOZ_WIDGET_ANDROID) } void VRShMem::PushSystemState(const mozilla::gfx::VRSystemState& aState) { if (!mExternalShmem) { return; } // Copying the VR service state to the shmem is atomic, infallable, // and non-blocking on x86/x64 architectures. Arm requires a mutex // that is locked for the duration of the memcpy to and from shmem on // both sides. #if defined(MOZ_WIDGET_ANDROID) // TODO: This code is out-of-date and fails to compile, as // VRService isn't compiled for Android (Bug 1563234) MOZ_ASSERT(false, "JoinShMem not implemented"); /* if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->systemMutex)) == 0) { // We are casting away the volatile keyword, which is not accepted by // memcpy. It is possible (although very unlikely) that the compiler // may optimize out the memcpy here as memcpy isn't explicitly safe for // volatile memory in the C++ standard. memcpy((void*)&mExternalShmem->state, &aState, sizeof(VRSystemState)); pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex)); } */ #else bool lockState = true; # if defined(XP_WIN) if (mRequiresMutex) { // TODO: Is this scoped lock okay? Seems like it should allow some // race condition (Bug 1563234) WaitForMutex lock(mMutex); lockState = lock.GetStatus(); } # endif // defined(XP_WIN) if (lockState) { mExternalShmem->generationA = mExternalShmem->generationA + 1; memcpy((void*)&mExternalShmem->state, &aState, sizeof(VRSystemState)); mExternalShmem->generationB = mExternalShmem->generationB + 1; } #endif // defined(MOZ_WIDGET_ANDROID) } #if defined(MOZ_WIDGET_ANDROID) void VRShMem::PullSystemState( VRDisplayState& aDisplayState, VRHMDSensorState& aSensorState, std::array* const aControllerState, bool& aEnumerationCompleted, const std::function& aWaitCondition /* = nullptr */) { if (!mExternalShmem) { return; } bool done = false; while (!done) { if (pthread_mutex_lock((pthread_mutex_t*)&(mExternalShmem->systemMutex)) == 0) { while (true) { memcpy(&aDisplayState, (void*)&(mExternalShmem->state.displayState), sizeof(VRDisplayState)); memcpy(&aSensorState, (void*)&(mExternalShmem->state.sensorState), sizeof(VRHMDSensorState)); memcpy(aControllerState->data(), (void*)&(mExternalShmem->state.controllerState), sizeof(aControllerState->at(0)) * aControllerState->size()); aEnumerationCompleted = mExternalShmem->state.enumerationCompleted; if (!aWaitCondition || aWaitCondition()) { done = true; break; } // Block current thead using the condition variable until data // changes pthread_cond_wait((pthread_cond_t*)&mExternalShmem->systemCond, (pthread_mutex_t*)&mExternalShmem->systemMutex); } // while (true) pthread_mutex_unlock((pthread_mutex_t*)&(mExternalShmem->systemMutex)); } else if (!aWaitCondition) { // pthread_mutex_lock failed and we are not waiting for a condition to // exit from PullState call. return; } } // while (!done) { } #else void VRShMem::PullSystemState( VRDisplayState& aDisplayState, VRHMDSensorState& aSensorState, std::array* const aControllerState, bool& aEnumerationCompleted, const std::function& aWaitCondition /* = nullptr */) { MOZ_ASSERT(mExternalShmem); if (!mExternalShmem) { return; } while (true) { { // Scope for WaitForMutex # if defined(XP_WIN) bool status = true; WaitForMutex lock(mMutex); status = lock.GetStatus(); if (status) { # endif // defined(XP_WIN) VRExternalShmem tmp; memcpy(&tmp, (void*)mExternalShmem, sizeof(VRExternalShmem)); bool isCleanCopy = tmp.generationA == tmp.generationB && tmp.generationA != 0; if (isCleanCopy) { memcpy(&aDisplayState, &tmp.state.displayState, sizeof(VRDisplayState)); memcpy(&aSensorState, &tmp.state.sensorState, sizeof(VRHMDSensorState)); *aControllerState = tmp.state.controllerState; aEnumerationCompleted = tmp.state.enumerationCompleted; // Check for wait condition if (!aWaitCondition || aWaitCondition()) { return; } } else if (!aWaitCondition) { // We did not get a clean copy, and we are not waiting for a condition // to exit from PullState call. return; } // Yield the thread while polling YieldThread(); # if defined(XP_WIN) } else if (!aWaitCondition) { // WaitForMutex failed and we are not waiting for a condition to // exit from PullState call. return; } # endif // defined(XP_WIN) } // End: Scope for WaitForMutex // Yield the thread while polling YieldThread(); } // while (!true) } #endif // defined(MOZ_WIDGET_ANDROID) void VRShMem::PushWindowState(VRWindowState& aState) { #if defined(XP_WIN) if (!mExternalShmem) { return; } bool status = true; WaitForMutex lock(mMutex); status = lock.GetStatus(); if (status) { memcpy((void*)&(mExternalShmem->windowState), (void*)&aState, sizeof(VRWindowState)); } #endif // defined(XP_WIN) } void VRShMem::PullWindowState(VRWindowState& aState) { #if defined(XP_WIN) if (!mExternalShmem) { return; } bool status = true; WaitForMutex lock(mMutex); status = lock.GetStatus(); if (status) { memcpy((void*)&aState, (void*)&(mExternalShmem->windowState), sizeof(VRWindowState)); } #endif // defined(XP_WIN) } void VRShMem::PushTelemetryState(VRTelemetryState& aState) { #if defined(XP_WIN) if (!mExternalShmem) { return; } bool status = true; WaitForMutex lock(mMutex); status = lock.GetStatus(); if (status) { memcpy((void*)&(mExternalShmem->telemetryState), (void*)&aState, sizeof(VRTelemetryState)); } #endif // defined(XP_WIN) } void VRShMem::PullTelemetryState(VRTelemetryState& aState) { #if defined(XP_WIN) if (!mExternalShmem) { return; } bool status = true; WaitForMutex lock(mMutex); status = lock.GetStatus(); if (status) { memcpy((void*)&aState, (void*)&(mExternalShmem->telemetryState), sizeof(VRTelemetryState)); } #endif // defined(XP_WIN) } void VRShMem::SendEvent(uint64_t aWindowID, mozilla::gfx::VRFxEventType aEventType, mozilla::gfx::VRFxEventState aEventState) { MOZ_ASSERT(!HasExternalShmem()); if (JoinShMem()) { mozilla::gfx::VRWindowState windowState = {0}; PullWindowState(windowState); windowState.windowID = aWindowID; windowState.eventType = aEventType; windowState.eventState = aEventState; PushWindowState(windowState); LeaveShMem(); #if defined(XP_WIN) // Notify the waiting host process that the data is now available HANDLE hSignal = ::OpenEventA(EVENT_ALL_ACCESS, // dwDesiredAccess FALSE, // bInheritHandle windowState.signalName // lpName ); ::SetEvent(hSignal); ::CloseHandle(hSignal); #endif // defined(XP_WIN) } } void VRShMem::SendIMEState(uint64_t aWindowID, mozilla::gfx::VRFxEventState aEventState) { SendEvent(aWindowID, mozilla::gfx::VRFxEventType::IME, aEventState); } void VRShMem::SendFullscreenState(uint64_t aWindowID, bool aFullscreen) { SendEvent(aWindowID, mozilla::gfx::VRFxEventType::FULLSCREEN, aFullscreen ? mozilla::gfx::VRFxEventState::FULLSCREEN_ENTER : mozilla::gfx::VRFxEventState::FULLSCREEN_EXIT); } // Note: this should be called from the VRShMem instance that created // the external shmem rather than joined it. void VRShMem::SendShutdowmState(uint64_t aWindowID) { MOZ_ASSERT(HasExternalShmem()); mozilla::gfx::VRWindowState windowState = {0}; PullWindowState(windowState); windowState.windowID = aWindowID; windowState.eventType = mozilla::gfx::VRFxEventType::SHUTDOWN; PushWindowState(windowState); #if defined(XP_WIN) // Notify the waiting host process that the data is now available HANDLE hSignal = ::OpenEventA(EVENT_ALL_ACCESS, // dwDesiredAccess FALSE, // bInheritHandle windowState.signalName // lpName ); ::SetEvent(hSignal); ::CloseHandle(hSignal); #endif // defined(XP_WIN) }