SDL: SDL_DelayNS() will attempt to sleep exactly the requested amount of time

From 033df70d4c0f1e3acdbfe246a9133e2d1a721178 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 15 Jul 2024 18:05:31 -0700
Subject: [PATCH] SDL_DelayNS() will attempt to sleep exactly the requested
 amount of time

This provides a highly accurate sleep function for your application, although you are still subject to being switched out occasionally.

Fixes https://github.com/libsdl-org/SDL/issues/10210
---
 include/SDL3/SDL_timer.h         |  4 ++--
 src/timer/SDL_timer.c            | 23 ++++++++++++++++++++++-
 src/timer/SDL_timer_c.h          |  2 ++
 src/timer/haiku/SDL_systimer.c   |  2 +-
 src/timer/n3ds/SDL_systimer.c    |  2 +-
 src/timer/ngage/SDL_systimer.cpp |  2 +-
 src/timer/ps2/SDL_systimer.c     |  2 +-
 src/timer/psp/SDL_systimer.c     |  2 +-
 src/timer/unix/SDL_systimer.c    |  2 +-
 src/timer/vita/SDL_systimer.c    |  2 +-
 src/timer/windows/SDL_systimer.c |  2 +-
 test/testtimer.c                 | 28 ++++++++++++++++++++++++++++
 12 files changed, 62 insertions(+), 11 deletions(-)

diff --git a/include/SDL3/SDL_timer.h b/include/SDL3/SDL_timer.h
index c127da1fd1a26..fa40a6f82ca62 100644
--- a/include/SDL3/SDL_timer.h
+++ b/include/SDL3/SDL_timer.h
@@ -115,8 +115,8 @@ extern SDL_DECLSPEC void SDLCALL SDL_Delay(Uint32 ms);
  * Wait a specified number of nanoseconds before returning.
  *
  * This function waits a specified number of nanoseconds before returning. It
- * waits at least the specified time, but possibly longer due to OS
- * scheduling.
+ * will attempt to wait as close to the requested time as possible, busy waiting
+ * if necessary, but could return later due to OS scheduling.
  *
  * \param ns the number of nanoseconds to delay.
  *
diff --git a/src/timer/SDL_timer.c b/src/timer/SDL_timer.c
index ca98271d66de1..4c5bd018c614a 100644
--- a/src/timer/SDL_timer.c
+++ b/src/timer/SDL_timer.c
@@ -643,5 +643,26 @@ Uint64 SDL_GetTicks(void)
 
 void SDL_Delay(Uint32 ms)
 {
-    SDL_DelayNS(SDL_MS_TO_NS(ms));
+    SDL_SYS_DelayNS(SDL_MS_TO_NS(ms));
+}
+
+void SDL_DelayNS(Uint64 ns)
+{
+    Uint64 current_value = SDL_GetTicksNS();
+    Uint64 target_value = current_value + ns;
+
+    // Sleep for a short number of cycles
+    // We'll use 1 ms as a scheduling timeslice, it's a good value for modern operating systems
+    const int SCHEDULING_TIMESLICE_NS = 1 * SDL_NS_PER_MS;
+    while (current_value < target_value) {
+        Uint64 remaining_ns = (target_value - current_value);
+        if (remaining_ns > (SCHEDULING_TIMESLICE_NS + SDL_NS_PER_US)) {
+            // Sleep for a short time, less than the scheduling timeslice
+            SDL_SYS_DelayNS(SCHEDULING_TIMESLICE_NS - SDL_NS_PER_US);
+        } else {
+            // Spin for any remaining time
+            SDL_CPUPauseInstruction();
+        }
+        current_value = SDL_GetTicksNS();
+    }
 }
diff --git a/src/timer/SDL_timer_c.h b/src/timer/SDL_timer_c.h
index 161fb91434c71..9c202a368908c 100644
--- a/src/timer/SDL_timer_c.h
+++ b/src/timer/SDL_timer_c.h
@@ -34,4 +34,6 @@ extern void SDL_QuitTicks(void);
 extern int SDL_InitTimers(void);
 extern void SDL_QuitTimers(void);
 
+extern void SDL_SYS_DelayNS(Uint64 ns);
+
 #endif /* SDL_timer_c_h_ */
diff --git a/src/timer/haiku/SDL_systimer.c b/src/timer/haiku/SDL_systimer.c
index 726e4150180fc..8f844b6d30a5e 100644
--- a/src/timer/haiku/SDL_systimer.c
+++ b/src/timer/haiku/SDL_systimer.c
@@ -35,7 +35,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return SDL_US_PER_SECOND;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     snooze((bigtime_t)SDL_NS_TO_US(ns));
 }
