OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
sleep.hpp
1#pragma once
2
3#include <ossia/detail/yield.hpp>
4
5#if defined(_WIN32)
6#include <Windows.h>
7#include <timeapi.h>
8#elif defined(__APPLE__)
9#include <mach/mach.h>
10#include <mach/mach_time.h>
11#include <mach/thread_policy.h>
12#include <pthread.h>
13#else
14#include <time.h>
15#include <pthread.h>
16#include <sched.h>
17#include <unistd.h>
18#include <sys/resource.h>
19#include <sys/syscall.h>
20#endif
21
22#include <cinttypes>
23#include <cstdint>
24#include <cmath>
25#include <algorithm>
26
27namespace ossia
28{
29
30namespace detail
31{
32#if defined(_WIN32)
33static const LARGE_INTEGER g_performance_frequency = []() {
34 LARGE_INTEGER f;
35 QueryPerformanceFrequency(&f);
36 return f;
37}();
38#elif defined(__APPLE__)
39static const mach_timebase_info_data_t g_timebase_info = []() {
40 mach_timebase_info_data_t tb;
41 mach_timebase_info(&tb);
42 return tb;
43}();
44#endif
45}
46
47// Returns current time in nanoseconds
48inline uint64_t now_ns()
49{
50#if defined(_WIN32)
51 LARGE_INTEGER now;
52 QueryPerformanceCounter(&now);
53 return static_cast<uint64_t>(now.QuadPart * 1'000'000'000ULL / detail::g_performance_frequency.QuadPart);
54#elif defined(__APPLE__)
55 return mach_absolute_time() * detail::g_timebase_info.numer / detail::g_timebase_info.denom;
56#else
57 struct timespec ts;
58 clock_gettime(CLOCK_MONOTONIC, &ts);
59 return static_cast<uint64_t>(ts.tv_sec) * 1'000'000'000ULL + ts.tv_nsec;
60#endif
61}
62
63static void coarse_sleep(int64_t ns)
64{
65#if defined(_WIN32)
66 Sleep(static_cast<DWORD>(ns / 1'000'000));
67#elif defined(__APPLE__)
68 uint64_t now = mach_absolute_time();
69 uint64_t deltaMach = static_cast<uint64_t>(ns) * detail::g_timebase_info.denom / detail::g_timebase_info.numer;
70 mach_wait_until(now + deltaMach);
71#else
72 struct timespec ts;
73 ts.tv_sec = ns / 1'000'000'000LL;
74 ts.tv_nsec = ns % 1'000'000'000LL;
75 clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, nullptr);
76#endif
77}
78
79class adaptive_sleep
80{
81 static constexpr int64_t max_count = 1000;
82 static constexpr int64_t min_sleep_ns = 1'000'000; // Don't bother sleeping < 1ms
83
84public:
85 void sleep_until(uint64_t target_ns)
86 {
87 const uint64_t current = now_ns();
88 if (target_ns <= current)
89 return;
90
91 const int64_t remaining = static_cast<int64_t>(target_ns - current);
92 const int64_t to_sleep = remaining - static_cast<int64_t>(m_margin_ns);
93
94 if (to_sleep > min_sleep_ns)
95 {
96 const uint64_t start_ns = now_ns();
97 coarse_sleep(to_sleep);
98 const uint64_t end_ns = now_ns();
99
100 // Measure overshoot: how much longer than requested?
101 const double overshoot = static_cast<double>(static_cast<int64_t>(end_ns - start_ns) - to_sleep);
102 update(overshoot);
103 }
104
105 // Spin for final precision
106 while (now_ns() < target_ns)
107 {
108 ossia_rwlock_pause();
109 }
110 }
111
112private:
113 void update(double overshoot)
114 {
115 if (m_count >= max_count)
116 {
117 m_count = max_count / 2;
118 m_m2 /= 2.0;
119 }
120
121 ++m_count;
122 const double delta = overshoot - m_mean_overshoot_ns;
123 m_mean_overshoot_ns += delta / static_cast<double>(m_count);
124 m_m2 += delta * (overshoot - m_mean_overshoot_ns);
125
126 const double stddev = (m_count > 1) ? std::sqrt(m_m2 / static_cast<double>(m_count - 1)) : 0.0;
127
128 // Margin = mean overshoot + 2 stddev (to wake up early, not late)
129 m_margin_ns = std::max(1'000'000.0, m_mean_overshoot_ns + 2.0 * stddev);
130 }
131
132 double m_margin_ns = 2'000'000.0; // 2ms estimate
133 double m_mean_overshoot_ns = 0.0;
134 double m_m2 = 0.0;
135 int64_t m_count = 1;
136};
137
138class 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
146public:
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
165private:
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)
191class 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
199public:
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
265private:
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
291}
Definition git_info.h:7