diff --git a/Directory.Packages.props b/Directory.Packages.props
index c7b3e9ec..3c469247 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,7 +30,7 @@
-
+
diff --git a/README.md b/README.md
index da4c4b65..025dcafe 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,8 @@ The ZX Spectrum was my first computer, and I still have a deep affection for it.
- [x] Kempston mouse emulation
- [x] ZX Printer emulation
- [x] Integrated tape and disk viewers
-- [x] Adjustable emulation speed
+- [x] Adjustable emulation speed (frames/second)
+- [x] Adjustable clock speed (3.5, 7, 14 and 28 MHz)
- [x] Keyboard-based joystick emulation: Kempston, Sinclair, Cursor, and Fuller
- [x] Audio and video recording
- [x] Support for alternative and custom ROMs
@@ -68,15 +69,18 @@ The solution consists of several projects:
There are some test releases that can be found in the project [Releases](https://github.com/oldbit-com/Spectron/releases)
if you don't want to build the project. These are self-contained and do not need .NET Framework to be installed separetely.
-## Running the emulator
+## Running the emulator using source code
Requires .NET 10 to build and run the emulator.
Grab the latest code from the repository, build and run the emulator:
```shell
+git clone https://github.com/oldbit-com/Spectron.git
+
+cd Spectron
+
dotnet build -c Release
dotnet run --project ./src/Spectron
```
-You can also run the emulator from the [Releases](https://github.com/oldbit-com/Spectron/releases) page.
### Command line options
Command lines allow overriding most of the default options and loading a specified file. Full list of available
@@ -97,8 +101,17 @@ Test programs can be found in the [Tests](https://github.com/oldbit-com/Spectron
I have developed a custom [Z80 CPU](https://github.com/oldbit-com/Z80/tree/spectron) emulator library for this project.
The emulation is highly accurate and supports many undocumented instructions, as well as memory and I/O contention.
+## Emulation Speed
+There are two options for adjusting the speed:
+- **At n% of normal speed** - this option allows running the emulator at a higher or slower speed, e.g. more or less frames per second.
+*Everything* will run at selected speed, e.g. like using fast-forward or slow-motion playback.
+
+- **At higher CPU clock** - this option runs the CPU at a higher frequency. Standard clock is 3.5 MHz, but it can be increased to 7 MHz, 14 MHz
+or 28 MHz. This can be useful for games that are normally sluggish. Some software and games may not work properly with higher clock speeds.
+
## Session Persistence
-When the emulator is closed, it automatically saves its current state, which is then restored upon restart. This behavior can be toggled in the settings.
+When the emulator is closed, it automatically saves its current state, which is then restored upon restart.
+This behavior can be toggled in the settings.
## Tape Loading
Tape loading is supported for **TAP** and **TZX** files (including ZIP archives). Three loading speeds are available:
diff --git a/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs b/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs
index a3c48dbe..7d7b37aa 100644
--- a/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs
+++ b/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs
@@ -140,7 +140,7 @@ private void DebuggerStepInto()
if (Emulator.Cpu.Clock.IsFrameComplete)
{
- Emulator.Cpu.Clock.NewFrame(Emulator.TicksPerFrame);
+ Emulator.Cpu.Clock.NewFrame(Emulator.Clock.TicksPerFrame);
}
Refresh(refreshMemory: false);
diff --git a/src/Spectron.Emulation/Devices/Audio/AY/AyAudio.cs b/src/Spectron.Emulation/Devices/Audio/AY/AyAudio.cs
index ba889be2..baa3940c 100644
--- a/src/Spectron.Emulation/Devices/Audio/AY/AyAudio.cs
+++ b/src/Spectron.Emulation/Devices/Audio/AY/AyAudio.cs
@@ -1,5 +1,3 @@
-using OldBit.Z80Cpu;
-
namespace OldBit.Spectron.Emulation.Devices.Audio.AY;
///
@@ -17,10 +15,10 @@ internal sealed class AyAudio
private int _ayTicks;
private long _clockStepCounter;
- private readonly Clock _clock;
+ private readonly EmulatorClock _clock;
private readonly AyDevice _ay;
- public AyAudio(Clock clock, AyDevice ay, double statesPerSample)
+ public AyAudio(EmulatorClock clock, AyDevice ay, double statesPerSample)
{
_clock = clock;
_ay = ay;
@@ -28,7 +26,7 @@ public AyAudio(Clock clock, AyDevice ay, double statesPerSample)
_statesPerSample = (long)(Multiplier * statesPerSample);
_sampleRate = statesPerSample / AyCycles;
- _ay.OnUpdateAudio = () => Update(clock.FrameTicks);
+ _ay.OnUpdateAudio = () => Update(clock.UlaTicks);
}
internal void NewFrame()
@@ -40,8 +38,8 @@ internal void NewFrame()
internal void EndFrame()
{
- Update(_clock.FrameTicks);
- _ayTicks -= _clock.FrameTicks;
+ Update(_clock.UlaTicks);
+ _ayTicks -= _clock.UlaTicks;
}
internal void Reset() => _ay.Reset();
diff --git a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs
index ed23893b..c20cc82e 100644
--- a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs
+++ b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs
@@ -2,7 +2,6 @@
using OldBit.Spectron.Emulation.Devices.Audio.AY;
using OldBit.Spectron.Emulation.Devices.Audio.Beeper;
using OldBit.Spectron.Emulation.Tape;
-using OldBit.Z80Cpu;
namespace OldBit.Spectron.Emulation.Devices.Audio;
@@ -82,7 +81,7 @@ public bool IsAyEnabled
public bool IsAySupportedStandardSpectrum { get; set; } = true;
- internal AudioManager(Clock clock, CassettePlayer? cassettePlayer, HardwareSettings hardware, Func isUlaPort)
+ internal AudioManager(EmulatorClock clock, CassettePlayer? cassettePlayer, HardwareSettings hardware, Func isUlaPort)
{
IsAySupported = hardware.HasAyChip;
diff --git a/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperAudio.cs b/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperAudio.cs
index 767bdd43..976aaaff 100644
--- a/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperAudio.cs
+++ b/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperAudio.cs
@@ -1,15 +1,13 @@
-using OldBit.Z80Cpu;
-
namespace OldBit.Spectron.Emulation.Devices.Audio.Beeper;
internal sealed class BeeperAudio
{
- private const int Multiplier = 1000; // Used to avoid floating point arithmetic and rounding errors
+ private const int AccuracyMultiplier = 1000; // Used to avoid floating point arithmetic and rounding errors
private byte _lastEarMic;
private int _remainingTicks;
- private readonly Clock _clock;
+ private readonly EmulatorClock _clock;
private readonly int _statesPerSample;
private readonly BeeperStates _beeperStates = new();
private readonly LowPassFilter _lowPassFilter;
@@ -25,10 +23,10 @@ internal sealed class BeeperAudio
internal AudioSamples Samples { get; } = new();
- internal BeeperAudio(Clock clock, double statesPerSample, float clockMhz)
+ internal BeeperAudio(EmulatorClock clock, double statesPerSample, float clockMhz)
{
_clock = clock;
- _statesPerSample = (int)(Multiplier * statesPerSample);
+ _statesPerSample = (int)(AccuracyMultiplier * statesPerSample);
_lowPassFilter = new LowPassFilter(statesPerSample, clockMhz * 1_000_000);
}
@@ -39,8 +37,8 @@ internal void EndFrame()
{
var runningTicks = 0;
- var ticks = _beeperStates.Count == 0 ? _clock.FrameTicks : _beeperStates[0].Ticks;
- var duration = ticks * Multiplier;
+ var ticks = _beeperStates.Count == 0 ? _clock.UlaTicks : _beeperStates[0].Ticks;
+ var duration = ticks * AccuracyMultiplier;
for (var i = 0; i <= _beeperStates.Count; i++)
{
@@ -67,8 +65,8 @@ internal void EndFrame()
break;
}
- ticks = i == _beeperStates.Count - 1 ? _clock.FrameTicks : _beeperStates[i + 1].Ticks;
- duration = ticks * Multiplier - runningTicks;
+ ticks = i == _beeperStates.Count - 1 ? _clock.UlaTicks : _beeperStates[i + 1].Ticks;
+ duration = ticks * AccuracyMultiplier - runningTicks;
}
_beeperStates.Reset();
@@ -80,7 +78,7 @@ internal void Update(byte value)
if (_lastEarMic != earMic)
{
- _beeperStates.Add(_clock.FrameTicks, _lastEarMic);
+ _beeperStates.Add(_clock.UlaTicks, _lastEarMic);
}
_lastEarMic = (byte)earMic;
diff --git a/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs
index 26c8e9f4..adede553 100644
--- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs
+++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs
@@ -2,17 +2,26 @@
namespace OldBit.Spectron.Emulation.Devices.Contention;
-internal sealed class ContentionProvider128K(int firstPixelTick, int ticksPerLine) : IContentionProvider
+internal sealed class ContentionProvider128K(int firstPixelTick, int ticksPerLine, int clockMultiplier = 1) : IContentionProvider
{
private readonly int[] _contentionTable = ContentionProvider.BuildContentionTable(firstPixelTick, ticksPerLine);
+ internal int ClockMultiplier { get; set; } = clockMultiplier;
internal int ActiveRamBankId { get; set; }
- public int GetMemoryContention(int ticks, Word address) =>
- ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0;
+ public int GetMemoryContention(int ticks, Word address)
+ {
+ var ulaTicks = ticks / ClockMultiplier;
- public int GetPortContention(int ticks, Word port) =>
- ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0;
+ return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0;
+ }
+
+ public int GetPortContention(int ticks, Word port)
+ {
+ var ulaTicks = ticks / ClockMultiplier;
+
+ return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0;
+ }
public bool IsAddressContended(Word address) =>
address is >= 0x4000 and <= 0x7FFF ||
diff --git a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs
index ee982dde..c4d403bf 100644
--- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs
+++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs
@@ -2,15 +2,25 @@
namespace OldBit.Spectron.Emulation.Devices.Contention;
-internal sealed class ContentionProvider48K(int firstPixelTick, int ticksPerLine) : IContentionProvider
+internal sealed class ContentionProvider48K(int firstPixelTick, int ticksPerLine, int clockMultiplier = 1) : IContentionProvider
{
private readonly int[] _contentionTable = ContentionProvider.BuildContentionTable(firstPixelTick, ticksPerLine);
- public int GetMemoryContention(int ticks, Word address) =>
- ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0;
+ internal int ClockMultiplier { get; set; } = clockMultiplier;
- public int GetPortContention(int ticks, Word port) =>
- ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0;
+ public int GetMemoryContention(int ticks, Word address)
+ {
+ var ulaTicks = ticks / ClockMultiplier;
+
+ return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0;
+ }
+
+ public int GetPortContention(int ticks, Word port)
+ {
+ var ulaTicks = ticks / ClockMultiplier;
+
+ return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0;
+ }
public bool IsAddressContended(Word address) => address is >= 0x4000 and <= 0x7FFF;
diff --git a/src/Spectron.Emulation/Devices/FloatingBus.cs b/src/Spectron.Emulation/Devices/FloatingBus.cs
index f34d3362..03a78f06 100644
--- a/src/Spectron.Emulation/Devices/FloatingBus.cs
+++ b/src/Spectron.Emulation/Devices/FloatingBus.cs
@@ -13,13 +13,13 @@ internal sealed class FloatingBus : IDevice
{
private readonly HardwareSettings _hardware;
private readonly IMemory _memory;
- private readonly Clock _clock;
+ private readonly EmulatorClock _clock;
private readonly Func _isUlaPort;
private readonly Dictionary _floatingBusAddressIndex = new();
public bool IsEnabled { get; set; } = true;
- internal FloatingBus(HardwareSettings hardware, IMemory memory, Clock clock, Func isUlaPort)
+ internal FloatingBus(HardwareSettings hardware, IMemory memory, EmulatorClock clock, Func isUlaPort)
{
_hardware = hardware;
_memory = memory;
@@ -36,13 +36,13 @@ internal FloatingBus(HardwareSettings hardware, IMemory memory, Clock clock, Fun
return null;
}
- if (_clock.FrameTicks < _hardware.FloatingBusStartTicks || _clock.FrameTicks > _hardware.LastPixelTicks)
+ if (_clock.UlaTicks < _hardware.FloatingBusStartTicks || _clock.UlaTicks > _hardware.LastPixelTicks)
{
return null;
}
// Note that the Z80 samples the data bus during the final T-state of the I/O machine cycle
- return _floatingBusAddressIndex.TryGetValue(_clock.FrameTicks - 1, out var screenAddress)
+ return _floatingBusAddressIndex.TryGetValue(_clock.UlaTicks - 1, out var screenAddress)
? _memory.Read(screenAddress)
: null;
}
diff --git a/src/Spectron.Emulation/Devices/Ula.cs b/src/Spectron.Emulation/Devices/Ula.cs
index 80ec9d8d..80a765bc 100644
--- a/src/Spectron.Emulation/Devices/Ula.cs
+++ b/src/Spectron.Emulation/Devices/Ula.cs
@@ -8,6 +8,7 @@ namespace OldBit.Spectron.Emulation.Devices;
internal class Ula(
KeyboardState keyboardState,
ScreenBuffer screenBuffer,
+ EmulatorClock clock,
Z80 cpu,
TapeManager tapeManager) : IDevice
{
@@ -32,7 +33,7 @@ public virtual void WritePort(Word address, byte value)
}
var color = SpectrumPalette.GetBorderColor(value);
- screenBuffer.UpdateBorder(color, cpu.Clock.FrameTicks);
+ screenBuffer.UpdateBorder(color, clock.UlaTicks);
}
internal virtual bool IsUlaPort(Word address) => (address & 0x01) == 0x00;
@@ -41,7 +42,7 @@ internal virtual void Reset() { }
private void UpdateEarBit(ref byte value)
{
- tapeManager.DetectLoader(cpu.Clock.FrameTicks, cpu.Registers.PC);
+ tapeManager.DetectLoader(clock.UlaTicks, cpu.Registers.PC);
if (tapeManager.CassettePlayer?.IsPlaying != true)
{
diff --git a/src/Spectron.Emulation/Devices/UlaTimex.cs b/src/Spectron.Emulation/Devices/UlaTimex.cs
index 6280d494..b7ecbba9 100644
--- a/src/Spectron.Emulation/Devices/UlaTimex.cs
+++ b/src/Spectron.Emulation/Devices/UlaTimex.cs
@@ -8,8 +8,9 @@ namespace OldBit.Spectron.Emulation.Devices;
internal sealed class UlaTimex(
KeyboardState keyboardState,
ScreenBuffer screenBuffer,
+ EmulatorClock clock,
Z80 cpu,
- TapeManager tapeManager) : Ula(keyboardState, screenBuffer, cpu, tapeManager)
+ TapeManager tapeManager) : Ula(keyboardState, screenBuffer, clock, cpu, tapeManager)
{
private byte _lastControlValue;
diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs
index 6a4e6ddf..cdf8eda3 100644
--- a/src/Spectron.Emulation/Emulator.cs
+++ b/src/Spectron.Emulation/Emulator.cs
@@ -27,7 +27,8 @@ namespace OldBit.Spectron.Emulation;
///
public sealed class Emulator
{
- private readonly HardwareSettings _hardware;
+ private const int MaxEmulationSpeed = -1;
+
private readonly TimeMachine _timeMachine;
private readonly ILogger _logger;
private readonly EmulatorTimer _emulationTimer;
@@ -80,8 +81,7 @@ public bool IsFloatingBusEnabled
public Interface1Device Interface1 { get; }
public ZxPrinter Printer { get; }
public ScreenBuffer ScreenBuffer { get; }
-
- public int TicksPerFrame => _hardware.TicksPerFrame;
+ public EmulatorClock Clock { get; }
internal UlaPlus UlaPlus { get; }
@@ -97,7 +97,6 @@ internal Emulator(
CommandManager commandManager,
ILogger logger)
{
- _hardware = hardware;
KeyboardState = keyboardState;
_timeMachine = timeMachine;
_logger = logger;
@@ -111,21 +110,20 @@ internal Emulator(
RomType = emulatorArgs.RomType;
Memory = emulatorArgs.Memory;
- Cpu = new Z80(emulatorArgs.Memory)
+ Clock = new EmulatorClock(hardware.TicksPerFrame, emulatorArgs.ClockMultiplier)
{
- Clock =
- {
- InterruptDuration = hardware.InterruptDuration,
- ContentionProvider = emulatorArgs.ContentionProvider
- }
+ InterruptDuration = hardware.InterruptDuration,
+ ContentionProvider = emulatorArgs.ContentionProvider
};
+ Cpu = new Z80(emulatorArgs.Memory, Clock);
+
UlaPlus = new UlaPlus();
ScreenBuffer = new ScreenBuffer(hardware, emulatorArgs.Memory, UlaPlus);
Ula = ComputerType == ComputerType.Timex2048
- ? new UlaTimex(KeyboardState, ScreenBuffer, Cpu, TapeManager)
- : new Ula(KeyboardState, ScreenBuffer, Cpu, TapeManager);
+ ? new UlaTimex(KeyboardState, ScreenBuffer, Clock, Cpu, TapeManager)
+ : new Ula(KeyboardState, ScreenBuffer, Clock, Cpu, TapeManager);
_screenMemoryHandler = new ScreenMemoryHandler(Memory, ScreenBuffer);
_screenMemoryHandler.SetScreenMode(Ula as UlaTimex);
@@ -137,12 +135,12 @@ internal Emulator(
KeyboardState.Reset();
TapeManager.Attach(Cpu, Memory, hardware);
- _floatingBus = new FloatingBus(_hardware, Memory, Cpu.Clock, Ula.IsUlaPort);
+ _floatingBus = new FloatingBus(hardware, Memory, Clock, Ula.IsUlaPort);
- AudioManager = new AudioManager(Cpu.Clock, tapeManager.CassettePlayer, hardware, Ula.IsUlaPort);
+ AudioManager = new AudioManager(Clock, tapeManager.CassettePlayer, hardware, Ula.IsUlaPort);
DivMmc = new DivMmcDevice(Cpu, Memory, logger);
- Beta128 = new Beta128Device(Cpu, _hardware.ClockMhz, Memory, ComputerType, diskDriveManager);
+ Beta128 = new Beta128Device(Cpu, hardware.ClockMhz, Memory, ComputerType, diskDriveManager);
Interface1 = microdriveManager.CreateDevice(Cpu, Memory);
Printer = new ZxPrinter();
@@ -215,10 +213,13 @@ public void Break()
public void RequestNmi() => _isNmiRequested = true;
- public void SetEmulationSpeed(int emulationSpeedPercentage) =>
- _emulationTimer.Interval = emulationSpeedPercentage == int.MaxValue ?
- TimeSpan.Zero :
- TimeSpan.FromMilliseconds(20 * (100f / emulationSpeedPercentage));
+ public int EmulationSpeed
+ {
+ set =>
+ _emulationTimer.Interval = value == MaxEmulationSpeed ?
+ TimeSpan.Zero :
+ TimeSpan.FromMilliseconds(20 * (100f / value));
+ }
private void OnTimerElapsed(object? sender, EventArgs e)
{
@@ -239,7 +240,7 @@ private void OnTimerElapsed(object? sender, EventArgs e)
private void AddEventHandlers()
{
- Cpu.Clock.TicksAdded += (_, previousFrameTicks, _) => ScreenBuffer.UpdateScreen(previousFrameTicks);
+ Cpu.Clock.TicksAdded += (_, previousFrameTicks, _) => ScreenBuffer.UpdateScreen(previousFrameTicks / Clock.Multiplier);
Cpu.BeforeInstruction += BeforeInstruction;
UlaPlus.ActiveChanged += _ => _invalidateScreen = true;
Beta128.DiskActivity += _ => DiskDriveManager.OnDiskActivity();
@@ -247,7 +248,7 @@ private void AddEventHandlers()
if (Ula is UlaTimex ulaTimex)
{
ulaTimex.ScreenModeChanged += (sender, _) =>
- _screenMemoryHandler.SetScreenMode(sender as UlaTimex, Cpu.Clock.FrameTicks);
+ _screenMemoryHandler.SetScreenMode(sender as UlaTimex, Clock.UlaTicks);
}
}
@@ -313,7 +314,7 @@ private void StartFrame()
}
else
{
- Cpu.Clock.NewFrame(_hardware.TicksPerFrame);
+ Cpu.Clock.NewFrame(Clock.TicksPerFrame);
}
ScreenBuffer.NewFrame();
@@ -323,11 +324,11 @@ private void StartFrame()
private void EndFrame()
{
- _ticksSinceReset += Cpu.Clock.FrameTicks;
+ _ticksSinceReset += Clock.UlaTicks;
var audioBuffer = AudioManager.EndFrame();
- ScreenBuffer.EndFrame(Cpu.Clock.FrameTicks);
+ ScreenBuffer.EndFrame(Clock.UlaTicks);
FrameCompleted?.Invoke(ScreenBuffer.FrameBuffer, audioBuffer);
@@ -359,7 +360,7 @@ private void BeforeInstruction(Word pc)
break;
case TapeSpeed.Accelerated:
- SetEmulationSpeed(int.MaxValue);
+ EmulationSpeed = MaxEmulationSpeed;
_isAcceleratedTapeSpeed = true;
break;
}
@@ -373,7 +374,7 @@ private void BeforeInstruction(Word pc)
break;
case TapeSpeed.Accelerated:
- SetEmulationSpeed(int.MaxValue);
+ EmulationSpeed = MaxEmulationSpeed;
_isAcceleratedTapeSpeed = true;
break;
}
@@ -385,7 +386,7 @@ private void BeforeInstruction(Word pc)
case RomRoutines.ERROR_1:
if (_isAcceleratedTapeSpeed)
{
- SetEmulationSpeed(100);
+ EmulationSpeed = 100;
_isAcceleratedTapeSpeed = false;
}
diff --git a/src/Spectron.Emulation/EmulatorClock.cs b/src/Spectron.Emulation/EmulatorClock.cs
new file mode 100644
index 00000000..9eaf3136
--- /dev/null
+++ b/src/Spectron.Emulation/EmulatorClock.cs
@@ -0,0 +1,33 @@
+using OldBit.Spectron.Emulation.Devices.Contention;
+using OldBit.Z80Cpu;
+
+namespace OldBit.Spectron.Emulation;
+
+public sealed class EmulatorClock : Clock
+{
+ private readonly int _ticksPerFrame;
+
+ public EmulatorClock(int ticksPerFrame, int multiplier = 1)
+ {
+ _ticksPerFrame = ticksPerFrame;
+ Multiplier = multiplier;
+
+ TicksPerFrame = _ticksPerFrame * multiplier;
+ }
+
+ public int Multiplier
+ {
+ get;
+ set
+ {
+ field = value;
+ TicksPerFrame = _ticksPerFrame * value;
+
+ (ContentionProvider as ContentionProvider48K)?.ClockMultiplier = value;
+ (ContentionProvider as ContentionProvider128K)?.ClockMultiplier = value;
+ }
+ }
+
+ internal int TicksPerFrame { get; private set; }
+ internal int UlaTicks => FrameTicks / Multiplier;
+}
\ No newline at end of file
diff --git a/src/Spectron.Emulation/EmulatorFactory.cs b/src/Spectron.Emulation/EmulatorFactory.cs
index 43755d3e..49419d98 100644
--- a/src/Spectron.Emulation/EmulatorFactory.cs
+++ b/src/Spectron.Emulation/EmulatorFactory.cs
@@ -17,6 +17,7 @@ internal sealed record EmulatorArgs(
ComputerType ComputerType,
RomType RomType,
IEmulatorMemory Memory,
+ int ClockMultiplier,
IContentionProvider ContentionProvider);
public sealed class EmulatorFactory(
@@ -29,7 +30,7 @@ public sealed class EmulatorFactory(
CommandManager commandManager,
ILogger logger)
{
- public Emulator Create(ComputerType computerType, RomType romType, byte[]? customRom = null)
+ public Emulator Create(ComputerType computerType, RomType romType, int clockMultiplier = 1, byte[]? customRom = null)
{
byte[] rom;
@@ -37,27 +38,27 @@ public Emulator Create(ComputerType computerType, RomType romType, byte[]? custo
{
case ComputerType.Spectrum16K:
rom = customRom ?? GetSpectrum48KRom(romType);
- return CreateSpectrum(computerType, romType, new Memory16K(rom));
+ return CreateSpectrum(computerType, romType, new Memory16K(rom), clockMultiplier);
case ComputerType.Spectrum48K:
rom = customRom ?? GetSpectrum48KRom(romType);
- return CreateSpectrum(computerType, romType, new Memory48K(rom));
+ return CreateSpectrum(computerType, romType, new Memory48K(rom), clockMultiplier);
case ComputerType.Spectrum128K:
rom = customRom != null ? customRom[..0x4000] : GetSpectrum128KRom(romType);
var bank1Rom = customRom?.Length == 0x8000 ? customRom[0x4000..] : RomReader.ReadRom(RomType.Original128Bank1);
- return CreateSpectrum128K(romType, new Memory128K(rom, bank1Rom));
+ return CreateSpectrum128K(romType, new Memory128K(rom, bank1Rom), clockMultiplier);
case ComputerType.Timex2048:
rom = customRom ?? GetTimex2048Rom(romType);
- return CreateTimex(romType, new Memory48K(rom));
+ return CreateTimex(romType, new Memory48K(rom), clockMultiplier);
default:
throw new ArgumentOutOfRangeException(nameof(computerType));
}
}
- private Emulator CreateSpectrum128K(RomType romType, Memory128K memory)
+ private Emulator CreateSpectrum128K(RomType romType, Memory128K memory, int clockMultiplier)
{
var contentionProvider = new ContentionProvider128K(
Hardware.Spectrum128K.ContentionStartTicks,
@@ -69,6 +70,7 @@ private Emulator CreateSpectrum128K(RomType romType, Memory128K memory)
ComputerType.Spectrum128K,
romType,
memory,
+ clockMultiplier,
contentionProvider);
return new Emulator(
@@ -84,7 +86,7 @@ private Emulator CreateSpectrum128K(RomType romType, Memory128K memory)
logger);
}
- private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmulatorMemory memory)
+ private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmulatorMemory memory, int clockMultiplier)
{
var contentionProvider = new ContentionProvider48K(
Hardware.Spectrum48K.ContentionStartTicks,
@@ -94,6 +96,7 @@ private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmu
computerType,
romType,
memory,
+ clockMultiplier,
contentionProvider);
return new Emulator(
@@ -109,7 +112,7 @@ private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmu
logger);
}
- private Emulator CreateTimex(RomType romType, IEmulatorMemory memory)
+ private Emulator CreateTimex(RomType romType, IEmulatorMemory memory, int clockMultiplier)
{
var contentionProvider = new ContentionProvider48K(
Hardware.Timex2048.ContentionStartTicks,
@@ -119,6 +122,7 @@ private Emulator CreateTimex(RomType romType, IEmulatorMemory memory)
ComputerType.Timex2048,
romType,
memory,
+ clockMultiplier,
contentionProvider);
return new Emulator(
diff --git a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs
index cebfa12a..bd07c3d7 100644
--- a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs
+++ b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs
@@ -40,7 +40,7 @@ internal Emulator CreateEmulator(SzxFile snapshot)
var romType = snapshot.CustomRom?.Data != null ? RomType.Custom : RomType.Original;
- var emulator = emulatorFactory.Create(computerType, romType, snapshot.CustomRom?.Data);
+ var emulator = emulatorFactory.Create(computerType, romType, customRom: snapshot.CustomRom?.Data);
Update(emulator, snapshot);
diff --git a/src/Spectron.Emulation/State/StateSnapshotManager.cs b/src/Spectron.Emulation/State/StateSnapshotManager.cs
index 4fd4a7df..16b0989d 100644
--- a/src/Spectron.Emulation/State/StateSnapshotManager.cs
+++ b/src/Spectron.Emulation/State/StateSnapshotManager.cs
@@ -22,7 +22,7 @@ public Emulator CreateEmulator(StateSnapshot snapshot)
{
var romType = snapshot.CustomRom?.RomType ?? RomType.Original;
- var emulator = emulatorFactory.Create(snapshot.ComputerType, romType, snapshot.CustomRom?.Concatenated);
+ var emulator = emulatorFactory.Create(snapshot.ComputerType, romType, customRom: snapshot.CustomRom?.Concatenated);
LoadCpu(emulator.Cpu, snapshot.Cpu);
LoadMemory(emulator.Memory, snapshot.Memory);
diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml
index a46db534..8da36ba6 100644
--- a/src/Spectron/Controls/MainMenu.axaml
+++ b/src/Spectron/Controls/MainMenu.axaml
@@ -12,6 +12,7 @@
xmlns:converters="clr-namespace:OldBit.Spectron.Converters"
xmlns:microdrive="clr-namespace:OldBit.Spectron.Emulation.Devices.Interface1.Microdrives;assembly=OldBit.Spectron.Emulation"
xmlns:diskDrive="clr-namespace:OldBit.Spectron.Emulation.Devices.Beta128.Drive;assembly=OldBit.Spectron.Emulation"
+ xmlns:system="clr-namespace:System;assembly=System.Runtime"
x:DataType="viewModels:MainViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="OldBit.Spectron.Controls.MainMenu">
@@ -21,6 +22,26 @@
+
+
+ 1
+ 2
+ 4
+ 8
+
+
+ 25
+ 50
+ 75
+ 100
+ 125
+ 150
+ 200
+ 250
+ 300
+ 400
+ 500
+ -1
@@ -84,18 +105,24 @@