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 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/Spectron/Controls/NativeMainMenu.cs b/src/Spectron/Controls/NativeMainMenu.cs index 1b325fc8..93adf105 100644 --- a/src/Spectron/Controls/NativeMainMenu.cs +++ b/src/Spectron/Controls/NativeMainMenu.cs @@ -25,7 +25,8 @@ public sealed class NativeMainMenu private readonly Dictionary _romTypes = new(); private readonly Dictionary _joystickTypes = new(); private readonly Dictionary _mouseTypes = new(); - private readonly Dictionary _emulationSpeeds = new(); + private readonly Dictionary _emulationSpeeds = new(); + private readonly Dictionary _clockMultipliers = new(); private readonly Dictionary _borderSizes = new(); private readonly Dictionary _screenEffects = new(); private readonly Dictionary _tapeLoadingSpeeds = new(); @@ -49,6 +50,7 @@ public NativeMainMenu(MainViewModel viewModel) CreateJoystickTypeMenu(); CreateMouseTypeMenu(); CreateSpeedOptionMenu(); + CreateClockMultiplierOptionMenu(); CreateBorderSizeMenu(); CreateScreenEffectMenu(); CreateTapeLoadingSpeedMenu(); @@ -137,6 +139,14 @@ private void ViewModelPropertyChanged(string? propertyName) break; + case nameof(MainViewModel.ClockMultiplier): + foreach (var multiplier in _clockMultipliers.Keys) + { + _clockMultipliers[multiplier].IsChecked = _viewModel.ClockMultiplier == multiplier; + } + + break; + case nameof(MainViewModel.IsPaused): _pauseMenuItem?.IsChecked = _viewModel.IsPaused; @@ -358,18 +368,29 @@ private NativeMenuItem CreateControlMenu() { Menu = [ - _emulationSpeeds["25"], - _emulationSpeeds["50"], - _emulationSpeeds["75"], - _emulationSpeeds["100"], - _emulationSpeeds["125"], - _emulationSpeeds["150"], - _emulationSpeeds["200"], - _emulationSpeeds["250"], - _emulationSpeeds["300"], - _emulationSpeeds["400"], - _emulationSpeeds["500"], - _emulationSpeeds["Max"], + _emulationSpeeds[25], + _emulationSpeeds[50], + _emulationSpeeds[75], + _emulationSpeeds[100], + _emulationSpeeds[125], + _emulationSpeeds[150], + _emulationSpeeds[200], + _emulationSpeeds[250], + _emulationSpeeds[300], + _emulationSpeeds[400], + _emulationSpeeds[500], + _emulationSpeeds[-1], + ] + }, + + new NativeMenuItem("Clock") + { + Menu = + [ + _clockMultipliers[1], + _clockMultipliers[2], + _clockMultipliers[4], + _clockMultipliers[8], ] }, @@ -904,18 +925,18 @@ private void CreateSpeedOptionMenu() { var speeds = new[] { - new { Value = "25", DisplayName = "25%" }, - new { Value = "50", DisplayName = "50%" }, - new { Value = "75", DisplayName = "75%" }, - new { Value = "100", DisplayName = "Normal" }, - new { Value = "125", DisplayName = "125%" }, - new { Value = "150", DisplayName = "150%" }, - new { Value = "200", DisplayName = "200%" }, - new { Value = "250", DisplayName = "250%" }, - new { Value = "300", DisplayName = "300%" }, - new { Value = "400", DisplayName = "400%" }, - new { Value = "500", DisplayName = "500%" }, - new { Value = "Max", DisplayName = "Max" }, + new { Value = 25, DisplayName = "25%" }, + new { Value = 50, DisplayName = "50%" }, + new { Value = 75, DisplayName = "75%" }, + new { Value = 100, DisplayName = "Normal" }, + new { Value = 125, DisplayName = "125%" }, + new { Value = 150, DisplayName = "150%" }, + new { Value = 200, DisplayName = "200%" }, + new { Value = 250, DisplayName = "250%" }, + new { Value = 300, DisplayName = "300%" }, + new { Value = 400, DisplayName = "400%" }, + new { Value = 500, DisplayName = "500%" }, + new { Value = -1, DisplayName = "Max" }, }; foreach (var speed in speeds) @@ -931,6 +952,29 @@ private void CreateSpeedOptionMenu() } } + private void CreateClockMultiplierOptionMenu() + { + var clocks = new[] + { + new { Value = 1, DisplayName = "3.5 MHz" }, + new { Value = 2, DisplayName = "7 MHz" }, + new { Value = 4, DisplayName = "14 MHz" }, + new { Value = 8, DisplayName = "28 MHz" }, + }; + + foreach (var clock in clocks) + { + _clockMultipliers[clock.Value] = new NativeMenuItem(clock.DisplayName) + { + ToggleType = MenuItemToggleType.Radio, + Command = _viewModel.SetClockMultiplierCommand, + CommandParameter = clock.Value, + IsChecked = _viewModel.ClockMultiplier == clock.Value, + IsEnabled = true + }; + } + } + private void CreateBorderSizeMenu() { var borders = new[] diff --git a/src/Spectron/Controls/StatusBar.axaml b/src/Spectron/Controls/StatusBar.axaml index bc8912fb..f4a256c7 100644 --- a/src/Spectron/Controls/StatusBar.axaml +++ b/src/Spectron/Controls/StatusBar.axaml @@ -137,6 +137,17 @@ + + + + + + + _debuggerViewModel?.HandlePause(value, _breakpointHitEventArgs); diff --git a/src/Spectron/ViewModels/MainViewModel.Window.cs b/src/Spectron/ViewModels/MainViewModel.Window.cs index 84354f40..2ccd37f6 100644 --- a/src/Spectron/ViewModels/MainViewModel.Window.cs +++ b/src/Spectron/ViewModels/MainViewModel.Window.cs @@ -88,6 +88,7 @@ private async Task WindowOpenedAsync() CreateEmulator( CommandLineArgs?.ComputerType ??_preferences.ComputerType, CommandLineArgs?.RomType ?? _preferences.RomType, + ClockMultiplier, customRom); Emulator.ConfigureTape(_preferences.Tape); diff --git a/src/Spectron/ViewModels/MainViewModel.cs b/src/Spectron/ViewModels/MainViewModel.cs index ca74acd6..34db0d02 100644 --- a/src/Spectron/ViewModels/MainViewModel.cs +++ b/src/Spectron/ViewModels/MainViewModel.cs @@ -139,7 +139,10 @@ public partial class MainViewModel : ObservableObject, IDisposable public partial bool IsTimeMachineCountdownVisible { get; set; } [ObservableProperty] - public partial string EmulationSpeed { get; set; } = "100"; + public partial int EmulationSpeed { get; set; } = 100; + + [ObservableProperty] + public partial int ClockMultiplier { get; set; } = 1; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsFullScreen))] @@ -244,7 +247,10 @@ public partial class MainViewModel : ObservableObject, IDisposable // Control [RelayCommand] - private void SetEmulationSpeed(string emulationSpeed) => HandleSetEmulationSpeed(emulationSpeed); + private void SetEmulationSpeed(int emulationSpeed) => HandleSetEmulationSpeed(emulationSpeed); + + [RelayCommand] + private void SetClockMultiplier(int clockMultiplier) => HandleSetClockMultiplier(clockMultiplier); [RelayCommand] private void TogglePause() => HandleTogglePause(); diff --git a/src/Spectron/ViewModels/StatusBarViewModel.cs b/src/Spectron/ViewModels/StatusBarViewModel.cs index 36b5b020..97d7428a 100644 --- a/src/Spectron/ViewModels/StatusBarViewModel.cs +++ b/src/Spectron/ViewModels/StatusBarViewModel.cs @@ -33,7 +33,10 @@ public partial class StatusBarViewModel : ObservableObject public partial JoystickType JoystickType { get; set; } [ObservableProperty] - public partial string Speed { get; set; } = "100"; + public partial string Speed { get; set; } = "100%"; + + [ObservableProperty] + public partial string Clock { get; set; } = string.Empty; [ObservableProperty] public partial bool IsDivMmcEnabled { get; set; } diff --git a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs index 360ff3c9..8530a4ad 100644 --- a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs +++ b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs @@ -1,6 +1,5 @@ using OldBit.Spectron.Emulation; using OldBit.Spectron.Emulation.Devices.Audio; -using OldBit.Z80Cpu; namespace OldBit.Spectron.Emulator.Tests.Devices.Audio; @@ -9,7 +8,7 @@ public class AudioManagerTests [Fact] public void AudioManager_ShouldCreateCorrectly() { - var clock = new Clock(); + var clock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame); var audionManager = new AudioManager(clock, null, Hardware.Spectrum128K, port => (port & 0x01) == 0); audionManager.IsAySupported.ShouldBeTrue(); diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs index 6e6ca235..8e8e9814 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs @@ -9,12 +9,12 @@ public class FloatingBusTests128 { private readonly FloatingBus _floatingBus; private readonly Memory128K _memory; - private readonly Clock _clock; + private readonly EmulatorClock _clock; public FloatingBusTests128() { _memory = new Memory128K(new byte[16384], new byte[16384]); - _clock = new Clock(); + _clock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame); _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, _clock, port => (port & 0x01) == 0); } diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs index a7735758..1e7bc225 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs @@ -9,12 +9,12 @@ public class FloatingBusTests48 { private readonly FloatingBus _floatingBus; private readonly Memory48K _memory; - private readonly Clock _clock; + private readonly EmulatorClock _clock; public FloatingBusTests48() { _memory = new Memory48K(new byte[16384]); - _clock = new Clock(); + _clock = new EmulatorClock(Hardware.Spectrum48K.TicksPerFrame); _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, _clock, port => (port & 0x01) == 0); } diff --git a/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs b/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs index 677b00b4..06a25693 100644 --- a/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs +++ b/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs @@ -19,7 +19,7 @@ public UlaTimexTests() { var keyboardState = new KeyboardState(); - var memory = new Memory128K(RomReader.ReadRom(RomType.Original128Bank0), RomReader.ReadRom(RomType.Original128Bank0)); + var memory = new Memory48K(RomReader.ReadRom(RomType.Timex2048)); var screenBuffer = new ScreenBuffer( Hardware.Timex2048, @@ -28,7 +28,9 @@ public UlaTimexTests() var z80 = new Z80(memory); - _ulaTimex = new UlaTimex(keyboardState, screenBuffer, z80, new TapeManager()); + var emulatorClock = new EmulatorClock(Hardware.Timex2048.TicksPerFrame); + + _ulaTimex = new UlaTimex(keyboardState, screenBuffer, emulatorClock, z80, new TapeManager()); } [Fact] diff --git a/tests/Spectron.Tests/ViewModels/MainViewModelTests.cs b/tests/Spectron.Tests/ViewModels/MainViewModelTests.cs index 32ef6ff8..816a8eb9 100644 --- a/tests/Spectron.Tests/ViewModels/MainViewModelTests.cs +++ b/tests/Spectron.Tests/ViewModels/MainViewModelTests.cs @@ -49,7 +49,7 @@ public void ViewModel_ShouldInitialize() _viewModel.IsAudioMuted.ShouldBeFalse(); _viewModel.IsTimeMachineEnabled.ShouldBeFalse(); _viewModel.IsTimeMachineCountdownVisible.ShouldBeFalse(); - _viewModel.EmulationSpeed.ShouldBe("100"); + _viewModel.EmulationSpeed.ShouldBe(100); _viewModel.Title.ShouldBe("Spectron - ZX Spectrum Emulator"); _viewModel.WindowState.ShouldBe(WindowState.Normal); _viewModel.IsFullScreen.ShouldBeFalse();