Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
110982b
Initial Timex 2048 support
voytas Apr 4, 2026
1fa67ba
Extend Timex 2048 support: add state handling, snapshot support, and …
voytas Apr 4, 2026
6b3c9f9
Merge remote-tracking branch 'origin/main' into timex2048
voytas Apr 4, 2026
3131e52
Extend ULA port handling: refactor ULA detection, add Timex 2048-spec…
voytas Apr 4, 2026
7d383e5
Merge remote-tracking branch 'origin/main' into timex2048
voytas Apr 4, 2026
64dc7b5
Timex emulation
voytas Apr 5, 2026
3ccc1f7
Timex screen mode 1 handling
voytas Apr 6, 2026
d1ff034
Add Timex HiColor screen mode memory handling logic
voytas Apr 6, 2026
4f77d91
Timex hi-color screen mode
voytas Apr 7, 2026
868ee06
Merge remote-tracking branch 'origin/main' into timex2048
voytas Apr 7, 2026
0fa2eb0
Timex hi-color screen mode fixes
voytas Apr 7, 2026
1a1c03a
Refactor memory addresses in `TimexHiColorScreenUpdater` to use named…
voytas Apr 7, 2026
ac13f90
Add support for TimexHiColorAlt screen mode and improve screen mode h…
voytas Apr 7, 2026
44318dc
Add bitmap interpolation mode for ScreenImage and refactor framebuffe…
voytas Apr 7, 2026
7337802
Remove unused variable `rowBytes` from FrameBufferConverter
voytas Apr 7, 2026
2f358c6
Rename `TimexScreen1Address` to `AttributesScreenAddress` for clarity…
voytas Apr 7, 2026
e0fd598
Refactor Timex ULA. Implement Timex snapshot save/load.
voytas Apr 8, 2026
6b3b075
Refactor UlaTimex to improve screen mode handling and color managemen…
voytas Apr 8, 2026
87b474f
Add summary comment for `TimexHiResScreenUpdater` detailing memory usage
voytas Apr 8, 2026
06693e5
Timex hi-res rendering
voytas Apr 10, 2026
f45bfba
Remove unused `Type` field from `BorderTick` and cleanup related code
voytas Apr 10, 2026
9994a97
Undo memory read method that broke 128k test
voytas Apr 10, 2026
767867b
Do not re-crate FrameBuffer, always use larger buffer
voytas Apr 10, 2026
4bd9234
Refactor BorderTick handling for flexible pixel shifts and improve Ti…
voytas Apr 10, 2026
350d44f
Add frameTicks support to screen mode updates for synchronized render…
voytas Apr 10, 2026
1d03ae8
Reduce screen flicketing
voytas Apr 10, 2026
cae9767
Update README.md to include Timex TC2048 support and features
voytas Apr 10, 2026
2c16010
Add support for TimexHiResAttr screen mode and refactor related rende…
voytas Apr 11, 2026
c8e60d2
Merge remote-tracking branch 'origin/main' into timex2048
voytas Apr 11, 2026
c3f0528
Rename `TimexScreen1` to `TimexSecondScreen` for consistency across s…
voytas Apr 11, 2026
8d95aec
Add support for TimexHiResAttrAlt screen mode and enhance related ren…
voytas Apr 11, 2026
12f8679
Improve handling of Timex hi-res screen mode by adjusting frame buffe…
voytas Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![Build and Test](https://github.com/oldbit-com/Spectron/actions/workflows/build.yml/badge.svg)

# Spectron ― ZX Spectrum 16/48/128 Emulator ☺
Here is Spectron, my personal ZX Spectrum emulator written in C# and Avalonia UI. It emulates the classic ZX Spectrum 16K, 48K, and 128K computers.
# Spectron ― ZX Spectrum 16/48/128/TC2048 Emulator ☺
Here is Spectron, my personal ZX Spectrum emulator written in C# and Avalonia UI. It emulates the classic ZX Spectrum 16K, 48K, 128K and Timex Computer 2048.

It is accurate and stable, capable of running most games and demos without issues, including support for loading protected tapes.

Expand All @@ -14,7 +14,7 @@ The ZX Spectrum was my first computer, and I still have a deep affection for it.
![Main Window](docs/default.png?raw=true "Main Window")

## Features
- [x] Emulation of classic machines: ZX Spectrum 16K, 48K, and 128K
- [x] Emulation of classic machines: ZX Spectrum 16K, 48K, 128K and Timex Computer 2048
- [x] Time Machine: Rewind and continue from any point in the past
- [x] State persistence: Emulator state is automatically saved and restored on restart
- [x] Wide format support: SNA, SZX, Z80, TAP, TZX, MDR, TRD, and SCL (including ZIP archives)
Expand All @@ -37,6 +37,7 @@ The ZX Spectrum was my first computer, and I still have a deep affection for it.
- [x] Built-in debugger
- [x] Favorites manager
- [x] Screen effects: Blur and CRT/Scanlines
- [x] TC2048 extended graphics support (hi-res and hi-color)
- [x] And more to come...

Spectron relies on several custom libraries developed specifically for this project:
Expand Down
4 changes: 2 additions & 2 deletions src/Spectron.Emulation/Devices/Audio/AY/AyDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ private void SetRegisterValue(int register, byte value)
}
}

