nuclear@0: /************************************************************************************ nuclear@0: nuclear@0: Filename : CAPI_FrameTimeManager.cpp nuclear@0: Content : Manage frame timing and pose prediction for rendering nuclear@0: Created : November 30, 2013 nuclear@0: Authors : Volga Aksoy, Michael Antonov nuclear@0: nuclear@0: Copyright : Copyright 2014 Oculus VR, LLC All Rights reserved. nuclear@0: nuclear@0: Licensed under the Oculus VR Rift SDK License Version 3.2 (the "License"); nuclear@0: you may not use the Oculus VR Rift SDK except in compliance with the License, nuclear@0: which is provided at the time of installation or download, or which nuclear@0: otherwise accompanies this software in either electronic or hard copy form. nuclear@0: nuclear@0: You may obtain a copy of the License at nuclear@0: nuclear@0: http://www.oculusvr.com/licenses/LICENSE-3.2 nuclear@0: nuclear@0: Unless required by applicable law or agreed to in writing, the Oculus VR SDK nuclear@0: distributed under the License is distributed on an "AS IS" BASIS, nuclear@0: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. nuclear@0: See the License for the specific language governing permissions and nuclear@0: limitations under the License. nuclear@0: nuclear@0: ************************************************************************************/ nuclear@0: nuclear@0: #include "CAPI_FrameTimeManager.h" nuclear@0: nuclear@0: #include "../Kernel/OVR_Log.h" nuclear@0: nuclear@0: namespace OVR { namespace CAPI { nuclear@0: nuclear@0: nuclear@0: //------------------------------------------------------------------------------------- nuclear@0: // ***** FrameLatencyTracker nuclear@0: nuclear@0: nuclear@0: FrameLatencyTracker::FrameLatencyTracker() nuclear@0: { nuclear@0: Reset(); nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameLatencyTracker::Reset() nuclear@0: { nuclear@0: TrackerEnabled = true; nuclear@0: WaitMode = SampleWait_Zeroes; nuclear@0: MatchCount = 0; nuclear@0: memset(FrameEndTimes, 0, sizeof(FrameEndTimes)); nuclear@0: FrameIndex = 0; nuclear@0: //FrameDeltas nuclear@0: RenderLatencySeconds = 0.0; nuclear@0: TimewarpLatencySeconds = 0.0; nuclear@0: LatencyRecordTime = 0.0; nuclear@0: nuclear@0: FrameDeltas.Clear(); nuclear@0: } nuclear@0: nuclear@0: nuclear@0: unsigned char FrameLatencyTracker::GetNextDrawColor() nuclear@0: { nuclear@0: if (!TrackerEnabled || (WaitMode == SampleWait_Zeroes) || nuclear@0: (FrameIndex >= FramesTracked)) nuclear@0: { nuclear@0: return (unsigned char)Util::FrameTimeRecord::ReadbackIndexToColor(0); nuclear@0: } nuclear@0: nuclear@0: OVR_ASSERT(FrameIndex < FramesTracked); nuclear@0: return (unsigned char)Util::FrameTimeRecord::ReadbackIndexToColor(FrameIndex+1); nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameLatencyTracker::SaveDrawColor(unsigned char drawColor, double endFrameTime, nuclear@0: double renderIMUTime, double timewarpIMUTime ) nuclear@0: { nuclear@0: if (!TrackerEnabled || (WaitMode == SampleWait_Zeroes)) nuclear@0: return; nuclear@0: nuclear@0: if (FrameIndex < FramesTracked) nuclear@0: { nuclear@0: OVR_ASSERT(Util::FrameTimeRecord::ReadbackIndexToColor(FrameIndex+1) == drawColor); nuclear@0: OVR_UNUSED(drawColor); nuclear@0: nuclear@0: // saves {color, endFrame time} nuclear@0: FrameEndTimes[FrameIndex].ReadbackIndex = FrameIndex + 1; nuclear@0: FrameEndTimes[FrameIndex].TimeSeconds = endFrameTime; nuclear@0: FrameEndTimes[FrameIndex].RenderIMUTimeSeconds = renderIMUTime; nuclear@0: FrameEndTimes[FrameIndex].TimewarpIMUTimeSeconds= timewarpIMUTime; nuclear@0: FrameEndTimes[FrameIndex].MatchedRecord = false; nuclear@0: FrameIndex++; nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: // If the request was outstanding for too long, switch to zero mode to restart. nuclear@0: if (endFrameTime > (FrameEndTimes[FrameIndex-1].TimeSeconds + 0.15)) nuclear@0: { nuclear@0: if (MatchCount == 0) nuclear@0: { nuclear@0: // If nothing was matched, we have no latency reading. nuclear@0: RenderLatencySeconds = 0.0; nuclear@0: TimewarpLatencySeconds = 0.0; nuclear@0: } nuclear@0: nuclear@0: WaitMode = SampleWait_Zeroes; nuclear@0: MatchCount = 0; nuclear@0: FrameIndex = 0; nuclear@0: } nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameLatencyTracker::MatchRecord(const Util::FrameTimeRecordSet &r) nuclear@0: { nuclear@0: if (!TrackerEnabled) nuclear@0: return; nuclear@0: nuclear@0: if (WaitMode == SampleWait_Zeroes) nuclear@0: { nuclear@0: // Do we have all zeros? nuclear@0: if (r.IsAllZeroes()) nuclear@0: { nuclear@0: OVR_ASSERT(FrameIndex == 0); nuclear@0: WaitMode = SampleWait_Match; nuclear@0: MatchCount = 0; nuclear@0: } nuclear@0: return; nuclear@0: } nuclear@0: nuclear@0: // We are in Match Mode. Wait until all colors are matched or timeout, nuclear@0: // at which point we go back to zeros. nuclear@0: nuclear@0: for (int i = 0; i < FrameIndex; i++) nuclear@0: { nuclear@0: int recordIndex = 0; nuclear@0: int consecutiveMatch = 0; nuclear@0: nuclear@0: OVR_ASSERT(FrameEndTimes[i].ReadbackIndex != 0); nuclear@0: nuclear@0: if (r.FindReadbackIndex(&recordIndex, FrameEndTimes[i].ReadbackIndex)) nuclear@0: { nuclear@0: // Advance forward to see that we have several more matches. nuclear@0: int ri = recordIndex + 1; nuclear@0: int j = i + 1; nuclear@0: nuclear@0: consecutiveMatch++; nuclear@0: nuclear@0: for (; (j < FrameIndex) && (ri < Util::FrameTimeRecordSet::RecordCount); j++, ri++) nuclear@0: { nuclear@0: if (r[ri].ReadbackIndex != FrameEndTimes[j].ReadbackIndex) nuclear@0: break; nuclear@0: consecutiveMatch++; nuclear@0: } nuclear@0: nuclear@0: // Match at least 2 items in the row, to avoid accidentally matching color. nuclear@0: if (consecutiveMatch > 1) nuclear@0: { nuclear@0: // Record latency values for all but last samples. Keep last 2 samples nuclear@0: // for the future to simplify matching. nuclear@0: for (int q = 0; q < consecutiveMatch; q++) nuclear@0: { nuclear@0: const Util::FrameTimeRecord &scanoutFrame = r[recordIndex+q]; nuclear@0: FrameTimeRecordEx &renderFrame = FrameEndTimes[i+q]; nuclear@0: nuclear@0: if (!renderFrame.MatchedRecord) nuclear@0: { nuclear@0: double deltaSeconds = scanoutFrame.TimeSeconds - renderFrame.TimeSeconds; nuclear@0: if (deltaSeconds > 0.0) nuclear@0: { nuclear@0: FrameDeltas.AddTimeDelta(deltaSeconds); nuclear@0: nuclear@0: // FIRMWARE HACK: don't take new readings if they're 10ms higher than previous reading nuclear@0: // but only do that for 1 second, after that accept it regardless of the timing difference nuclear@0: double newRenderLatency = scanoutFrame.TimeSeconds - renderFrame.RenderIMUTimeSeconds; nuclear@0: if( newRenderLatency < RenderLatencySeconds + 0.01 || nuclear@0: scanoutFrame.TimeSeconds > LatencyRecordTime + 1.0) nuclear@0: { nuclear@0: LatencyRecordTime = scanoutFrame.TimeSeconds; nuclear@0: RenderLatencySeconds = scanoutFrame.TimeSeconds - renderFrame.RenderIMUTimeSeconds; nuclear@0: TimewarpLatencySeconds = (renderFrame.TimewarpIMUTimeSeconds == 0.0) ? 0.0 : nuclear@0: (scanoutFrame.TimeSeconds - renderFrame.TimewarpIMUTimeSeconds); nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: renderFrame.MatchedRecord = true; nuclear@0: MatchCount++; nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: // Exit for. nuclear@0: break; nuclear@0: } nuclear@0: } nuclear@0: } // for ( i => FrameIndex ) nuclear@0: nuclear@0: nuclear@0: // If we matched all frames, start over. nuclear@0: if (MatchCount == FramesTracked) nuclear@0: { nuclear@0: WaitMode = SampleWait_Zeroes; nuclear@0: MatchCount = 0; nuclear@0: FrameIndex = 0; nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: bool FrameLatencyTracker::IsLatencyTimingAvailable() nuclear@0: { nuclear@0: return ovr_GetTimeInSeconds() < (LatencyRecordTime + 2.0); nuclear@0: } nuclear@0: nuclear@0: void FrameLatencyTracker::GetLatencyTimings(float& latencyRender, float& latencyTimewarp, float& latencyPostPresent) nuclear@0: { nuclear@0: if (!IsLatencyTimingAvailable()) nuclear@0: { nuclear@0: latencyRender = 0.0f; nuclear@0: latencyTimewarp = 0.0f; nuclear@0: latencyPostPresent = 0.0f; nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: latencyRender = (float)RenderLatencySeconds; nuclear@0: latencyTimewarp = (float)TimewarpLatencySeconds; nuclear@0: latencyPostPresent = (float)FrameDeltas.GetMedianTimeDelta(); nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: nuclear@0: //------------------------------------------------------------------------------------- nuclear@0: // ***** FrameTimeManager nuclear@0: nuclear@0: FrameTimeManager::FrameTimeManager(bool vsyncEnabled) : nuclear@0: RenderInfo(), nuclear@0: FrameTimeDeltas(), nuclear@0: DistortionRenderTimes(), nuclear@0: ScreenLatencyTracker(), nuclear@0: VsyncEnabled(vsyncEnabled), nuclear@0: DynamicPrediction(true), nuclear@0: SdkRender(false), nuclear@0: //DirectToRift(false), Initialized below. nuclear@0: //VSyncToScanoutDelay(0.0), Initialized below. nuclear@0: //NoVSyncToScanoutDelay(0.0), Initialized below. nuclear@0: ScreenSwitchingDelay(0.0), nuclear@0: FrameTiming(), nuclear@0: LocklessTiming(), nuclear@0: RenderIMUTimeSeconds(0.0), nuclear@0: TimewarpIMUTimeSeconds(0.0) nuclear@0: { nuclear@0: // If driver is in use, nuclear@0: DirectToRift = !Display::InCompatibilityMode(false); nuclear@0: if (DirectToRift) nuclear@0: { nuclear@0: // The latest driver provides a post-present vsync-to-scan-out delay nuclear@0: // that is roughly zero. The latency tester will provide real numbers nuclear@0: // but when it is unavailable for some reason, we should default to nuclear@0: // an expected value. nuclear@0: VSyncToScanoutDelay = 0.0001f; nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: // HACK: SyncToScanoutDelay observed close to 1 frame in video cards. nuclear@0: // Overwritten by dynamic latency measurement on DK2. nuclear@0: VSyncToScanoutDelay = 0.013f; nuclear@0: } nuclear@0: NoVSyncToScanoutDelay = 0.004f; nuclear@0: } nuclear@0: nuclear@0: void FrameTimeManager::Init(HmdRenderInfo& renderInfo) nuclear@0: { nuclear@0: // Set up prediction distances. nuclear@0: // With-Vsync timings. nuclear@0: RenderInfo = renderInfo; nuclear@0: nuclear@0: ScreenSwitchingDelay = RenderInfo.Shutter.PixelSettleTime * 0.5f + nuclear@0: RenderInfo.Shutter.PixelPersistence * 0.5f; nuclear@0: } nuclear@0: nuclear@0: void FrameTimeManager::ResetFrameTiming(unsigned frameIndex, nuclear@0: bool dynamicPrediction, nuclear@0: bool sdkRender) nuclear@0: { nuclear@0: DynamicPrediction = dynamicPrediction; nuclear@0: SdkRender = sdkRender; nuclear@0: nuclear@0: FrameTimeDeltas.Clear(); nuclear@0: DistortionRenderTimes.Clear(); nuclear@0: ScreenLatencyTracker.Reset(); nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: //TimewarpAdjuster.Reset(); nuclear@0: nuclear@0: FrameTiming.FrameIndex = frameIndex; nuclear@0: FrameTiming.NextFrameTime = 0.0; nuclear@0: FrameTiming.ThisFrameTime = 0.0; nuclear@0: FrameTiming.Inputs.FrameDelta = calcFrameDelta(); nuclear@0: // This one is particularly critical, and has been missed in the past because nuclear@0: // this init function wasn't called for app-rendered. nuclear@0: FrameTiming.Inputs.ScreenDelay = calcScreenDelay(); nuclear@0: FrameTiming.Inputs.TimewarpWaitDelta = 0.0f; nuclear@0: nuclear@0: LocklessTiming.SetState(FrameTiming); nuclear@0: } nuclear@0: nuclear@0: nuclear@0: double FrameTimeManager::calcFrameDelta() const nuclear@0: { nuclear@0: // Timing difference between frame is tracked by FrameTimeDeltas, or nuclear@0: // is a hard-coded value of 1/FrameRate. nuclear@0: double frameDelta; nuclear@0: nuclear@0: if (!VsyncEnabled) nuclear@0: { nuclear@0: frameDelta = 0.0; nuclear@0: } nuclear@0: else if (FrameTimeDeltas.GetCount() > 3) nuclear@0: { nuclear@0: frameDelta = FrameTimeDeltas.GetMedianTimeDelta(); nuclear@0: if (frameDelta > (RenderInfo.Shutter.VsyncToNextVsync + 0.001)) nuclear@0: frameDelta = RenderInfo.Shutter.VsyncToNextVsync; nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: frameDelta = RenderInfo.Shutter.VsyncToNextVsync; nuclear@0: } nuclear@0: nuclear@0: return frameDelta; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: double FrameTimeManager::calcScreenDelay() const nuclear@0: { nuclear@0: double screenDelay = ScreenSwitchingDelay; nuclear@0: double measuredVSyncToScanout; nuclear@0: nuclear@0: // Use real-time DK2 latency tester HW for prediction if its is working. nuclear@0: // Do sanity check under 60 ms nuclear@0: if (!VsyncEnabled) nuclear@0: { nuclear@0: screenDelay += NoVSyncToScanoutDelay; nuclear@0: } nuclear@0: else if ( DynamicPrediction && nuclear@0: (ScreenLatencyTracker.FrameDeltas.GetCount() > 3) && nuclear@0: (measuredVSyncToScanout = ScreenLatencyTracker.FrameDeltas.GetMedianTimeDelta(), nuclear@0: (measuredVSyncToScanout > -0.0001) && (measuredVSyncToScanout < 0.06)) ) nuclear@0: { nuclear@0: screenDelay += measuredVSyncToScanout; nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: screenDelay += VSyncToScanoutDelay; nuclear@0: } nuclear@0: nuclear@0: return screenDelay; nuclear@0: } nuclear@0: nuclear@0: double FrameTimeManager::calcTimewarpWaitDelta() const nuclear@0: { nuclear@0: // If timewarp timing hasn't been calculated, we should wait. nuclear@0: if (!VsyncEnabled) nuclear@0: return 0.0; nuclear@0: nuclear@0: if (SdkRender) nuclear@0: { nuclear@0: if (NeedDistortionTimeMeasurement()) nuclear@0: return 0.0; nuclear@0: return -(DistortionRenderTimes.GetMedianTimeDelta() + 0.0035); nuclear@0: nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: /*return -(DistortionRenderTimes.GetMedianTimeDelta() + 0.002 + nuclear@0: TimewarpAdjuster.GetDelayReduction());*/ nuclear@0: } nuclear@0: nuclear@0: // Just a hard-coded "high" value for game-drawn code. nuclear@0: // TBD: Just return 0 and let users calculate this themselves? nuclear@0: return -0.004; nuclear@0: nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: //return -(0.003 + TimewarpAdjuster.GetDelayReduction()); nuclear@0: } nuclear@0: nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: /* nuclear@0: void FrameTimeManager::updateTimewarpTiming() nuclear@0: { nuclear@0: // If timewarp timing changes based on this sample, update it. nuclear@0: double newTimewarpWaitDelta = calcTimewarpWaitDelta(); nuclear@0: if (newTimewarpWaitDelta != FrameTiming.Inputs.TimewarpWaitDelta) nuclear@0: { nuclear@0: FrameTiming.Inputs.TimewarpWaitDelta = newTimewarpWaitDelta; nuclear@0: LocklessTiming.SetState(FrameTiming); nuclear@0: } nuclear@0: } nuclear@0: */ nuclear@0: nuclear@0: void FrameTimeManager::Timing::InitTimingFromInputs(const FrameTimeManager::TimingInputs& inputs, nuclear@0: HmdShutterTypeEnum shutterType, nuclear@0: double thisFrameTime, unsigned int frameIndex) nuclear@0: { nuclear@0: // ThisFrameTime comes from the end of last frame, unless it it changed. nuclear@0: double nextFrameBase; nuclear@0: double frameDelta = inputs.FrameDelta; nuclear@0: nuclear@0: FrameIndex = frameIndex; nuclear@0: nuclear@0: ThisFrameTime = thisFrameTime; nuclear@0: NextFrameTime = ThisFrameTime + frameDelta; nuclear@0: nextFrameBase = NextFrameTime + inputs.ScreenDelay; nuclear@0: MidpointTime = nextFrameBase + frameDelta * 0.5; nuclear@0: TimewarpPointTime = (inputs.TimewarpWaitDelta == 0.0) ? nuclear@0: 0.0 : (NextFrameTime + inputs.TimewarpWaitDelta); nuclear@0: nuclear@0: // Calculate absolute points in time when eye rendering or corresponding time-warp nuclear@0: // screen edges will become visible. nuclear@0: // This only matters with VSync. nuclear@0: switch(shutterType) nuclear@0: { nuclear@0: case HmdShutter_RollingTopToBottom: nuclear@0: EyeRenderTimes[0] = MidpointTime; nuclear@0: EyeRenderTimes[1] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[0][0] = nextFrameBase; nuclear@0: TimeWarpStartEndTimes[0][1] = nextFrameBase + frameDelta; nuclear@0: TimeWarpStartEndTimes[1][0] = nextFrameBase; nuclear@0: TimeWarpStartEndTimes[1][1] = nextFrameBase + frameDelta; nuclear@0: break; nuclear@0: case HmdShutter_RollingLeftToRight: nuclear@0: EyeRenderTimes[0] = nextFrameBase + frameDelta * 0.25; nuclear@0: EyeRenderTimes[1] = nextFrameBase + frameDelta * 0.75; nuclear@0: nuclear@0: /* nuclear@0: // TBD: MA: It is probably better if mesh sets it up per-eye. nuclear@0: // Would apply if screen is 0 -> 1 for each eye mesh nuclear@0: TimeWarpStartEndTimes[0][0] = nextFrameBase; nuclear@0: TimeWarpStartEndTimes[0][1] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[1][0] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[1][1] = nextFrameBase + frameDelta; nuclear@0: */ nuclear@0: nuclear@0: // Mesh is set up to vary from Edge of scree 0 -> 1 across both eyes nuclear@0: TimeWarpStartEndTimes[0][0] = nextFrameBase; nuclear@0: TimeWarpStartEndTimes[0][1] = nextFrameBase + frameDelta; nuclear@0: TimeWarpStartEndTimes[1][0] = nextFrameBase; nuclear@0: TimeWarpStartEndTimes[1][1] = nextFrameBase + frameDelta; nuclear@0: nuclear@0: break; nuclear@0: case HmdShutter_RollingRightToLeft: nuclear@0: nuclear@0: EyeRenderTimes[0] = nextFrameBase + frameDelta * 0.75; nuclear@0: EyeRenderTimes[1] = nextFrameBase + frameDelta * 0.25; nuclear@0: nuclear@0: // This is *Correct* with Tom's distortion mesh organization. nuclear@0: TimeWarpStartEndTimes[0][0] = nextFrameBase ; nuclear@0: TimeWarpStartEndTimes[0][1] = nextFrameBase + frameDelta; nuclear@0: TimeWarpStartEndTimes[1][0] = nextFrameBase ; nuclear@0: TimeWarpStartEndTimes[1][1] = nextFrameBase + frameDelta; nuclear@0: break; nuclear@0: case HmdShutter_Global: nuclear@0: // TBD nuclear@0: EyeRenderTimes[0] = MidpointTime; nuclear@0: EyeRenderTimes[1] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[0][0] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[0][1] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[1][0] = MidpointTime; nuclear@0: TimeWarpStartEndTimes[1][1] = MidpointTime; nuclear@0: break; nuclear@0: default: nuclear@0: break; nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: nuclear@0: double FrameTimeManager::BeginFrame(unsigned frameIndex) nuclear@0: { nuclear@0: RenderIMUTimeSeconds = 0.0; nuclear@0: TimewarpIMUTimeSeconds = 0.0; nuclear@0: nuclear@0: // TPH - putting an assert so this doesn't remain a hidden problem. nuclear@0: OVR_ASSERT(FrameTiming.Inputs.ScreenDelay != 0); nuclear@0: nuclear@0: // ThisFrameTime comes from the end of last frame, unless it it changed. nuclear@0: double thisFrameTime = (FrameTiming.NextFrameTime != 0.0) ? nuclear@0: FrameTiming.NextFrameTime : ovr_GetTimeInSeconds(); nuclear@0: nuclear@0: // We are starting to process a new frame... nuclear@0: FrameTiming.InitTimingFromInputs(FrameTiming.Inputs, RenderInfo.Shutter.Type, nuclear@0: thisFrameTime, frameIndex); nuclear@0: nuclear@0: return FrameTiming.ThisFrameTime; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameTimeManager::EndFrame() nuclear@0: { nuclear@0: // Record timing since last frame; must be called after Present & sync. nuclear@0: FrameTiming.NextFrameTime = ovr_GetTimeInSeconds(); nuclear@0: if (FrameTiming.ThisFrameTime > 0.0) nuclear@0: { nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: /* nuclear@0: double actualFrameDelta = FrameTiming.NextFrameTime - FrameTiming.ThisFrameTime; nuclear@0: nuclear@0: if (VsyncEnabled) nuclear@0: TimewarpAdjuster.UpdateTimewarpWaitIfSkippedFrames(this, actualFrameDelta, nuclear@0: FrameTiming.NextFrameTime); nuclear@0: nuclear@0: FrameTimeDeltas.AddTimeDelta(actualFrameDelta); nuclear@0: */ nuclear@0: FrameTimeDeltas.AddTimeDelta(FrameTiming.NextFrameTime - FrameTiming.ThisFrameTime); nuclear@0: FrameTiming.Inputs.FrameDelta = calcFrameDelta(); nuclear@0: } nuclear@0: nuclear@0: // Write to Lock-less nuclear@0: LocklessTiming.SetState(FrameTiming); nuclear@0: } nuclear@0: nuclear@0: // Thread-safe function to query timing for a future frame nuclear@0: nuclear@0: FrameTimeManager::Timing FrameTimeManager::GetFrameTiming(unsigned frameIndex) nuclear@0: { nuclear@0: Timing frameTiming = LocklessTiming.GetState(); nuclear@0: nuclear@0: if (frameTiming.ThisFrameTime == 0.0) nuclear@0: { nuclear@0: // If timing hasn't been initialized, starting based on "now" is the best guess. nuclear@0: frameTiming.InitTimingFromInputs(frameTiming.Inputs, RenderInfo.Shutter.Type, nuclear@0: ovr_GetTimeInSeconds(), frameIndex); nuclear@0: } nuclear@0: nuclear@0: else if (frameIndex > frameTiming.FrameIndex) nuclear@0: { nuclear@0: unsigned frameDelta = frameIndex - frameTiming.FrameIndex; nuclear@0: double thisFrameTime = frameTiming.NextFrameTime + nuclear@0: double(frameDelta-1) * frameTiming.Inputs.FrameDelta; nuclear@0: // Don't run away too far into the future beyond rendering. nuclear@0: OVR_DEBUG_LOG_COND(frameDelta >= 6, ("GetFrameTiming is 6 or more frames in future beyond rendering!")); nuclear@0: nuclear@0: frameTiming.InitTimingFromInputs(frameTiming.Inputs, RenderInfo.Shutter.Type, nuclear@0: thisFrameTime, frameIndex); nuclear@0: } nuclear@0: nuclear@0: return frameTiming; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: double FrameTimeManager::GetEyePredictionTime(ovrEyeType eye, unsigned int frameIndex) nuclear@0: { nuclear@0: if (VsyncEnabled) nuclear@0: { nuclear@0: FrameTimeManager::Timing frameTiming = GetFrameTiming(frameIndex); nuclear@0: nuclear@0: // Special case: ovrEye_Count predicts to midpoint nuclear@0: return (eye == ovrEye_Count) ? frameTiming.MidpointTime : frameTiming.EyeRenderTimes[eye]; nuclear@0: } nuclear@0: nuclear@0: // No VSync: Best guess for the near future nuclear@0: return ovr_GetTimeInSeconds() + ScreenSwitchingDelay + NoVSyncToScanoutDelay; nuclear@0: } nuclear@0: nuclear@0: ovrTrackingState FrameTimeManager::GetEyePredictionTracking(ovrHmd hmd, ovrEyeType eye, unsigned int frameIndex) nuclear@0: { nuclear@0: double eyeRenderTime = GetEyePredictionTime(eye, frameIndex); nuclear@0: ovrTrackingState eyeState = ovrHmd_GetTrackingState(hmd, eyeRenderTime); nuclear@0: nuclear@0: // Record view pose sampling time for Latency reporting. nuclear@0: if (RenderIMUTimeSeconds == 0.0) nuclear@0: { nuclear@0: // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds() nuclear@0: //RenderIMUTimeSeconds = eyeState.RawSensorData.TimeInSeconds; nuclear@0: RenderIMUTimeSeconds = ovr_GetTimeInSeconds(); nuclear@0: } nuclear@0: nuclear@0: return eyeState; nuclear@0: } nuclear@0: nuclear@0: Posef FrameTimeManager::GetEyePredictionPose(ovrHmd hmd, ovrEyeType eye) nuclear@0: { nuclear@0: double eyeRenderTime = GetEyePredictionTime(eye, 0); nuclear@0: ovrTrackingState eyeState = ovrHmd_GetTrackingState(hmd, eyeRenderTime); nuclear@0: nuclear@0: // Record view pose sampling time for Latency reporting. nuclear@0: if (RenderIMUTimeSeconds == 0.0) nuclear@0: { nuclear@0: // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds() nuclear@0: //RenderIMUTimeSeconds = eyeState.RawSensorData.TimeInSeconds; nuclear@0: RenderIMUTimeSeconds = ovr_GetTimeInSeconds(); nuclear@0: } nuclear@0: nuclear@0: return eyeState.HeadPose.ThePose; nuclear@0: } nuclear@0: nuclear@0: void FrameTimeManager::GetTimewarpPredictions(ovrEyeType eye, double timewarpStartEnd[2]) nuclear@0: { nuclear@0: if (VsyncEnabled) nuclear@0: { nuclear@0: timewarpStartEnd[0] = FrameTiming.TimeWarpStartEndTimes[eye][0]; nuclear@0: timewarpStartEnd[1] = FrameTiming.TimeWarpStartEndTimes[eye][1]; nuclear@0: return; nuclear@0: } nuclear@0: nuclear@0: // Free-running, so this will be displayed immediately. nuclear@0: // Unfortunately we have no idea which bit of the screen is actually going to be displayed. nuclear@0: // TODO: guess which bit of the screen is being displayed! nuclear@0: // (e.g. use DONOTWAIT on present and see when the return isn't WASSTILLWAITING?) nuclear@0: nuclear@0: // We have no idea where scan-out is currently, so we can't usefully warp the screen spatially. nuclear@0: timewarpStartEnd[0] = ovr_GetTimeInSeconds() + ScreenSwitchingDelay + NoVSyncToScanoutDelay; nuclear@0: timewarpStartEnd[1] = timewarpStartEnd[0]; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameTimeManager::GetTimewarpMatrices(ovrHmd hmd, ovrEyeType eyeId, nuclear@0: ovrPosef renderPose, ovrMatrix4f twmOut[2], nuclear@0: double debugTimingOffsetInSeconds) nuclear@0: { nuclear@0: if (!hmd) nuclear@0: { nuclear@0: return; nuclear@0: } nuclear@0: nuclear@0: double timewarpStartEnd[2] = { 0.0, 0.0 }; nuclear@0: GetTimewarpPredictions(eyeId, timewarpStartEnd); nuclear@0: nuclear@0: //TPH, to vary timing, to allow developers to debug, to shunt the predicted time forward nuclear@0: //and back, and see if the SDK is truly delivering the correct time. Also to allow nuclear@0: //illustration of the detrimental effects when this is not done right. nuclear@0: timewarpStartEnd[0] += debugTimingOffsetInSeconds; nuclear@0: timewarpStartEnd[1] += debugTimingOffsetInSeconds; nuclear@0: nuclear@0: nuclear@0: //HMDState* p = (HMDState*)hmd; nuclear@0: ovrTrackingState startState = ovrHmd_GetTrackingState(hmd, timewarpStartEnd[0]); nuclear@0: ovrTrackingState endState = ovrHmd_GetTrackingState(hmd, timewarpStartEnd[1]); nuclear@0: nuclear@0: if (TimewarpIMUTimeSeconds == 0.0) nuclear@0: { nuclear@0: // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds() nuclear@0: //TimewarpIMUTimeSeconds = startState.RawSensorData.TimeInSeconds; nuclear@0: TimewarpIMUTimeSeconds = ovr_GetTimeInSeconds(); nuclear@0: } nuclear@0: nuclear@0: Quatf quatFromStart = startState.HeadPose.ThePose.Orientation; nuclear@0: Quatf quatFromEnd = endState.HeadPose.ThePose.Orientation; nuclear@0: Quatf quatFromEye = renderPose.Orientation; //EyeRenderPoses[eyeId].Orientation; nuclear@0: quatFromEye.Invert(); // because we need the view matrix, not the camera matrix nuclear@0: nuclear@0: Quatf timewarpStartQuat = quatFromEye * quatFromStart; nuclear@0: Quatf timewarpEndQuat = quatFromEye * quatFromEnd; nuclear@0: nuclear@0: Matrix4f timewarpStart(timewarpStartQuat); nuclear@0: Matrix4f timewarpEnd(timewarpEndQuat); nuclear@0: nuclear@0: nuclear@0: // The real-world orientations have: X=right, Y=up, Z=backwards. nuclear@0: // The vectors inside the mesh are in NDC to keep the shader simple: X=right, Y=down, Z=forwards. nuclear@0: // So we need to perform a similarity transform on this delta matrix. nuclear@0: // The verbose code would look like this: nuclear@0: /* nuclear@0: Matrix4f matBasisChange; nuclear@0: matBasisChange.SetIdentity(); nuclear@0: matBasisChange.M[0][0] = 1.0f; nuclear@0: matBasisChange.M[1][1] = -1.0f; nuclear@0: matBasisChange.M[2][2] = -1.0f; nuclear@0: Matrix4f matBasisChangeInv = matBasisChange.Inverted(); nuclear@0: matRenderFromNow = matBasisChangeInv * matRenderFromNow * matBasisChange; nuclear@0: */ nuclear@0: // ...but of course all the above is a constant transform and much more easily done. nuclear@0: // We flip the signs of the Y&Z row, then flip the signs of the Y&Z column, nuclear@0: // and of course most of the flips cancel: nuclear@0: // +++ +-- +-- nuclear@0: // +++ -> flip Y&Z columns -> +-- -> flip Y&Z rows -> -++ nuclear@0: // +++ +-- -++ nuclear@0: timewarpStart.M[0][1] = -timewarpStart.M[0][1]; nuclear@0: timewarpStart.M[0][2] = -timewarpStart.M[0][2]; nuclear@0: timewarpStart.M[1][0] = -timewarpStart.M[1][0]; nuclear@0: timewarpStart.M[2][0] = -timewarpStart.M[2][0]; nuclear@0: nuclear@0: timewarpEnd .M[0][1] = -timewarpEnd .M[0][1]; nuclear@0: timewarpEnd .M[0][2] = -timewarpEnd .M[0][2]; nuclear@0: timewarpEnd .M[1][0] = -timewarpEnd .M[1][0]; nuclear@0: timewarpEnd .M[2][0] = -timewarpEnd .M[2][0]; nuclear@0: nuclear@0: twmOut[0] = timewarpStart; nuclear@0: twmOut[1] = timewarpEnd; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: // Used by renderer to determine if it should time distortion rendering. nuclear@0: bool FrameTimeManager::NeedDistortionTimeMeasurement() const nuclear@0: { nuclear@0: if (!VsyncEnabled) nuclear@0: return false; nuclear@0: return DistortionRenderTimes.GetCount() < DistortionRenderTimes.Capacity; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameTimeManager::AddDistortionTimeMeasurement(double distortionTimeSeconds) nuclear@0: { nuclear@0: DistortionRenderTimes.AddTimeDelta(distortionTimeSeconds); nuclear@0: nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: //updateTimewarpTiming(); nuclear@0: nuclear@0: // If timewarp timing changes based on this sample, update it. nuclear@0: double newTimewarpWaitDelta = calcTimewarpWaitDelta(); nuclear@0: if (newTimewarpWaitDelta != FrameTiming.Inputs.TimewarpWaitDelta) nuclear@0: { nuclear@0: FrameTiming.Inputs.TimewarpWaitDelta = newTimewarpWaitDelta; nuclear@0: LocklessTiming.SetState(FrameTiming); nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameTimeManager::UpdateFrameLatencyTrackingAfterEndFrame( nuclear@0: unsigned char frameLatencyTestColor[3], nuclear@0: const Util::FrameTimeRecordSet& rs) nuclear@0: { nuclear@0: // FrameTiming.NextFrameTime in this context (after EndFrame) is the end frame time. nuclear@0: ScreenLatencyTracker.SaveDrawColor(frameLatencyTestColor[0], nuclear@0: FrameTiming.NextFrameTime, nuclear@0: RenderIMUTimeSeconds, nuclear@0: TimewarpIMUTimeSeconds); nuclear@0: nuclear@0: ScreenLatencyTracker.MatchRecord(rs); nuclear@0: nuclear@0: // If screen delay changed, update timing. nuclear@0: double newScreenDelay = calcScreenDelay(); nuclear@0: if (newScreenDelay != FrameTiming.Inputs.ScreenDelay) nuclear@0: { nuclear@0: FrameTiming.Inputs.ScreenDelay = newScreenDelay; nuclear@0: LocklessTiming.SetState(FrameTiming); nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: nuclear@0: //----------------------------------------------------------------------------------- nuclear@0: //Revisit dynamic pre-Timewarp delay adjustment logic nuclear@0: /* nuclear@0: void FrameTimeManager::TimewarpDelayAdjuster::Reset() nuclear@0: { nuclear@0: State = State_WaitingToReduceLevel; nuclear@0: DelayLevel = 0; nuclear@0: InitialFrameCounter = 0; nuclear@0: TimewarpDelayReductionSeconds = 0.0; nuclear@0: DelayLevelFinishTime = 0.0; nuclear@0: nuclear@0: memset(WaitTimeIndexForLevel, 0, sizeof(WaitTimeIndexForLevel)); nuclear@0: // If we are at level 0, waits are infinite. nuclear@0: WaitTimeIndexForLevel[0] = MaxTimeIndex; nuclear@0: } nuclear@0: nuclear@0: nuclear@0: void FrameTimeManager::TimewarpDelayAdjuster:: nuclear@0: UpdateTimewarpWaitIfSkippedFrames(FrameTimeManager* manager, nuclear@0: double measuredFrameDelta, double nextFrameTime) nuclear@0: { nuclear@0: // Times in seconds nuclear@0: const static double delayTimingTiers[7] = { 1.0, 5.0, 15.0, 30.0, 60.0, 120.0, 1000000.0 }; nuclear@0: nuclear@0: const double currentFrameDelta = manager->FrameTiming.Inputs.FrameDelta; nuclear@0: nuclear@0: nuclear@0: // Once we detected frame spike, we skip several frames before testing again. nuclear@0: if (InitialFrameCounter > 0) nuclear@0: { nuclear@0: InitialFrameCounter --; nuclear@0: return; nuclear@0: } nuclear@0: nuclear@0: // Skipped frame would usually take 2x longer then regular frame nuclear@0: if (measuredFrameDelta > currentFrameDelta * 1.8) nuclear@0: { nuclear@0: if (State == State_WaitingToReduceLevel) nuclear@0: { nuclear@0: // If we got here, escalate the level again. nuclear@0: if (DelayLevel < MaxDelayLevel) nuclear@0: { nuclear@0: DelayLevel++; nuclear@0: InitialFrameCounter = 3; nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: else if (State == State_VerifyingAfterReduce) nuclear@0: { nuclear@0: // So we went down to this level and tried to wait to see if there was nuclear@0: // as skipped frame and there is -> go back up a level and incrment its timing tier nuclear@0: if (DelayLevel < MaxDelayLevel) nuclear@0: { nuclear@0: DelayLevel++; nuclear@0: State = State_WaitingToReduceLevel; nuclear@0: nuclear@0: // For higher level delays reductions, i.e. more then half a frame, nuclear@0: // we don't go into the infinite wait tier. nuclear@0: int maxTimingTier = MaxTimeIndex; nuclear@0: if (DelayLevel > MaxInfiniteTimingLevel) nuclear@0: maxTimingTier--; nuclear@0: nuclear@0: if (WaitTimeIndexForLevel[DelayLevel] < maxTimingTier ) nuclear@0: WaitTimeIndexForLevel[DelayLevel]++; nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: DelayLevelFinishTime = nextFrameTime + nuclear@0: delayTimingTiers[WaitTimeIndexForLevel[DelayLevel]]; nuclear@0: TimewarpDelayReductionSeconds = currentFrameDelta * 0.125 * DelayLevel; nuclear@0: manager->updateTimewarpTiming(); nuclear@0: nuclear@0: } nuclear@0: nuclear@0: else if (nextFrameTime > DelayLevelFinishTime) nuclear@0: { nuclear@0: if (State == State_WaitingToReduceLevel) nuclear@0: { nuclear@0: if (DelayLevel > 0) nuclear@0: { nuclear@0: DelayLevel--; nuclear@0: State = State_VerifyingAfterReduce; nuclear@0: // Always use 1 sec to see if "down sampling mode" caused problems nuclear@0: DelayLevelFinishTime = nextFrameTime + 1.0f; nuclear@0: } nuclear@0: } nuclear@0: else if (State == State_VerifyingAfterReduce) nuclear@0: { nuclear@0: // Prior display level successfully reduced, nuclear@0: // try to see we we could go down further after wait. nuclear@0: WaitTimeIndexForLevel[DelayLevel+1] = 0; nuclear@0: State = State_WaitingToReduceLevel; nuclear@0: DelayLevelFinishTime = nextFrameTime + nuclear@0: delayTimingTiers[WaitTimeIndexForLevel[DelayLevel]]; nuclear@0: } nuclear@0: nuclear@0: // TBD: Update TimeWarpTiming nuclear@0: TimewarpDelayReductionSeconds = currentFrameDelta * 0.125 * DelayLevel; nuclear@0: manager->updateTimewarpTiming(); nuclear@0: } nuclear@0: nuclear@0: nuclear@0: //static int oldDelayLevel = 0; nuclear@0: nuclear@0: //if (oldDelayLevel != DelayLevel) nuclear@0: //{ nuclear@0: //OVR_DEBUG_LOG(("DelayLevel:%d tReduction = %0.5f ", DelayLevel, TimewarpDelayReductionSeconds)); nuclear@0: //oldDelayLevel = DelayLevel; nuclear@0: //} nuclear@0: } nuclear@0: */ nuclear@0: nuclear@0: //----------------------------------------------------------------------------------- nuclear@0: // ***** TimeDeltaCollector nuclear@0: nuclear@0: void TimeDeltaCollector::AddTimeDelta(double timeSeconds) nuclear@0: { nuclear@0: // avoid adding invalid timing values nuclear@0: if(timeSeconds < 0.0f) nuclear@0: return; nuclear@0: nuclear@0: if (Count == Capacity) nuclear@0: { nuclear@0: for(int i=0; i< Count-1; i++) nuclear@0: TimeBufferSeconds[i] = TimeBufferSeconds[i+1]; nuclear@0: Count--; nuclear@0: } nuclear@0: TimeBufferSeconds[Count++] = timeSeconds; nuclear@0: nuclear@0: ReCalcMedian = true; nuclear@0: } nuclear@0: nuclear@0: // KevinJ: Better median function nuclear@0: double CalculateListMedianRecursive(const double inputList[TimeDeltaCollector::Capacity], int inputListLength, int lessThanSum, int greaterThanSum) nuclear@0: { nuclear@0: double lessThanMedian[TimeDeltaCollector::Capacity], greaterThanMedian[TimeDeltaCollector::Capacity]; nuclear@0: int lessThanMedianListLength = 0, greaterThanMedianListLength = 0; nuclear@0: double median = inputList[0]; nuclear@0: int i; nuclear@0: for (i = 1; i < inputListLength; i++) nuclear@0: { nuclear@0: // If same value, spread among lists evenly nuclear@0: if (inputList[i] < median || ((i & 1) == 0 && inputList[i] == median)) nuclear@0: lessThanMedian[lessThanMedianListLength++] = inputList[i]; nuclear@0: else nuclear@0: greaterThanMedian[greaterThanMedianListLength++] = inputList[i]; nuclear@0: } nuclear@0: if (lessThanMedianListLength + lessThanSum == greaterThanMedianListLength + greaterThanSum + 1 || nuclear@0: lessThanMedianListLength + lessThanSum == greaterThanMedianListLength + greaterThanSum - 1) nuclear@0: return median; nuclear@0: nuclear@0: if (lessThanMedianListLength + lessThanSum < greaterThanMedianListLength + greaterThanSum) nuclear@0: { nuclear@0: lessThanMedian[lessThanMedianListLength++] = median; nuclear@0: return CalculateListMedianRecursive(greaterThanMedian, greaterThanMedianListLength, lessThanMedianListLength + lessThanSum, greaterThanSum); nuclear@0: } nuclear@0: else nuclear@0: { nuclear@0: greaterThanMedian[greaterThanMedianListLength++] = median; nuclear@0: return CalculateListMedianRecursive(lessThanMedian, lessThanMedianListLength, lessThanSum, greaterThanMedianListLength + greaterThanSum); nuclear@0: } nuclear@0: } nuclear@0: // KevinJ: Excludes Firmware hack nuclear@0: double TimeDeltaCollector::GetMedianTimeDeltaNoFirmwareHack() const nuclear@0: { nuclear@0: if (ReCalcMedian) nuclear@0: { nuclear@0: ReCalcMedian = false; nuclear@0: Median = CalculateListMedianRecursive(TimeBufferSeconds, Count, 0, 0); nuclear@0: } nuclear@0: return Median; nuclear@0: } nuclear@0: double TimeDeltaCollector::GetMedianTimeDelta() const nuclear@0: { nuclear@0: if(ReCalcMedian) nuclear@0: { nuclear@0: double SortedList[Capacity]; nuclear@0: bool used[Capacity]; nuclear@0: nuclear@0: memset(used, 0, sizeof(used)); nuclear@0: SortedList[0] = 0.0; // In case Count was 0... nuclear@0: nuclear@0: // Probably the slowest way to find median... nuclear@0: for (int i=0; i