mirror of
https://github.com/livingcomputermuseum/sImlac.git
synced 2026-01-11 23:53:24 +00:00
210 lines
7.7 KiB
C#
210 lines
7.7 KiB
C#
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.ComponentModel;
|
|
using System.Threading;
|
|
|
|
namespace imlac
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current time in seconds.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="framesPerSecond">The frame rate to sync to.</param>
|
|
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()
|
|
{
|
|
//
|
|
// 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for the timer to fire.
|
|
/// </summary>
|
|
public void WaitForFrame()
|
|
{
|
|
_event.WaitOne();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback from timer queue. Work done here is executed on the timer's thread, so must be quick.
|
|
/// </summary>
|
|
/// <param name="lpParameter"></param>
|
|
/// <param name="TimerOrWaitFired"></param>
|
|
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;
|
|
}
|
|
}
|