|
4 | 4 |
|
5 | 5 | #if defined(_WIN32) |
6 | 6 | #include <Windows.h> |
| 7 | +#include <timeapi.h> |
7 | 8 | #elif defined(__APPLE__) |
8 | 9 | #include <mach/mach.h> |
9 | 10 | #include <mach/mach_time.h> |
@@ -134,4 +135,157 @@ class adaptive_sleep |
134 | 135 | int64_t m_count = 1; |
135 | 136 | }; |
136 | 137 |
|
| 138 | +class frame_pacing_sleep |
| 139 | +{ |
| 140 | + static constexpr int64_t max_count = 500; |
| 141 | + static constexpr double min_margin_ns = 500'000.0; // 0.5ms floor |
| 142 | + static constexpr double max_margin_ns = 5'000'000.0; // 5ms ceiling |
| 143 | + static constexpr double target_error_ns = -500'000.0; // Target waking 0.5ms early |
| 144 | + static constexpr double correction_rate = 0.1; |
| 145 | + |
| 146 | +public: |
| 147 | + void sleep_until(uint64_t target_ns) |
| 148 | + { |
| 149 | + const int64_t to_sleep = static_cast<int64_t>(target_ns - now_ns()) - static_cast<int64_t>(m_margin_ns); |
| 150 | + |
| 151 | + if (to_sleep > 0) |
| 152 | + coarse_sleep(to_sleep); |
| 153 | + |
| 154 | + // Spin for final precision |
| 155 | + while (now_ns() < target_ns) |
| 156 | + { |
| 157 | + ossia_rwlock_pause(); |
| 158 | + } |
| 159 | + |
| 160 | + // Measure error: negative = early (good), positive = late (bad) |
| 161 | + const int64_t error_ns = static_cast<int64_t>(now_ns() - target_ns); |
| 162 | + update(static_cast<double>(error_ns)); |
| 163 | + } |
| 164 | + |
| 165 | +private: |
| 166 | + void update(double error_ns) |
| 167 | + { |
| 168 | + if (m_count >= max_count) |
| 169 | + { |
| 170 | + m_count = max_count / 2; |
| 171 | + m_m2 /= 2.0; |
| 172 | + } |
| 173 | + |
| 174 | + ++m_count; |
| 175 | + const double delta = error_ns - m_mean_error_ns; |
| 176 | + m_mean_error_ns += delta / static_cast<double>(m_count); |
| 177 | + m_m2 += delta * (error_ns - m_mean_error_ns); |
| 178 | + |
| 179 | + // Adjust margin to push mean error toward target (slightly early) |
| 180 | + const double correction = (m_mean_error_ns - target_error_ns) * correction_rate; |
| 181 | + m_margin_ns = std::clamp(m_margin_ns + correction, min_margin_ns, max_margin_ns); |
| 182 | + } |
| 183 | + |
| 184 | + double m_margin_ns = 2'000'000.0; |
| 185 | + double m_mean_error_ns = 0.0; |
| 186 | + double m_m2 = 0.0; |
| 187 | + int64_t m_count = 0; |
| 188 | +}; |
| 189 | + |
| 190 | +#if defined(_WIN32) |
| 191 | +class windows_timer_sleep |
| 192 | +{ |
| 193 | + static constexpr int64_t max_count = 500; |
| 194 | + static constexpr double min_margin_ns = 100'000.0; // 0.1ms floor (high-res timer is good) |
| 195 | + static constexpr double max_margin_ns = 2'000'000.0; // 2ms ceiling |
| 196 | + static constexpr double target_error_ns = -100'000.0; // Target waking 0.1ms early |
| 197 | + static constexpr double correction_rate = 0.1; |
| 198 | + |
| 199 | +public: |
| 200 | + windows_timer_sleep() |
| 201 | + { |
| 202 | + // Request 1ms system timer resolution |
| 203 | + timeBeginPeriod(1); |
| 204 | + |
| 205 | + // High-resolution waitable timer |
| 206 | + m_timer = CreateWaitableTimerExW( |
| 207 | + nullptr, nullptr, |
| 208 | + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, |
| 209 | + TIMER_ALL_ACCESS |
| 210 | + ); |
| 211 | + |
| 212 | + if (m_timer) |
| 213 | + { |
| 214 | + m_high_resolution = true; |
| 215 | + } |
| 216 | + else |
| 217 | + { |
| 218 | + // Fallback to standard waitable timer |
| 219 | + m_timer = CreateWaitableTimerW(nullptr, TRUE, nullptr); |
| 220 | + m_high_resolution = false; |
| 221 | + m_margin_ns = 1'500'000.0; // Need more margin without high-res |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + ~windows_timer_sleep() |
| 226 | + { |
| 227 | + if (m_timer) |
| 228 | + CloseHandle(m_timer); |
| 229 | + timeEndPeriod(1); |
| 230 | + } |
| 231 | + |
| 232 | + windows_timer_sleep(const windows_timer_sleep&) = delete; |
| 233 | + windows_timer_sleep& operator=(const windows_timer_sleep&) = delete; |
| 234 | + |
| 235 | + void sleep_until(uint64_t target_ns) |
| 236 | + { |
| 237 | + const uint64_t now = ossia::now_ns(); |
| 238 | + if (target_ns <= now) |
| 239 | + return; |
| 240 | + |
| 241 | + const int64_t remaining_ns = static_cast<int64_t>(target_ns - now); |
| 242 | + const int64_t sleep_ns = remaining_ns - static_cast<int64_t>(m_margin_ns); |
| 243 | + |
| 244 | + if (sleep_ns > 0) |
| 245 | + { |
| 246 | + // Negative value = relative time, in 100ns units |
| 247 | + LARGE_INTEGER due_time; |
| 248 | + due_time.QuadPart = -(sleep_ns / 100); |
| 249 | + |
| 250 | + SetWaitableTimerEx(m_timer, &due_time, 0, nullptr, nullptr, nullptr, 0); |
| 251 | + WaitForSingleObject(m_timer, INFINITE); |
| 252 | + } |
| 253 | + |
| 254 | + // Spin for final precision |
| 255 | + while (ossia::now_ns() < target_ns) |
| 256 | + { |
| 257 | + YieldProcessor(); |
| 258 | + } |
| 259 | + |
| 260 | + // Measure actual error and adapt |
| 261 | + const int64_t error_ns = static_cast<int64_t>(ossia::now_ns() - target_ns); |
| 262 | + update(static_cast<double>(error_ns)); |
| 263 | + } |
| 264 | + |
| 265 | +private: |
| 266 | + void update(double error_ns) |
| 267 | + { |
| 268 | + if (m_count >= max_count) |
| 269 | + { |
| 270 | + m_count = max_count / 2; |
| 271 | + m_m2 /= 2.0; |
| 272 | + } |
| 273 | + |
| 274 | + ++m_count; |
| 275 | + const double delta = error_ns - m_mean_error_ns; |
| 276 | + m_mean_error_ns += delta / static_cast<double>(m_count); |
| 277 | + m_m2 += delta * (error_ns - m_mean_error_ns); |
| 278 | + |
| 279 | + const double correction = (m_mean_error_ns - target_error_ns) * correction_rate; |
| 280 | + m_margin_ns = std::clamp(m_margin_ns + correction, min_margin_ns, max_margin_ns); |
| 281 | + } |
| 282 | + |
| 283 | + HANDLE m_timer = nullptr; |
| 284 | + bool m_high_resolution = false; |
| 285 | + double m_margin_ns = 500'000.0; |
| 286 | + double m_mean_error_ns = 0.0; |
| 287 | + double m_m2 = 0.0; |
| 288 | + int64_t m_count = 0; |
| 289 | +}; |
| 290 | +#endif |
137 | 291 | } |
0 commit comments