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
30typedef 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
42typedef 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
50typedef 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
72static 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
80static 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
100static 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(&current->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(&current->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
209bool 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
238error:
239 SDL_SetInitialized(&data->init, true);
240 SDL_QuitTimers();
241 return false;
242}
243
244void 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
293static bool SDL_CheckInitTimers(void)
294{
295 return SDL_InitTimers();
296}
297
298static 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
361SDL_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
366SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata)
367{
368 return SDL_CreateTimer(interval, NULL, callback, userdata);
369}
370
371bool 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
415typedef 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
426typedef struct
427{
428 SDL_TimerMap *timermap;
429} SDL_TimerData;
430
431static SDL_TimerData SDL_timer_data;
432
433static 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
448bool SDL_InitTimers(void)
449{
450 return true;
451}
452
453void 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
465static 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
495SDL_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
500SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata)
501{
502 return SDL_CreateTimer(interval, NULL, callback, userdata);
503}
504
505bool 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
538static Uint64 tick_start;
539static Uint32 tick_numerator_ns;
540static Uint32 tick_denominator_ns;
541static Uint32 tick_numerator_ms;
542static 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
549static 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
568static 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
583void 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
614void 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
624Uint64 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
639Uint64 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
654void SDL_Delay(Uint32 ms)
655{
656 SDL_SYS_DelayNS(SDL_MS_TO_NS(ms));
657}
658
659void SDL_DelayNS(Uint64 ns)
660{
661 SDL_SYS_DelayNS(ns);
662}
663
664void 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