Skip to content
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<PackageVersion Include="OldBit.Beep" Version="1.0.3" />
<PackageVersion Include="OldBit.Joypad" Version="0.0.2-rc.3" />
<PackageVersion Include="OldBit.Spectron.Files" Version="1.0.7" />
<PackageVersion Include="OldBit.Z80Cpu.Spectron" Version="1.0.7" />
<PackageVersion Include="OldBit.Z80Cpu.Spectron" Version="1.0.10" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.4-preview.1.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 5 additions & 7 deletions src/Spectron.Emulation/Devices/Audio/AY/AyAudio.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using OldBit.Z80Cpu;

namespace OldBit.Spectron.Emulation.Devices.Audio.AY;

/// <summary>
Expand All @@ -17,18 +15,18 @@ 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;

_statesPerSample = (long)(Multiplier * statesPerSample);
_sampleRate = statesPerSample / AyCycles;

_ay.OnUpdateAudio = () => Update(clock.FrameTicks);
_ay.OnUpdateAudio = () => Update(clock.UlaTicks);
}

internal void NewFrame()
Expand All @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions src/Spectron.Emulation/Devices/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -82,7 +81,7 @@ public bool IsAyEnabled

public bool IsAySupportedStandardSpectrum { get; set; } = true;

internal AudioManager(Clock clock, CassettePlayer? cassettePlayer, HardwareSettings hardware, Func<Word, bool> isUlaPort)
internal AudioManager(EmulatorClock clock, CassettePlayer? cassettePlayer, HardwareSettings hardware, Func<Word, bool> isUlaPort)
{
IsAySupported = hardware.HasAyChip;

Expand Down
20 changes: 9 additions & 11 deletions src/Spectron.Emulation/Devices/Audio/Beeper/BeeperAudio.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
Expand All @@ -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++)
{
Expand All @@ -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();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
20 changes: 15 additions & 5 deletions src/Spectron.Emulation/Devices/Contention/ContentionProvider48K.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
8 changes: 4 additions & 4 deletions src/Spectron.Emulation/Devices/FloatingBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Word, bool> _isUlaPort;
private readonly Dictionary<int, Word> _floatingBusAddressIndex = new();

public bool IsEnabled { get; set; } = true;

internal FloatingBus(HardwareSettings hardware, IMemory memory, Clock clock, Func<Word, bool> isUlaPort)
internal FloatingBus(HardwareSettings hardware, IMemory memory, EmulatorClock clock, Func<Word, bool> isUlaPort)
{
_hardware = hardware;
_memory = memory;
Expand All @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions src/Spectron.Emulation/Devices/Ula.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace OldBit.Spectron.Emulation.Devices;
internal class Ula(
KeyboardState keyboardState,
ScreenBuffer screenBuffer,
EmulatorClock clock,
Z80 cpu,
TapeManager tapeManager) : IDevice
{
Expand All @@ -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;
Expand All @@ -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)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Spectron.Emulation/Devices/UlaTimex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading
Loading