// // StepTimer.h - A simple timer that provides elapsed time information // #pragma once #include #include #include namespace DX { // Helper class for animation and simulation timing. class StepTimer { public: StepTimer() noexcept(false) : m_elapsedTicks(0), m_totalTicks(0), m_leftOverTicks(0), m_frameCount(0), m_framesPerSecond(0), m_framesThisSecond(0), m_qpcSecondCounter(0), m_isFixedTimeStep(false), m_targetElapsedTicks(TicksPerSecond / 60) { if (!QueryPerformanceFrequency(&m_qpcFrequency)) { throw std::exception( "QueryPerformanceFrequency" ); } if (!QueryPerformanceCounter(&m_qpcLastTime)) { throw std::exception( "QueryPerformanceCounter" ); } // Initialize max delta to 1/10 of a second. m_qpcMaxDelta = static_cast(m_qpcFrequency.QuadPart / 10); } // Get elapsed time since the previous Update call. uint64_t GetElapsedTicks() const noexcept { return m_elapsedTicks; } double GetElapsedSeconds() const noexcept { return TicksToSeconds(m_elapsedTicks); } // Get total time since the start of the program. uint64_t GetTotalTicks() const noexcept { return m_totalTicks; } double GetTotalSeconds() const noexcept { return TicksToSeconds(m_totalTicks); } // Get total number of updates since start of the program. uint32_t GetFrameCount() const noexcept { return m_frameCount; } // Get the current framerate. uint32_t GetFramesPerSecond() const noexcept { return m_framesPerSecond; } // Set whether to use fixed or variable timestep mode. void SetFixedTimeStep(bool isFixedTimestep) noexcept { m_isFixedTimeStep = isFixedTimestep; } // Set how often to call Update when in fixed timestep mode. void SetTargetElapsedTicks(uint64_t targetElapsed) noexcept { m_targetElapsedTicks = targetElapsed; } void SetTargetElapsedSeconds(double targetElapsed) noexcept { m_targetElapsedTicks = SecondsToTicks(targetElapsed); } // Integer format represents time using 10,000,000 ticks per second. static const uint64_t TicksPerSecond = 10000000; static constexpr double TicksToSeconds(uint64_t ticks) noexcept { return static_cast(ticks) / TicksPerSecond; } static constexpr uint64_t SecondsToTicks(double seconds) noexcept { return static_cast(seconds * TicksPerSecond); } // After an intentional timing discontinuity (for instance a blocking IO operation) // call this to avoid having the fixed timestep logic attempt a set of catch-up // Update calls. void ResetElapsedTime() { if (!QueryPerformanceCounter(&m_qpcLastTime)) { throw std::exception("QueryPerformanceCounter"); } m_leftOverTicks = 0; m_framesPerSecond = 0; m_framesThisSecond = 0; m_qpcSecondCounter = 0; } // Update timer state, calling the specified Update function the appropriate number of times. template void Tick(const TUpdate& update) { // Query the current time. LARGE_INTEGER currentTime; if (!QueryPerformanceCounter(¤tTime)) { throw std::exception( "QueryPerformanceCounter" ); } uint64_t timeDelta = static_cast(currentTime.QuadPart - m_qpcLastTime.QuadPart); m_qpcLastTime = currentTime; m_qpcSecondCounter += timeDelta; // Clamp excessively large time deltas (e.g. after paused in the debugger). if (timeDelta > m_qpcMaxDelta) { timeDelta = m_qpcMaxDelta; } // Convert QPC units into a canonical tick format. This cannot overflow due to the previous clamp. timeDelta *= TicksPerSecond; timeDelta /= static_cast(m_qpcFrequency.QuadPart); uint32_t lastFrameCount = m_frameCount; if (m_isFixedTimeStep) { // Fixed timestep update logic // If the app is running very close to the target elapsed time (within 1/4 of a millisecond) just clamp // the clock to exactly match the target value. This prevents tiny and irrelevant errors // from accumulating over time. Without this clamping, a game that requested a 60 fps // fixed update, running with vsync enabled on a 59.94 NTSC display, would eventually // accumulate enough tiny errors that it would drop a frame. It is better to just round // small deviations down to zero to leave things running smoothly. if (static_cast(std::abs(static_cast(timeDelta - m_targetElapsedTicks))) < TicksPerSecond / 4000) { timeDelta = m_targetElapsedTicks; } m_leftOverTicks += timeDelta; while (m_leftOverTicks >= m_targetElapsedTicks) { m_elapsedTicks = m_targetElapsedTicks; m_totalTicks += m_targetElapsedTicks; m_leftOverTicks -= m_targetElapsedTicks; m_frameCount++; update(); } } else { // Variable timestep update logic. m_elapsedTicks = timeDelta; m_totalTicks += timeDelta; m_leftOverTicks = 0; m_frameCount++; update(); } // Track the current framerate. if (m_frameCount != lastFrameCount) { m_framesThisSecond++; } if (m_qpcSecondCounter >= static_cast(m_qpcFrequency.QuadPart)) { m_framesPerSecond = m_framesThisSecond; m_framesThisSecond = 0; m_qpcSecondCounter %= static_cast(m_qpcFrequency.QuadPart); } } private: // Source timing data uses QPC units. LARGE_INTEGER m_qpcFrequency; LARGE_INTEGER m_qpcLastTime; uint64_t m_qpcMaxDelta; // Derived timing data uses a canonical tick format. uint64_t m_elapsedTicks; uint64_t m_totalTicks; uint64_t m_leftOverTicks; // Members for tracking the framerate. uint32_t m_frameCount; uint32_t m_framesPerSecond; uint32_t m_framesThisSecond; uint64_t m_qpcSecondCounter; // Members for configuring fixed timestep mode. bool m_isFixedTimeStep; uint64_t m_targetElapsedTicks; }; }