1 module pixelperfectengine.system.timer; 2 /* 3 * Copyright (C) 2015-2021, by Laszlo Szeremi under the Boost license. 4 * 5 * Pixel Perfect Engine, system.timer module 6 */ 7 import collections.sortedlist; 8 import std.typecons : BitFlags; 9 public import core.time; 10 11 static CoarseTimer timer; 12 13 static this() { 14 timer = new CoarseTimer(); 15 } 16 /** 17 * Implements a coarse timer, that checks the time periodically (e.g. on VSYNC), then calls the delegate if the time 18 * assigned to it has been lapsed. 19 * Is fast and can effectively test for multiple elements, but is inaccurate which can even fluctuate if tests are done 20 * on VSYNC intervals. This will make the duration longer in every case (up to 16.7ms on 60Hz displays), but this 21 * still should be accurate enough for many cases. 22 * In theory, calls for test can be done separate from VSYNC intervals by running it in its own thread, but that can 23 * cause potential race issues. 24 * Delegates take the `jitter` argument, which is the overshoot of the time. 25 * Can be suspended for game pausing, etc., while keeping time deltas mostly accurate. Multiple instances of this class 26 * can be used to create various effects from suspending the timer. 27 */ 28 public class CoarseTimer { 29 alias TimerReceiver = void delegate(Duration jitter); 30 /** 31 * A timer entry. 32 */ 33 protected struct Entry { 34 TimerReceiver onLapse; ///The delegate to be called. 35 MonoTime when; ///When the delegate must be called, in system time. 36 int opCmp(const Entry other) const @nogc @trusted pure nothrow { 37 return when.opCmp(other.when); 38 } 39 bool opEquals(const Entry other) const @nogc @trusted pure nothrow { 40 return when == other.when; 41 } 42 size_t toHash() const @nogc @safe pure nothrow { 43 return cast(size_t)when.ticks; 44 } 45 } 46 protected enum StatusFlags { 47 isTesting = 1<<0, 48 isPaused = 1<<1, 49 } 50 protected SortedList!Entry timerList; ///The list of timer entries. 51 protected Entry[] timerRegs; ///Secondary timer list. 52 protected BitFlags!StatusFlags status; ///Contains various status flags. 53 protected MonoTime timeSuspend; ///Time when suspension happened. 54 ///CTOR (empty) 55 public this() @safe pure nothrow { 56 57 } 58 /** 59 * Registers an entry for the timer. 60 * Params: 61 * dg = the delegate to be called when the event is lapsed. 62 * delta = sets the amount of time into the future. 63 */ 64 public void register(TimerReceiver dg, Duration delta) @safe nothrow { 65 if (!status.isTesting) 66 timerList.put(Entry(dg, MonoTime.currTime + delta)); 67 else 68 timerRegs ~= Entry(dg, MonoTime.currTime + delta); 69 } 70 /** 71 * Tests the entries. 72 * If enough time has passed, then those entries will be called and deleted. 73 */ 74 public void test() { 75 status.isTesting = true; 76 while (timerList.length && !status.isPaused) { 77 if (MonoTime.currTime >= timerList[0].when) { 78 timerList[0].onLapse(MonoTime.currTime - timerList[0].when); 79 timerList.remove(0); 80 } else { 81 break; 82 } 83 } 84 foreach (e ; timerRegs) 85 timerList.put(e); 86 timerRegs.length = 0; 87 status.isTesting = true; 88 } 89 /** 90 * Suspends the timer and saves the current timestamp to calculate timeshift caused by suspension. 91 */ 92 public void suspendTimer() { 93 if(!status.isPaused) { 94 status.isPaused = true; 95 timeSuspend = MonoTime.currTime; 96 } 97 } 98 alias suspend = suspendTimer; 99 /** 100 * Resumes timer and shifts all entries by given time delta. 101 */ 102 public void resumeTimer() { 103 if(status.isPaused) { 104 status.isPaused = false; 105 const MonoTime timeResume = MonoTime.currTime; 106 const Duration timeShift = timeResume - timeSuspend; 107 //in theory, every entry should keep their position, so no list rebuilding is needed 108 foreach (ref Entry key; timerList) { 109 key.when += timeShift; 110 } 111 } 112 } 113 alias resume = resumeTimer; 114 }