1 | /* |
2 | Simple DirectMedia Layer |
3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
4 | |
5 | This software is provided 'as-is', without any express or implied |
6 | warranty. In no event will the authors be held liable for any damages |
7 | arising from the use of this software. |
8 | |
9 | Permission is granted to anyone to use this software for any purpose, |
10 | including commercial applications, and to alter it and redistribute it |
11 | freely, subject to the following restrictions: |
12 | |
13 | 1. The origin of this software must not be misrepresented; you must not |
14 | claim that you wrote the original software. If you use this software |
15 | in a product, an acknowledgment in the product documentation would be |
16 | appreciated but is not required. |
17 | 2. Altered source versions must be plainly marked as such, and must not be |
18 | misrepresented as being the original software. |
19 | 3. This notice may not be removed or altered from any source distribution. |
20 | */ |
21 | #include "SDL_internal.h" |
22 | |
23 | #include "SDL_timer_c.h" |
24 | #include "../thread/SDL_systhread.h" |
25 | |
26 | // #define DEBUG_TIMERS |
27 | |
28 | #if !defined(SDL_PLATFORM_EMSCRIPTEN) || !defined(SDL_THREADS_DISABLED) |
29 | |
30 | typedef struct SDL_Timer |
31 | { |
32 | SDL_TimerID timerID; |
33 | SDL_TimerCallback callback_ms; |
34 | SDL_NSTimerCallback callback_ns; |
35 | void *userdata; |
36 | Uint64 interval; |
37 | Uint64 scheduled; |
38 | SDL_AtomicInt canceled; |
39 | struct SDL_Timer *next; |
40 | } SDL_Timer; |
41 | |
42 | typedef struct SDL_TimerMap |
43 | { |
44 | SDL_TimerID timerID; |
45 | SDL_Timer *timer; |
46 | struct SDL_TimerMap *next; |
47 | } SDL_TimerMap; |
48 | |
49 | // The timers are kept in a sorted list |
50 | typedef struct |
51 | { |
52 | // Data used by the main thread |
53 | SDL_InitState init; |
54 | SDL_Thread *thread; |
55 | SDL_TimerMap *timermap; |
56 | SDL_Mutex *timermap_lock; |
57 | |
58 | // Padding to separate cache lines between threads |
59 | char cache_pad[SDL_CACHELINE_SIZE]; |
60 | |
61 | // Data used to communicate with the timer thread |
62 | SDL_SpinLock lock; |
63 | SDL_Semaphore *sem; |
64 | SDL_Timer *pending; |
65 | SDL_Timer *freelist; |
66 | SDL_AtomicInt active; |
67 | |
68 | // List of timers - this is only touched by the timer thread |
69 | SDL_Timer *timers; |
70 | } SDL_TimerData; |
71 | |
72 | static SDL_TimerData SDL_timer_data; |
73 | |
74 | /* The idea here is that any thread might add a timer, but a single |
75 | * thread manages the active timer queue, sorted by scheduling time. |
76 | * |
77 | * Timers are removed by simply setting a canceled flag |
78 | */ |
79 | |
80 | static void SDL_AddTimerInternal(SDL_TimerData *data, SDL_Timer *timer) |
81 | { |
82 | SDL_Timer *prev, *curr; |
83 | |
84 | prev = NULL; |
85 | for (curr = data->timers; curr; prev = curr, curr = curr->next) { |
86 | if (curr->scheduled > timer->scheduled) { |
87 | break; |
88 | } |
89 | } |
90 | |
91 | // Insert the timer here! |
92 | if (prev) { |
93 | prev->next = timer; |
94 | } else { |
95 | data->timers = timer; |
96 | } |
97 | timer->next = curr; |
98 | } |
99 | |
100 | static int SDLCALL SDL_TimerThread(void *_data) |
101 | { |
102 | SDL_TimerData *data = (SDL_TimerData *)_data; |
103 | SDL_Timer *pending; |
104 | SDL_Timer *current; |
105 | SDL_Timer *freelist_head = NULL; |
106 | SDL_Timer *freelist_tail = NULL; |
107 | Uint64 tick, now, interval, delay; |
108 | |
109 | /* Threaded timer loop: |
110 | * 1. Queue timers added by other threads |
111 | * 2. Handle any timers that should dispatch this cycle |
112 | * 3. Wait until next dispatch time or new timer arrives |
113 | */ |
114 | for (;;) { |
115 | // Pending and freelist maintenance |
116 | SDL_LockSpinlock(&data->lock); |
117 | { |
118 | // Get any timers ready to be queued |
119 | pending = data->pending; |
120 | data->pending = NULL; |
121 | |
122 | // Make any unused timer structures available |
123 | if (freelist_head) { |
124 | freelist_tail->next = data->freelist; |
125 | data->freelist = freelist_head; |
126 | } |
127 | } |
128 | SDL_UnlockSpinlock(&data->lock); |
129 | |
130 | // Sort the pending timers into our list |
131 | while (pending) { |
132 | current = pending; |
133 | pending = pending->next; |
134 | SDL_AddTimerInternal(data, current); |
135 | } |
136 | freelist_head = NULL; |
137 | freelist_tail = NULL; |
138 | |
139 | // Check to see if we're still running, after maintenance |
140 | if (!SDL_GetAtomicInt(&data->active)) { |
141 | break; |
142 | } |
143 | |
144 | // Initial delay if there are no timers |
145 | delay = (Uint64)-1; |
146 | |
147 | tick = SDL_GetTicksNS(); |
148 | |
149 | // Process all the pending timers for this tick |
150 | while (data->timers) { |
151 | current = data->timers; |
152 | |
153 | if (tick < current->scheduled) { |
154 | // Scheduled for the future, wait a bit |
155 | delay = (current->scheduled - tick); |
156 | break; |
157 | } |
158 | |
159 | // We're going to do something with this timer |
160 | data->timers = current->next; |
161 | |
162 | if (SDL_GetAtomicInt(¤t->canceled)) { |
163 | interval = 0; |
164 | } else { |
165 | if (current->callback_ms) { |
166 | interval = SDL_MS_TO_NS(current->callback_ms(current->userdata, current->timerID, (Uint32)SDL_NS_TO_MS(current->interval))); |
167 | } else { |
168 | interval = current->callback_ns(current->userdata, current->timerID, current->interval); |
169 | } |
170 | } |
171 | |
172 | if (interval > 0) { |
173 | // Reschedule this timer |
174 | current->interval = interval; |
175 | current->scheduled = tick + interval; |
176 | SDL_AddTimerInternal(data, current); |
177 | } else { |
178 | if (!freelist_head) { |
179 | freelist_head = current; |
180 | } |
181 | if (freelist_tail) { |
182 | freelist_tail->next = current; |
183 | } |
184 | freelist_tail = current; |
185 | |
186 | SDL_SetAtomicInt(¤t->canceled, 1); |
187 | } |
188 | } |
189 | |
190 | // Adjust the delay based on processing time |
191 | now = SDL_GetTicksNS(); |
192 | interval = (now - tick); |
193 | if (interval > delay) { |
194 | delay = 0; |
195 | } else { |
196 | delay -= interval; |
197 | } |
198 | |
199 | /* Note that each time a timer is added, this will return |
200 | immediately, but we process the timers added all at once. |
201 | That's okay, it just means we run through the loop a few |
202 | extra times. |
203 | */ |
204 | SDL_WaitSemaphoreTimeoutNS(data->sem, delay); |
205 | } |
206 | return 0; |
207 | } |
208 | |
209 | bool SDL_InitTimers(void) |
210 | { |
211 | SDL_TimerData *data = &SDL_timer_data; |
212 | |
213 | if (!SDL_ShouldInit(&data->init)) { |
214 | return true; |
215 | } |
216 | |
217 | data->timermap_lock = SDL_CreateMutex(); |
218 | if (!data->timermap_lock) { |
219 | goto error; |
220 | } |
221 | |
222 | data->sem = SDL_CreateSemaphore(0); |
223 | if (!data->sem) { |
224 | goto error; |
225 | } |
226 | |
227 | SDL_SetAtomicInt(&data->active, true); |
228 | |
229 | // Timer threads use a callback into the app, so we can't set a limited stack size here. |
230 | data->thread = SDL_CreateThread(SDL_TimerThread, "SDLTimer" , data); |
231 | if (!data->thread) { |
232 | goto error; |
233 | } |
234 | |
235 | SDL_SetInitialized(&data->init, true); |
236 | return true; |
237 | |
238 | error: |
239 | SDL_SetInitialized(&data->init, true); |
240 | SDL_QuitTimers(); |
241 | return false; |
242 | } |
243 | |
244 | void SDL_QuitTimers(void) |
245 | { |
246 | SDL_TimerData *data = &SDL_timer_data; |
247 | SDL_Timer *timer; |
248 | SDL_TimerMap *entry; |
249 | |
250 | if (!SDL_ShouldQuit(&data->init)) { |
251 | return; |
252 | } |
253 | |
254 | SDL_SetAtomicInt(&data->active, false); |
255 | |
256 | // Shutdown the timer thread |
257 | if (data->thread) { |
258 | SDL_SignalSemaphore(data->sem); |
259 | SDL_WaitThread(data->thread, NULL); |
260 | data->thread = NULL; |
261 | } |
262 | |
263 | if (data->sem) { |
264 | SDL_DestroySemaphore(data->sem); |
265 | data->sem = NULL; |
266 | } |
267 | |
268 | // Clean up the timer entries |
269 | while (data->timers) { |
270 | timer = data->timers; |
271 | data->timers = timer->next; |
272 | SDL_free(timer); |
273 | } |
274 | while (data->freelist) { |
275 | timer = data->freelist; |
276 | data->freelist = timer->next; |
277 | SDL_free(timer); |
278 | } |
279 | while (data->timermap) { |
280 | entry = data->timermap; |
281 | data->timermap = entry->next; |
282 | SDL_free(entry); |
283 | } |
284 | |
285 | if (data->timermap_lock) { |
286 | SDL_DestroyMutex(data->timermap_lock); |
287 | data->timermap_lock = NULL; |
288 | } |
289 | |
290 | SDL_SetInitialized(&data->init, false); |
291 | } |
292 | |
293 | static bool SDL_CheckInitTimers(void) |
294 | { |
295 | return SDL_InitTimers(); |
296 | } |
297 | |
298 | static SDL_TimerID SDL_CreateTimer(Uint64 interval, SDL_TimerCallback callback_ms, SDL_NSTimerCallback callback_ns, void *userdata) |
299 | { |
300 | SDL_TimerData *data = &SDL_timer_data; |
301 | SDL_Timer *timer; |
302 | SDL_TimerMap *entry; |
303 | |
304 | if (!callback_ms && !callback_ns) { |
305 | SDL_InvalidParamError("callback" ); |
306 | return 0; |
307 | } |
308 | |
309 | if (!SDL_CheckInitTimers()) { |
310 | return 0; |
311 | } |
312 | |
313 | SDL_LockSpinlock(&data->lock); |
314 | timer = data->freelist; |
315 | if (timer) { |
316 | data->freelist = timer->next; |
317 | } |
318 | SDL_UnlockSpinlock(&data->lock); |
319 | |
320 | if (timer) { |
321 | SDL_RemoveTimer(timer->timerID); |
322 | } else { |
323 | timer = (SDL_Timer *)SDL_malloc(sizeof(*timer)); |
324 | if (!timer) { |
325 | return 0; |
326 | } |
327 | } |
328 | timer->timerID = SDL_GetNextObjectID(); |
329 | timer->callback_ms = callback_ms; |
330 | timer->callback_ns = callback_ns; |
331 | timer->userdata = userdata; |
332 | timer->interval = interval; |
333 | timer->scheduled = SDL_GetTicksNS() + timer->interval; |
334 | SDL_SetAtomicInt(&timer->canceled, 0); |
335 | |
336 | entry = (SDL_TimerMap *)SDL_malloc(sizeof(*entry)); |
337 | if (!entry) { |
338 | SDL_free(timer); |
339 | return 0; |
340 | } |
341 | entry->timer = timer; |
342 | entry->timerID = timer->timerID; |
343 | |
344 | SDL_LockMutex(data->timermap_lock); |
345 | entry->next = data->timermap; |
346 | data->timermap = entry; |
347 | SDL_UnlockMutex(data->timermap_lock); |
348 | |
349 | // Add the timer to the pending list for the timer thread |
350 | SDL_LockSpinlock(&data->lock); |
351 | timer->next = data->pending; |
352 | data->pending = timer; |
353 | SDL_UnlockSpinlock(&data->lock); |
354 | |
355 | // Wake up the timer thread if necessary |
356 | SDL_SignalSemaphore(data->sem); |
357 | |
358 | return entry->timerID; |
359 | } |
360 | |
361 | SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata) |
362 | { |
363 | return SDL_CreateTimer(SDL_MS_TO_NS(interval), callback, NULL, userdata); |
364 | } |
365 | |
366 | SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata) |
367 | { |
368 | return SDL_CreateTimer(interval, NULL, callback, userdata); |
369 | } |
370 | |
371 | bool SDL_RemoveTimer(SDL_TimerID id) |
372 | { |
373 | SDL_TimerData *data = &SDL_timer_data; |
374 | SDL_TimerMap *prev, *entry; |
375 | bool canceled = false; |
376 | |
377 | if (!id) { |
378 | return SDL_InvalidParamError("id" ); |
379 | } |
380 | |
381 | // Find the timer |
382 | SDL_LockMutex(data->timermap_lock); |
383 | prev = NULL; |
384 | for (entry = data->timermap; entry; prev = entry, entry = entry->next) { |
385 | if (entry->timerID == id) { |
386 | if (prev) { |
387 | prev->next = entry->next; |
388 | } else { |
389 | data->timermap = entry->next; |
390 | } |
391 | break; |
392 | } |
393 | } |
394 | SDL_UnlockMutex(data->timermap_lock); |
395 | |
396 | if (entry) { |
397 | if (!SDL_GetAtomicInt(&entry->timer->canceled)) { |
398 | SDL_SetAtomicInt(&entry->timer->canceled, 1); |
399 | canceled = true; |
400 | } |
401 | SDL_free(entry); |
402 | } |
403 | if (canceled) { |
404 | return true; |
405 | } else { |
406 | return SDL_SetError("Timer not found" ); |
407 | } |
408 | } |
409 | |
410 | #else |
411 | |
412 | #include <emscripten/emscripten.h> |
413 | #include <emscripten/eventloop.h> |
414 | |
415 | typedef struct SDL_TimerMap |
416 | { |
417 | SDL_TimerID timerID; |
418 | int timeoutID; |
419 | Uint64 interval; |
420 | SDL_TimerCallback callback_ms; |
421 | SDL_NSTimerCallback callback_ns; |
422 | void *userdata; |
423 | struct SDL_TimerMap *next; |
424 | } SDL_TimerMap; |
425 | |
426 | typedef struct |
427 | { |
428 | SDL_TimerMap *timermap; |
429 | } SDL_TimerData; |
430 | |
431 | static SDL_TimerData SDL_timer_data; |
432 | |
433 | static void SDL_Emscripten_TimerHelper(void *userdata) |
434 | { |
435 | SDL_TimerMap *entry = (SDL_TimerMap *)userdata; |
436 | if (entry->callback_ms) { |
437 | entry->interval = SDL_MS_TO_NS(entry->callback_ms(entry->userdata, entry->timerID, (Uint32)SDL_NS_TO_MS(entry->interval))); |
438 | } else { |
439 | entry->interval = entry->callback_ns(entry->userdata, entry->timerID, entry->interval); |
440 | } |
441 | if (entry->interval > 0) { |
442 | entry->timeoutID = emscripten_set_timeout(&SDL_Emscripten_TimerHelper, |
443 | SDL_NS_TO_MS(entry->interval), |
444 | entry); |
445 | } |
446 | } |
447 | |
448 | bool SDL_InitTimers(void) |
449 | { |
450 | return true; |
451 | } |
452 | |
453 | void SDL_QuitTimers(void) |
454 | { |
455 | SDL_TimerData *data = &SDL_timer_data; |
456 | SDL_TimerMap *entry; |
457 | |
458 | while (data->timermap) { |
459 | entry = data->timermap; |
460 | data->timermap = entry->next; |
461 | SDL_free(entry); |
462 | } |
463 | } |
464 | |
465 | static SDL_TimerID SDL_CreateTimer(Uint64 interval, SDL_TimerCallback callback_ms, SDL_NSTimerCallback callback_ns, void *userdata) |
466 | { |
467 | SDL_TimerData *data = &SDL_timer_data; |
468 | SDL_TimerMap *entry; |
469 | |
470 | if (!callback_ms && !callback_ns) { |
471 | SDL_InvalidParamError("callback" ); |
472 | return 0; |
473 | } |
474 | |
475 | entry = (SDL_TimerMap *)SDL_malloc(sizeof(*entry)); |
476 | if (!entry) { |
477 | return 0; |
478 | } |
479 | entry->timerID = SDL_GetNextObjectID(); |
480 | entry->callback_ms = callback_ms; |
481 | entry->callback_ns = callback_ns; |
482 | entry->userdata = userdata; |
483 | entry->interval = interval; |
484 | |
485 | entry->timeoutID = emscripten_set_timeout(&SDL_Emscripten_TimerHelper, |
486 | SDL_NS_TO_MS(entry->interval), |
487 | entry); |
488 | |
489 | entry->next = data->timermap; |
490 | data->timermap = entry; |
491 | |
492 | return entry->timerID; |
493 | } |
494 | |
495 | SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata) |
496 | { |
497 | return SDL_CreateTimer(SDL_MS_TO_NS(interval), callback, NULL, userdata); |
498 | } |
499 | |
500 | SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata) |
501 | { |
502 | return SDL_CreateTimer(interval, NULL, callback, userdata); |
503 | } |
504 | |
505 | bool SDL_RemoveTimer(SDL_TimerID id) |
506 | { |
507 | SDL_TimerData *data = &SDL_timer_data; |
508 | SDL_TimerMap *prev, *entry; |
509 | |
510 | if (!id) { |
511 | return SDL_InvalidParamError("id" ); |
512 | } |
513 | |
514 | // Find the timer |
515 | prev = NULL; |
516 | for (entry = data->timermap; entry; prev = entry, entry = entry->next) { |
517 | if (entry->timerID == id) { |
518 | if (prev) { |
519 | prev->next = entry->next; |
520 | } else { |
521 | data->timermap = entry->next; |
522 | } |
523 | break; |
524 | } |
525 | } |
526 | |
527 | if (entry) { |
528 | emscripten_clear_timeout(entry->timeoutID); |
529 | SDL_free(entry); |
530 | return true; |
531 | } else { |
532 | return SDL_SetError("Timer not found" ); |
533 | } |
534 | } |
535 | |
536 | #endif // !SDL_PLATFORM_EMSCRIPTEN || !SDL_THREADS_DISABLED |
537 | |
538 | static Uint64 tick_start; |
539 | static Uint32 tick_numerator_ns; |
540 | static Uint32 tick_denominator_ns; |
541 | static Uint32 tick_numerator_ms; |
542 | static Uint32 tick_denominator_ms; |
543 | |
544 | #if defined(SDL_TIMER_WINDOWS) && !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) |
545 | #include <mmsystem.h> |
546 | #define HAVE_TIME_BEGIN_PERIOD |
547 | #endif |
548 | |
549 | static void SDL_SetSystemTimerResolutionMS(int period) |
550 | { |
551 | #ifdef HAVE_TIME_BEGIN_PERIOD |
552 | static int timer_period = 0; |
553 | |
554 | if (period != timer_period) { |
555 | if (timer_period) { |
556 | timeEndPeriod((UINT)timer_period); |
557 | } |
558 | |
559 | timer_period = period; |
560 | |
561 | if (timer_period) { |
562 | timeBeginPeriod((UINT)timer_period); |
563 | } |
564 | } |
565 | #endif // HAVE_TIME_BEGIN_PERIOD |
566 | } |
567 | |
568 | static void SDLCALL SDL_TimerResolutionChanged(void *userdata, const char *name, const char *oldValue, const char *hint) |
569 | { |
570 | int period; |
571 | |
572 | // Unless the hint says otherwise, let's have good sleep precision |
573 | if (hint && *hint) { |
574 | period = SDL_atoi(hint); |
575 | } else { |
576 | period = 1; |
577 | } |
578 | if (period || oldValue != hint) { |
579 | SDL_SetSystemTimerResolutionMS(period); |
580 | } |
581 | } |
582 | |
583 | void SDL_InitTicks(void) |
584 | { |
585 | Uint64 tick_freq; |
586 | Uint32 gcd; |
587 | |
588 | if (tick_start) { |
589 | return; |
590 | } |
591 | |
592 | /* If we didn't set a precision, set it high. This affects lots of things |
593 | on Windows besides the SDL timers, like audio callbacks, etc. */ |
594 | SDL_AddHintCallback(SDL_HINT_TIMER_RESOLUTION, |
595 | SDL_TimerResolutionChanged, NULL); |
596 | |
597 | tick_freq = SDL_GetPerformanceFrequency(); |
598 | SDL_assert(tick_freq > 0 && tick_freq <= (Uint64)SDL_MAX_UINT32); |
599 | |
600 | gcd = SDL_CalculateGCD(SDL_NS_PER_SECOND, (Uint32)tick_freq); |
601 | tick_numerator_ns = (SDL_NS_PER_SECOND / gcd); |
602 | tick_denominator_ns = (Uint32)(tick_freq / gcd); |
603 | |
604 | gcd = SDL_CalculateGCD(SDL_MS_PER_SECOND, (Uint32)tick_freq); |
605 | tick_numerator_ms = (SDL_MS_PER_SECOND / gcd); |
606 | tick_denominator_ms = (Uint32)(tick_freq / gcd); |
607 | |
608 | tick_start = SDL_GetPerformanceCounter(); |
609 | if (!tick_start) { |
610 | --tick_start; |
611 | } |
612 | } |
613 | |
614 | void SDL_QuitTicks(void) |
615 | { |
616 | SDL_RemoveHintCallback(SDL_HINT_TIMER_RESOLUTION, |
617 | SDL_TimerResolutionChanged, NULL); |
618 | |
619 | SDL_SetSystemTimerResolutionMS(0); // always release our timer resolution request. |
620 | |
621 | tick_start = 0; |
622 | } |
623 | |
624 | Uint64 SDL_GetTicksNS(void) |
625 | { |
626 | Uint64 starting_value, value; |
627 | |
628 | if (!tick_start) { |
629 | SDL_InitTicks(); |
630 | } |
631 | |
632 | starting_value = (SDL_GetPerformanceCounter() - tick_start); |
633 | value = (starting_value * tick_numerator_ns); |
634 | SDL_assert(value >= starting_value); |
635 | value /= tick_denominator_ns; |
636 | return value; |
637 | } |
638 | |
639 | Uint64 SDL_GetTicks(void) |
640 | { |
641 | Uint64 starting_value, value; |
642 | |
643 | if (!tick_start) { |
644 | SDL_InitTicks(); |
645 | } |
646 | |
647 | starting_value = (SDL_GetPerformanceCounter() - tick_start); |
648 | value = (starting_value * tick_numerator_ms); |
649 | SDL_assert(value >= starting_value); |
650 | value /= tick_denominator_ms; |
651 | return value; |
652 | } |
653 | |
654 | void SDL_Delay(Uint32 ms) |
655 | { |
656 | SDL_SYS_DelayNS(SDL_MS_TO_NS(ms)); |
657 | } |
658 | |
659 | void SDL_DelayNS(Uint64 ns) |
660 | { |
661 | SDL_SYS_DelayNS(ns); |
662 | } |
663 | |
664 | void SDL_DelayPrecise(Uint64 ns) |
665 | { |
666 | Uint64 current_value = SDL_GetTicksNS(); |
667 | const Uint64 target_value = current_value + ns; |
668 | |
669 | // Sleep for a short number of cycles when real sleeps are desired. |
670 | // We'll use 1 ms, it's the minimum guaranteed to produce real sleeps across |
671 | // all platforms. |
672 | const Uint64 SHORT_SLEEP_NS = 1 * SDL_NS_PER_MS; |
673 | |
674 | // Try to sleep short of target_value. If for some crazy reason |
675 | // a particular platform sleeps for less than 1 ms when 1 ms was requested, |
676 | // that's fine, the code below can cope with that, but in practice no |
677 | // platforms behave that way. |
678 | Uint64 max_sleep_ns = SHORT_SLEEP_NS; |
679 | while (current_value + max_sleep_ns < target_value) { |
680 | // Sleep for a short time |
681 | SDL_SYS_DelayNS(SHORT_SLEEP_NS); |
682 | |
683 | const Uint64 now = SDL_GetTicksNS(); |
684 | const Uint64 next_sleep_ns = (now - current_value); |
685 | if (next_sleep_ns > max_sleep_ns) { |
686 | max_sleep_ns = next_sleep_ns; |
687 | } |
688 | current_value = now; |
689 | } |
690 | |
691 | // Do a shorter sleep of the remaining time here, less the max overshoot in |
692 | // the first loop. Due to maintaining max_sleep_ns as |
693 | // greater-than-or-equal-to-1 ms, we can always subtract off 1 ms to get |
694 | // the duration overshot beyond a 1 ms sleep request; if the system never |
695 | // overshot, great, it's zero duration. By choosing the max overshoot |
696 | // amount, we're likely to not overshoot here. If the sleep here ends up |
697 | // functioning like SDL_DelayNS(0) internally, that's fine, we just don't |
698 | // get to do a more-precise-than-1 ms-resolution sleep to undershoot by a |
699 | // small amount on the current system, but SDL_DelayNS(0) does at least |
700 | // introduce a small, yielding delay on many platforms, better than an |
701 | // unyielding busyloop. |
702 | // |
703 | // Note that we'll always do at least one sleep in this function, so the |
704 | // minimum resolution will be that of SDL_SYS_DelayNS() |
705 | if (current_value < target_value && (target_value - current_value) > (max_sleep_ns - SHORT_SLEEP_NS)) { |
706 | const Uint64 delay_ns = (target_value - current_value) - (max_sleep_ns - SHORT_SLEEP_NS); |
707 | SDL_SYS_DelayNS(delay_ns); |
708 | current_value = SDL_GetTicksNS(); |
709 | } |
710 | |
711 | // We've likely undershot target_value at this point by a pretty small |
712 | // amount, but maybe not. The footgun case if not handled here is where |
713 | // we've undershot by a large amount, like several ms, but still smaller |
714 | // than the amount max_sleep_ns overshot by; in such a situation, the above |
715 | // shorter-sleep block didn't do any delay, the if-block wasn't entered. |
716 | // Also, maybe the shorter-sleep undershot by several ms, so we still don't |
717 | // want to spin a lot then. In such a case, we accept the possibility of |
718 | // overshooting to not spin much, or if overshot here, not at all, keeping |
719 | // CPU/power usage down in any case. Due to scheduler sloppiness, it's |
720 | // entirely possible to end up undershooting/overshooting here by much less |
721 | // than 1 ms even if the current system's sleep function is only 1 |
722 | // ms-resolution, as SDL_GetTicksNS() generally is better resolution than 1 |
723 | // ms on the systems SDL supports. |
724 | while (current_value + SHORT_SLEEP_NS < target_value) { |
725 | SDL_SYS_DelayNS(SHORT_SLEEP_NS); |
726 | current_value = SDL_GetTicksNS(); |
727 | } |
728 | |
729 | // Spin for any remaining time |
730 | while (current_value < target_value) { |
731 | SDL_CPUPauseInstruction(); |
732 | current_value = SDL_GetTicksNS(); |
733 | } |
734 | } |
735 | |