// Register port 0xFFFD is decoded as: A15=1,A14=1 & A1=0
// Register port 0xFFFD is decoded as: A15=1,A14=1 & A1=0 for ZX Spectrum
private static bool IsRegisterPort(Word address) => (address & 0xC002) == 0xC000;

// Data port 0xBFFD is decoded as: A15=1 & A1=0
// Data port 0xBFFD is decoded as: A15=1 & A1=0 for ZX Spectrum
private static bool IsDataPort(Word address) => (address & 0x8002) == 0x8000;
}
4 changes: 2 additions & 2 deletions src/Spectron.Emulation/Devices/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ public bool IsAyEnabled

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

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

var statesPerSample = (double)hardware.TicksPerFrame / SamplesPerFrame;

_beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz);

Beeper = new BeeperDevice(cassettePlayer)
Beeper = new BeeperDevice(cassettePlayer, isUlaPort)
{
OnUpdateBeeper = _beeperAudio.Update
};
Expand Down
9 changes: 7 additions & 2 deletions src/Spectron.Emulation/Devices/Audio/Beeper/BeeperDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ namespace OldBit.Spectron.Emulation.Devices.Audio.Beeper;
internal class BeeperDevice : IDevice
{
private readonly CassettePlayer? _cassettePlayer;
private readonly Func<Word, bool> _isUlaPort;

internal bool IsEnabled { get; set; }

internal Action<byte> OnUpdateBeeper { get; init; } = _ => { };

internal BeeperDevice(CassettePlayer? cassettePlayer) => _cassettePlayer = cassettePlayer;
internal BeeperDevice(CassettePlayer? cassettePlayer, Func<Word, bool> isUlaPort)
{
_cassettePlayer = cassettePlayer;
_isUlaPort = isUlaPort;
}

public void WritePort(Word address, byte value)
{
if (!IsEnabled || !Ula.IsUlaPort(address))
if (!IsEnabled || !_isUlaPort(address))
{
return;
}
Expand Down
6 changes: 4 additions & 2 deletions src/Spectron.Emulation/Devices/FloatingBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ internal sealed class FloatingBus : IDevice
private readonly HardwareSettings _hardware;
private readonly IMemory _memory;
private readonly Clock _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)
internal FloatingBus(HardwareSettings hardware, IMemory memory, Clock clock, Func<Word, bool> isUlaPort)
{
_hardware = hardware;
_memory = memory;
_clock = clock;
_isUlaPort = isUlaPort;

BuildFloatingBusTable();
}

public byte? ReadPort(Word address)
{
if (!IsEnabled || Ula.IsUlaPort(address))
if (!IsEnabled || _isUlaPort(address))
{
return null;
}
Expand Down
4 changes: 3 additions & 1 deletion src/Spectron.Emulation/Devices/Ula.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace OldBit.Spectron.Emulation.Devices;

internal sealed class Ula(
ComputerType computerType,
KeyboardState keyboardState,
ScreenBuffer screenBuffer,
Z80 cpu,
Expand Down Expand Up @@ -35,7 +36,8 @@ public void WritePort(Word address, byte value)
screenBuffer.UpdateBorder(color, cpu.Clock.FrameTicks);
}

internal static bool IsUlaPort(Word address) => (address & 0x01) == 0x00;
internal bool IsUlaPort(Word address) =>
computerType == ComputerType.Timex2048 ? (address & 0xFF) == 0xFE : (address & 0x01) == 0x00;

private void UpdateEarBit(ref byte value)
{
Expand Down
81 changes: 81 additions & 0 deletions src/Spectron.Emulation/Devices/UlaTimex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using OldBit.Spectron.Emulation.Screen;

namespace OldBit.Spectron.Emulation.Devices;

public sealed class UlaTimex : IDevice
{
private byte _lastControlValue;

internal const int ControlPort = 0xFF;
internal ScreenMode ScreenMode { get; private set; }
internal Color Paper { get; private set; }
internal Color Ink { get; private set; }

internal event EventHandler<EventArgs>? ScreenModeChanged;

public byte? ReadPort(Word address)
{
if ((address & 0xFF) != ControlPort)
{
return null;
}

return _lastControlValue;
}

public void WritePort(Word address, byte value)
{
if ((address & 0xFF) != ControlPort)
{
return;
}

ScreenMode = (value & 0b111) switch
{
0b000 => ScreenMode.Spectrum,
0b001 => ScreenMode.TimexSecondScreen,
0b010 => ScreenMode.TimexHiColor,
0b011 => ScreenMode.TimexHiColorAlt,
0b100 => ScreenMode.TimexHiResAttr,
0b101 => ScreenMode.TimexHiResAttrAlt,
0b110 => ScreenMode.TimexHiRes,
0b111 => ScreenMode.TimexHiResDouble,
_ => ScreenMode
};

Ink = (value & 0b111_000) switch
{
0b000_000 => SpectrumPalette.Black,
0b001_000 => SpectrumPalette.BrightBlue,
0b010_000 => SpectrumPalette.BrightRed,
0b011_000 => SpectrumPalette.BrightMagenta,
0b100_000 => SpectrumPalette.BrightGreen,
0b101_000 => SpectrumPalette.BrightCyan,
0b110_000 => SpectrumPalette.BrightYellow,
0b111_000 => SpectrumPalette.BrightWhite,
_ => throw new ArgumentOutOfRangeException(nameof(value))
};

Paper = (value & 0b111_000) switch
{
0b000_000 => SpectrumPalette.BrightWhite,
0b001_000 => SpectrumPalette.BrightYellow,
0b010_000 => SpectrumPalette.BrightCyan,
0b011_000 => SpectrumPalette.BrightGreen,
0b100_000 => SpectrumPalette.BrightMagenta,
0b101_000 => SpectrumPalette.BrightRed,
0b110_000 => SpectrumPalette.BrightBlue,
0b111_000 => SpectrumPalette.Black,
_ => throw new ArgumentOutOfRangeException(nameof(value))
};

if (_lastControlValue != value)
{
ScreenModeChanged?.Invoke(this, EventArgs.Empty);
}

_lastControlValue = value;
}

internal void Reset() => WritePort(ControlPort, 0);
}
73 changes: 46 additions & 27 deletions src/Spectron.Emulation/Emulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ public sealed class Emulator
private readonly ILogger _logger;
private readonly SpectrumBus _spectrumBus;
private readonly EmulatorTimer _emulationTimer;
private readonly IEmulatorMemory _memory;
private readonly FloatingBus _floatingBus;
private readonly Ula _ula;
private readonly ScreenMemoryHandler _screenMemoryHandler;

private bool _isDebuggerResume;
private bool _invalidateScreen;
private bool _isAcceleratedTapeSpeed;
private bool _isNmiRequested;
private bool _isDebuggerBreak;
private long _ticksSinceReset;
private FloatingBus _floatingBus = null!;

public delegate void FrameEvent(FrameBuffer frameBuffer, AudioBuffer audioBuffer);
public event FrameEvent? FrameCompleted;
Expand Down Expand Up @@ -71,16 +72,17 @@ public bool IsFloatingBusEnabled
public RomType RomType { get; }

public Z80 Cpu { get; }
public IEmulatorMemory Memory => _memory;
public IEmulatorMemory Memory { get; }
public IBus Bus => _spectrumBus;
public DivMmcDevice DivMmc { get; }
public Beta128Device Beta128 { get; }
public Interface1Device Interface1 { get; }
public ZxPrinter Printer { get; }
public UlaTimex? UlaTimex { get; }
public ScreenBuffer ScreenBuffer { get; }

public int TicksPerFrame => _hardware.TicksPerFrame;

internal ScreenBuffer ScreenBuffer { get; }
internal UlaPlus UlaPlus { get; }

internal Emulator(
Expand All @@ -107,11 +109,21 @@ internal Emulator(
GamepadManager = gamepadManager;
ComputerType = emulatorArgs.ComputerType;
RomType = emulatorArgs.RomType;
_memory = emulatorArgs.Memory;
Memory = emulatorArgs.Memory;

UlaPlus = new UlaPlus();
_spectrumBus = new SpectrumBus();

ScreenBuffer = new ScreenBuffer(hardware, emulatorArgs.Memory, UlaPlus);

if (ComputerType == ComputerType.Timex2048)
{
UlaTimex = new UlaTimex();
}

_screenMemoryHandler = new ScreenMemoryHandler(Memory, ScreenBuffer);
_screenMemoryHandler.SetScreenMode(UlaTimex);

Cpu = new Z80(emulatorArgs.Memory)
{
Clock =
Expand All @@ -126,15 +138,19 @@ internal Emulator(
KeyboardState.Reset();
TapeManager.Attach(Cpu, Memory, hardware);

AudioManager = new AudioManager(Cpu.Clock, tapeManager.CassettePlayer, hardware);
_ula = new Ula(hardware.ComputerType, KeyboardState, ScreenBuffer, Cpu, TapeManager);

_floatingBus = new FloatingBus(_hardware, Memory, Cpu.Clock, _ula.IsUlaPort);

AudioManager = new AudioManager(Cpu.Clock, tapeManager.CassettePlayer, hardware, _ula.IsUlaPort);

DivMmc = new DivMmcDevice(Cpu, _memory, logger);
Beta128 = new Beta128Device(Cpu, _hardware.ClockMhz, _memory, ComputerType, diskDriveManager);
Interface1 = microdriveManager.CreateDevice(Cpu, _memory);
DivMmc = new DivMmcDevice(Cpu, Memory, logger);
Beta128 = new Beta128Device(Cpu, _hardware.ClockMhz, Memory, ComputerType, diskDriveManager);
Interface1 = microdriveManager.CreateDevice(Cpu, Memory);
Printer = new ZxPrinter();

SetupUlaAndDevices();
SetupEventHandlers();
AddDevices();
AddEventHandlers();

_emulationTimer = new EmulatorTimer();
_emulationTimer.Elapsed += OnTimerElapsed;
Expand Down Expand Up @@ -190,14 +206,15 @@ public void Reset()
_ticksSinceReset = 0;

AudioManager.ResetAudio();
_memory.Reset();
Memory.Reset();
Cpu.Reset();
ScreenBuffer.Reset();
UlaPlus.Reset();
KeyboardState.Reset();
Interface1.Reset();
DivMmc.Reset();
Beta128.Reset();
UlaTimex?.Reset();
}

public void Break()
Expand Down Expand Up @@ -231,28 +248,25 @@ private void OnTimerElapsed(object? sender, EventArgs e)
}
}

private void SetupEventHandlers()
private void AddEventHandlers()
{
_memory.MemoryUpdated += (address, _) =>
{
if (address < 0x5B00)
{
ScreenBuffer.UpdateScreen(address);
}
};
Cpu.Clock.TicksAdded += (_, previousFrameTicks, _) => ScreenBuffer.UpdateScreen(previousFrameTicks);
Cpu.BeforeInstruction += BeforeInstruction;
UlaPlus.ActiveChanged += _ => _invalidateScreen = true;
Beta128.DiskActivity += _ => DiskDriveManager.OnDiskActivity();

if (UlaTimex != null)
{
UlaTimex.ScreenModeChanged += (sender, _) =>
_screenMemoryHandler.SetScreenMode(sender as UlaTimex, Cpu.Clock.FrameTicks);
}
}

private void SetupUlaAndDevices()
private void AddDevices()
{
var ula = new Ula(KeyboardState, ScreenBuffer, Cpu, TapeManager);

_spectrumBus.AddDevice(ula);
_spectrumBus.AddDevice(_ula);
_spectrumBus.AddDevice(UlaPlus);
_spectrumBus.AddDevice(_memory);
_spectrumBus.AddDevice(Memory);
_spectrumBus.AddDevice(AudioManager.Beeper);
_spectrumBus.AddDevice(AudioManager.Ay);
_spectrumBus.AddDevice(Printer);
Expand All @@ -261,7 +275,11 @@ private void SetupUlaAndDevices()
_spectrumBus.AddDevice(Beta128);
_spectrumBus.AddDevice(new RtcDevice(DivMmc));

_floatingBus = new FloatingBus(_hardware, Memory, Cpu.Clock);
if (UlaTimex != null)
{
_spectrumBus.AddDevice(UlaTimex);
}

_spectrumBus.AddDevice(_floatingBus);

Cpu.AddBus(_spectrumBus);
Expand Down Expand Up @@ -371,7 +389,8 @@ private void BeforeInstruction(Word pc)

// case 0x1AF1:
// case RomRoutines.SAVE_ETC:
// break;
// SnapshotManager.Save("2048.szx", this);
// break;
}
}
}
Loading
Loading