diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index c5d15926..ed38a614 100644 --- a/src/Spectron.Emulation/EmulatorTimer.cs +++ b/src/Spectron.Emulation/EmulatorTimer.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using OldBit.Spectron.Emulation.Platforms; namespace OldBit.Spectron.Emulation; @@ -12,6 +13,9 @@ internal sealed class EmulatorTimer : IDisposable private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly ManualResetEventSlim _stoppedEvent = new(initialState: false); private readonly ManualResetEventSlim _pausedEvent = new(initialState: false); + private readonly TimerResolutionScope _timerResolutionScope; + + private volatile bool _isDisposed; internal bool IsPaused { get; private set; } internal ThreadPriority Priority { get; set; } = ThreadPriority.AboveNormal; @@ -20,12 +24,17 @@ internal sealed class EmulatorTimer : IDisposable internal event EventHandler? Elapsed; - internal EmulatorTimer() => _worker = new Thread(Worker) + internal EmulatorTimer() { - IsBackground = true, - Priority = Priority, - Name = "Emulator Timer" - }; + _timerResolutionScope = new TimerResolutionScope(); + + _worker = new Thread(Worker) + { + IsBackground = true, + Priority = Priority, + Name = "Emulator Timer" + }; + } internal void Start() => _worker.Start(); @@ -37,6 +46,11 @@ internal void Stop() internal void Pause() { + if (_isDisposed) + { + return; + } + _pausedEvent.Reset(); IsPaused = true; @@ -106,6 +120,10 @@ private void Worker() case < 10: Thread.SpinWait(25); break; + + default: + Thread.Sleep(Math.Max(1, (int)timeToWait.TotalMilliseconds - 5)); + break; } } } @@ -119,6 +137,9 @@ private void Worker() public void Dispose() { + _isDisposed = true; + + _timerResolutionScope.Dispose(); _cancellationTokenSource.Dispose(); _stoppedEvent.Dispose(); _pausedEvent.Dispose(); diff --git a/src/Spectron.Emulation/Platforms/TimerResolutionScope.cs b/src/Spectron.Emulation/Platforms/TimerResolutionScope.cs new file mode 100644 index 00000000..fe6d99e1 --- /dev/null +++ b/src/Spectron.Emulation/Platforms/TimerResolutionScope.cs @@ -0,0 +1,24 @@ +using OldBit.Spectron.Emulation.Platforms.Windows.Interop; + +namespace OldBit.Spectron.Emulation.Platforms; + +internal sealed class TimerResolutionScope : IDisposable +{ + private const int TimerResolutionMs = 1; + + internal TimerResolutionScope() + { + if (OperatingSystem.IsWindows()) + { + _ = Winmm.TimeBeginPeriod(TimerResolutionMs); + } + } + + public void Dispose() + { + if (OperatingSystem.IsWindows()) + { + _ = Winmm.TimeEndPeriod(TimerResolutionMs); + } + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs b/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs new file mode 100644 index 00000000..5535d474 --- /dev/null +++ b/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace OldBit.Spectron.Emulation.Platforms.Windows.Interop; + +[SupportedOSPlatform("windows")] +internal static partial class Winmm +{ + /// + /// The timeBeginPeriod function requests a minimum resolution for periodic timers. + /// + /// Minimum timer resolution, in milliseconds, for the application or device driver. + /// A lower value specifies a higher (more accurate) resolution. + /// Returns TIMERR_NOERROR if successful or TIMERR_NOCANDO if the resolution specified in uPeriod is out of range. + [LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + internal static partial uint TimeBeginPeriod(uint uPeriod); + + /// + /// The timeEndPeriod function clears a previously set minimum timer resolution. + /// + /// Minimum timer resolution specified in the previous call to the timeBeginPeriod function. + /// Returns TIMERR_NOERROR if successful or TIMERR_NOCANDO if the resolution specified in uPeriod is out of range. + [LibraryImport("winmm.dll", EntryPoint = "timeEndPeriod")] + internal static partial uint TimeEndPeriod(uint uPeriod); +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Spectron.Emulation.csproj b/src/Spectron.Emulation/Spectron.Emulation.csproj index dceed476..d84b160f 100644 --- a/src/Spectron.Emulation/Spectron.Emulation.csproj +++ b/src/Spectron.Emulation/Spectron.Emulation.csproj @@ -4,6 +4,7 @@ net10.0 enable enable + true OldBit.Spectron.Emulation OldBit.Spectron.Emulation 1.0.0 diff --git a/src/Spectron/Screen/FrameBufferConverter.cs b/src/Spectron/Screen/FrameBufferConverter.cs index 1abe8541..f0324d2d 100644 --- a/src/Spectron/Screen/FrameBufferConverter.cs +++ b/src/Spectron/Screen/FrameBufferConverter.cs @@ -34,25 +34,22 @@ internal void UpdateBitmap() { using var lockedBitmap = ScreenBitmap.Lock(); - var targetAddress = lockedBitmap.Address; + var colCount = _endFrameBufferCol - _startFrameBufferCol + 1; + var rowBytes = colCount * 4; - for (var frameBufferRow = _startFrameBufferRow; frameBufferRow <= _endFrameBufferRow; frameBufferRow++) + unsafe { - var rowOffset = frameBufferRow * _frameBuffer.Width; - - for (var frameBufferCol = _startFrameBufferCol; frameBufferCol <= _endFrameBufferCol; frameBufferCol++) + fixed (Color* pixelsBase = _frameBuffer.Pixels) { - var pixelIndex = rowOffset + frameBufferCol; + var destination = (byte*)lockedBitmap.Address; - unsafe + for (var row = _startFrameBufferRow; row <= _endFrameBufferRow; row++) { - fixed (Color* color = &_frameBuffer.Pixels[pixelIndex]) - { - var pixelColor = *(uint*)color; - *(uint*)targetAddress = pixelColor; + var source = (byte*)(pixelsBase + row * _frameBuffer.Width + _startFrameBufferCol); + + Buffer.MemoryCopy(source, destination, rowBytes, rowBytes); - targetAddress += 4; - } + destination += rowBytes; } } }