/* This file is part of sImlac. sImlac is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. sImlac is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with sImlac. If not, see . */ using System; using System.Runtime.InteropServices; using System.ComponentModel; using System.Threading; namespace imlac { /// /// HighResTimer gives us access to NT's very-high-resolution PerformanceCounters. /// This gives us the precision we need to sync emulation to any speed we desire. /// public sealed class HighResTimer { [DllImport("Kernel32.dll")] private static extern bool QueryPerformanceCounter( out long lpPerformanceCount); [DllImport("Kernel32.dll")] private static extern bool QueryPerformanceFrequency( out long lpFrequency); public HighResTimer() { //What's the frequency, Kenneth? if (QueryPerformanceFrequency(out _frequency) == false) { // high-performance counter not supported throw new Win32Exception(); } } /// /// Returns the current time in seconds. /// /// public double GetCurrentTime() { long currentTime; QueryPerformanceCounter(out currentTime); return (double)(currentTime) / (double)_frequency; } private long _frequency; } public sealed class FrameTimer { [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)] static extern UInt32 TimeGetDevCaps(ref TimeCaps timeCaps, UInt32 sizeTimeCaps); [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] static extern UInt32 TimeBeginPeriod(UInt32 uPeriod); [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] public static extern uint TimeEndPeriod(uint uMilliseconds); [DllImport("kernel32.dll", EntryPoint = "CreateTimerQueue")] public static extern IntPtr CreateTimerQueue(); [DllImport("kernel32.dll", EntryPoint = "DeleteTimerQueueEx")] public static extern bool DeleteTimerQueue(IntPtr hTimerQueue, IntPtr hCompletionEvent); [DllImport("kernel32.dll", EntryPoint = "CreateTimerQueueTimer")] public static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer, IntPtr hTimerQueue, IntPtr Callback, IntPtr Parameter, UInt32 DueTime, UInt32 Period, uint Flags); [DllImport("kernel32.dll", EntryPoint = "ChangeTimerQueueTimer")] public static extern bool ChangeTimerQueueTimer(IntPtr hTimerQueue, IntPtr hTimer, UInt32 DueTime, UInt32 Period); [DllImport("kernel32.dll", EntryPoint = "DeleteTimerQueueTimer")] public static extern bool DeleteTimerQueueTimer(IntPtr hTimerQueue, IntPtr hTimer, IntPtr hCompletionEvent); [StructLayout(LayoutKind.Sequential)] public struct TimeCaps { public UInt32 wPeriodMin; public UInt32 wPeriodMax; }; [UnmanagedFunctionPointer(CallingConvention.Winapi)] public delegate void UnmanagedTimerCallback(IntPtr param, bool timerOrWait); /// /// FrameTimer provides a simple method to synchronize execution to a given framerate. /// Calling WaitForFrame() blocks until the start of the next frame. /// /// NOTE: This code uses the Win32 TimerQueue APIs instead of the System.Threading.Timer /// APIs because the .NET APIs do not allow execution of the callback on the timer's thread -- /// it queues up a new worker thread. This lowers the accuracy of the timer, and since we /// need all the precision we can get they're not suitable here. /// /// The frame rate to sync to. public FrameTimer(double framesPerSecond) { // // Set the timer to the minimum value (1ms). This should be supported on any modern x86 system. // If not, too bad... // UInt32 res = TimeBeginPeriod(1); if (res != 0) { throw new InvalidOperationException("Unable to set timer period."); } // // Create a new timer queue // _hTimerQueue = CreateTimerQueue(); if (_hTimerQueue == IntPtr.Zero) { throw new InvalidOperationException("Unable to create timer queue."); } // // Since we only have a resolution of 1ms, we have to do some hackery to get a slightly more accurate framerate. // (60fps NTSC requires 16 2/3s ms frame delay.) // We alternate between two rates at varying intervals and this gets us fairly close to the desired frame rate // (~60.25 fps for NTSC), which is almost impercetible. // _callback = new UnmanagedTimerCallback(TimerCallbackFn); _highPeriod = (uint)Math.Ceiling(1000.0 * (1.0 / framesPerSecond)); _lowPeriod = (uint)Math.Floor(1000.0 * (1.0 / framesPerSecond)); _periodTenths = _periodSwitch = (uint)((1000.0 * (1.0 / framesPerSecond) - Math.Floor(1000.0 * (1.0 / framesPerSecond))) * 10.0); if (!CreateTimerQueueTimer(out _hTimer, _hTimerQueue, Marshal.GetFunctionPointerForDelegate(_callback), IntPtr.Zero, _lowPeriod, _lowPeriod, 0x00000020 /* execute in timer thread */)) { throw new InvalidOperationException("Unable to create timer queue timer."); } _event = new AutoResetEvent(false); _lowTimer = 0; } ~FrameTimer() { try { // // Clean stuff up // DeleteTimerQueueTimer(_hTimerQueue, _hTimer, IntPtr.Zero); DeleteTimerQueue(_hTimerQueue, IntPtr.Zero); // // Fire off a final event to release any call that's waiting... // _event.Set(); } catch { // Nothing. If the above fails it's due to the Win32 APIs not being present. // Just fail quietly. } } /// /// Waits for the timer to fire. /// public void WaitForFrame() { _event.WaitOne(); } /// /// Callback from timer queue. Work done here is executed on the timer's thread, so must be quick. /// /// /// private void TimerCallbackFn(IntPtr lpParameter, bool TimerOrWaitFired) { _event.Set(); _lowTimer++; if (_lowTimer >= _periodSwitch) { _lowTimer = 0; _period = !_period; ChangeTimerQueueTimer(_hTimerQueue, _hTimer, _period ? _lowPeriod : _highPeriod, _period ? _lowPeriod : _highPeriod); _periodSwitch = !_period ? _periodTenths : 10 - _periodTenths; } } private IntPtr _hTimerQueue; private IntPtr _hTimer; private AutoResetEvent _event; private UnmanagedTimerCallback _callback; private uint _lowPeriod; private uint _highPeriod; private uint _periodSwitch; private uint _periodTenths; private int _lowTimer; private bool _period; } }