// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Diagnostics; namespace SharpEmu.Libs; /// /// High-resolution host sleeps for guest pacing. /// routinely overshoots by a scheduler quantum, which turns per-frame waits /// (flip pacing, usleep-based game loops) into a hard frame-rate cap; these /// helpers sleep coarsely for the bulk of the interval and yield-spin the /// remainder so wakeups land within tens of microseconds of the target. /// internal static class HostTiming { /// /// Blocks until reaches /// . /// public static void SleepUntil(long targetTimestamp) { while (true) { var remainingTicks = targetTimestamp - Stopwatch.GetTimestamp(); if (remainingTicks <= 0) { return; } if (remainingTicks > Stopwatch.Frequency * 60) { // Far-future target: coarse sleep avoids overflowing the // microsecond conversion below and needs no precision. Thread.Sleep(30_000); continue; } var remainingMicroseconds = remainingTicks * 1_000_000 / Stopwatch.Frequency; if (remainingMicroseconds > 2200) { // Coarse sleep for the bulk; macOS overshoots ~0.5-1.5 ms. Thread.Sleep((int)((remainingMicroseconds - 1200) / 1000)); } else if (remainingMicroseconds > 1200) { // A 1 ms nap typically wakes within the margin and costs no // CPU, unlike yield-spinning through the whole tail. Thread.Sleep(1); } else if (remainingMicroseconds > 100) { Thread.Sleep(0); } else { Thread.SpinWait(64); } } } /// Blocks for the given number of microseconds. public static void SleepMicroseconds(long microseconds) { if (microseconds <= 0) { return; } if (microseconds >= 10_000_000) { // Long/sentinel sleeps (usleep(-1) parks): sub-millisecond // precision is irrelevant and the tick conversion below would // overflow, which used to turn the park into a hot spin. Thread.Sleep((int)Math.Min(microseconds / 1000, int.MaxValue)); return; } var ticks = microseconds * Stopwatch.Frequency / 1_000_000; SleepUntil(Stopwatch.GetTimestamp() + ticks); } }