Skip to content

Commit 9bbd8bc

Browse files
authored
Performance improvements (#65)
* Improve performance * Check disposed state before pausing * Improve `EmulatorTimer` precision by adjusting thread sleeping logic and managing Windows timer resolution * Add comment to clarify Winmm * Use adjusted thread sleeping logic * Refactor Windows timer resolution management with `TimerResolutionScope` for improved resource handling.
1 parent bf7f185 commit 9bbd8bc

5 files changed

Lines changed: 86 additions & 18 deletions

File tree

src/Spectron.Emulation/EmulatorTimer.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using OldBit.Spectron.Emulation.Platforms;
23

34
namespace OldBit.Spectron.Emulation;
45

@@ -12,6 +13,9 @@ internal sealed class EmulatorTimer : IDisposable
1213
private readonly CancellationTokenSource _cancellationTokenSource = new();
1314
private readonly ManualResetEventSlim _stoppedEvent = new(initialState: false);
1415
private readonly ManualResetEventSlim _pausedEvent = new(initialState: false);
16+
private readonly TimerResolutionScope _timerResolutionScope;
17+
18+
private volatile bool _isDisposed;
1519

1620
internal bool IsPaused { get; private set; }
1721
internal ThreadPriority Priority { get; set; } = ThreadPriority.AboveNormal;
@@ -20,12 +24,17 @@ internal sealed class EmulatorTimer : IDisposable
2024

2125
internal event EventHandler? Elapsed;
2226

23-
internal EmulatorTimer() => _worker = new Thread(Worker)
27+
internal EmulatorTimer()
2428
{
25-
IsBackground = true,
26-
Priority = Priority,
27-
Name = "Emulator Timer"
28-
};
29+
_timerResolutionScope = new TimerResolutionScope();
30+
31+
_worker = new Thread(Worker)
32+
{
33+
IsBackground = true,
34+
Priority = Priority,
35+
Name = "Emulator Timer"
36+
};
37+
}
2938

3039
internal void Start() => _worker.Start();
3140

@@ -37,6 +46,11 @@ internal void Stop()
3746

3847
internal void Pause()
3948
{
49+
if (_isDisposed)
50+
{
51+
return;
52+
}
53+
4054
_pausedEvent.Reset();
4155
IsPaused = true;
4256

@@ -106,6 +120,10 @@ private void Worker()
106120
case < 10:
107121
Thread.SpinWait(25);
108122
break;
123+
124+
default:
125+
Thread.Sleep(Math.Max(1, (int)timeToWait.TotalMilliseconds - 5));
126+
break;
109127
}
110128
}
111129
}
@@ -119,6 +137,9 @@ private void Worker()
119137

120138
public void Dispose()
121139
{
140+
_isDisposed = true;
141+
142+
_timerResolutionScope.Dispose();
122143
_cancellationTokenSource.Dispose();
123144
_stoppedEvent.Dispose();
124145
_pausedEvent.Dispose();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using OldBit.Spectron.Emulation.Platforms.Windows.Interop;
2+
3+
namespace OldBit.Spectron.Emulation.Platforms;
4+
5+
internal sealed class TimerResolutionScope : IDisposable
6+
{
7+
private const int TimerResolutionMs = 1;
8+
9+
internal TimerResolutionScope()
10+
{
11+
if (OperatingSystem.IsWindows())
12+
{
13+
_ = Winmm.TimeBeginPeriod(TimerResolutionMs);
14+
}
15+
}
16+
17+
public void Dispose()
18+
{
19+
if (OperatingSystem.IsWindows())
20+
{
21+
_ = Winmm.TimeEndPeriod(TimerResolutionMs);
22+
}
23+
}
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Runtime.InteropServices;
2+
using System.Runtime.Versioning;
3+
4+
namespace OldBit.Spectron.Emulation.Platforms.Windows.Interop;
5+
6+
[SupportedOSPlatform("windows")]
7+
internal static partial class Winmm
8+
{
9+
/// <summary>
10+
/// The timeBeginPeriod function requests a minimum resolution for periodic timers.
11+
/// </summary>
12+
/// <param name="uPeriod">Minimum timer resolution, in milliseconds, for the application or device driver.
13+
/// A lower value specifies a higher (more accurate) resolution.</param>
14+
/// <returns>Returns TIMERR_NOERROR if successful or TIMERR_NOCANDO if the resolution specified in uPeriod is out of range.</returns>
15+
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
16+
internal static partial uint TimeBeginPeriod(uint uPeriod);
17+
18+
/// <summary>
19+
/// The timeEndPeriod function clears a previously set minimum timer resolution.
20+
/// </summary>
21+
/// <param name="uPeriod">Minimum timer resolution specified in the previous call to the timeBeginPeriod function.</param>
22+
/// <returns>Returns TIMERR_NOERROR if successful or TIMERR_NOCANDO if the resolution specified in uPeriod is out of range.</returns>
23+
[LibraryImport("winmm.dll", EntryPoint = "timeEndPeriod")]
24+
internal static partial uint TimeEndPeriod(uint uPeriod);
25+
}

src/Spectron.Emulation/Spectron.Emulation.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
78
<AssemblyName>OldBit.Spectron.Emulation</AssemblyName>
89
<RootNamespace>OldBit.Spectron.Emulation</RootNamespace>
910
<Version>1.0.0</Version>

src/Spectron/Screen/FrameBufferConverter.cs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,22 @@ internal void UpdateBitmap()
3434
{
3535
using var lockedBitmap = ScreenBitmap.Lock();
3636

37-
var targetAddress = lockedBitmap.Address;
37+
var colCount = _endFrameBufferCol - _startFrameBufferCol + 1;
38+
var rowBytes = colCount * 4;
3839

39-
for (var frameBufferRow = _startFrameBufferRow; frameBufferRow <= _endFrameBufferRow; frameBufferRow++)
40+
unsafe
4041
{
41-
var rowOffset = frameBufferRow * _frameBuffer.Width;
42-
43-
for (var frameBufferCol = _startFrameBufferCol; frameBufferCol <= _endFrameBufferCol; frameBufferCol++)
42+
fixed (Color* pixelsBase = _frameBuffer.Pixels)
4443
{
45-
var pixelIndex = rowOffset + frameBufferCol;
44+
var destination = (byte*)lockedBitmap.Address;
4645

47-
unsafe
46+
for (var row = _startFrameBufferRow; row <= _endFrameBufferRow; row++)
4847
{
49-
fixed (Color* color = &_frameBuffer.Pixels[pixelIndex])
50-
{
51-
var pixelColor = *(uint*)color;
52-
*(uint*)targetAddress = pixelColor;
48+
var source = (byte*)(pixelsBase + row * _frameBuffer.Width + _startFrameBufferCol);
49+
50+
Buffer.MemoryCopy(source, destination, rowBytes, rowBytes);
5351

54-
targetAddress += 4;
55-
}
52+
destination += rowBytes;
5653
}
5754
}
5855
}

0 commit comments

Comments
 (0)