/* -*- 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 "VRPuppetCommandBuffer.h" #include "prthread.h" #include "mozilla/ClearOnShutdown.h" namespace mozilla::gfx { static StaticRefPtr sVRPuppetCommandBufferSingleton; /* static */ VRPuppetCommandBuffer& VRPuppetCommandBuffer::Get() { if (sVRPuppetCommandBufferSingleton == nullptr) { sVRPuppetCommandBufferSingleton = new VRPuppetCommandBuffer(); ClearOnShutdown(&sVRPuppetCommandBufferSingleton); } return *sVRPuppetCommandBufferSingleton; } /* static */ bool VRPuppetCommandBuffer::IsCreated() { return sVRPuppetCommandBufferSingleton != nullptr; } VRPuppetCommandBuffer::VRPuppetCommandBuffer() : mMutex("VRPuppetCommandBuffer::mMutex") { MOZ_COUNT_CTOR(VRPuppetCommandBuffer); MOZ_ASSERT(sVRPuppetCommandBufferSingleton == nullptr); Reset(); } VRPuppetCommandBuffer::~VRPuppetCommandBuffer() { MOZ_COUNT_DTOR(VRPuppetCommandBuffer); } void VRPuppetCommandBuffer::Submit(const nsTArray& aBuffer) { MutexAutoLock lock(mMutex); mBuffer.AppendElements(aBuffer); mEnded = false; mEndedWithTimeout = false; } bool VRPuppetCommandBuffer::HasEnded() { MutexAutoLock lock(mMutex); return mEnded; } void VRPuppetCommandBuffer::Reset() { MutexAutoLock lock(mMutex); memset(&mPendingState, 0, sizeof(VRSystemState)); memset(&mCommittedState, 0, sizeof(VRSystemState)); for (int iControllerIdx = 0; iControllerIdx < kVRControllerMaxCount; iControllerIdx++) { for (int iHaptic = 0; iHaptic < kNumPuppetHaptics; iHaptic++) { mHapticPulseRemaining[iControllerIdx][iHaptic] = 0.0f; mHapticPulseIntensity[iControllerIdx][iHaptic] = 0.0f; } } mDataOffset = 0; mPresentationRequested = false; mFrameSubmitted = false; mFrameAccepted = false; mTimeoutDuration = 10.0f; mWaitRemaining = 0.0f; mBlockedTime = 0.0f; mTimerElapsed = 0.0f; mEnded = true; mEndedWithTimeout = false; mLastRunTimestamp = TimeStamp(); mTimerSamples.Clear(); mBuffer.Clear(); } bool VRPuppetCommandBuffer::RunCommand(uint64_t aCommand, double aDeltaTime) { /** * Run a single command. If the command is blocking on a state change and * can't be executed, return false. * * VRPuppetCommandBuffer::RunCommand is only called by *VRPuppetCommandBuffer::Run(), which is already holding the mutex. * * Note that VRPuppetCommandBuffer::RunCommand may potentially be called >1000 *times per frame. It might not hurt to add an assert here, but we should *avoid adding code which may potentially malloc (eg string handling) even for *debug builds here. This function will need to be reasonably fast, even in *debug builds which will be using it during tests. **/ switch ((VRPuppet_Command)(aCommand & 0xff00000000000000)) { case VRPuppet_Command::VRPuppet_End: CompleteTest(false); break; case VRPuppet_Command::VRPuppet_ClearAll: memset(&mPendingState, 0, sizeof(VRSystemState)); memset(&mCommittedState, 0, sizeof(VRSystemState)); mPresentationRequested = false; mFrameSubmitted = false; mFrameAccepted = false; break; case VRPuppet_Command::VRPuppet_ClearController: { uint8_t controllerIdx = aCommand & 0x00000000000000ff; if (controllerIdx < kVRControllerMaxCount) { mPendingState.controllerState[controllerIdx].Clear(); } } break; case VRPuppet_Command::VRPuppet_Timeout: mTimeoutDuration = (double)(aCommand & 0x00000000ffffffff) / 1000.0f; break; case VRPuppet_Command::VRPuppet_Wait: if (mWaitRemaining == 0.0f) { mWaitRemaining = (double)(aCommand & 0x00000000ffffffff) / 1000.0f; // Wait timer started, block return false; } mWaitRemaining -= aDeltaTime; if (mWaitRemaining > 0.0f) { // Wait timer still running, block return false; } // Wait timer has elapsed, unblock mWaitRemaining = 0.0f; break; case VRPuppet_Command::VRPuppet_WaitSubmit: if (!mFrameSubmitted) { return false; } break; case VRPuppet_Command::VRPuppet_CaptureFrame: // TODO - Capture the frame and record the output (Bug 1555180) break; case VRPuppet_Command::VRPuppet_AcknowledgeFrame: mFrameSubmitted = false; mFrameAccepted = true; break; case VRPuppet_Command::VRPuppet_RejectFrame: mFrameSubmitted = false; mFrameAccepted = false; break; case VRPuppet_Command::VRPuppet_WaitPresentationStart: if (!mPresentationRequested) { return false; } break; case VRPuppet_Command::VRPuppet_WaitPresentationEnd: if (mPresentationRequested) { return false; } break; case VRPuppet_Command::VRPuppet_WaitHapticIntensity: { // 0x0800cchhvvvvvvvv - VRPuppet_WaitHapticIntensity(c, h, v) uint8_t iControllerIdx = (aCommand & 0x0000ff0000000000) >> 40; if (iControllerIdx >= kVRControllerMaxCount) { // Puppet test is broken, ensure it fails return false; } uint8_t iHapticIdx = (aCommand & 0x000000ff00000000) >> 32; if (iHapticIdx >= kNumPuppetHaptics) { // Puppet test is broken, ensure it fails return false; } uint32_t iHapticIntensity = aCommand & 0x00000000ffffffff; // interpreted as 16.16 fixed point SimulateHaptics(aDeltaTime); uint64_t iCurrentIntensity = round(mHapticPulseIntensity[iControllerIdx][iHapticIdx] * (1 << 16)); // convert to 16.16 fixed point if (iCurrentIntensity > 0xffffffff) { iCurrentIntensity = 0xffffffff; } if (iCurrentIntensity != iHapticIntensity) { return false; } } break; case VRPuppet_Command::VRPuppet_StartTimer: mTimerElapsed = 0.0f; break; case VRPuppet_Command::VRPuppet_StopTimer: mTimerSamples.AppendElements(mTimerElapsed); // TODO - Return the timer samples to Javascript once the command buffer // is complete (Bug 1555182) break; case VRPuppet_Command::VRPuppet_UpdateDisplay: mDataOffset = (uint8_t*)&mPendingState.displayState - (uint8_t*)&mPendingState + (aCommand & 0x00000000ffffffff); break; case VRPuppet_Command::VRPuppet_UpdateSensor: mDataOffset = (uint8_t*)&mPendingState.sensorState - (uint8_t*)&mPendingState + (aCommand & 0x00000000ffffffff); break; case VRPuppet_Command::VRPuppet_UpdateControllers: mDataOffset = (uint8_t*)&mPendingState.controllerState - (uint8_t*)&mPendingState + (aCommand & 0x00000000ffffffff); break; case VRPuppet_Command::VRPuppet_Commit: memcpy(&mCommittedState, &mPendingState, sizeof(VRSystemState)); break; case VRPuppet_Command::VRPuppet_Data7: WriteData((aCommand & 0x00ff000000000000) >> 48); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data6: WriteData((aCommand & 0x0000ff0000000000) >> 40); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data5: WriteData((aCommand & 0x000000ff00000000) >> 32); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data4: WriteData((aCommand & 0x00000000ff000000) >> 24); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data3: WriteData((aCommand & 0x0000000000ff0000) >> 16); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data2: WriteData((aCommand & 0x000000000000ff00) >> 8); [[fallthrough]]; // Purposefully, no break case VRPuppet_Command::VRPuppet_Data1: WriteData(aCommand & 0x00000000000000ff); break; } return true; } void VRPuppetCommandBuffer::WriteData(uint8_t aData) { if (mDataOffset && mDataOffset < sizeof(VRSystemState)) { ((uint8_t*)&mPendingState)[mDataOffset++] = aData; } } void VRPuppetCommandBuffer::Run() { MutexAutoLock lock(mMutex); TimeStamp now = TimeStamp::Now(); double deltaTime = 0.0f; if (!mLastRunTimestamp.IsNull()) { deltaTime = (now - mLastRunTimestamp).ToSeconds(); } mLastRunTimestamp = now; mTimerElapsed += deltaTime; size_t transactionLength = 0; while (transactionLength < mBuffer.Length() && !mEnded) { if (RunCommand(mBuffer[transactionLength], deltaTime)) { mBlockedTime = 0.0f; transactionLength++; } else { mBlockedTime += deltaTime; if (mBlockedTime > mTimeoutDuration) { CompleteTest(true); } // If a command is blocked, we don't increment transactionLength, // allowing the command to be retried on the next cycle break; } } mBuffer.RemoveElementsAt(0, transactionLength); } void VRPuppetCommandBuffer::Run(VRSystemState& aState) { Run(); // We don't want to stomp over some members bool bEnumerationCompleted = aState.enumerationCompleted; bool bShutdown = aState.displayState.shutdown; uint32_t minRestartInterval = aState.displayState.minRestartInterval; // Overwrite it all memcpy(&aState, &mCommittedState, sizeof(VRSystemState)); // Restore the members aState.enumerationCompleted = bEnumerationCompleted; aState.displayState.shutdown = bShutdown; aState.displayState.minRestartInterval = minRestartInterval; } void VRPuppetCommandBuffer::StartPresentation() { mPresentationRequested = true; Run(); } void VRPuppetCommandBuffer::StopPresentation() { mPresentationRequested = false; Run(); } bool VRPuppetCommandBuffer::SubmitFrame() { // Emulate blocking behavior of various XR API's as // described by puppet script mFrameSubmitted = true; mFrameAccepted = false; while (true) { Run(); if (!mFrameSubmitted || mEnded) { break; } PR_Sleep(PR_INTERVAL_NO_WAIT); // Yield } return mFrameAccepted; } void VRPuppetCommandBuffer::VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex, float aIntensity, float aDuration) { if (aHapticIndex >= kNumPuppetHaptics || aControllerIdx >= kVRControllerMaxCount) { return; } // We must Run() before and after updating haptic state to avoid script // deadlocks // The deadlocks would be caused by scripts that include two // VRPuppet_WaitHapticIntensity commands. If // VRPuppetCommandBuffer::VibrateHaptic() is called twice without advancing // through the command buffer with VRPuppetCommandBuffer::Run() in between, // the first VRPuppet_WaitHapticInensity may not see the transient value that // it is waiting for, thus blocking forever and deadlocking the script. Run(); mHapticPulseRemaining[aControllerIdx][aHapticIndex] = aDuration; mHapticPulseIntensity[aControllerIdx][aHapticIndex] = aIntensity; Run(); } void VRPuppetCommandBuffer::StopVibrateHaptic(uint32_t aControllerIdx) { if (aControllerIdx >= kVRControllerMaxCount) { return; } // We must Run() before and after updating haptic state to avoid script // deadlocks Run(); for (int iHaptic = 0; iHaptic < kNumPuppetHaptics; iHaptic++) { mHapticPulseRemaining[aControllerIdx][iHaptic] = 0.0f; mHapticPulseIntensity[aControllerIdx][iHaptic] = 0.0f; } Run(); } void VRPuppetCommandBuffer::StopAllHaptics() { // We must Run() before and after updating haptic state to avoid script // deadlocks Run(); for (int iControllerIdx = 0; iControllerIdx < kVRControllerMaxCount; iControllerIdx++) { for (int iHaptic = 0; iHaptic < kNumPuppetHaptics; iHaptic++) { mHapticPulseRemaining[iControllerIdx][iHaptic] = 0.0f; mHapticPulseIntensity[iControllerIdx][iHaptic] = 0.0f; } } Run(); } void VRPuppetCommandBuffer::SimulateHaptics(double aDeltaTime) { for (int iControllerIdx = 0; iControllerIdx < kVRControllerMaxCount; iControllerIdx++) { for (int iHaptic = 0; iHaptic < kNumPuppetHaptics; iHaptic++) { if (mHapticPulseIntensity[iControllerIdx][iHaptic] > 0.0f) { mHapticPulseRemaining[iControllerIdx][iHaptic] -= aDeltaTime; if (mHapticPulseRemaining[iControllerIdx][iHaptic] <= 0.0f) { mHapticPulseRemaining[iControllerIdx][iHaptic] = 0.0f; mHapticPulseIntensity[iControllerIdx][iHaptic] = 0.0f; } } } } } void VRPuppetCommandBuffer::CompleteTest(bool aTimedOut) { mEndedWithTimeout = aTimedOut; mEnded = true; } /** * Generates a sequence of VRPuppet_Data# commands, as described * in VRPuppetCommandBuffer.h, to encode the changes to be made to * a "destination" structure to match the "source" structure. * As the commands are encoded, the destination structure is updated * to match the source. * * @param aBuffer * The buffer in which the commands will be appended. * @param aSrcStart * Byte pointer to the start of the structure that * will be copied from. * @param aDstStart * Byte pointer to the start of the structure that * will be copied to. * @param aLength * Length of the structure that will be copied. * @param aUpdateCommand * VRPuppet_... command indicating which structure is being * copied: * VRPuppet_Command::VRPuppet_UpdateDisplay: * A single VRDisplayState struct * VRPuppet_Command::VRPuppet_UpdateSensor: * A single VRHMDSensorState struct * VRPuppet_Command::VRPuppet_UpdateControllers: * An array of VRControllerState structs with a * count of kVRControllerMaxCount */ void VRPuppetCommandBuffer::EncodeStruct(nsTArray& aBuffer, uint8_t* aSrcStart, uint8_t* aDstStart, size_t aLength, VRPuppet_Command aUpdateCommand) { // Naive implementation, but will not be executed in realtime, so will not // affect test timer results. Could be improved to avoid unaligned reads and // to use SSE. // Pointer to source byte being compared+copied uint8_t* src = aSrcStart; // Pointer to destination byte being compared+copied uint8_t* dst = aDstStart; // Number of bytes packed into bufData uint8_t bufLen = 0; // 64-bits to be interpreted as up to 7 separate bytes // This will form the lower 56 bits of the command uint64_t bufData = 0; // purgebuffer takes the bytes stored in bufData and generates a VRPuppet // command representing those bytes as "VRPuppet Data". // VRPUppet_Data1 encodes 1 byte // VRPuppet_Data2 encodes 2 bytes // and so on, until.. // VRPuppet_Data7 encodes 7 bytes // This command is appended to aBuffer, then bufLen and bufData are reset auto purgeBuffer = [&]() { // Only emit a command if there are data bytes in bufData if (bufLen > 0) { MOZ_ASSERT(bufLen <= 7); uint64_t command = (uint64_t)VRPuppet_Command::VRPuppet_Data1; command += ((uint64_t)VRPuppet_Command::VRPuppet_Data2 - (uint64_t)VRPuppet_Command::VRPuppet_Data1) * (bufLen - 1); command |= bufData; aBuffer.AppendElement(command); bufLen = 0; bufData = 0; } }; // Loop through the bytes of the structs. // While copying the struct at aSrcStart to aDstStart, // the differences are encoded as VRPuppet commands and // appended to aBuffer. for (size_t i = 0; i < aLength; i++) { if (*src != *dst) { // This byte is different // Copy the byte to the destination *dst = *src; if (bufLen == 0) { // This is the start of a new span of changed bytes // Output a command to specify the offset of the // span. aBuffer.AppendElement((uint64_t)aUpdateCommand + i); // Store this first byte in bufData. // We will batch up to 7 bytes in one VRPuppet_DataXX // command, so we won't emit it yet. bufLen = 1; bufData = *src; } else if (bufLen <= 6) { // This is the continuation of a span of changed bytes. // There is room to add more bytes to bufData. // Store the next byte in bufData. // We will batch up to 7 bytes in one VRPuppet_DataXX // command, so we won't emit it yet. bufData = (bufData << 8) | *src; bufLen++; } else { MOZ_ASSERT(bufLen == 7); // This is the continuation of a span of changed bytes. // There are already 7 bytes in bufData, so we must emit // the VRPuppet_Data7 command for the prior bytes before // starting a new command. aBuffer.AppendElement((uint64_t)VRPuppet_Command::VRPuppet_Data7 + bufData); // Store this byte to be included in the next VRPuppet_DataXX // command. bufLen = 1; bufData = *src; } } else { // This byte is the same. // If there are bytes in bufData, the span has now ended and we must // emit a VRPuppet_DataXX command for the accumulated bytes. // purgeBuffer will not emit any commands if there are no bytes // accumulated. purgeBuffer(); } // Advance to the next source and destination byte. ++src; ++dst; } // In the event that the very last byte of the structs differ, we must // ensure that the accumulated bytes are emitted as a VRPuppet_DataXX // command. purgeBuffer(); } } // namespace mozilla::gfx