From 6bf7a85ef8f009bd948319c4bf12a37904d63eb2 Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 18:56:20 +0100 Subject: [PATCH 01/13] Add clock multiplier option menu --- src/Spectron/Controls/MainMenu.axaml | 6 +++ src/Spectron/Controls/NativeMainMenu.cs | 44 +++++++++++++++++++ .../ViewModels/MainViewModel.Emulator.cs | 5 +++ src/Spectron/ViewModels/MainViewModel.cs | 6 +++ 4 files changed, 61 insertions(+) diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml index a46db534..adbf9ad6 100644 --- a/src/Spectron/Controls/MainMenu.axaml +++ b/src/Spectron/Controls/MainMenu.axaml @@ -97,6 +97,12 @@ + + + + + + diff --git a/src/Spectron/Controls/NativeMainMenu.cs b/src/Spectron/Controls/NativeMainMenu.cs index 1b325fc8..fd8d1834 100644 --- a/src/Spectron/Controls/NativeMainMenu.cs +++ b/src/Spectron/Controls/NativeMainMenu.cs @@ -26,6 +26,7 @@ public sealed class NativeMainMenu private readonly Dictionary _joystickTypes = new(); private readonly Dictionary _mouseTypes = 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; @@ -373,6 +383,17 @@ private NativeMenuItem CreateControlMenu() ] }, + new NativeMenuItem("Clock") + { + Menu = + [ + _clockMultipliers["1"], + _clockMultipliers["2"], + _clockMultipliers["4"], + _clockMultipliers["8"], + ] + }, + new NativeMenuItemSeparator(), _pauseMenuItem, @@ -931,6 +952,29 @@ private void CreateSpeedOptionMenu() } } + private void CreateClockMultiplierOptionMenu() + { + var speeds = 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 speed in speeds) + { + _clockMultipliers[speed.Value] = new NativeMenuItem(speed.DisplayName) + { + ToggleType = MenuItemToggleType.Radio, + Command = _viewModel.SetClockMultiplierCommand, + CommandParameter = speed.Value, + IsChecked = _viewModel.ClockMultiplier == speed.Value, + IsEnabled = true + }; + } + } + private void CreateBorderSizeMenu() { var borders = new[] diff --git a/src/Spectron/ViewModels/MainViewModel.Emulator.cs b/src/Spectron/ViewModels/MainViewModel.Emulator.cs index f9011e6c..baf5258f 100644 --- a/src/Spectron/ViewModels/MainViewModel.Emulator.cs +++ b/src/Spectron/ViewModels/MainViewModel.Emulator.cs @@ -248,6 +248,11 @@ private void HandleSetEmulationSpeed(string emulationSpeed) Emulator?.SetEmulationSpeed(emulationSpeedValue); } + private void HandleSetClockMultiplier(string clockMultiplier) + { + // TODO: Implement clock multiplier + } + private async Task HandleChangeRomAsync(RomType romType) { var oldRomType = RomType; diff --git a/src/Spectron/ViewModels/MainViewModel.cs b/src/Spectron/ViewModels/MainViewModel.cs index ca74acd6..9521b29c 100644 --- a/src/Spectron/ViewModels/MainViewModel.cs +++ b/src/Spectron/ViewModels/MainViewModel.cs @@ -141,6 +141,9 @@ public partial class MainViewModel : ObservableObject, IDisposable [ObservableProperty] public partial string EmulationSpeed { get; set; } = "100"; + [ObservableProperty] + public partial string ClockMultiplier { get; set; } = "1"; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsFullScreen))] public partial WindowState WindowState { get; set; } = WindowState.Normal; @@ -246,6 +249,9 @@ public partial class MainViewModel : ObservableObject, IDisposable [RelayCommand] private void SetEmulationSpeed(string emulationSpeed) => HandleSetEmulationSpeed(emulationSpeed); + [RelayCommand] + private void SetClockMultiplier(string clockMultiplier) => HandleSetClockMultiplier(clockMultiplier); + [RelayCommand] private void TogglePause() => HandleTogglePause(); From d6dff672032bc9a9f612b2ec8ef41f095dd34e6c Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 20:20:23 +0100 Subject: [PATCH 02/13] Ensure audio works with overclocking by using UlaTicks --- .../Devices/Audio/AY/AyAudio.cs | 12 +++++------ .../Devices/Audio/AudioManager.cs | 5 ++--- .../Devices/Audio/Beeper/BeeperAudio.cs | 20 +++++++++---------- .../Devices/Audio/AudioManagerTests.cs | 2 +- 4 files changed, 17 insertions(+), 22 deletions(-) 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..4154fc4e 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,13 +81,13 @@ 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; var statesPerSample = (double)hardware.TicksPerFrame / SamplesPerFrame; - _beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz); + _beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz * clock.Multiplier); Beeper = new BeeperDevice(cassettePlayer, isUlaPort) { 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/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs index 360ff3c9..2745848e 100644 --- a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs +++ b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs @@ -9,7 +9,7 @@ public class AudioManagerTests [Fact] public void AudioManager_ShouldCreateCorrectly() { - var clock = new Clock(); + var clock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame, new Clock()); var audionManager = new AudioManager(clock, null, Hardware.Spectrum128K, port => (port & 0x01) == 0); audionManager.IsAySupported.ShouldBeTrue(); From e22f4ecfdd8a0fcf4ffbe2c96bb77f58a2af59d8 Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 20:40:04 +0100 Subject: [PATCH 03/13] Handle overclocking by Ula --- src/Spectron.Emulation/Devices/Ula.cs | 5 +++-- src/Spectron.Emulation/Devices/UlaTimex.cs | 3 ++- src/Spectron.Emulation/EmulatorClock.cs | 20 +++++++++++++++++++ .../Devices/UlaTimexTests.cs | 6 ++++-- 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 src/Spectron.Emulation/EmulatorClock.cs 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/EmulatorClock.cs b/src/Spectron.Emulation/EmulatorClock.cs new file mode 100644 index 00000000..e03994ba --- /dev/null +++ b/src/Spectron.Emulation/EmulatorClock.cs @@ -0,0 +1,20 @@ +using OldBit.Z80Cpu; + +namespace OldBit.Spectron.Emulation; + +public sealed class EmulatorClock(int ticksPerFrame, Clock clock, int multiplier = 1) +{ + public int Multiplier + { + get; + set + { + field = value; + TicksPerFrame = ticksPerFrame * Multiplier; + } + } = multiplier; + + internal int TicksPerFrame = ticksPerFrame * multiplier; + internal int FrameTicks => clock.FrameTicks * Multiplier; + internal int UlaTicks => clock.FrameTicks / Multiplier; +} \ No newline at end of file diff --git a/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs b/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs index 677b00b4..aab450f7 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, z80.Clock); + + _ulaTimex = new UlaTimex(keyboardState, screenBuffer, emulatorClock, z80, new TapeManager()); } [Fact] From 515a7f700955110cc060314b9feb704db255f1bd Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 20:42:48 +0100 Subject: [PATCH 04/13] Update to use new clock multiplier --- .../ViewModels/DebuggerViewModel.cs | 2 +- src/Spectron.Emulation/EmulatorClock.cs | 1 - src/Spectron.Emulation/EmulatorFactory.cs | 20 +++++++++++-------- .../Snapshot/SzxSnapshot.cs | 2 +- .../State/StateSnapshotManager.cs | 2 +- src/Spectron/Controls/MainMenu.axaml | 6 +++--- .../ViewModels/MainViewModel.Window.cs | 1 + 7 files changed, 19 insertions(+), 15 deletions(-) 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/EmulatorClock.cs b/src/Spectron.Emulation/EmulatorClock.cs index e03994ba..e5fcfcb5 100644 --- a/src/Spectron.Emulation/EmulatorClock.cs +++ b/src/Spectron.Emulation/EmulatorClock.cs @@ -15,6 +15,5 @@ public int Multiplier } = multiplier; internal int TicksPerFrame = ticksPerFrame * multiplier; - internal int FrameTicks => clock.FrameTicks * Multiplier; internal int UlaTicks => clock.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 adbf9ad6..46354d42 100644 --- a/src/Spectron/Controls/MainMenu.axaml +++ b/src/Spectron/Controls/MainMenu.axaml @@ -99,9 +99,9 @@ - - - + + + 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); From 21a86d7c9015f24aaf465d9d8a5e030945f8c39d Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 21:15:10 +0100 Subject: [PATCH 05/13] Floating bus clock multiplier --- .../Contention/ContentionProvider128K.cs | 18 ++++++++++++----- .../Contention/ContentionProvider48K.cs | 20 ++++++++++++++----- src/Spectron.Emulation/Devices/FloatingBus.cs | 8 ++++---- src/Spectron.Emulation/Emulator.cs | 19 +++++++++--------- .../ViewModels/MainViewModel.Emulator.cs | 15 ++++++++------ .../Devices/FloatingBusTests128.cs | 3 ++- .../Devices/FloatingBusTests48.cs | 3 ++- 7 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs index 26c8e9f4..8f15db05 100644 --- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs +++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs @@ -2,17 +2,23 @@ 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 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) + { + ticks = GetUlaTicks(ticks); + return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + } - public int GetPortContention(int ticks, Word port) => - ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0; + public int GetPortContention(int ticks, Word port) + { + ticks = GetUlaTicks(ticks); + return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + } public bool IsAddressContended(Word address) => address is >= 0x4000 and <= 0x7FFF || @@ -25,4 +31,6 @@ public bool IsPortContended(Word port) => public bool IsPortLatched(Word address) => (address & 0x8002) == 0; private bool IsRamBankContended => (ActiveRamBankId & 0x01) == 0x01; // Bank 1, 3, 5 or 7 + + private int GetUlaTicks(int ticks) => ticks / clockMultiplier; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs index ee982dde..03f21860 100644 --- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs +++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs @@ -2,17 +2,27 @@ 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; + public int GetMemoryContention(int ticks, Word address) + { + ticks = GetUlaTicks(ticks); - public int GetPortContention(int ticks, Word port) => - ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] : 0; + return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + } + + public int GetPortContention(int ticks, Word port) + { + ticks = GetUlaTicks(ticks); + + return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + } public bool IsAddressContended(Word address) => address is >= 0x4000 and <= 0x7FFF; public bool IsPortContended(Word port) => port is >= 0x4000 and <= 0x7FFF; + + private int GetUlaTicks(int ticks) => ticks / clockMultiplier; } \ No newline at end of file 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/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 6a4e6ddf..626967bb 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -80,8 +80,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; } @@ -120,12 +119,14 @@ internal Emulator( } }; + Clock = new EmulatorClock(hardware.TicksPerFrame, Cpu.Clock, emulatorArgs.ClockMultiplier); + 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,9 +138,9 @@ 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); @@ -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); diff --git a/src/Spectron/ViewModels/MainViewModel.Emulator.cs b/src/Spectron/ViewModels/MainViewModel.Emulator.cs index baf5258f..81a74ad6 100644 --- a/src/Spectron/ViewModels/MainViewModel.Emulator.cs +++ b/src/Spectron/ViewModels/MainViewModel.Emulator.cs @@ -18,9 +18,9 @@ namespace OldBit.Spectron.ViewModels; partial class MainViewModel { - private void CreateEmulator(ComputerType computerType, RomType romType, byte[]? customRom = null, bool hardReset = false) + private void CreateEmulator(ComputerType computerType, RomType romType, string clockMultiplier, byte[]? customRom = null, bool hardReset = false) { - var emulator = _emulatorFactory.Create(computerType, romType, customRom); + var emulator = _emulatorFactory.Create(computerType, romType, int.Parse(clockMultiplier), customRom); ApplyEmulatorDefaults(emulator, hardReset); @@ -86,11 +86,13 @@ private bool CreateEmulator(Stream stream, FileType fileType, FavoriteProgram? f if (fileType.IsSnapshot()) { emulator = _snapshotManager.Load(stream, fileType); + emulator.Clock.Multiplier = int.Parse(ClockMultiplier); emulator.ConfigureAudio(_preferences.Audio); } else if (fileType.IsTape()) { emulator = _loader.EnterLoadCommand(favorite?.ComputerType ?? ComputerType); + emulator.Clock.Multiplier = int.Parse(ClockMultiplier); emulator.TapeManager.InsertTape(stream, fileType, _preferences.Tape.IsAutoPlayEnabled && (favorite?.TapeLoadSpeed ?? TapeLoadSpeed) != TapeSpeed.Instant); @@ -213,7 +215,7 @@ private void HandleMachineReset(bool hardReset = false) if (hardReset) { - CreateEmulator(_preferences.ComputerType, _preferences.RomType, hardReset: true); + CreateEmulator(_preferences.ComputerType, _preferences.RomType, ClockMultiplier, hardReset: true); } else if (Emulator != null) { @@ -250,7 +252,8 @@ private void HandleSetEmulationSpeed(string emulationSpeed) private void HandleSetClockMultiplier(string clockMultiplier) { - // TODO: Implement clock multiplier + ClockMultiplier = clockMultiplier; + Emulator?.Clock.Multiplier = int.Parse(clockMultiplier); } private async Task HandleChangeRomAsync(RomType romType) @@ -280,7 +283,7 @@ private async Task HandleChangeRomAsync(RomType romType) await _messageDialogs.Error(ex.Message); } - CreateEmulator(ComputerType, RomType, customRom); + CreateEmulator(ComputerType, RomType, ClockMultiplier, customRom); } private void HandleChangeComputerType(ComputerType computerType) @@ -292,7 +295,7 @@ private void HandleChangeComputerType(ComputerType computerType) RomType = RomType.Original; } - CreateEmulator(ComputerType, RomType); + CreateEmulator(ComputerType, RomType, ClockMultiplier); } partial void OnIsPausedChanged(bool value) => _debuggerViewModel?.HandlePause(value, _breakpointHitEventArgs); diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs index 6e6ca235..a4a20b97 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs @@ -15,8 +15,9 @@ public FloatingBusTests128() { _memory = new Memory128K(new byte[16384], new byte[16384]); _clock = new Clock(); + var emulatorClock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame, _clock); - _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, _clock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, emulatorClock, port => (port & 0x01) == 0); } [Fact] diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs index a7735758..123438aa 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs @@ -15,8 +15,9 @@ public FloatingBusTests48() { _memory = new Memory48K(new byte[16384]); _clock = new Clock(); + var emulatorClock = new EmulatorClock(Hardware.Spectrum48K.TicksPerFrame, _clock); - _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, _clock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, emulatorClock, port => (port & 0x01) == 0); } [Fact] From 3c583214290ee5aecb9f2ff88d449b33ba4ff2da Mon Sep 17 00:00:00 2001 From: voytas Date: Mon, 11 May 2026 21:28:01 +0100 Subject: [PATCH 06/13] Update README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da4c4b65..25ffa7c0 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 @@ -97,6 +98,13 @@ 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: +- *Speed multiplier* - this option allows running the emulator at a higher or slower speed, e.g. more frames per second. +This means that **everything** will run at selected speed. +- *CPU clock multiplier* - this option allows running just CPU at a higher frequency. This can make some slower games more responsive. +However, it may also cause compatibility issues with some games and programs. + ## 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. From 79bf90544b4d4cfd4470abb3a1e74da4445448c5 Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 12 May 2026 18:08:09 +0100 Subject: [PATCH 07/13] Refactor EmulatorClock --- Directory.Packages.props | 2 +- README.md | 19 ++++-- .../Contention/ContentionProvider128K.cs | 13 ++-- .../Contention/ContentionProvider48K.cs | 12 ++-- src/Spectron.Emulation/Emulator.cs | 11 ++- src/Spectron.Emulation/EmulatorClock.cs | 24 +++++-- src/Spectron/Controls/NativeMainMenu.cs | 68 +++++++++---------- .../ViewModels/MainViewModel.Emulator.cs | 32 +++------ .../Devices/Audio/AudioManagerTests.cs | 3 +- .../Devices/FloatingBusTests128.cs | 5 +- .../Devices/FloatingBusTests48.cs | 5 +- .../Devices/UlaTimexTests.cs | 2 +- 12 files changed, 98 insertions(+), 98 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c7b3e9ec..67f763a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,7 +30,7 @@ - + diff --git a/README.md b/README.md index 25ffa7c0..025dcafe 100644 --- a/README.md +++ b/README.md @@ -69,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 @@ -100,13 +103,15 @@ The emulation is highly accurate and supports many undocumented instructions, as ## Emulation Speed There are two options for adjusting the speed: -- *Speed multiplier* - this option allows running the emulator at a higher or slower speed, e.g. more frames per second. -This means that **everything** will run at selected speed. -- *CPU clock multiplier* - this option allows running just CPU at a higher frequency. This can make some slower games more responsive. -However, it may also cause compatibility issues with some games and programs. +- **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.Emulation/Devices/Contention/ContentionProvider128K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs index 8f15db05..adede553 100644 --- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs +++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider128K.cs @@ -6,18 +6,21 @@ internal sealed class ContentionProvider128K(int firstPixelTick, int ticksPerLin { 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 = GetUlaTicks(ticks); - return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + var ulaTicks = ticks / ClockMultiplier; + + return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0; } public int GetPortContention(int ticks, Word port) { - ticks = GetUlaTicks(ticks); - return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + var ulaTicks = ticks / ClockMultiplier; + + return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0; } public bool IsAddressContended(Word address) => @@ -31,6 +34,4 @@ public bool IsPortContended(Word port) => public bool IsPortLatched(Word address) => (address & 0x8002) == 0; private bool IsRamBankContended => (ActiveRamBankId & 0x01) == 0x01; // Bank 1, 3, 5 or 7 - - private int GetUlaTicks(int ticks) => ticks / clockMultiplier; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs index 03f21860..c4d403bf 100644 --- a/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs +++ b/src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs @@ -6,23 +6,23 @@ internal sealed class ContentionProvider48K(int firstPixelTick, int ticksPerLine { private readonly int[] _contentionTable = ContentionProvider.BuildContentionTable(firstPixelTick, ticksPerLine); + internal int ClockMultiplier { get; set; } = clockMultiplier; + public int GetMemoryContention(int ticks, Word address) { - ticks = GetUlaTicks(ticks); + var ulaTicks = ticks / ClockMultiplier; - return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0; } public int GetPortContention(int ticks, Word port) { - ticks = GetUlaTicks(ticks); + var ulaTicks = ticks / ClockMultiplier; - return ticks >= 0 && ticks < _contentionTable.Length ? _contentionTable[ticks] * clockMultiplier : 0; + return ulaTicks >= 0 && ulaTicks < _contentionTable.Length ? _contentionTable[ulaTicks] * ClockMultiplier : 0; } public bool IsAddressContended(Word address) => address is >= 0x4000 and <= 0x7FFF; public bool IsPortContended(Word port) => port is >= 0x4000 and <= 0x7FFF; - - private int GetUlaTicks(int ticks) => ticks / clockMultiplier; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 626967bb..214249c9 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -110,16 +110,13 @@ 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 }; - Clock = new EmulatorClock(hardware.TicksPerFrame, Cpu.Clock, emulatorArgs.ClockMultiplier); + Cpu = new Z80(emulatorArgs.Memory, Clock); UlaPlus = new UlaPlus(); ScreenBuffer = new ScreenBuffer(hardware, emulatorArgs.Memory, UlaPlus); diff --git a/src/Spectron.Emulation/EmulatorClock.cs b/src/Spectron.Emulation/EmulatorClock.cs index e5fcfcb5..9eaf3136 100644 --- a/src/Spectron.Emulation/EmulatorClock.cs +++ b/src/Spectron.Emulation/EmulatorClock.cs @@ -1,19 +1,33 @@ +using OldBit.Spectron.Emulation.Devices.Contention; using OldBit.Z80Cpu; namespace OldBit.Spectron.Emulation; -public sealed class EmulatorClock(int ticksPerFrame, Clock clock, int multiplier = 1) +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 * Multiplier; + TicksPerFrame = _ticksPerFrame * value; + + (ContentionProvider as ContentionProvider48K)?.ClockMultiplier = value; + (ContentionProvider as ContentionProvider128K)?.ClockMultiplier = value; } - } = multiplier; + } - internal int TicksPerFrame = ticksPerFrame * multiplier; - internal int UlaTicks => clock.FrameTicks / Multiplier; + internal int TicksPerFrame { get; private set; } + internal int UlaTicks => FrameTicks / Multiplier; } \ No newline at end of file diff --git a/src/Spectron/Controls/NativeMainMenu.cs b/src/Spectron/Controls/NativeMainMenu.cs index fd8d1834..cea2e5a9 100644 --- a/src/Spectron/Controls/NativeMainMenu.cs +++ b/src/Spectron/Controls/NativeMainMenu.cs @@ -25,8 +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 _clockMultipliers = 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(); @@ -368,18 +368,18 @@ 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], ] }, @@ -387,10 +387,10 @@ private NativeMenuItem CreateControlMenu() { Menu = [ - _clockMultipliers["1"], - _clockMultipliers["2"], - _clockMultipliers["4"], - _clockMultipliers["8"], + _clockMultipliers[1], + _clockMultipliers[2], + _clockMultipliers[4], + _clockMultipliers[8], ] }, @@ -925,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) @@ -956,10 +956,10 @@ private void CreateClockMultiplierOptionMenu() { var speeds = 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" }, + 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 speed in speeds) diff --git a/src/Spectron/ViewModels/MainViewModel.Emulator.cs b/src/Spectron/ViewModels/MainViewModel.Emulator.cs index 81a74ad6..7de405f1 100644 --- a/src/Spectron/ViewModels/MainViewModel.Emulator.cs +++ b/src/Spectron/ViewModels/MainViewModel.Emulator.cs @@ -18,9 +18,9 @@ namespace OldBit.Spectron.ViewModels; partial class MainViewModel { - private void CreateEmulator(ComputerType computerType, RomType romType, string clockMultiplier, byte[]? customRom = null, bool hardReset = false) + private void CreateEmulator(ComputerType computerType, RomType romType, int clockMultiplier, byte[]? customRom = null, bool hardReset = false) { - var emulator = _emulatorFactory.Create(computerType, romType, int.Parse(clockMultiplier), customRom); + var emulator = _emulatorFactory.Create(computerType, romType, clockMultiplier, customRom); ApplyEmulatorDefaults(emulator, hardReset); @@ -86,13 +86,13 @@ private bool CreateEmulator(Stream stream, FileType fileType, FavoriteProgram? f if (fileType.IsSnapshot()) { emulator = _snapshotManager.Load(stream, fileType); - emulator.Clock.Multiplier = int.Parse(ClockMultiplier); + emulator.Clock.Multiplier = ClockMultiplier; emulator.ConfigureAudio(_preferences.Audio); } else if (fileType.IsTape()) { emulator = _loader.EnterLoadCommand(favorite?.ComputerType ?? ComputerType); - emulator.Clock.Multiplier = int.Parse(ClockMultiplier); + emulator.Clock.Multiplier = ClockMultiplier; emulator.TapeManager.InsertTape(stream, fileType, _preferences.Tape.IsAutoPlayEnabled && (favorite?.TapeLoadSpeed ?? TapeLoadSpeed) != TapeSpeed.Instant); @@ -229,31 +229,17 @@ private void HandleMachineReset(bool hardReset = false) UpdateWindowTitle(); } - private void HandleSetEmulationSpeed(string emulationSpeed) + private void HandleSetEmulationSpeed(int emulationSpeed) { - int emulationSpeedValue; - - if (emulationSpeed.Equals("max", StringComparison.OrdinalIgnoreCase)) - { - emulationSpeedValue = int.MaxValue; - } - else - { - if (!int.TryParse(emulationSpeed, out emulationSpeedValue)) - { - return; - } - } - EmulationSpeed = emulationSpeed; - StatusBarViewModel.Speed = emulationSpeed; - Emulator?.SetEmulationSpeed(emulationSpeedValue); + StatusBarViewModel.Speed = emulationSpeed == -1 ? "Max" : $"{emulationSpeed}%"; + Emulator?.SetEmulationSpeed(emulationSpeed); } - private void HandleSetClockMultiplier(string clockMultiplier) + private void HandleSetClockMultiplier(int clockMultiplier) { ClockMultiplier = clockMultiplier; - Emulator?.Clock.Multiplier = int.Parse(clockMultiplier); + Emulator?.Clock.Multiplier = clockMultiplier; } private async Task HandleChangeRomAsync(RomType romType) diff --git a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs index 2745848e..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 EmulatorClock(Hardware.Spectrum128K.TicksPerFrame, 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 a4a20b97..9fb2f2b0 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs @@ -14,10 +14,9 @@ public class FloatingBusTests128 public FloatingBusTests128() { _memory = new Memory128K(new byte[16384], new byte[16384]); - _clock = new Clock(); - var emulatorClock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame, _clock); + _clock = new EmulatorClock(Hardware.Spectrum128K.TicksPerFrame); - _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, emulatorClock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, (EmulatorClock)_clock, port => (port & 0x01) == 0); } [Fact] diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs index 123438aa..a2aeff0f 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs @@ -14,10 +14,9 @@ public class FloatingBusTests48 public FloatingBusTests48() { _memory = new Memory48K(new byte[16384]); - _clock = new Clock(); - var emulatorClock = new EmulatorClock(Hardware.Spectrum48K.TicksPerFrame, _clock); + _clock = new EmulatorClock(Hardware.Spectrum48K.TicksPerFrame); - _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, emulatorClock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, (EmulatorClock)_clock, port => (port & 0x01) == 0); } [Fact] diff --git a/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs b/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs index aab450f7..06a25693 100644 --- a/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs +++ b/tests/Spectron.Emulator.Tests/Devices/UlaTimexTests.cs @@ -28,7 +28,7 @@ public UlaTimexTests() var z80 = new Z80(memory); - var emulatorClock = new EmulatorClock(Hardware.Timex2048.TicksPerFrame, z80.Clock); + var emulatorClock = new EmulatorClock(Hardware.Timex2048.TicksPerFrame); _ulaTimex = new UlaTimex(keyboardState, screenBuffer, emulatorClock, z80, new TapeManager()); } From a2517c6ff58941c03a132c9555986e5dcaa58695 Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 12 May 2026 18:08:22 +0100 Subject: [PATCH 08/13] Convert `EmulationSpeed` and `ClockMultiplier` to integers and use static resources in XAML bindings. --- src/Spectron.Emulation/Emulator.cs | 2 +- src/Spectron/Controls/MainMenu.axaml | 53 +++++++++++++------ src/Spectron/ViewModels/MainViewModel.cs | 8 +-- .../ViewModels/MainViewModelTests.cs | 2 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 214249c9..68a65905 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -214,7 +214,7 @@ public void Break() public void RequestNmi() => _isNmiRequested = true; public void SetEmulationSpeed(int emulationSpeedPercentage) => - _emulationTimer.Interval = emulationSpeedPercentage == int.MaxValue ? + _emulationTimer.Interval = emulationSpeedPercentage == -1 ? TimeSpan.Zero : TimeSpan.FromMilliseconds(20 * (100f / emulationSpeedPercentage)); diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml index 46354d42..b1ff7cd8 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,24 +105,24 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + diff --git a/src/Spectron/ViewModels/MainViewModel.cs b/src/Spectron/ViewModels/MainViewModel.cs index 9521b29c..34db0d02 100644 --- a/src/Spectron/ViewModels/MainViewModel.cs +++ b/src/Spectron/ViewModels/MainViewModel.cs @@ -139,10 +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 string ClockMultiplier { get; set; } = "1"; + public partial int ClockMultiplier { get; set; } = 1; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsFullScreen))] @@ -247,10 +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(string clockMultiplier) => HandleSetClockMultiplier(clockMultiplier); + private void SetClockMultiplier(int clockMultiplier) => HandleSetClockMultiplier(clockMultiplier); [RelayCommand] private void TogglePause() => HandleTogglePause(); 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(); From a924f6f4447d9416bd05a973377f927f402b93da Mon Sep 17 00:00:00 2001 From: voytas Date: Tue, 12 May 2026 19:32:25 +0100 Subject: [PATCH 09/13] Set clock and speed when initializing Emulator. Show overclocking in status bar. --- src/Spectron.Emulation/Emulator.cs | 25 +++++++++++-------- src/Spectron/Controls/StatusBar.axaml | 11 ++++++++ .../ViewModels/MainViewModel.Emulator.cs | 8 +++--- src/Spectron/ViewModels/StatusBarViewModel.cs | 3 +++ 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 68a65905..cff98cfd 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; @@ -96,7 +97,6 @@ internal Emulator( CommandManager commandManager, ILogger logger) { - _hardware = hardware; KeyboardState = keyboardState; _timeMachine = timeMachine; _logger = logger; @@ -135,12 +135,12 @@ internal Emulator( KeyboardState.Reset(); TapeManager.Attach(Cpu, Memory, hardware); - _floatingBus = new FloatingBus(_hardware, Memory, Clock, Ula.IsUlaPort); + _floatingBus = new FloatingBus(hardware, Memory, Clock, 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(); @@ -213,10 +213,13 @@ public void Break() public void RequestNmi() => _isNmiRequested = true; - public void SetEmulationSpeed(int emulationSpeedPercentage) => - _emulationTimer.Interval = emulationSpeedPercentage == -1 ? - 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) { @@ -357,7 +360,7 @@ private void BeforeInstruction(Word pc) break; case TapeSpeed.Accelerated: - SetEmulationSpeed(int.MaxValue); + EmulationSpeed = MaxEmulationSpeed; _isAcceleratedTapeSpeed = true; break; } @@ -371,7 +374,7 @@ private void BeforeInstruction(Word pc) break; case TapeSpeed.Accelerated: - SetEmulationSpeed(int.MaxValue); + EmulationSpeed = MaxEmulationSpeed; _isAcceleratedTapeSpeed = true; break; } @@ -383,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/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 @@ + + + + + + + Date: Tue, 12 May 2026 20:08:24 +0100 Subject: [PATCH 10/13] Reset CPU clock during emulator reset --- Directory.Packages.props | 2 +- src/Spectron.Emulation/Emulator.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 67f763a7..0e819559 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,7 +30,7 @@ - + diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index cff98cfd..6eb1934a 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -195,6 +195,7 @@ public void Reset() AudioManager.ResetAudio(); Memory.Reset(); Cpu.Reset(); + Cpu.Clock.Reset(); ScreenBuffer.Reset(); UlaPlus.Reset(); KeyboardState.Reset(); From 780d1cfc9a183ba54a0f3b0e20e3598718f4b4d5 Mon Sep 17 00:00:00 2001 From: voytas Date: Wed, 13 May 2026 22:35:26 +0100 Subject: [PATCH 11/13] Fix reset blocking test issue --- src/Spectron.Emulation/Emulator.cs | 1 - src/Spectron/ViewModels/MainViewModel.Emulator.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 6eb1934a..cff98cfd 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -195,7 +195,6 @@ public void Reset() AudioManager.ResetAudio(); Memory.Reset(); Cpu.Reset(); - Cpu.Clock.Reset(); ScreenBuffer.Reset(); UlaPlus.Reset(); KeyboardState.Reset(); diff --git a/src/Spectron/ViewModels/MainViewModel.Emulator.cs b/src/Spectron/ViewModels/MainViewModel.Emulator.cs index 4f1f2f0c..c61f6e13 100644 --- a/src/Spectron/ViewModels/MainViewModel.Emulator.cs +++ b/src/Spectron/ViewModels/MainViewModel.Emulator.cs @@ -214,7 +214,7 @@ private void HandleMachineReset(bool hardReset = false) { _pokeFile = null; - if (hardReset) + if (hardReset || _rzxController != null) { CreateEmulator(_preferences.ComputerType, _preferences.RomType, ClockMultiplier, hardReset: true); } @@ -226,6 +226,7 @@ private void HandleMachineReset(bool hardReset = false) ConfigureDebugging(Emulator); } + _rzxController = null; RecentFilesViewModel.CurrentFileName = string.Empty; UpdateWindowTitle(); } From b853f27936084b897113cca0941ac18cd08ec9b3 Mon Sep 17 00:00:00 2001 From: voytas Date: Wed, 13 May 2026 23:30:01 +0100 Subject: [PATCH 12/13] Cleanup --- Directory.Packages.props | 2 +- src/Spectron/Controls/MainMenu.axaml | 2 +- src/Spectron/Controls/NativeMainMenu.cs | 10 +++++----- src/Spectron/ViewModels/StatusBarViewModel.cs | 2 +- .../Devices/FloatingBusTests128.cs | 4 ++-- .../Devices/FloatingBusTests48.cs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0e819559..3c469247 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,7 +30,7 @@ - + diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml index b1ff7cd8..8da36ba6 100644 --- a/src/Spectron/Controls/MainMenu.axaml +++ b/src/Spectron/Controls/MainMenu.axaml @@ -107,7 +107,7 @@ - + diff --git a/src/Spectron/Controls/NativeMainMenu.cs b/src/Spectron/Controls/NativeMainMenu.cs index cea2e5a9..93adf105 100644 --- a/src/Spectron/Controls/NativeMainMenu.cs +++ b/src/Spectron/Controls/NativeMainMenu.cs @@ -954,7 +954,7 @@ private void CreateSpeedOptionMenu() private void CreateClockMultiplierOptionMenu() { - var speeds = new[] + var clocks = new[] { new { Value = 1, DisplayName = "3.5 MHz" }, new { Value = 2, DisplayName = "7 MHz" }, @@ -962,14 +962,14 @@ private void CreateClockMultiplierOptionMenu() new { Value = 8, DisplayName = "28 MHz" }, }; - foreach (var speed in speeds) + foreach (var clock in clocks) { - _clockMultipliers[speed.Value] = new NativeMenuItem(speed.DisplayName) + _clockMultipliers[clock.Value] = new NativeMenuItem(clock.DisplayName) { ToggleType = MenuItemToggleType.Radio, Command = _viewModel.SetClockMultiplierCommand, - CommandParameter = speed.Value, - IsChecked = _viewModel.ClockMultiplier == speed.Value, + CommandParameter = clock.Value, + IsChecked = _viewModel.ClockMultiplier == clock.Value, IsEnabled = true }; } diff --git a/src/Spectron/ViewModels/StatusBarViewModel.cs b/src/Spectron/ViewModels/StatusBarViewModel.cs index 445f5cc4..97d7428a 100644 --- a/src/Spectron/ViewModels/StatusBarViewModel.cs +++ b/src/Spectron/ViewModels/StatusBarViewModel.cs @@ -33,7 +33,7 @@ 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; diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs index 9fb2f2b0..8e8e9814 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs @@ -9,14 +9,14 @@ 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 EmulatorClock(Hardware.Spectrum128K.TicksPerFrame); - _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, (EmulatorClock)_clock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, _clock, port => (port & 0x01) == 0); } [Fact] diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs index a2aeff0f..1e7bc225 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs @@ -9,14 +9,14 @@ 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 EmulatorClock(Hardware.Spectrum48K.TicksPerFrame); - _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, (EmulatorClock)_clock, port => (port & 0x01) == 0); + _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, _clock, port => (port & 0x01) == 0); } [Fact] From b71e9c2a02b2c152e09cc6866ec0e6f97c9f5012 Mon Sep 17 00:00:00 2001 From: voytas Date: Fri, 15 May 2026 20:09:00 +0100 Subject: [PATCH 13/13] Small tweaks --- src/Spectron.Emulation/Devices/Audio/AudioManager.cs | 2 +- src/Spectron.Emulation/Emulator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs index 4154fc4e..c20cc82e 100644 --- a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs +++ b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs @@ -87,7 +87,7 @@ internal AudioManager(EmulatorClock clock, CassettePlayer? cassettePlayer, Hardw var statesPerSample = (double)hardware.TicksPerFrame / SamplesPerFrame; - _beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz * clock.Multiplier); + _beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz); Beeper = new BeeperDevice(cassettePlayer, isUlaPort) { diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index cff98cfd..cdf8eda3 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -240,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(); @@ -248,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); } }