From a5a33103d042bf1e24a7c03010bbd836b13796db Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 19 May 2026 20:58:44 +0100 Subject: [PATCH 1/6] Improve performance --- src/Spectron.Emulation/EmulatorTimer.cs | 4 ++++ src/Spectron/Screen/FrameBufferConverter.cs | 23 +++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index c5d15926..ccc80511 100644 --- a/src/Spectron.Emulation/EmulatorTimer.cs +++ b/src/Spectron.Emulation/EmulatorTimer.cs @@ -106,6 +106,10 @@ private void Worker() case < 10: Thread.SpinWait(25); break; + + default: + Thread.Sleep(Math.Max(1, (int)timeToWait.TotalMilliseconds - 5)); + break; } } } 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; } } } From 6593e98e8364d5fa24e365196af640efffe5d1fb Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 19 May 2026 21:08:18 +0100 Subject: [PATCH 2/6] Check disposed state before pausing --- src/Spectron.Emulation/EmulatorTimer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index ccc80511..4992d453 100644 --- a/src/Spectron.Emulation/EmulatorTimer.cs +++ b/src/Spectron.Emulation/EmulatorTimer.cs @@ -12,6 +12,7 @@ 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 bool _isDisposed; internal bool IsPaused { get; private set; } internal ThreadPriority Priority { get; set; } = ThreadPriority.AboveNormal; @@ -37,6 +38,11 @@ internal void Stop() internal void Pause() { + if (_isDisposed) + { + return; + } + _pausedEvent.Reset(); IsPaused = true; @@ -123,6 +129,7 @@ private void Worker() public void Dispose() { + _isDisposed = true; _cancellationTokenSource.Dispose(); _stoppedEvent.Dispose(); _pausedEvent.Dispose(); From 049aff4e9c216809767268b96f699ea8660d16d9 Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 19 May 2026 22:12:11 +0100 Subject: [PATCH 3/6] Improve `EmulatorTimer` precision by adjusting thread sleeping logic and managing Windows timer resolution --- src/Spectron.Emulation/EmulatorTimer.cs | 13 +++++++++-- src/Spectron.Emulation/Platforms/Platform.cs | 22 +++++++++++++++++++ .../Platforms/Windows/Interop/Winmm.cs | 14 ++++++++++++ .../Spectron.Emulation.csproj | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/Spectron.Emulation/Platforms/Platform.cs create mode 100644 src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index 4992d453..80af79a3 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; @@ -28,10 +29,17 @@ internal sealed class EmulatorTimer : IDisposable Name = "Emulator Timer" }; - internal void Start() => _worker.Start(); + internal void Start() + { + Platform.RequestMinimumTimerResolution(); + + _worker.Start(); + } internal void Stop() { + Platform.ReleaseMinimumTimerResolution(); + _cancellationTokenSource.Cancel(); _stoppedEvent.Wait(); } @@ -114,7 +122,7 @@ private void Worker() break; default: - Thread.Sleep(Math.Max(1, (int)timeToWait.TotalMilliseconds - 5)); + Thread.Sleep(1); break; } } @@ -130,6 +138,7 @@ private void Worker() public void Dispose() { _isDisposed = true; + _cancellationTokenSource.Dispose(); _stoppedEvent.Dispose(); _pausedEvent.Dispose(); diff --git a/src/Spectron.Emulation/Platforms/Platform.cs b/src/Spectron.Emulation/Platforms/Platform.cs new file mode 100644 index 00000000..3a956a65 --- /dev/null +++ b/src/Spectron.Emulation/Platforms/Platform.cs @@ -0,0 +1,22 @@ +using OldBit.Spectron.Emulation.Platforms.Windows.Interop; + +namespace OldBit.Spectron.Emulation.Platforms; + +internal static class Platform +{ + internal static void RequestMinimumTimerResolution() + { + if (OperatingSystem.IsWindows()) + { + _ = Winmm.TimeBeginPeriod(1); + } + } + + internal static void ReleaseMinimumTimerResolution() + { + if (OperatingSystem.IsWindows()) + { + _ = Winmm.TimeEndPeriod(1); + } + } +} \ 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..856d60d1 --- /dev/null +++ b/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace OldBit.Spectron.Emulation.Platforms.Windows.Interop; + +[SupportedOSPlatform("windows")] +internal static partial class Winmm +{ + [LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + internal static partial uint TimeBeginPeriod(uint uPeriod); + + [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 From 5e92471fce6f1810b56f93d971a2180e333aeea3 Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 19 May 2026 22:14:43 +0100 Subject: [PATCH 4/6] Add comment to clarify Winmm --- .../Platforms/Windows/Interop/Winmm.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs b/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs index 856d60d1..5535d474 100644 --- a/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs +++ b/src/Spectron.Emulation/Platforms/Windows/Interop/Winmm.cs @@ -6,9 +6,20 @@ 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 From d4038780ccdf0da2f8e02424aeb756c958dfddfe Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 19 May 2026 22:18:24 +0100 Subject: [PATCH 5/6] Use adjusted thread sleeping logic --- src/Spectron.Emulation/EmulatorTimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index 80af79a3..cd5e19d1 100644 --- a/src/Spectron.Emulation/EmulatorTimer.cs +++ b/src/Spectron.Emulation/EmulatorTimer.cs @@ -122,7 +122,7 @@ private void Worker() break; default: - Thread.Sleep(1); + Thread.Sleep(Math.Max(1, (int)timeToWait.TotalMilliseconds - 5)); break; } } From e3521dcf82094528acf80e44f286e7c569f55169 Mon Sep 17 00:00:00 2001 From: voytas Date: Thu, 21 May 2026 18:06:32 +0100 Subject: [PATCH 6/6] Refactor Windows timer resolution management with `TimerResolutionScope` for improved resource handling. --- src/Spectron.Emulation/EmulatorTimer.cs | 27 ++++++++++--------- src/Spectron.Emulation/Platforms/Platform.cs | 22 --------------- .../Platforms/TimerResolutionScope.cs | 24 +++++++++++++++++ 3 files changed, 38 insertions(+), 35 deletions(-) delete mode 100644 src/Spectron.Emulation/Platforms/Platform.cs create mode 100644 src/Spectron.Emulation/Platforms/TimerResolutionScope.cs diff --git a/src/Spectron.Emulation/EmulatorTimer.cs b/src/Spectron.Emulation/EmulatorTimer.cs index cd5e19d1..ed38a614 100644 --- a/src/Spectron.Emulation/EmulatorTimer.cs +++ b/src/Spectron.Emulation/EmulatorTimer.cs @@ -13,7 +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 bool _isDisposed; + private readonly TimerResolutionScope _timerResolutionScope; + + private volatile bool _isDisposed; internal bool IsPaused { get; private set; } internal ThreadPriority Priority { get; set; } = ThreadPriority.AboveNormal; @@ -22,24 +24,22 @@ internal sealed class EmulatorTimer : IDisposable internal event EventHandler? Elapsed; - internal EmulatorTimer() => _worker = new Thread(Worker) - { - IsBackground = true, - Priority = Priority, - Name = "Emulator Timer" - }; - - internal void Start() + internal EmulatorTimer() { - Platform.RequestMinimumTimerResolution(); + _timerResolutionScope = new TimerResolutionScope(); - _worker.Start(); + _worker = new Thread(Worker) + { + IsBackground = true, + Priority = Priority, + Name = "Emulator Timer" + }; } + internal void Start() => _worker.Start(); + internal void Stop() { - Platform.ReleaseMinimumTimerResolution(); - _cancellationTokenSource.Cancel(); _stoppedEvent.Wait(); } @@ -139,6 +139,7 @@ public void Dispose() { _isDisposed = true; + _timerResolutionScope.Dispose(); _cancellationTokenSource.Dispose(); _stoppedEvent.Dispose(); _pausedEvent.Dispose(); diff --git a/src/Spectron.Emulation/Platforms/Platform.cs b/src/Spectron.Emulation/Platforms/Platform.cs deleted file mode 100644 index 3a956a65..00000000 --- a/src/Spectron.Emulation/Platforms/Platform.cs +++ /dev/null @@ -1,22 +0,0 @@ -using OldBit.Spectron.Emulation.Platforms.Windows.Interop; - -namespace OldBit.Spectron.Emulation.Platforms; - -internal static class Platform -{ - internal static void RequestMinimumTimerResolution() - { - if (OperatingSystem.IsWindows()) - { - _ = Winmm.TimeBeginPeriod(1); - } - } - - internal static void ReleaseMinimumTimerResolution() - { - if (OperatingSystem.IsWindows()) - { - _ = Winmm.TimeEndPeriod(1); - } - } -} \ No newline at end of file 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