Skip to content

Commit 0d8a9fe

Browse files
committed
timers: add an implementation that uses windows timers
1 parent d17069d commit 0d8a9fe

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

src/ossia/detail/sleep.hpp

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#if defined(_WIN32)
66
#include <Windows.h>
7+
#include <timeapi.h>
78
#elif defined(__APPLE__)
89
#include <mach/mach.h>
910
#include <mach/mach_time.h>
@@ -134,4 +135,157 @@ class adaptive_sleep
134135
int64_t m_count = 1;
135136
};
136137

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
137291
}

0 commit comments

Comments
 (0)