ovr_sdk

diff LibOVR/Src/CAPI/CAPI_FrameTimeManager.cpp @ 0:1b39a1b46319

initial 0.4.4
author John Tsiombikas <nuclear@member.fsf.org>
date Wed, 14 Jan 2015 06:51:16 +0200
parents
children
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/LibOVR/Src/CAPI/CAPI_FrameTimeManager.cpp	Wed Jan 14 06:51:16 2015 +0200
     1.3 @@ -0,0 +1,946 @@
     1.4 +/************************************************************************************
     1.5 +
     1.6 +Filename    :   CAPI_FrameTimeManager.cpp
     1.7 +Content     :   Manage frame timing and pose prediction for rendering
     1.8 +Created     :   November 30, 2013
     1.9 +Authors     :   Volga Aksoy, Michael Antonov
    1.10 +
    1.11 +Copyright   :   Copyright 2014 Oculus VR, LLC All Rights reserved.
    1.12 +
    1.13 +Licensed under the Oculus VR Rift SDK License Version 3.2 (the "License"); 
    1.14 +you may not use the Oculus VR Rift SDK except in compliance with the License, 
    1.15 +which is provided at the time of installation or download, or which 
    1.16 +otherwise accompanies this software in either electronic or hard copy form.
    1.17 +
    1.18 +You may obtain a copy of the License at
    1.19 +
    1.20 +http://www.oculusvr.com/licenses/LICENSE-3.2 
    1.21 +
    1.22 +Unless required by applicable law or agreed to in writing, the Oculus VR SDK 
    1.23 +distributed under the License is distributed on an "AS IS" BASIS,
    1.24 +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    1.25 +See the License for the specific language governing permissions and
    1.26 +limitations under the License.
    1.27 +
    1.28 +************************************************************************************/
    1.29 +
    1.30 +#include "CAPI_FrameTimeManager.h"
    1.31 +
    1.32 +#include "../Kernel/OVR_Log.h"
    1.33 +
    1.34 +namespace OVR { namespace CAPI {
    1.35 +
    1.36 +
    1.37 +//-------------------------------------------------------------------------------------
    1.38 +// ***** FrameLatencyTracker
    1.39 +    
    1.40 +
    1.41 +FrameLatencyTracker::FrameLatencyTracker()
    1.42 +{
    1.43 +   Reset();
    1.44 +}
    1.45 +
    1.46 +
    1.47 +void FrameLatencyTracker::Reset()
    1.48 +{
    1.49 +    TrackerEnabled         = true;
    1.50 +    WaitMode               = SampleWait_Zeroes;
    1.51 +    MatchCount             = 0;
    1.52 +    memset(FrameEndTimes, 0, sizeof(FrameEndTimes));
    1.53 +    FrameIndex             = 0;
    1.54 +  //FrameDeltas
    1.55 +    RenderLatencySeconds   = 0.0;
    1.56 +    TimewarpLatencySeconds = 0.0;
    1.57 +    LatencyRecordTime      = 0.0;
    1.58 +
    1.59 +    FrameDeltas.Clear();
    1.60 +}
    1.61 +
    1.62 +
    1.63 +unsigned char FrameLatencyTracker::GetNextDrawColor()
    1.64 +{   
    1.65 +    if (!TrackerEnabled || (WaitMode == SampleWait_Zeroes) ||
    1.66 +        (FrameIndex >= FramesTracked))
    1.67 +    {        
    1.68 +        return (unsigned char)Util::FrameTimeRecord::ReadbackIndexToColor(0);
    1.69 +    }
    1.70 +
    1.71 +    OVR_ASSERT(FrameIndex < FramesTracked);    
    1.72 +    return (unsigned char)Util::FrameTimeRecord::ReadbackIndexToColor(FrameIndex+1);
    1.73 +}
    1.74 +
    1.75 +
    1.76 +void FrameLatencyTracker::SaveDrawColor(unsigned char drawColor, double endFrameTime,
    1.77 +                                        double renderIMUTime, double timewarpIMUTime )
    1.78 +{
    1.79 +    if (!TrackerEnabled || (WaitMode == SampleWait_Zeroes))
    1.80 +        return;
    1.81 +
    1.82 +    if (FrameIndex < FramesTracked)
    1.83 +    {
    1.84 +        OVR_ASSERT(Util::FrameTimeRecord::ReadbackIndexToColor(FrameIndex+1) == drawColor);
    1.85 +        OVR_UNUSED(drawColor);
    1.86 +
    1.87 +        // saves {color, endFrame time}
    1.88 +        FrameEndTimes[FrameIndex].ReadbackIndex         = FrameIndex + 1;
    1.89 +        FrameEndTimes[FrameIndex].TimeSeconds           = endFrameTime;
    1.90 +        FrameEndTimes[FrameIndex].RenderIMUTimeSeconds  = renderIMUTime;
    1.91 +        FrameEndTimes[FrameIndex].TimewarpIMUTimeSeconds= timewarpIMUTime;
    1.92 +        FrameEndTimes[FrameIndex].MatchedRecord         = false;
    1.93 +        FrameIndex++;
    1.94 +    }
    1.95 +    else
    1.96 +    {
    1.97 +        // If the request was outstanding for too long, switch to zero mode to restart.
    1.98 +        if (endFrameTime > (FrameEndTimes[FrameIndex-1].TimeSeconds + 0.15))
    1.99 +        {
   1.100 +            if (MatchCount == 0)
   1.101 +            {
   1.102 +                // If nothing was matched, we have no latency reading.
   1.103 +                RenderLatencySeconds   = 0.0;
   1.104 +                TimewarpLatencySeconds = 0.0;
   1.105 +            }
   1.106 +
   1.107 +            WaitMode   =  SampleWait_Zeroes;
   1.108 +            MatchCount = 0;
   1.109 +            FrameIndex = 0;
   1.110 +        }
   1.111 +    }
   1.112 +}
   1.113 +
   1.114 +
   1.115 +void FrameLatencyTracker::MatchRecord(const Util::FrameTimeRecordSet &r)
   1.116 +{
   1.117 +    if (!TrackerEnabled)
   1.118 +        return;
   1.119 +
   1.120 +    if (WaitMode == SampleWait_Zeroes)
   1.121 +    {
   1.122 +        // Do we have all zeros?
   1.123 +        if (r.IsAllZeroes())
   1.124 +        {
   1.125 +            OVR_ASSERT(FrameIndex == 0);
   1.126 +            WaitMode = SampleWait_Match;
   1.127 +            MatchCount = 0;
   1.128 +        }
   1.129 +        return;
   1.130 +    }
   1.131 +
   1.132 +    // We are in Match Mode. Wait until all colors are matched or timeout,
   1.133 +    // at which point we go back to zeros.
   1.134 +
   1.135 +    for (int i = 0; i < FrameIndex; i++)
   1.136 +    {
   1.137 +        int recordIndex = 0;
   1.138 +        int consecutiveMatch  = 0;
   1.139 +
   1.140 +        OVR_ASSERT(FrameEndTimes[i].ReadbackIndex != 0);
   1.141 +
   1.142 +        if (r.FindReadbackIndex(&recordIndex, FrameEndTimes[i].ReadbackIndex))
   1.143 +        {
   1.144 +            // Advance forward to see that we have several more matches.
   1.145 +            int  ri = recordIndex + 1;
   1.146 +            int  j  = i + 1;
   1.147 +
   1.148 +            consecutiveMatch++;
   1.149 +
   1.150 +            for (; (j < FrameIndex) && (ri < Util::FrameTimeRecordSet::RecordCount); j++, ri++)
   1.151 +            {
   1.152 +                if (r[ri].ReadbackIndex != FrameEndTimes[j].ReadbackIndex)
   1.153 +                    break;
   1.154 +                consecutiveMatch++;
   1.155 +            }
   1.156 +
   1.157 +            // Match at least 2 items in the row, to avoid accidentally matching color.
   1.158 +            if (consecutiveMatch > 1)
   1.159 +            {
   1.160 +                // Record latency values for all but last samples. Keep last 2 samples
   1.161 +                // for the future to simplify matching.
   1.162 +                for (int q = 0; q < consecutiveMatch; q++)
   1.163 +                {
   1.164 +                    const Util::FrameTimeRecord &scanoutFrame = r[recordIndex+q];
   1.165 +                    FrameTimeRecordEx           &renderFrame  = FrameEndTimes[i+q];
   1.166 +                    
   1.167 +                    if (!renderFrame.MatchedRecord)
   1.168 +                    {
   1.169 +                        double deltaSeconds = scanoutFrame.TimeSeconds - renderFrame.TimeSeconds;
   1.170 +                        if (deltaSeconds > 0.0)
   1.171 +                        {
   1.172 +                            FrameDeltas.AddTimeDelta(deltaSeconds);
   1.173 +
   1.174 +                            // FIRMWARE HACK: don't take new readings if they're 10ms higher than previous reading
   1.175 +                            // but only do that for 1 second, after that accept it regardless of the timing difference
   1.176 +                            double newRenderLatency = scanoutFrame.TimeSeconds - renderFrame.RenderIMUTimeSeconds;                            
   1.177 +                            if( newRenderLatency < RenderLatencySeconds + 0.01 ||
   1.178 +                                scanoutFrame.TimeSeconds > LatencyRecordTime + 1.0)
   1.179 +                            {
   1.180 +                                LatencyRecordTime      = scanoutFrame.TimeSeconds;
   1.181 +                                RenderLatencySeconds   = scanoutFrame.TimeSeconds - renderFrame.RenderIMUTimeSeconds;
   1.182 +                                TimewarpLatencySeconds = (renderFrame.TimewarpIMUTimeSeconds == 0.0)  ?  0.0 :
   1.183 +                                                         (scanoutFrame.TimeSeconds - renderFrame.TimewarpIMUTimeSeconds);
   1.184 +                            }
   1.185 +                        }
   1.186 +
   1.187 +                        renderFrame.MatchedRecord = true;
   1.188 +                        MatchCount++;
   1.189 +                    }
   1.190 +                }
   1.191 +
   1.192 +                // Exit for.
   1.193 +                break;
   1.194 +            }
   1.195 +        }
   1.196 +    } // for ( i => FrameIndex )
   1.197 +
   1.198 +
   1.199 +    // If we matched all frames, start over.
   1.200 +    if (MatchCount == FramesTracked)
   1.201 +    {
   1.202 +        WaitMode   =  SampleWait_Zeroes;
   1.203 +        MatchCount = 0;
   1.204 +        FrameIndex = 0;
   1.205 +    }
   1.206 +}
   1.207 +
   1.208 +bool FrameLatencyTracker::IsLatencyTimingAvailable()
   1.209 +{
   1.210 +    return ovr_GetTimeInSeconds() < (LatencyRecordTime + 2.0);
   1.211 +}
   1.212 +
   1.213 +void FrameLatencyTracker::GetLatencyTimings(float& latencyRender, float& latencyTimewarp, float& latencyPostPresent)
   1.214 +{
   1.215 +    if (!IsLatencyTimingAvailable())
   1.216 +    {
   1.217 +        latencyRender = 0.0f;
   1.218 +        latencyTimewarp = 0.0f;
   1.219 +        latencyPostPresent = 0.0f;
   1.220 +    }
   1.221 +    else
   1.222 +    {
   1.223 +        latencyRender = (float)RenderLatencySeconds;
   1.224 +        latencyTimewarp = (float)TimewarpLatencySeconds;
   1.225 +        latencyPostPresent = (float)FrameDeltas.GetMedianTimeDelta();
   1.226 +    }    
   1.227 +}
   1.228 +
   1.229 +
   1.230 +//-------------------------------------------------------------------------------------
   1.231 +// ***** FrameTimeManager
   1.232 +
   1.233 +FrameTimeManager::FrameTimeManager(bool vsyncEnabled) :
   1.234 +    RenderInfo(),
   1.235 +    FrameTimeDeltas(),
   1.236 +    DistortionRenderTimes(),
   1.237 +    ScreenLatencyTracker(),
   1.238 +    VsyncEnabled(vsyncEnabled),
   1.239 +    DynamicPrediction(true),
   1.240 +    SdkRender(false),
   1.241 +  //DirectToRift(false), Initialized below.
   1.242 +  //VSyncToScanoutDelay(0.0), Initialized below.
   1.243 +  //NoVSyncToScanoutDelay(0.0), Initialized below.
   1.244 +    ScreenSwitchingDelay(0.0),
   1.245 +    FrameTiming(),
   1.246 +    LocklessTiming(),
   1.247 +    RenderIMUTimeSeconds(0.0),
   1.248 +    TimewarpIMUTimeSeconds(0.0)
   1.249 +{
   1.250 +    // If driver is in use,
   1.251 +    DirectToRift = !Display::InCompatibilityMode(false);
   1.252 +    if (DirectToRift)
   1.253 +    {
   1.254 +        // The latest driver provides a post-present vsync-to-scan-out delay
   1.255 +        // that is roughly zero.  The latency tester will provide real numbers
   1.256 +        // but when it is unavailable for some reason, we should default to
   1.257 +        // an expected value.
   1.258 +        VSyncToScanoutDelay = 0.0001f;
   1.259 +    }
   1.260 +    else
   1.261 +    {
   1.262 +        // HACK: SyncToScanoutDelay observed close to 1 frame in video cards.
   1.263 +        //       Overwritten by dynamic latency measurement on DK2.
   1.264 +        VSyncToScanoutDelay = 0.013f;
   1.265 +    }
   1.266 +    NoVSyncToScanoutDelay = 0.004f;
   1.267 +}
   1.268 +
   1.269 +void FrameTimeManager::Init(HmdRenderInfo& renderInfo)
   1.270 +{
   1.271 +    // Set up prediction distances.
   1.272 +    // With-Vsync timings.
   1.273 +    RenderInfo = renderInfo;
   1.274 +
   1.275 +    ScreenSwitchingDelay = RenderInfo.Shutter.PixelSettleTime * 0.5f + 
   1.276 +                           RenderInfo.Shutter.PixelPersistence * 0.5f;
   1.277 +}
   1.278 +
   1.279 +void FrameTimeManager::ResetFrameTiming(unsigned frameIndex,
   1.280 +                                        bool dynamicPrediction,
   1.281 +                                        bool sdkRender)
   1.282 +{    
   1.283 +    DynamicPrediction   = dynamicPrediction;
   1.284 +    SdkRender           = sdkRender;
   1.285 +
   1.286 +    FrameTimeDeltas.Clear();
   1.287 +    DistortionRenderTimes.Clear();
   1.288 +    ScreenLatencyTracker.Reset();
   1.289 +    //Revisit dynamic pre-Timewarp delay adjustment logic
   1.290 +    //TimewarpAdjuster.Reset();
   1.291 +
   1.292 +    FrameTiming.FrameIndex               = frameIndex;
   1.293 +    FrameTiming.NextFrameTime            = 0.0;
   1.294 +    FrameTiming.ThisFrameTime            = 0.0;
   1.295 +    FrameTiming.Inputs.FrameDelta        = calcFrameDelta();
   1.296 +    // This one is particularly critical, and has been missed in the past because
   1.297 +    // this init function wasn't called for app-rendered.
   1.298 +    FrameTiming.Inputs.ScreenDelay       = calcScreenDelay();
   1.299 +    FrameTiming.Inputs.TimewarpWaitDelta = 0.0f;
   1.300 +
   1.301 +    LocklessTiming.SetState(FrameTiming);
   1.302 +}
   1.303 +
   1.304 +
   1.305 +double  FrameTimeManager::calcFrameDelta() const
   1.306 +{
   1.307 +    // Timing difference between frame is tracked by FrameTimeDeltas, or
   1.308 +    // is a hard-coded value of 1/FrameRate.
   1.309 +    double  frameDelta;    
   1.310 +
   1.311 +    if (!VsyncEnabled)
   1.312 +    {
   1.313 +        frameDelta = 0.0;
   1.314 +    }
   1.315 +    else if (FrameTimeDeltas.GetCount() > 3)
   1.316 +    {
   1.317 +        frameDelta = FrameTimeDeltas.GetMedianTimeDelta();
   1.318 +        if (frameDelta > (RenderInfo.Shutter.VsyncToNextVsync + 0.001))
   1.319 +            frameDelta = RenderInfo.Shutter.VsyncToNextVsync;
   1.320 +    }
   1.321 +    else
   1.322 +    {
   1.323 +        frameDelta = RenderInfo.Shutter.VsyncToNextVsync;
   1.324 +    }
   1.325 +
   1.326 +    return frameDelta;
   1.327 +}
   1.328 +
   1.329 +
   1.330 +double  FrameTimeManager::calcScreenDelay() const
   1.331 +{
   1.332 +    double  screenDelay = ScreenSwitchingDelay;
   1.333 +    double  measuredVSyncToScanout;
   1.334 +
   1.335 +    // Use real-time DK2 latency tester HW for prediction if its is working.
   1.336 +    // Do sanity check under 60 ms
   1.337 +    if (!VsyncEnabled)
   1.338 +    {
   1.339 +        screenDelay += NoVSyncToScanoutDelay;
   1.340 +    }
   1.341 +    else if ( DynamicPrediction &&
   1.342 +              (ScreenLatencyTracker.FrameDeltas.GetCount() > 3) &&
   1.343 +              (measuredVSyncToScanout = ScreenLatencyTracker.FrameDeltas.GetMedianTimeDelta(),
   1.344 +               (measuredVSyncToScanout > -0.0001) && (measuredVSyncToScanout < 0.06)) ) 
   1.345 +    {
   1.346 +        screenDelay += measuredVSyncToScanout;
   1.347 +    }
   1.348 +    else
   1.349 +    {
   1.350 +        screenDelay += VSyncToScanoutDelay;
   1.351 +    }
   1.352 +
   1.353 +    return screenDelay;
   1.354 +}
   1.355 +
   1.356 +double FrameTimeManager::calcTimewarpWaitDelta() const
   1.357 +{
   1.358 +    // If timewarp timing hasn't been calculated, we should wait.
   1.359 +    if (!VsyncEnabled)
   1.360 +        return 0.0;
   1.361 +
   1.362 +    if (SdkRender)
   1.363 +    {
   1.364 +        if (NeedDistortionTimeMeasurement())
   1.365 +            return 0.0;
   1.366 +        return -(DistortionRenderTimes.GetMedianTimeDelta() + 0.0035);
   1.367 +
   1.368 +        //Revisit dynamic pre-Timewarp delay adjustment logic
   1.369 +        /*return -(DistortionRenderTimes.GetMedianTimeDelta() + 0.002 +
   1.370 +                 TimewarpAdjuster.GetDelayReduction());*/
   1.371 +    }
   1.372 +   
   1.373 +    // Just a hard-coded "high" value for game-drawn code.
   1.374 +    // TBD: Just return 0 and let users calculate this themselves?
   1.375 +    return -0.004;
   1.376 +
   1.377 +    //Revisit dynamic pre-Timewarp delay adjustment logic
   1.378 +    //return -(0.003 + TimewarpAdjuster.GetDelayReduction());
   1.379 +}
   1.380 +
   1.381 +//Revisit dynamic pre-Timewarp delay adjustment logic
   1.382 +/*
   1.383 +void FrameTimeManager::updateTimewarpTiming()
   1.384 +{
   1.385 +    // If timewarp timing changes based on this sample, update it.
   1.386 +    double newTimewarpWaitDelta = calcTimewarpWaitDelta();
   1.387 +    if (newTimewarpWaitDelta != FrameTiming.Inputs.TimewarpWaitDelta)
   1.388 +    {
   1.389 +        FrameTiming.Inputs.TimewarpWaitDelta = newTimewarpWaitDelta;
   1.390 +        LocklessTiming.SetState(FrameTiming);
   1.391 +    }
   1.392 +}
   1.393 +*/
   1.394 +
   1.395 +void FrameTimeManager::Timing::InitTimingFromInputs(const FrameTimeManager::TimingInputs& inputs,
   1.396 +                                                    HmdShutterTypeEnum shutterType,
   1.397 +                                                    double thisFrameTime, unsigned int frameIndex)
   1.398 +{    
   1.399 +    // ThisFrameTime comes from the end of last frame, unless it it changed.  
   1.400 +    double  nextFrameBase;
   1.401 +    double  frameDelta = inputs.FrameDelta;
   1.402 +
   1.403 +    FrameIndex        = frameIndex;
   1.404 +
   1.405 +    ThisFrameTime     = thisFrameTime;
   1.406 +    NextFrameTime     = ThisFrameTime + frameDelta;
   1.407 +    nextFrameBase     = NextFrameTime + inputs.ScreenDelay;
   1.408 +    MidpointTime      = nextFrameBase + frameDelta * 0.5;
   1.409 +    TimewarpPointTime = (inputs.TimewarpWaitDelta == 0.0) ?
   1.410 +                        0.0 : (NextFrameTime + inputs.TimewarpWaitDelta);
   1.411 +
   1.412 +    // Calculate absolute points in time when eye rendering or corresponding time-warp
   1.413 +    // screen edges will become visible.
   1.414 +    // This only matters with VSync.
   1.415 +    switch(shutterType)
   1.416 +    {            
   1.417 +    case HmdShutter_RollingTopToBottom:
   1.418 +        EyeRenderTimes[0]               = MidpointTime;
   1.419 +        EyeRenderTimes[1]               = MidpointTime;
   1.420 +        TimeWarpStartEndTimes[0][0]     = nextFrameBase;
   1.421 +        TimeWarpStartEndTimes[0][1]     = nextFrameBase + frameDelta;
   1.422 +        TimeWarpStartEndTimes[1][0]     = nextFrameBase;
   1.423 +        TimeWarpStartEndTimes[1][1]     = nextFrameBase + frameDelta;
   1.424 +        break;
   1.425 +    case HmdShutter_RollingLeftToRight:
   1.426 +        EyeRenderTimes[0]               = nextFrameBase + frameDelta * 0.25;
   1.427 +        EyeRenderTimes[1]               = nextFrameBase + frameDelta * 0.75;
   1.428 +
   1.429 +        /*
   1.430 +        // TBD: MA: It is probably better if mesh sets it up per-eye.
   1.431 +        // Would apply if screen is 0 -> 1 for each eye mesh
   1.432 +        TimeWarpStartEndTimes[0][0]     = nextFrameBase;
   1.433 +        TimeWarpStartEndTimes[0][1]     = MidpointTime;
   1.434 +        TimeWarpStartEndTimes[1][0]     = MidpointTime;
   1.435 +        TimeWarpStartEndTimes[1][1]     = nextFrameBase + frameDelta;
   1.436 +        */
   1.437 +
   1.438 +        // Mesh is set up to vary from Edge of scree 0 -> 1 across both eyes
   1.439 +        TimeWarpStartEndTimes[0][0]     = nextFrameBase;
   1.440 +        TimeWarpStartEndTimes[0][1]     = nextFrameBase + frameDelta;
   1.441 +        TimeWarpStartEndTimes[1][0]     = nextFrameBase;
   1.442 +        TimeWarpStartEndTimes[1][1]     = nextFrameBase + frameDelta;
   1.443 +
   1.444 +        break;
   1.445 +    case HmdShutter_RollingRightToLeft:
   1.446 +
   1.447 +        EyeRenderTimes[0]               = nextFrameBase + frameDelta * 0.75;
   1.448 +        EyeRenderTimes[1]               = nextFrameBase + frameDelta * 0.25;
   1.449 +        
   1.450 +        // This is *Correct* with Tom's distortion mesh organization.
   1.451 +        TimeWarpStartEndTimes[0][0]     = nextFrameBase ;
   1.452 +        TimeWarpStartEndTimes[0][1]     = nextFrameBase + frameDelta;
   1.453 +        TimeWarpStartEndTimes[1][0]     = nextFrameBase ;
   1.454 +        TimeWarpStartEndTimes[1][1]     = nextFrameBase + frameDelta;
   1.455 +        break;
   1.456 +    case HmdShutter_Global:
   1.457 +        // TBD
   1.458 +        EyeRenderTimes[0]               = MidpointTime;
   1.459 +        EyeRenderTimes[1]               = MidpointTime;
   1.460 +        TimeWarpStartEndTimes[0][0]     = MidpointTime;
   1.461 +        TimeWarpStartEndTimes[0][1]     = MidpointTime;
   1.462 +        TimeWarpStartEndTimes[1][0]     = MidpointTime;
   1.463 +        TimeWarpStartEndTimes[1][1]     = MidpointTime;
   1.464 +        break;
   1.465 +    default:
   1.466 +        break;
   1.467 +    }
   1.468 +}
   1.469 +
   1.470 +  
   1.471 +double FrameTimeManager::BeginFrame(unsigned frameIndex)
   1.472 +{    
   1.473 +    RenderIMUTimeSeconds = 0.0;
   1.474 +    TimewarpIMUTimeSeconds = 0.0;
   1.475 +
   1.476 +    // TPH - putting an assert so this doesn't remain a hidden problem.
   1.477 +    OVR_ASSERT(FrameTiming.Inputs.ScreenDelay != 0);
   1.478 +
   1.479 +    // ThisFrameTime comes from the end of last frame, unless it it changed.
   1.480 +    double thisFrameTime = (FrameTiming.NextFrameTime != 0.0) ?
   1.481 +                           FrameTiming.NextFrameTime : ovr_GetTimeInSeconds();
   1.482 +    
   1.483 +    // We are starting to process a new frame...
   1.484 +    FrameTiming.InitTimingFromInputs(FrameTiming.Inputs, RenderInfo.Shutter.Type,
   1.485 +                                     thisFrameTime, frameIndex);
   1.486 +
   1.487 +    return FrameTiming.ThisFrameTime;
   1.488 +}
   1.489 +
   1.490 +
   1.491 +void FrameTimeManager::EndFrame()
   1.492 +{
   1.493 +    // Record timing since last frame; must be called after Present & sync.
   1.494 +    FrameTiming.NextFrameTime = ovr_GetTimeInSeconds();    
   1.495 +    if (FrameTiming.ThisFrameTime > 0.0)
   1.496 +    {
   1.497 +    //Revisit dynamic pre-Timewarp delay adjustment logic
   1.498 +    /*
   1.499 +        double actualFrameDelta = FrameTiming.NextFrameTime - FrameTiming.ThisFrameTime;
   1.500 +
   1.501 +        if (VsyncEnabled)
   1.502 +            TimewarpAdjuster.UpdateTimewarpWaitIfSkippedFrames(this, actualFrameDelta,
   1.503 +                                                               FrameTiming.NextFrameTime);
   1.504 +
   1.505 +        FrameTimeDeltas.AddTimeDelta(actualFrameDelta);
   1.506 +    */
   1.507 +        FrameTimeDeltas.AddTimeDelta(FrameTiming.NextFrameTime - FrameTiming.ThisFrameTime);
   1.508 +        FrameTiming.Inputs.FrameDelta = calcFrameDelta();
   1.509 +    }
   1.510 +
   1.511 +    // Write to Lock-less
   1.512 +    LocklessTiming.SetState(FrameTiming);
   1.513 +}
   1.514 +
   1.515 +// Thread-safe function to query timing for a future frame
   1.516 +
   1.517 +FrameTimeManager::Timing FrameTimeManager::GetFrameTiming(unsigned frameIndex)
   1.518 +{
   1.519 +    Timing frameTiming = LocklessTiming.GetState();
   1.520 +
   1.521 +    if (frameTiming.ThisFrameTime == 0.0)
   1.522 +    {
   1.523 +        // If timing hasn't been initialized, starting based on "now" is the best guess.
   1.524 +        frameTiming.InitTimingFromInputs(frameTiming.Inputs, RenderInfo.Shutter.Type,
   1.525 +                                         ovr_GetTimeInSeconds(), frameIndex);
   1.526 +    }
   1.527 +    
   1.528 +    else if (frameIndex > frameTiming.FrameIndex)
   1.529 +    {
   1.530 +        unsigned frameDelta    = frameIndex - frameTiming.FrameIndex;
   1.531 +        double   thisFrameTime = frameTiming.NextFrameTime +
   1.532 +                                 double(frameDelta-1) * frameTiming.Inputs.FrameDelta;
   1.533 +        // Don't run away too far into the future beyond rendering.
   1.534 +		OVR_DEBUG_LOG_COND(frameDelta >= 6, ("GetFrameTiming is 6 or more frames in future beyond rendering!"));
   1.535 +
   1.536 +        frameTiming.InitTimingFromInputs(frameTiming.Inputs, RenderInfo.Shutter.Type,
   1.537 +                                         thisFrameTime, frameIndex);
   1.538 +    }
   1.539 +
   1.540 +    return frameTiming;
   1.541 +}
   1.542 +
   1.543 +
   1.544 +double FrameTimeManager::GetEyePredictionTime(ovrEyeType eye, unsigned int frameIndex)
   1.545 +{
   1.546 +    if (VsyncEnabled)
   1.547 +    {
   1.548 +        FrameTimeManager::Timing frameTiming = GetFrameTiming(frameIndex);
   1.549 +
   1.550 +        // Special case: ovrEye_Count predicts to midpoint
   1.551 +        return (eye == ovrEye_Count) ? frameTiming.MidpointTime : frameTiming.EyeRenderTimes[eye];
   1.552 +    }
   1.553 +
   1.554 +    // No VSync: Best guess for the near future
   1.555 +    return ovr_GetTimeInSeconds() + ScreenSwitchingDelay + NoVSyncToScanoutDelay;
   1.556 +}
   1.557 +
   1.558 +ovrTrackingState FrameTimeManager::GetEyePredictionTracking(ovrHmd hmd, ovrEyeType eye, unsigned int frameIndex)
   1.559 +{
   1.560 +    double           eyeRenderTime = GetEyePredictionTime(eye, frameIndex);
   1.561 +    ovrTrackingState eyeState      = ovrHmd_GetTrackingState(hmd, eyeRenderTime);
   1.562 +        
   1.563 +    // Record view pose sampling time for Latency reporting.
   1.564 +    if (RenderIMUTimeSeconds == 0.0)
   1.565 +    {
   1.566 +        // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds()
   1.567 +        //RenderIMUTimeSeconds = eyeState.RawSensorData.TimeInSeconds;
   1.568 +        RenderIMUTimeSeconds = ovr_GetTimeInSeconds();
   1.569 +    }
   1.570 +
   1.571 +    return eyeState;
   1.572 +}
   1.573 +
   1.574 +Posef FrameTimeManager::GetEyePredictionPose(ovrHmd hmd, ovrEyeType eye)
   1.575 +{
   1.576 +    double           eyeRenderTime = GetEyePredictionTime(eye, 0);
   1.577 +    ovrTrackingState eyeState      = ovrHmd_GetTrackingState(hmd, eyeRenderTime);
   1.578 +
   1.579 +    // Record view pose sampling time for Latency reporting.
   1.580 +    if (RenderIMUTimeSeconds == 0.0)
   1.581 +    {
   1.582 +        // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds()
   1.583 +        //RenderIMUTimeSeconds = eyeState.RawSensorData.TimeInSeconds;
   1.584 +        RenderIMUTimeSeconds = ovr_GetTimeInSeconds();
   1.585 +    }
   1.586 +
   1.587 +    return eyeState.HeadPose.ThePose;
   1.588 +}
   1.589 +
   1.590 +void FrameTimeManager::GetTimewarpPredictions(ovrEyeType eye, double timewarpStartEnd[2])
   1.591 +{
   1.592 +    if (VsyncEnabled)
   1.593 +    {
   1.594 +        timewarpStartEnd[0] = FrameTiming.TimeWarpStartEndTimes[eye][0];
   1.595 +        timewarpStartEnd[1] = FrameTiming.TimeWarpStartEndTimes[eye][1];
   1.596 +        return;
   1.597 +    }    
   1.598 +
   1.599 +    // Free-running, so this will be displayed immediately.
   1.600 +    // Unfortunately we have no idea which bit of the screen is actually going to be displayed.
   1.601 +    // TODO: guess which bit of the screen is being displayed!
   1.602 +    // (e.g. use DONOTWAIT on present and see when the return isn't WASSTILLWAITING?)
   1.603 +
   1.604 +    // We have no idea where scan-out is currently, so we can't usefully warp the screen spatially.
   1.605 +    timewarpStartEnd[0] = ovr_GetTimeInSeconds() + ScreenSwitchingDelay + NoVSyncToScanoutDelay;
   1.606 +    timewarpStartEnd[1] = timewarpStartEnd[0];
   1.607 +}
   1.608 +
   1.609 +
   1.610 +void FrameTimeManager::GetTimewarpMatrices(ovrHmd hmd, ovrEyeType eyeId,
   1.611 +                                           ovrPosef renderPose, ovrMatrix4f twmOut[2],
   1.612 +										   double debugTimingOffsetInSeconds)
   1.613 +{
   1.614 +    if (!hmd)
   1.615 +    {
   1.616 +        return;
   1.617 +    }
   1.618 +
   1.619 +    double timewarpStartEnd[2] = { 0.0, 0.0 };    
   1.620 +    GetTimewarpPredictions(eyeId, timewarpStartEnd);
   1.621 +
   1.622 +	//TPH, to vary timing, to allow developers to debug, to shunt the predicted time forward 
   1.623 +	//and back, and see if the SDK is truly delivering the correct time.  Also to allow
   1.624 +	//illustration of the detrimental effects when this is not done right. 
   1.625 +	timewarpStartEnd[0] += debugTimingOffsetInSeconds;
   1.626 +	timewarpStartEnd[1] += debugTimingOffsetInSeconds;
   1.627 +
   1.628 +      
   1.629 +    //HMDState* p = (HMDState*)hmd;
   1.630 +    ovrTrackingState startState = ovrHmd_GetTrackingState(hmd, timewarpStartEnd[0]);
   1.631 +    ovrTrackingState endState   = ovrHmd_GetTrackingState(hmd, timewarpStartEnd[1]);
   1.632 +
   1.633 +    if (TimewarpIMUTimeSeconds == 0.0)
   1.634 +    {
   1.635 +        // TODO: Figure out why this are not as accurate as ovr_GetTimeInSeconds()
   1.636 +        //TimewarpIMUTimeSeconds = startState.RawSensorData.TimeInSeconds;
   1.637 +        TimewarpIMUTimeSeconds = ovr_GetTimeInSeconds();
   1.638 +    }
   1.639 +
   1.640 +    Quatf quatFromStart = startState.HeadPose.ThePose.Orientation;
   1.641 +    Quatf quatFromEnd   = endState.HeadPose.ThePose.Orientation;
   1.642 +    Quatf quatFromEye   = renderPose.Orientation; //EyeRenderPoses[eyeId].Orientation;
   1.643 +    quatFromEye.Invert();   // because we need the view matrix, not the camera matrix
   1.644 +    
   1.645 +    Quatf timewarpStartQuat = quatFromEye * quatFromStart;
   1.646 +    Quatf timewarpEndQuat   = quatFromEye * quatFromEnd;
   1.647 +
   1.648 +    Matrix4f timewarpStart(timewarpStartQuat);
   1.649 +    Matrix4f timewarpEnd(timewarpEndQuat);
   1.650 +    
   1.651 +
   1.652 +    // The real-world orientations have:                                  X=right, Y=up,   Z=backwards.
   1.653 +    // The vectors inside the mesh are in NDC to keep the shader simple: X=right, Y=down, Z=forwards.
   1.654 +    // So we need to perform a similarity transform on this delta matrix.
   1.655 +    // The verbose code would look like this:
   1.656 +    /*
   1.657 +    Matrix4f matBasisChange;
   1.658 +    matBasisChange.SetIdentity();
   1.659 +    matBasisChange.M[0][0] =  1.0f;
   1.660 +    matBasisChange.M[1][1] = -1.0f;
   1.661 +    matBasisChange.M[2][2] = -1.0f;
   1.662 +    Matrix4f matBasisChangeInv = matBasisChange.Inverted();
   1.663 +    matRenderFromNow = matBasisChangeInv * matRenderFromNow * matBasisChange;
   1.664 +    */
   1.665 +    // ...but of course all the above is a constant transform and much more easily done.
   1.666 +    // We flip the signs of the Y&Z row, then flip the signs of the Y&Z column,
   1.667 +    // and of course most of the flips cancel:
   1.668 +    // +++                        +--                     +--
   1.669 +    // +++ -> flip Y&Z columns -> +-- -> flip Y&Z rows -> -++
   1.670 +    // +++                        +--                     -++
   1.671 +    timewarpStart.M[0][1] = -timewarpStart.M[0][1];
   1.672 +    timewarpStart.M[0][2] = -timewarpStart.M[0][2];
   1.673 +    timewarpStart.M[1][0] = -timewarpStart.M[1][0];
   1.674 +    timewarpStart.M[2][0] = -timewarpStart.M[2][0];
   1.675 +
   1.676 +    timewarpEnd  .M[0][1] = -timewarpEnd  .M[0][1];
   1.677 +    timewarpEnd  .M[0][2] = -timewarpEnd  .M[0][2];
   1.678 +    timewarpEnd  .M[1][0] = -timewarpEnd  .M[1][0];
   1.679 +    timewarpEnd  .M[2][0] = -timewarpEnd  .M[2][0];
   1.680 +
   1.681 +    twmOut[0] = timewarpStart;
   1.682 +    twmOut[1] = timewarpEnd;
   1.683 +}
   1.684 +
   1.685 +
   1.686 +// Used by renderer to determine if it should time distortion rendering.
   1.687 +bool  FrameTimeManager::NeedDistortionTimeMeasurement() const
   1.688 +{
   1.689 +    if (!VsyncEnabled)
   1.690 +        return false;
   1.691 +    return DistortionRenderTimes.GetCount() < DistortionRenderTimes.Capacity;
   1.692 +}
   1.693 +
   1.694 +
   1.695 +void  FrameTimeManager::AddDistortionTimeMeasurement(double distortionTimeSeconds)
   1.696 +{
   1.697 +    DistortionRenderTimes.AddTimeDelta(distortionTimeSeconds);
   1.698 +
   1.699 +    //Revisit dynamic pre-Timewarp delay adjustment logic
   1.700 +    //updateTimewarpTiming();
   1.701 +
   1.702 +    // If timewarp timing changes based on this sample, update it.
   1.703 +    double newTimewarpWaitDelta = calcTimewarpWaitDelta();
   1.704 +    if (newTimewarpWaitDelta != FrameTiming.Inputs.TimewarpWaitDelta)
   1.705 +    {
   1.706 +        FrameTiming.Inputs.TimewarpWaitDelta = newTimewarpWaitDelta;
   1.707 +        LocklessTiming.SetState(FrameTiming);
   1.708 +    }
   1.709 +}
   1.710 +
   1.711 +
   1.712 +void FrameTimeManager::UpdateFrameLatencyTrackingAfterEndFrame(
   1.713 +                                    unsigned char frameLatencyTestColor[3],
   1.714 +                                    const Util::FrameTimeRecordSet& rs)
   1.715 +{    
   1.716 +    // FrameTiming.NextFrameTime in this context (after EndFrame) is the end frame time.
   1.717 +    ScreenLatencyTracker.SaveDrawColor(frameLatencyTestColor[0],
   1.718 +                                       FrameTiming.NextFrameTime,
   1.719 +                                       RenderIMUTimeSeconds,
   1.720 +                                       TimewarpIMUTimeSeconds);
   1.721 +
   1.722 +    ScreenLatencyTracker.MatchRecord(rs);
   1.723 +
   1.724 +    // If screen delay changed, update timing.
   1.725 +    double newScreenDelay = calcScreenDelay();
   1.726 +    if (newScreenDelay != FrameTiming.Inputs.ScreenDelay)
   1.727 +    {
   1.728 +        FrameTiming.Inputs.ScreenDelay = newScreenDelay;
   1.729 +        LocklessTiming.SetState(FrameTiming);
   1.730 +    }
   1.731 +}
   1.732 +
   1.733 +
   1.734 +//-----------------------------------------------------------------------------------
   1.735 +//Revisit dynamic pre-Timewarp delay adjustment logic
   1.736 +/*
   1.737 +void FrameTimeManager::TimewarpDelayAdjuster::Reset()    
   1.738 +{
   1.739 +    State                           = State_WaitingToReduceLevel;
   1.740 +    DelayLevel                      = 0;
   1.741 +    InitialFrameCounter             = 0;
   1.742 +    TimewarpDelayReductionSeconds   = 0.0;
   1.743 +    DelayLevelFinishTime            = 0.0;
   1.744 +
   1.745 +    memset(WaitTimeIndexForLevel, 0, sizeof(WaitTimeIndexForLevel));
   1.746 +    // If we are at level 0, waits are infinite.
   1.747 +    WaitTimeIndexForLevel[0] = MaxTimeIndex;
   1.748 +}
   1.749 +
   1.750 +
   1.751 +void FrameTimeManager::TimewarpDelayAdjuster::
   1.752 +        UpdateTimewarpWaitIfSkippedFrames(FrameTimeManager* manager,
   1.753 +                                          double measuredFrameDelta, double nextFrameTime)
   1.754 +{    
   1.755 +    // Times in seconds
   1.756 +    const static double delayTimingTiers[7] = { 1.0, 5.0, 15.0, 30.0, 60.0, 120.0, 1000000.0 };
   1.757 +
   1.758 +    const double currentFrameDelta = manager->FrameTiming.Inputs.FrameDelta;
   1.759 +
   1.760 +
   1.761 +    // Once we detected frame spike, we skip several frames before testing again.    
   1.762 +    if (InitialFrameCounter > 0)
   1.763 +    {
   1.764 +        InitialFrameCounter --;
   1.765 +        return;
   1.766 +    }
   1.767 +
   1.768 +    // Skipped frame would usually take 2x longer then regular frame
   1.769 +    if (measuredFrameDelta > currentFrameDelta * 1.8)
   1.770 +    {        
   1.771 +        if (State == State_WaitingToReduceLevel)
   1.772 +        {
   1.773 +            // If we got here, escalate the level again. 
   1.774 +            if (DelayLevel < MaxDelayLevel)
   1.775 +            {
   1.776 +                DelayLevel++;
   1.777 +                InitialFrameCounter = 3;
   1.778 +            }
   1.779 +        }
   1.780 +
   1.781 +        else if (State == State_VerifyingAfterReduce)
   1.782 +        {
   1.783 +            // So we went down to this level and tried to wait to see if there was
   1.784 +            // as skipped frame and there is -> go back up a level and incrment its timing tier
   1.785 +            if (DelayLevel < MaxDelayLevel)
   1.786 +            {
   1.787 +                DelayLevel++;
   1.788 +                State = State_WaitingToReduceLevel;
   1.789 +                
   1.790 +                // For higher level delays reductions, i.e. more then half a frame,
   1.791 +                // we don't go into the infinite wait tier.
   1.792 +                int maxTimingTier = MaxTimeIndex;
   1.793 +                if (DelayLevel > MaxInfiniteTimingLevel)
   1.794 +                    maxTimingTier--;
   1.795 +
   1.796 +                if (WaitTimeIndexForLevel[DelayLevel] < maxTimingTier )
   1.797 +                    WaitTimeIndexForLevel[DelayLevel]++;
   1.798 +            }
   1.799 +        }
   1.800 +
   1.801 +        DelayLevelFinishTime          = nextFrameTime +
   1.802 +                                        delayTimingTiers[WaitTimeIndexForLevel[DelayLevel]];
   1.803 +        TimewarpDelayReductionSeconds = currentFrameDelta * 0.125 * DelayLevel;
   1.804 +        manager->updateTimewarpTiming();
   1.805 +
   1.806 +    }
   1.807 +
   1.808 +    else if (nextFrameTime > DelayLevelFinishTime)
   1.809 +    {        
   1.810 +        if (State == State_WaitingToReduceLevel)
   1.811 +        {
   1.812 +            if (DelayLevel > 0)
   1.813 +            {
   1.814 +                DelayLevel--;
   1.815 +                State = State_VerifyingAfterReduce;
   1.816 +                // Always use 1 sec to see if "down sampling mode" caused problems
   1.817 +                DelayLevelFinishTime = nextFrameTime + 1.0f; 
   1.818 +            }
   1.819 +        }
   1.820 +        else if (State == State_VerifyingAfterReduce)
   1.821 +        {
   1.822 +            // Prior display level successfully reduced,
   1.823 +            // try to see we we could go down further after wait.
   1.824 +            WaitTimeIndexForLevel[DelayLevel+1] = 0;            
   1.825 +            State                               = State_WaitingToReduceLevel;
   1.826 +            DelayLevelFinishTime                = nextFrameTime +
   1.827 +                                                  delayTimingTiers[WaitTimeIndexForLevel[DelayLevel]];
   1.828 +        }
   1.829 +
   1.830 +        // TBD: Update TimeWarpTiming
   1.831 +        TimewarpDelayReductionSeconds = currentFrameDelta * 0.125 * DelayLevel;
   1.832 +        manager->updateTimewarpTiming();
   1.833 +    }
   1.834 +
   1.835 +
   1.836 +    //static int oldDelayLevel = 0;
   1.837 +
   1.838 +    //if (oldDelayLevel != DelayLevel)
   1.839 +    //{
   1.840 +        //OVR_DEBUG_LOG(("DelayLevel:%d tReduction = %0.5f ", DelayLevel, TimewarpDelayReductionSeconds));
   1.841 +        //oldDelayLevel = DelayLevel;
   1.842 +    //}
   1.843 +    }
   1.844 +    */
   1.845 +
   1.846 +//-----------------------------------------------------------------------------------
   1.847 +// ***** TimeDeltaCollector
   1.848 +
   1.849 +void TimeDeltaCollector::AddTimeDelta(double timeSeconds)
   1.850 +{
   1.851 +    // avoid adding invalid timing values
   1.852 +    if(timeSeconds < 0.0f)
   1.853 +        return;
   1.854 +
   1.855 +    if (Count == Capacity)
   1.856 +    {
   1.857 +        for(int i=0; i< Count-1; i++)
   1.858 +            TimeBufferSeconds[i] = TimeBufferSeconds[i+1];
   1.859 +        Count--;
   1.860 +    }
   1.861 +    TimeBufferSeconds[Count++] = timeSeconds;
   1.862 +
   1.863 +    ReCalcMedian = true;
   1.864 +}
   1.865 +
   1.866 +// KevinJ: Better median function
   1.867 +double CalculateListMedianRecursive(const double inputList[TimeDeltaCollector::Capacity], int inputListLength, int lessThanSum, int greaterThanSum)
   1.868 +{
   1.869 +    double lessThanMedian[TimeDeltaCollector::Capacity], greaterThanMedian[TimeDeltaCollector::Capacity];
   1.870 +    int lessThanMedianListLength = 0, greaterThanMedianListLength = 0;
   1.871 +    double median = inputList[0];
   1.872 +    int i;
   1.873 +    for (i = 1; i < inputListLength; i++)
   1.874 +    {
   1.875 +        // If same value, spread among lists evenly
   1.876 +        if (inputList[i] < median || ((i & 1) == 0 && inputList[i] == median))
   1.877 +            lessThanMedian[lessThanMedianListLength++] = inputList[i];
   1.878 +        else
   1.879 +            greaterThanMedian[greaterThanMedianListLength++] = inputList[i];
   1.880 +    }
   1.881 +    if (lessThanMedianListLength + lessThanSum == greaterThanMedianListLength + greaterThanSum + 1 ||
   1.882 +        lessThanMedianListLength + lessThanSum == greaterThanMedianListLength + greaterThanSum - 1)
   1.883 +        return median;
   1.884 +
   1.885 +    if (lessThanMedianListLength + lessThanSum < greaterThanMedianListLength + greaterThanSum)
   1.886 +    {
   1.887 +        lessThanMedian[lessThanMedianListLength++] = median;
   1.888 +        return CalculateListMedianRecursive(greaterThanMedian, greaterThanMedianListLength, lessThanMedianListLength + lessThanSum, greaterThanSum);
   1.889 +    }
   1.890 +    else
   1.891 +    {
   1.892 +        greaterThanMedian[greaterThanMedianListLength++] = median;
   1.893 +        return CalculateListMedianRecursive(lessThanMedian, lessThanMedianListLength, lessThanSum, greaterThanMedianListLength + greaterThanSum);
   1.894 +    }
   1.895 +}
   1.896 +// KevinJ: Excludes Firmware hack
   1.897 +double TimeDeltaCollector::GetMedianTimeDeltaNoFirmwareHack() const
   1.898 +{
   1.899 +    if (ReCalcMedian)
   1.900 +    {
   1.901 +        ReCalcMedian = false;
   1.902 +        Median = CalculateListMedianRecursive(TimeBufferSeconds, Count, 0, 0);
   1.903 +    }
   1.904 +    return Median;
   1.905 +}
   1.906 +double TimeDeltaCollector::GetMedianTimeDelta() const
   1.907 +{
   1.908 +    if(ReCalcMedian)
   1.909 +    {
   1.910 +        double  SortedList[Capacity];
   1.911 +        bool    used[Capacity];
   1.912 +
   1.913 +        memset(used, 0, sizeof(used));
   1.914 +        SortedList[0] = 0.0; // In case Count was 0...
   1.915 +
   1.916 +        // Probably the slowest way to find median...
   1.917 +        for (int i=0; i<Count; i++)
   1.918 +        {
   1.919 +            double smallestDelta = 1000000.0;
   1.920 +            int    index = 0;
   1.921 +
   1.922 +            for (int j = 0; j < Count; j++)
   1.923 +            {
   1.924 +                if (!used[j])
   1.925 +                {                
   1.926 +                    if (TimeBufferSeconds[j] < smallestDelta)
   1.927 +                    {
   1.928 +                        smallestDelta = TimeBufferSeconds[j];
   1.929 +                        index = j;
   1.930 +                    }
   1.931 +                }
   1.932 +            }
   1.933 +
   1.934 +            // Mark as used
   1.935 +            used[index]   = true;
   1.936 +            SortedList[i] = smallestDelta;
   1.937 +        }
   1.938 +
   1.939 +        // FIRMWARE HACK: Don't take the actual median, but err on the low time side
   1.940 +        Median = SortedList[Count/4];
   1.941 +        ReCalcMedian = false;
   1.942 +    }
   1.943 +
   1.944 +    return Median;
   1.945 +}
   1.946 +      
   1.947 +
   1.948 +}} // namespace OVR::CAPI
   1.949 +