diff --git a/src/timer/n3ds/SDL_systimer.c b/src/timer/n3ds/SDL_systimer.c
index 99389a4c7626d..e90b99dc7cd75 100644
--- a/src/timer/n3ds/SDL_systimer.c
+++ b/src/timer/n3ds/SDL_systimer.c
@@ -35,7 +35,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return SYSCLOCK_ARM11;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     svcSleepThread(ns);
 }
diff --git a/src/timer/ngage/SDL_systimer.cpp b/src/timer/ngage/SDL_systimer.cpp
index f7f11ea3d3202..ce0082a40de88 100644
--- a/src/timer/ngage/SDL_systimer.cpp
+++ b/src/timer/ngage/SDL_systimer.cpp
@@ -43,7 +43,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return SDL_US_PER_SECOND;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     const Uint64 max_delay = 0x7fffffffLL * SDL_NS_PER_US;
     if (ns > max_delay) {
diff --git a/src/timer/ps2/SDL_systimer.c b/src/timer/ps2/SDL_systimer.c
index 7070e17af49ff..8d13c4f599ddd 100644
--- a/src/timer/ps2/SDL_systimer.c
+++ b/src/timer/ps2/SDL_systimer.c
@@ -39,7 +39,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return kBUSCLK;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     struct timespec tv;
     tv.tv_sec = (ns / SDL_NS_PER_SECOND);
diff --git a/src/timer/psp/SDL_systimer.c b/src/timer/psp/SDL_systimer.c
index b91ea244a0d3a..0a33f45822397 100644
--- a/src/timer/psp/SDL_systimer.c
+++ b/src/timer/psp/SDL_systimer.c
@@ -42,7 +42,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return sceRtcGetTickResolution();
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     const Uint64 max_delay = 0xffffffffLL * SDL_NS_PER_US;
     if (ns > max_delay) {
diff --git a/src/timer/unix/SDL_systimer.c b/src/timer/unix/SDL_systimer.c
index 0a0c5e807035a..6f9a5c8a4f3a4 100644
--- a/src/timer/unix/SDL_systimer.c
+++ b/src/timer/unix/SDL_systimer.c
@@ -135,7 +135,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return SDL_US_PER_SECOND;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     int was_error;
 
diff --git a/src/timer/vita/SDL_systimer.c b/src/timer/vita/SDL_systimer.c
index a99b8e1c2b359..d9e856ef6d81c 100644
--- a/src/timer/vita/SDL_systimer.c
+++ b/src/timer/vita/SDL_systimer.c
@@ -39,7 +39,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return SDL_US_PER_SECOND;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     const Uint64 max_delay = 0xffffffffLL * SDL_NS_PER_US;
     if (ns > max_delay) {
diff --git a/src/timer/windows/SDL_systimer.c b/src/timer/windows/SDL_systimer.c
index d82d6f8bc23bb..5f1ba26bf7f0e 100644
--- a/src/timer/windows/SDL_systimer.c
+++ b/src/timer/windows/SDL_systimer.c
@@ -66,7 +66,7 @@ Uint64 SDL_GetPerformanceFrequency(void)
     return (Uint64)frequency.QuadPart;
 }
 
-void SDL_DelayNS(Uint64 ns)
+void SDL_SYS_DelayNS(Uint64 ns)
 {
     /* CREATE_WAITABLE_TIMER_HIGH_RESOLUTION flag was added in Windows 10 version 1803.
      *
diff --git a/test/testtimer.c b/test/testtimer.c
index de38842928061..249b76a7ebcee 100644
--- a/test/testtimer.c
+++ b/test/testtimer.c
@@ -186,6 +186,34 @@ int main(int argc, char *argv[])
     /* Wait for the results to be seen */
     SDL_Delay(1 * 1000);
 
+    /* Check accuracy of precise delay */
+    {
+        Uint64 desired_delay = SDL_NS_PER_SECOND / 60;
+        Uint64 actual_delay;
+        Uint64 total_overslept = 0;
+
+        start = SDL_GetTicksNS();
+        SDL_DelayNS(1);
+        now = SDL_GetTicksNS();
+        actual_delay = (now - start);
+        SDL_Log("Minimum precise delay: %" SDL_PRIu64 " ns\n", actual_delay);
+
+        SDL_Log("Timing 100 frames at 60 FPS\n");
+        for (i = 0; i < 100; ++i) {
+            start = SDL_GetTicksNS();
+            SDL_DelayNS(desired_delay);
+            now = SDL_GetTicksNS();
+            actual_delay = (now - start);
+            if (actual_delay > desired_delay) {
+                total_overslept += (actual_delay - desired_delay);
+            }
+        }
+        SDL_Log("Overslept %.2f ms\n", (double)total_overslept / SDL_NS_PER_MS);
+    }
+
+    /* Wait for the results to be seen */
+    SDL_Delay(1 * 1000);
+
     /* Test multiple timers */
     SDL_Log("Testing multiple timers...\n");
     t1 = SDL_AddTimer(100, callback, (void *)1);