diff --git a/README.md b/README.md index 76f19a6c..9c63b8a2 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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) @@ -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: diff --git a/src/Spectron.Emulation/Devices/Audio/AY/AyDevice.cs b/src/Spectron.Emulation/Devices/Audio/AY/AyDevice.cs index 2fa50481..a61a0895 100644 --- a/src/Spectron.Emulation/Devices/Audio/AY/AyDevice.cs +++ b/src/Spectron.Emulation/Devices/Audio/AY/AyDevice.cs @@ -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; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs index c9fb702a..24ea01d9 100644 --- a/src/Spectron.Emulation/Devices/Audio/AudioManager.cs +++ b/src/Spectron.Emulation/Devices/Audio/AudioManager.cs @@ -80,7 +80,7 @@ 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 isUlaPort) { IsAySupported = hardware.HasAyChip; @@ -88,7 +88,7 @@ internal AudioManager(Clock clock, CassettePlayer? cassettePlayer, HardwareSetti _beeperAudio = new BeeperAudio(clock, statesPerSample, hardware.ClockMhz); - Beeper = new BeeperDevice(cassettePlayer) + Beeper = new BeeperDevice(cassettePlayer, isUlaPort) { OnUpdateBeeper = _beeperAudio.Update }; diff --git a/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperDevice.cs b/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperDevice.cs index 70137282..022515ad 100644 --- a/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperDevice.cs +++ b/src/Spectron.Emulation/Devices/Audio/Beeper/BeeperDevice.cs @@ -5,16 +5,21 @@ namespace OldBit.Spectron.Emulation.Devices.Audio.Beeper; internal class BeeperDevice : IDevice { private readonly CassettePlayer? _cassettePlayer; + private readonly Func _isUlaPort; internal bool IsEnabled { get; set; } internal Action OnUpdateBeeper { get; init; } = _ => { }; - internal BeeperDevice(CassettePlayer? cassettePlayer) => _cassettePlayer = cassettePlayer; + internal BeeperDevice(CassettePlayer? cassettePlayer, Func isUlaPort) + { + _cassettePlayer = cassettePlayer; + _isUlaPort = isUlaPort; + } public void WritePort(Word address, byte value) { - if (!IsEnabled || !Ula.IsUlaPort(address)) + if (!IsEnabled || !_isUlaPort(address)) { return; } diff --git a/src/Spectron.Emulation/Devices/FloatingBus.cs b/src/Spectron.Emulation/Devices/FloatingBus.cs index 4c51ddce..38dce28d 100644 --- a/src/Spectron.Emulation/Devices/FloatingBus.cs +++ b/src/Spectron.Emulation/Devices/FloatingBus.cs @@ -14,22 +14,24 @@ internal sealed class FloatingBus : IDevice private readonly HardwareSettings _hardware; private readonly IMemory _memory; private readonly Clock _clock; + private readonly Func _isUlaPort; private readonly Dictionary _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 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; } diff --git a/src/Spectron.Emulation/Devices/Ula.cs b/src/Spectron.Emulation/Devices/Ula.cs index 5a433f75..a09e5130 100644 --- a/src/Spectron.Emulation/Devices/Ula.cs +++ b/src/Spectron.Emulation/Devices/Ula.cs @@ -6,6 +6,7 @@ namespace OldBit.Spectron.Emulation.Devices; internal sealed class Ula( + ComputerType computerType, KeyboardState keyboardState, ScreenBuffer screenBuffer, Z80 cpu, @@ -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) { diff --git a/src/Spectron.Emulation/Devices/UlaTimex.cs b/src/Spectron.Emulation/Devices/UlaTimex.cs new file mode 100644 index 00000000..db5d1389 --- /dev/null +++ b/src/Spectron.Emulation/Devices/UlaTimex.cs @@ -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? 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); +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 283fc0e3..f81a795d 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -31,7 +31,9 @@ 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; @@ -39,7 +41,6 @@ public sealed class Emulator 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; @@ -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( @@ -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 = @@ -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; @@ -190,7 +206,7 @@ public void Reset() _ticksSinceReset = 0; AudioManager.ResetAudio(); - _memory.Reset(); + Memory.Reset(); Cpu.Reset(); ScreenBuffer.Reset(); UlaPlus.Reset(); @@ -198,6 +214,7 @@ public void Reset() Interface1.Reset(); DivMmc.Reset(); Beta128.Reset(); + UlaTimex?.Reset(); } public void Break() @@ -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); @@ -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); @@ -371,7 +389,8 @@ private void BeforeInstruction(Word pc) // case 0x1AF1: // case RomRoutines.SAVE_ETC: - // break; + // SnapshotManager.Save("2048.szx", this); + // break; } } } \ No newline at end of file diff --git a/src/Spectron.Emulation/EmulatorFactory.cs b/src/Spectron.Emulation/EmulatorFactory.cs index 39629247..04833665 100644 --- a/src/Spectron.Emulation/EmulatorFactory.cs +++ b/src/Spectron.Emulation/EmulatorFactory.cs @@ -48,7 +48,8 @@ public Emulator Create(ComputerType computerType, RomType romType, byte[]? custo return CreateSpectrum128K(romType, new Memory128K(rom, bank1Rom)); case ComputerType.Timex2048: - throw new NotImplementedException(); + rom = customRom ?? GetTimex2048Rom(romType); + return CreateTimex(romType, new Memory48K(rom)); default: throw new ArgumentOutOfRangeException(nameof(computerType)); @@ -107,6 +108,31 @@ private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmu logger); } + private Emulator CreateTimex(RomType romType, IEmulatorMemory memory) + { + var contentionProvider = new ContentionProvider( + Hardware.Timex2048.ContentionStartTicks, + Hardware.Timex2048.TicksPerLine); + + var emulatorSettings = new EmulatorArgs( + ComputerType.Timex2048, + romType, + memory, + contentionProvider); + + return new Emulator( + emulatorSettings, + Hardware.Timex2048, + tapeManager, + microdriveManager, + diskDriveManager, + gamepadManager, + keyboardState, + timeMachine, + commandManager, + logger); + } + private static byte[] GetSpectrum48KRom(RomType romType) => romType switch { RomType.Original or RomType.Custom => RomReader.ReadRom(RomType.Original48), @@ -119,4 +145,10 @@ private Emulator CreateSpectrum(ComputerType computerType, RomType romType, IEmu RomType.Original or RomType.Custom => RomReader.ReadRom(RomType.Original128Bank0), _ => RomReader.ReadRom(romType) }; + + private static byte[] GetTimex2048Rom(RomType romType) => romType switch + { + RomType.Original or RomType.Custom => RomReader.ReadRom(RomType.Timex2048), + _ => RomReader.ReadRom(romType) + }; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Extensions/ScreenModeExtension.cs b/src/Spectron.Emulation/Extensions/ScreenModeExtension.cs new file mode 100644 index 00000000..6ab01c7a --- /dev/null +++ b/src/Spectron.Emulation/Extensions/ScreenModeExtension.cs @@ -0,0 +1,8 @@ +using OldBit.Spectron.Emulation.Screen; + +namespace OldBit.Spectron.Emulation.Extensions; + +internal static class ScreenModeExtension +{ + internal static bool IsTimexHiRes(this ScreenMode screenMode) => ((int)screenMode & 0x04) == 0x04; +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Hardware.cs b/src/Spectron.Emulation/Hardware.cs index 397feb4a..edf4c421 100644 --- a/src/Spectron.Emulation/Hardware.cs +++ b/src/Spectron.Emulation/Hardware.cs @@ -49,4 +49,19 @@ internal static class Hardware LastPixelTicks: 14362 + (ScreenSize.ContentHeight - 1) * 228 + ContentLineTicks, TicksPerLine: 228, HasAyChip: true); + + internal static HardwareSettings Timex2048 { get; } = new( + ClockMhz: 3.5f, + ComputerType: ComputerType.Timex2048, + TicksPerFrame: 69888, + InterruptFrequency: 50, + RetraceTicks: 48, + BorderTop: 64, + ContentionStartTicks: 14336, + FloatingBusStartTicks: 14338, + InterruptDuration: 32, + FirstPixelTicks: 14336, + LastPixelTicks: 14336 + (ScreenSize.ContentHeight - 1) * 224 + ContentLineTicks, + TicksPerLine: 224, + HasAyChip: true); } \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Border.cs b/src/Spectron.Emulation/Screen/Border.cs index 1b4982bd..32e33bda 100644 --- a/src/Spectron.Emulation/Screen/Border.cs +++ b/src/Spectron.Emulation/Screen/Border.cs @@ -1,6 +1,8 @@ +using OldBit.Spectron.Emulation.Extensions; + namespace OldBit.Spectron.Emulation.Screen; -public record struct BorderTick(int StartTick, int EndTick, int StartPixel); +public record struct BorderTick(int StartTick, int EndTick, int StartPixel, int Shift = 1); internal sealed class Border(HardwareSettings hardwareSettings, FrameBuffer frameBuffer) { @@ -8,21 +10,39 @@ internal sealed class Border(HardwareSettings hardwareSettings, FrameBuffer fram private const int RightBorderTicks = 24; // Number of ticks for the right border (24T) private const int ContentLineTicks = 128; // Number of ticks for the screen line content (128T). - private readonly List _borderTickRanges = BuildBorderTickRanges( + private List _borderTickRanges = BuildBorderTickRanges( hardwareSettings.RetraceTicks, hardwareSettings.BorderTop); private int _lastRangeIndex; private int _offset; private Color _lastColor = SpectrumPalette.White; + private ScreenMode _screenMode = ScreenMode.Spectrum; + + internal void ChangeScreenMode(ScreenMode screenMode) + { + if (_screenMode == screenMode) + { + return; + } + + _borderTickRanges = BuildBorderTickRanges( + hardwareSettings.RetraceTicks, + hardwareSettings.BorderTop, + screenMode.IsTimexHiRes() ? 2 * ScreenSize.ContentWidth : ScreenSize.ContentWidth); + + _screenMode = screenMode; + + Invalidate(); + } internal void Update(Color color) => _lastColor = color; /// - /// Fill the border with the specified color up to the current tick. This fills frame buffer with the + /// Fill the border with the specified color up to the current tick. This fills the frame buffer with the /// current border color up to the current tick. /// /// The new color. - /// The current tick when border color is changing. + /// The current tick when the border color is changing. internal void Update(Color color, int frameTicks) { for (var rangeIndex = _lastRangeIndex; rangeIndex < _borderTickRanges.Count; rangeIndex++) @@ -33,8 +53,8 @@ internal void Update(Color color, int frameTicks) { if (frameTicks < tickRange.EndTick) { - var startPixel = tickRange.StartPixel + (_offset << 1); - var count = (frameTicks - (tickRange.StartTick + _offset) + 1) << 1; + var startPixel = tickRange.StartPixel + (_offset << tickRange.Shift); + var count = (frameTicks - (tickRange.StartTick + _offset) + 1) << tickRange.Shift; frameBuffer.Fill(startPixel, count, _lastColor); @@ -45,8 +65,8 @@ internal void Update(Color color, int frameTicks) } else { - var startPixel = tickRange.StartPixel + (_offset << 1); - var count = (tickRange.EndTick - (tickRange.StartTick + _offset) + 1) << 1; + var startPixel = tickRange.StartPixel + (_offset << tickRange.Shift); + var count = (tickRange.EndTick - (tickRange.StartTick + _offset) + 1) << tickRange.Shift; frameBuffer.Fill(startPixel, count, _lastColor); @@ -82,54 +102,79 @@ public void Invalidate() /// pixel position for a given tick. /// /// A list of border tick data. - internal static List BuildBorderTickRanges(int retraceTicks, int borderTop) + internal static List BuildBorderTickRanges(int retraceTicks, int borderTop, int contentWidth = ScreenSize.ContentWidth) { var ticksTable = new List(); + // 1 = 2 pixels per tick (standard), 2 = 4 pixels per tick (hi-res) + var shift = contentWidth > ScreenSize.ContentWidth ? 2 : 1; + var borderLeft = ScreenSize.BorderLeft * shift; + var borderRight = ScreenSize.BorderRight * shift; + var startTick = 0; - var endTick = ContentLineTicks + LeftBorderTicks; - var startPixel = ScreenSize.BorderLeft; + var startPixel = borderLeft; var totalLines = borderTop + ScreenSize.ContentHeight + ScreenSize.BorderBottom; for (var line = 0; line < totalLines; line++) { + // Top border if (line < borderTop) { - ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel)); + AddFullBorderLine(ticksTable, line, startTick, startPixel, shift); + + startTick += line == 0 + ? ContentLineTicks + RightBorderTicks + retraceTicks + : LeftBorderTicks + ContentLineTicks + RightBorderTicks + retraceTicks; - startTick = endTick + retraceTicks; - endTick = startTick + LeftBorderTicks + ContentLineTicks + RightBorderTicks; startPixel += line == 0 - ? ScreenSize.ContentWidth + ScreenSize.BorderRight - : ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight; + ? contentWidth + borderRight + : borderLeft + contentWidth + borderRight; } + // Bottom border else if (line >= borderTop + ScreenSize.ContentHeight) { - endTick = startTick + LeftBorderTicks + ContentLineTicks + RightBorderTicks; - - ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel)); - - startTick = endTick + retraceTicks; - startPixel += ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight; + AddFullBorderLine(ticksTable, line, startTick, startPixel, shift); + startTick += LeftBorderTicks + ContentLineTicks + RightBorderTicks + retraceTicks; + startPixel += borderLeft + contentWidth + borderRight; } else { // Left border - endTick = startTick + LeftBorderTicks; - ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel)); + var endTick = startTick + LeftBorderTicks; + ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel, shift)); // Skip content area startTick = endTick + ContentLineTicks; endTick = startTick + RightBorderTicks; - startPixel += ScreenSize.BorderLeft + ScreenSize.ContentWidth; + startPixel += borderLeft + contentWidth; // Right border - ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel)); + ticksTable.Add(new BorderTick(startTick, endTick - 1, startPixel, shift)); startTick = endTick + retraceTicks; - startPixel += ScreenSize.BorderRight; + startPixel += borderRight; } } return ticksTable; } + + private static void AddFullBorderLine(List ticksTable, int line, int startTick, int startPixel, int shift) + { + if (line == 0) + { + ticksTable.Add(new BorderTick( + startTick, + startTick + ContentLineTicks + RightBorderTicks - 1, + startPixel, + shift)); + + return; + } + + ticksTable.Add(new BorderTick( + startTick, + startTick + LeftBorderTicks + ContentLineTicks + RightBorderTicks - 1, + startPixel, + shift)); + } } \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Content.cs b/src/Spectron.Emulation/Screen/Content.cs index 3cf7c123..fd46500f 100644 --- a/src/Spectron.Emulation/Screen/Content.cs +++ b/src/Spectron.Emulation/Screen/Content.cs @@ -1,17 +1,42 @@ using OldBit.Spectron.Emulation.Devices; using OldBit.Spectron.Emulation.Devices.Memory; +using OldBit.Spectron.Emulation.Screen.Modes; namespace OldBit.Spectron.Emulation.Screen; -internal sealed class Content(HardwareSettings hardware, FrameBuffer frameBuffer, IEmulatorMemory memory, UlaPlus ulaPlus) +internal sealed class Content( + HardwareSettings hardware, + FrameBuffer frameBuffer, + IEmulatorMemory memory, + UlaPlus ulaPlus) { - private readonly ScreenRenderEvent[] _screenRenderEvents = FastLookup.GetScreenRenderEvents(hardware); - private readonly bool[] _dirtyAddresses = new bool[32 * 24 * 8]; + private ScreenRenderEvent[] _screenRenderEvents = FastLookup.GetScreenRenderEvents(hardware, frameBuffer); private int _frameCount = 1; - private bool _isFlashOnFrame; private int _fetchCycleIndex; + private IScreenUpdater _screenUpdater = new SpectrumScreenUpdater(frameBuffer, memory, ulaPlus, 0x4000); + + internal void ChangeScreenMode(ScreenMode screenMode, Color ink, Color paper) + { + _screenRenderEvents = FastLookup.GetScreenRenderEvents(hardware, frameBuffer, screenMode); + + _screenUpdater = screenMode switch + { + ScreenMode.Spectrum => new SpectrumScreenUpdater(frameBuffer, memory, ulaPlus, 0x4000), + ScreenMode.TimexSecondScreen => new SpectrumScreenUpdater(frameBuffer, memory, ulaPlus, 0x6000), + ScreenMode.TimexHiColor => new TimexHiColorScreenUpdater(frameBuffer, memory), + ScreenMode.TimexHiColorAlt => new TimexHiColorScreenUpdater(frameBuffer, memory, isAlternative: true), + ScreenMode.TimexHiRes => new TimexHiResScreenUpdater(frameBuffer, memory, ink, paper), + ScreenMode.TimexHiResAttr => new TimexHiResAttrScreenUpdater(frameBuffer, memory, ink, paper), + ScreenMode.TimexHiResAttrAlt => new TimexHiResAttrScreenUpdater(frameBuffer, memory, ink, paper, isAlternative: true), + ScreenMode.TimexHiResDouble => new TimexHiResDoubleScreenUpdater(frameBuffer, memory, ink, paper), + _ => _screenUpdater + }; + + Invalidate(); + } + /// /// Updates the frame buffer with the content of the screen at the specified frame ticks. This allows /// proper rendering of the special multicolor effects. @@ -35,12 +60,12 @@ internal void UpdateFrameBuffer(int frameTicks) } // First byte and attribute - UpdateFrameBuffer(fetchCycleData.FrameBufferIndex, fetchCycleData.BitmapAddress, - fetchCycleData.AttributeAddress); + _screenUpdater.Update(fetchCycleData.FrameBufferIndex, fetchCycleData.BitmapAddress, + fetchCycleData.AttributeAddress, 1); // Second byte and attribute - UpdateFrameBuffer(fetchCycleData.FrameBufferIndex + 8, (Word)(fetchCycleData.BitmapAddress + 1), - (Word)(fetchCycleData.AttributeAddress + 1)); + _screenUpdater.Update(fetchCycleData.FrameBufferIndex + 8, (Word)(fetchCycleData.BitmapAddress + 1), + (Word)(fetchCycleData.AttributeAddress + 1), 2); _fetchCycleIndex += 1; @@ -61,7 +86,7 @@ internal void NewFrame() return; } - ToggleFlash(); + _screenUpdater.ToggleFlash(); _frameCount = 1; } @@ -73,75 +98,7 @@ internal void Reset() Invalidate(); } - internal void Invalidate() => Array.Fill(_dirtyAddresses, true); - - internal void SetDirty(Word address) => SetDirty(address - 0x4000); + internal void Invalidate() => _screenUpdater.Invalidate(); - private void UpdateFrameBuffer(int frameBufferIndex, Word bitmapAddress, Word attributeAddress) - { - if (!_dirtyAddresses[bitmapAddress]) - { - return; - } - - var bitmap = memory.ReadScreen(bitmapAddress); - var attribute = memory.ReadScreen(attributeAddress); - - var attributeData = FastLookup.AttributeData[attribute]; - var isFlashOn = attributeData.IsFlashOn && _isFlashOnFrame; - - for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) - { - Color color; - - if (ulaPlus is { IsEnabled: true, IsActive: true }) - { - color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? ulaPlus.GetInkColor(attribute) : ulaPlus.GetPaperColor(attribute); - } - else - { - color = (bitmap & FastLookup.BitMasks[bit]) != 0 ^ isFlashOn ? attributeData.Ink : attributeData.Paper; - } - - frameBuffer.Pixels[frameBufferIndex + bit] = color; - } - - _dirtyAddresses[bitmapAddress] = false; - } - - private void SetDirty(int address) - { - if (address < 0x1800) - { - // Single screen byte - _dirtyAddresses[address] = true; - } - else - { - // Attribute byte affecting 8 screen bytes, unrolled for performance (~7x faster than a for loop) - var screenAddress = FastLookup.LineAddressForAttrAddress[address - 0x1800]; - - _dirtyAddresses[screenAddress] = true; - _dirtyAddresses[screenAddress + 256] = true; - _dirtyAddresses[screenAddress + 512] = true; - _dirtyAddresses[screenAddress + 768] = true; - _dirtyAddresses[screenAddress + 1024] = true; - _dirtyAddresses[screenAddress + 1280] = true; - _dirtyAddresses[screenAddress + 1536] = true; - _dirtyAddresses[screenAddress + 1792] = true; - } - } - - private void ToggleFlash() - { - _isFlashOnFrame = !_isFlashOnFrame; - - for (Word attrAddress = 0x1800; attrAddress < 0x1B00; attrAddress++) - { - if ((memory.ReadScreen(attrAddress) & 0x80) != 0) - { - SetDirty((int)attrAddress); - } - } - } + internal void SetDirty(int address) => _screenUpdater.SetDirty(address); } \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/FastLookup.cs b/src/Spectron.Emulation/Screen/FastLookup.cs index b9a5d39b..f6f4ee4f 100644 --- a/src/Spectron.Emulation/Screen/FastLookup.cs +++ b/src/Spectron.Emulation/Screen/FastLookup.cs @@ -1,3 +1,5 @@ +using OldBit.Spectron.Emulation.Extensions; + namespace OldBit.Spectron.Emulation.Screen; internal record AttributeColor(Color Paper, Color Ink, bool IsFlashOn); @@ -6,7 +8,7 @@ internal record ScreenRenderEvent(Word BitmapAddress, Word AttributeAddress, int internal static class FastLookup { - private static readonly Dictionary ScreenRenderEvents = new(); + private static readonly Dictionary ScreenRenderEvents = new(); /// /// Bit masks for each bit in a byte. @@ -32,17 +34,22 @@ internal static class FastLookup /// bytes of the screen. /// /// The hardware settings. + /// The frame buffer to use for screen rendering. + /// The screen mode to use for screen rendering. /// A list of events. - internal static ScreenRenderEvent[] GetScreenRenderEvents(HardwareSettings hardware) + internal static ScreenRenderEvent[] GetScreenRenderEvents(HardwareSettings hardware, + FrameBuffer frameBuffer, ScreenMode screenMode = ScreenMode.Spectrum) { - if (ScreenRenderEvents.TryGetValue(hardware.ComputerType, out var events)) + var key = $"{hardware.ComputerType}-{screenMode}"; + + if (ScreenRenderEvents.TryGetValue(key, out var events)) { return events; } - ScreenRenderEvents[hardware.ComputerType] = BuildScreenEventsTable(hardware); + ScreenRenderEvents[key] = BuildScreenEventsTable(hardware, frameBuffer); - return ScreenRenderEvents[hardware.ComputerType]; + return ScreenRenderEvents[key]; } private static AttributeColor[] BuildAttributeColorLookupTable() @@ -80,14 +87,15 @@ private static Word[] BuildScreenAddressLookupTable() return screenAddressLookup; } - private static ScreenRenderEvent[] BuildScreenEventsTable(HardwareSettings hardware) + private static ScreenRenderEvent[] BuildScreenEventsTable(HardwareSettings hardware, FrameBuffer frameBuffer) { + var hiResFactor = frameBuffer.ScreenMode.IsTimexHiRes() ? 2 : 1; var screenEvents = new List(); for (var y = 0; y < ScreenSize.ContentHeight; y++) { var rowTime = hardware.FirstPixelTicks + hardware.TicksPerLine * y; - var bufferLineIndex = FrameBuffer.GetLineIndex(y, hardware.BorderTop); + var bufferLineIndex = frameBuffer.GetLineIndex(y, hardware.BorderTop); for (var x = 0; x < 16; x++) { @@ -98,7 +106,7 @@ private static ScreenRenderEvent[] BuildScreenEventsTable(HardwareSettings hardw BitmapAddress: (Word)bitmapAddress, AttributeAddress: (Word)attributeAddress, Ticks: rowTime + 8 * x, - FrameBufferIndex: bufferLineIndex + 8 * x * 2)); + FrameBufferIndex: bufferLineIndex + 8 * x * 2 * hiResFactor)); } } diff --git a/src/Spectron.Emulation/Screen/FrameBuffer.cs b/src/Spectron.Emulation/Screen/FrameBuffer.cs index 38bd5239..4c1452d8 100644 --- a/src/Spectron.Emulation/Screen/FrameBuffer.cs +++ b/src/Spectron.Emulation/Screen/FrameBuffer.cs @@ -1,18 +1,49 @@ +using OldBit.Spectron.Emulation.Extensions; + namespace OldBit.Spectron.Emulation.Screen; /// /// Represents a buffer for the ZX Spectrum screen with predefined dimensions /// that includes all pixels for both content and border areas. /// -public sealed class FrameBuffer(Color fillColor) +public sealed class FrameBuffer { - public static int Width => ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight; + private static readonly Color DefaultFillColor = SpectrumPalette.White; + private int _borderLeft = ScreenSize.BorderLeft; + + internal ScreenMode ScreenMode { get; private set; } = ScreenMode.Spectrum; + + public int Width { get; private set; } = ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight; + public int Height { get; private set; } = ScreenSize.BorderTop + ScreenSize.ContentHeight + ScreenSize.BorderBottom; + + public readonly Color[] Pixels; + + public FrameBuffer() + { + const int hiResWidth = (ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight) * 2; + + ChangeScreenMode(ScreenMode); + + Pixels = Enumerable.Repeat(DefaultFillColor, hiResWidth * Height).ToArray(); + } + + internal void ChangeScreenMode(ScreenMode screenMode) + { + if (ScreenMode == screenMode) + { + return; + } + + ScreenMode = screenMode; - public static int Height => ScreenSize.BorderTop + ScreenSize.ContentHeight + ScreenSize.BorderBottom; + var hiResMultiplier = screenMode.IsTimexHiRes() ? 2 : 1; + _borderLeft = ScreenSize.BorderLeft * hiResMultiplier; - public readonly Color[] Pixels = Enumerable.Repeat(fillColor, Width * Height).ToArray(); + Width = (ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight) * hiResMultiplier; + Height = ScreenSize.BorderTop + ScreenSize.ContentHeight + ScreenSize.BorderBottom; + } internal void Fill(int start, int count, Color color) => Array.Fill(Pixels, color, start, count); - internal static int GetLineIndex(int line, int borderTop) => Width * borderTop + ScreenSize.BorderLeft + Width * line; + internal int GetLineIndex(int line, int borderTop) => Width * borderTop + _borderLeft + Width * line; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/IScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/IScreenUpdater.cs new file mode 100644 index 00000000..f6cba79b --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/IScreenUpdater.cs @@ -0,0 +1,12 @@ +namespace OldBit.Spectron.Emulation.Screen.Modes; + +public interface IScreenUpdater +{ + void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex); + + void Invalidate(); + + void SetDirty(int address); + + void ToggleFlash(); +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/SpectrumScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/SpectrumScreenUpdater.cs new file mode 100644 index 00000000..5ea69f03 --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/SpectrumScreenUpdater.cs @@ -0,0 +1,92 @@ +using OldBit.Spectron.Emulation.Devices; +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen.Modes; + +/// +/// Standard Spectrum screen updater. Also handles ULA+ coloring and Timex second screen. +/// +internal sealed class SpectrumScreenUpdater( + FrameBuffer frameBuffer, + IEmulatorMemory memory, + UlaPlus ulaPlus, + Word screenBaseAddress) : IScreenUpdater +{ + private readonly bool[] _dirtyAddresses = new bool[32 * 24 * 8]; + private bool _isFlashOnFrame; + + public void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex) + { + if (!_dirtyAddresses[bitmapAddress]) + { + return; + } + + var bitmap = memory.Read((Word)(bitmapAddress + screenBaseAddress)); + var attribute = memory.Read((Word)(attributeAddress + screenBaseAddress)); + + var attributeData = FastLookup.AttributeData[attribute]; + var isFlashOn = attributeData.IsFlashOn && _isFlashOnFrame; + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) + { + Color color; + + if (ulaPlus is { IsEnabled: true, IsActive: true }) + { + color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? ulaPlus.GetInkColor(attribute) : ulaPlus.GetPaperColor(attribute); + } + else + { + color = (bitmap & FastLookup.BitMasks[bit]) != 0 ^ isFlashOn ? attributeData.Ink : attributeData.Paper; + } + + frameBuffer.Pixels[frameBufferIndex + bit] = color; + } + + _dirtyAddresses[bitmapAddress] = false; + } + + public void Invalidate() => Array.Fill(_dirtyAddresses, true); + + public void SetDirty(int address) + { + address -= screenBaseAddress; + + if (address < 0x1800) + { + // Single screen byte + _dirtyAddresses[address] = true; + } + else + { + // Attribute byte affecting 8 screen bytes, unrolled for performance (~7x faster than a for loop) + var screenAddress = FastLookup.LineAddressForAttrAddress[address - 0x1800]; + + _dirtyAddresses[screenAddress] = true; + _dirtyAddresses[screenAddress + 256] = true; + _dirtyAddresses[screenAddress + 512] = true; + _dirtyAddresses[screenAddress + 768] = true; + _dirtyAddresses[screenAddress + 1024] = true; + _dirtyAddresses[screenAddress + 1280] = true; + _dirtyAddresses[screenAddress + 1536] = true; + _dirtyAddresses[screenAddress + 1792] = true; + } + } + + public void ToggleFlash() + { + _isFlashOnFrame = !_isFlashOnFrame; + + var startAttrAddress = 0x1800 + screenBaseAddress; + var endAttrAddress = 0x1B00 + screenBaseAddress; + + for (var attrAddress = startAttrAddress; attrAddress < endAttrAddress; attrAddress++) + { + if ((memory.Read((Word)attrAddress) & 0x80) != 0) + { + SetDirty(attrAddress); + } + } + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/TimexHiColorScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/TimexHiColorScreenUpdater.cs new file mode 100644 index 00000000..320d7abd --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/TimexHiColorScreenUpdater.cs @@ -0,0 +1,69 @@ +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen.Modes; + +/// +/// Timex HiColor screen updater. Uses standard screen for data and a second screen for attribute data. +/// +internal class TimexHiColorScreenUpdater( + FrameBuffer frameBuffer, + IEmulatorMemory memory, + bool isAlternative = false) : IScreenUpdater +{ + private const int SpectrumScreenAddress = 0x4000; + private const int AttributesScreenAddress = 0x6000; + + private readonly bool[] _dirtyAddresses = new bool[32 * 24 * 8]; + private bool _isFlashOnFrame; + + public void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex) + { + if (!_dirtyAddresses[bitmapAddress]) + { + return; + } + + var bitmap = memory.Read((Word)(bitmapAddress + (isAlternative ? AttributesScreenAddress : SpectrumScreenAddress))); + var attribute = memory.Read((Word)(bitmapAddress + AttributesScreenAddress)); + + var attributeData = FastLookup.AttributeData[attribute]; + var isFlashOn = attributeData.IsFlashOn && _isFlashOnFrame; + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) + { + var color = (bitmap & FastLookup.BitMasks[bit]) != 0 ^ isFlashOn ? attributeData.Ink : attributeData.Paper; + frameBuffer.Pixels[frameBufferIndex + bit] = color; + } + + _dirtyAddresses[bitmapAddress] = false; + } + + public void Invalidate() => Array.Fill(_dirtyAddresses, true); + + public void SetDirty(int address) + { + if (address >= AttributesScreenAddress) + { + // Attribute data + _dirtyAddresses[address - AttributesScreenAddress] = true; + } + else + { + // Pixel data + _dirtyAddresses[address - SpectrumScreenAddress] = true; + } + } + + public void ToggleFlash() + { + _isFlashOnFrame = !_isFlashOnFrame; + + for (var attrAddress = AttributesScreenAddress; attrAddress < AttributesScreenAddress + 0x1800; attrAddress++) + { + if ((memory.Read((Word)attrAddress) & 0x80) != 0) + { + SetDirty(attrAddress - 0x2000); + } + } + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/TimexHiResAttrScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/TimexHiResAttrScreenUpdater.cs new file mode 100644 index 00000000..127f4714 --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/TimexHiResAttrScreenUpdater.cs @@ -0,0 +1,84 @@ +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen.Modes; + +/// +/// Timex undocumented HiRes screen updater. Uses standard screen and attributes for data. +/// +internal class TimexHiResAttrScreenUpdater( + FrameBuffer frameBuffer, + IEmulatorMemory memory, + Color ink, + Color paper, + bool isAlternative = false) : IScreenUpdater +{ + private readonly bool[] _dirtyAddresses = new bool[16384]; + private readonly int _offset = isAlternative ? 0x2000 : 0; + + public void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex) + { + var actualBitmapAddress = (Word)(bitmapAddress + _offset); + var actualAttributeAddress = (Word)(attributeAddress + _offset); + + if (byteIndex == 1) + { + Update(frameBufferIndex, bitmapAddress, actualBitmapAddress); + Update(frameBufferIndex + 8, (Word)(bitmapAddress + 0x2000), actualAttributeAddress); + } + else + { + Update(frameBufferIndex + 8, bitmapAddress, actualBitmapAddress); + Update(frameBufferIndex + 16, (Word)(bitmapAddress + 0x2000), actualAttributeAddress); + } + } + + private void Update(int frameBufferIndex, Word bitmapAddress, Word actualBitmapAddress) + { + if (!_dirtyAddresses[bitmapAddress]) + { + return; + } + + var bitmap = memory.Read((Word)(actualBitmapAddress + 0x4000)); + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) + { + var color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? ink : paper; + frameBuffer.Pixels[frameBufferIndex + bit] = color; + } + + _dirtyAddresses[bitmapAddress] = false; + } + + public void Invalidate() => Array.Fill(_dirtyAddresses, true); + + public void SetDirty(int address) + { + address -= isAlternative ? 0x6000 : 0x4000; + + if (address < 0x1800) + { + // Single screen byte + _dirtyAddresses[address] = true; + } + else + { + // Attribute byte affecting 8 screen bytes, unrolled for performance (~7x faster than a for loop) + var screenAddress = FastLookup.LineAddressForAttrAddress[address - 0x1800] + 0x2000; + + _dirtyAddresses[screenAddress] = true; + _dirtyAddresses[screenAddress + 256] = true; + _dirtyAddresses[screenAddress + 512] = true; + _dirtyAddresses[screenAddress + 768] = true; + _dirtyAddresses[screenAddress + 1024] = true; + _dirtyAddresses[screenAddress + 1280] = true; + _dirtyAddresses[screenAddress + 1536] = true; + _dirtyAddresses[screenAddress + 1792] = true; + } + } + + public void ToggleFlash() + { + // Not supported in this mode + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/TimexHiResDoubleScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/TimexHiResDoubleScreenUpdater.cs new file mode 100644 index 00000000..ef56891f --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/TimexHiResDoubleScreenUpdater.cs @@ -0,0 +1,58 @@ +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen.Modes; + +/// +/// Timex HiRes screen updater with doubled effect. Uses second screen for data. +/// +public class TimexHiResDoubleScreenUpdater( + FrameBuffer frameBuffer, + IEmulatorMemory memory, + Color ink, + Color paper) : IScreenUpdater +{ + private readonly bool[] _dirtyAddresses = new bool[32 * 24 * 8]; + + public void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex) + { + if (!_dirtyAddresses[bitmapAddress]) + { + return; + } + + var actualBitmapAddress = (Word)(bitmapAddress + 0x6000); + + if (byteIndex == 1) + { + Update(frameBufferIndex, actualBitmapAddress); + Update(frameBufferIndex + 8, actualBitmapAddress); + } + else + { + Update(frameBufferIndex + 8, actualBitmapAddress); + Update(frameBufferIndex + 16, actualBitmapAddress); + } + + _dirtyAddresses[bitmapAddress] = false; + } + + private void Update(int frameBufferIndex, Word actualBitmapAddress) + { + var bitmap = memory.Read(actualBitmapAddress); + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) + { + var color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? ink : paper; + frameBuffer.Pixels[frameBufferIndex + bit] = color; + } + } + + public void Invalidate() => Array.Fill(_dirtyAddresses, true); + + public void SetDirty(int address) => _dirtyAddresses[address - 0x6000] = true; + + public void ToggleFlash() + { + // Not supported in this mode + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/Modes/TimexHiResScreenUpdater.cs b/src/Spectron.Emulation/Screen/Modes/TimexHiResScreenUpdater.cs new file mode 100644 index 00000000..1896a623 --- /dev/null +++ b/src/Spectron.Emulation/Screen/Modes/TimexHiResScreenUpdater.cs @@ -0,0 +1,56 @@ +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen.Modes; + +/// +/// Timex HiRes screen updater. Uses standard and second screen for data. +/// +internal class TimexHiResScreenUpdater( + FrameBuffer frameBuffer, + IEmulatorMemory memory, + Color ink, + Color paper) : IScreenUpdater +{ + private readonly bool[] _dirtyAddresses = new bool[16384]; + + public void Update(int frameBufferIndex, Word bitmapAddress, Word attributeAddress, int byteIndex) + { + if (byteIndex == 1) + { + Update(frameBufferIndex, bitmapAddress); + Update(frameBufferIndex + 8, (Word)(bitmapAddress + 0x2000)); + } + else + { + Update(frameBufferIndex + 8, bitmapAddress); + Update(frameBufferIndex + 16, (Word)(bitmapAddress + 0x2000)); + } + } + + private void Update(int frameBufferIndex, Word bitmapAddress) + { + if (!_dirtyAddresses[bitmapAddress]) + { + return; + } + + var bitmap = memory.Read((Word)(bitmapAddress + 0x4000)); + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) + { + var color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? ink : paper; + frameBuffer.Pixels[frameBufferIndex + bit] = color; + } + + _dirtyAddresses[bitmapAddress] = false; + } + + public void Invalidate() => Array.Fill(_dirtyAddresses, true); + + public void SetDirty(int address) => _dirtyAddresses[address - 0x4000] = true; + + public void ToggleFlash() + { + // Not supported in this mode + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/ScreenBuffer.cs b/src/Spectron.Emulation/Screen/ScreenBuffer.cs index a0854c4d..6e579da0 100644 --- a/src/Spectron.Emulation/Screen/ScreenBuffer.cs +++ b/src/Spectron.Emulation/Screen/ScreenBuffer.cs @@ -1,20 +1,28 @@ using OldBit.Spectron.Emulation.Devices; using OldBit.Spectron.Emulation.Devices.Memory; +using OldBit.Spectron.Emulation.Extensions; namespace OldBit.Spectron.Emulation.Screen; -internal sealed class ScreenBuffer +public sealed class ScreenBuffer { private readonly Border _border; private readonly Content _content; private bool _borderColorChanged = true; + private Color? _lockedBorderColor; + private ScreenMode _screenMode = ScreenMode.Spectrum; + + public FrameBuffer FrameBuffer { get; } - public FrameBuffer FrameBuffer { get; } = new(SpectrumPalette.White); internal Color LastBorderColor { get; private set; } = SpectrumPalette.White; + public event EventHandler? FrameBufferChanged; + internal ScreenBuffer(HardwareSettings hardware, IEmulatorMemory memory, UlaPlus ulaPlus) { + FrameBuffer = new FrameBuffer(); + _border = new Border(hardware, FrameBuffer); _content = new Content(hardware, FrameBuffer, memory, ulaPlus); @@ -24,14 +32,32 @@ internal ScreenBuffer(HardwareSettings hardware, IEmulatorMemory memory, UlaPlus } } + internal void ChangeScreenMode(ScreenMode screenMode, Color ink, Color paper, int frameTicks) + { + _lockedBorderColor = screenMode.IsTimexHiRes() ? paper : null; + + FrameBuffer.ChangeScreenMode(screenMode); + + _content.ChangeScreenMode(screenMode, ink, paper); + _border.ChangeScreenMode(screenMode); + + _border.Update(_lockedBorderColor ?? LastBorderColor, frameTicks); + + if (_screenMode != screenMode) + { + FrameBufferChanged?.Invoke(this, EventArgs.Empty); + } + + _screenMode = screenMode; + } + internal void NewFrame() { _border.NewFrame(); _content.NewFrame(); } - internal void EndFrame(int frameTicks) => - _border.Update(LastBorderColor, frameTicks); + internal void EndFrame(int frameTicks) => _border.Update(_lockedBorderColor ?? LastBorderColor, frameTicks); internal void UpdateBorder(Color borderColor, int frameTicks = 0) { @@ -46,7 +72,7 @@ internal void UpdateBorder(Color borderColor, int frameTicks = 0) return; } - _border.Update(borderColor, frameTicks); + _border.Update(_lockedBorderColor ?? borderColor, frameTicks); _borderColorChanged = false; } diff --git a/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs b/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs new file mode 100644 index 00000000..a4f4086f --- /dev/null +++ b/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs @@ -0,0 +1,149 @@ +using OldBit.Spectron.Emulation.Devices; +using OldBit.Spectron.Emulation.Devices.Memory; + +namespace OldBit.Spectron.Emulation.Screen; + +internal sealed class ScreenMemoryHandler +{ + private readonly IEmulatorMemory _memory; + private readonly ScreenBuffer _screenBuffer; + + internal ScreenMemoryHandler(IEmulatorMemory memory, ScreenBuffer screenBuffer) + { + _memory = memory; + _screenBuffer = screenBuffer; + + memory.MemoryUpdated += SpectrumHandler; + } + + internal void SetScreenMode(UlaTimex? ulaTimex, int frameTicks = 0) + { + RemoveHandlers(); + + var screenMode = ulaTimex?.ScreenMode ?? ScreenMode.Spectrum; + + switch (screenMode) + { + case ScreenMode.Spectrum: + _memory.MemoryUpdated += SpectrumHandler; + break; + + case ScreenMode.TimexSecondScreen: + _memory.MemoryUpdated += TimexSecondScreenHandler; + break; + + case ScreenMode.TimexHiColor: + case ScreenMode.TimexHiColorAlt: + _memory.MemoryUpdated += TimexHiColorHandler; + break; + + case ScreenMode.TimexHiRes: + _memory.MemoryUpdated += TimexHiResHandler; + break; + + case ScreenMode.TimexHiResAttr: + _memory.MemoryUpdated += TimexHiResAttrHandler; + break; + + case ScreenMode.TimexHiResAttrAlt: + _memory.MemoryUpdated += TimexHiResAttrAltHandler; + break; + + case ScreenMode.TimexHiResDouble: + _memory.MemoryUpdated += TimexHiResDoubleHandler; + break; + } + + _screenBuffer.ChangeScreenMode( + screenMode, + ulaTimex?.Ink ?? SpectrumPalette.Black, + ulaTimex?.Paper ?? SpectrumPalette.White, + frameTicks); + } + + private void RemoveHandlers() + { + _memory.MemoryUpdated -= SpectrumHandler; + _memory.MemoryUpdated -= TimexSecondScreenHandler; + _memory.MemoryUpdated -= TimexHiColorHandler; + _memory.MemoryUpdated -= TimexHiResHandler; + _memory.MemoryUpdated -= TimexHiResAttrHandler; + _memory.MemoryUpdated -= TimexHiResAttrAltHandler; + _memory.MemoryUpdated -= TimexHiResDoubleHandler; + } + + private void SpectrumHandler(Word address, byte value) + { + if (address < 0x5B00) + { + _screenBuffer.UpdateScreen(address); + } + } + + private void TimexSecondScreenHandler(Word address, byte value) + { + if (address is > 0x5FFF and < 0x7B00) + { + _screenBuffer.UpdateScreen(address); + } + } + + private void TimexHiColorHandler(Word address, byte value) + { + switch (address) + { + // Screen data, without attribute data + case > 0x3FFF and < 0x5800: + // Attribute data - Timex second screen + case > 0x5FFF and < 0x7800: + _screenBuffer.UpdateScreen(address); + break; + } + } + + private void TimexHiResHandler(Word address, byte value) + { + switch (address) + { + // Standard scree data + case > 0x3FFF and < 0x5800: + // Second screen data + case > 0x5FFF and < 0x7800: + _screenBuffer.UpdateScreen(address); + break; + } + } + + private void TimexHiResAttrHandler(Word address, byte value) + { + switch (address) + { + // Standard screen data and attributes + case > 0x3FFF and < 0x5B00: + _screenBuffer.UpdateScreen(address); + break; + } + } + + private void TimexHiResAttrAltHandler(Word address, byte value) + { + switch (address) + { + // Second screen data and attributes + case > 0x5FFF and < 0x7B00: + _screenBuffer.UpdateScreen(address); + break; + } + } + + private void TimexHiResDoubleHandler(Word address, byte value) + { + switch (address) + { + // Second screen data + case > 0x5FFF and < 0x7800: + _screenBuffer.UpdateScreen(address); + break; + } + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/ScreenMode.cs b/src/Spectron.Emulation/Screen/ScreenMode.cs new file mode 100644 index 00000000..efbb057c --- /dev/null +++ b/src/Spectron.Emulation/Screen/ScreenMode.cs @@ -0,0 +1,47 @@ +namespace OldBit.Spectron.Emulation.Screen; + +public enum ScreenMode +{ + /// + /// Standard Spectrum screen. + /// + Spectrum = 0x00, + + /// + /// Timex second screen, the same as Spectrum, but at 0x6000. + /// + TimexSecondScreen = 0x01, + + /// + /// Hi-color Timex screen. The standard screen is used for data, and the second screen for attributes. + /// + TimexHiColor = 0x02, + + /// + /// Hi-color Timex screen. Similar to TimexHiColor. The second screen is used for both data and attributes. + /// + TimexHiColorAlt = 0x03, + + /// + /// Hi-res Timex screen. The standard screen is used to display even bytes and + /// attributes for odd bytes. + /// + TimexHiResAttr = 0x04, + + /// + /// Hi-res Timex screen. The second screen is used to display even bytes and + /// attributes for odd bytes. + /// + TimexHiResAttrAlt = 0x05, + + /// + /// Hi-res Timex screen. The standard screen is used to display even bytes and + /// a second screen for odd bytes to create a 512x192 pixel screen. + /// + TimexHiRes = 0x06, + + /// + /// Hi-res Timex screen. The second is used to display even and odd bytes (duplicated). + /// + TimexHiResDouble = 0x07, +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/ScreenSize.cs b/src/Spectron.Emulation/Screen/ScreenSize.cs index f3290bde..914f9b90 100644 --- a/src/Spectron.Emulation/Screen/ScreenSize.cs +++ b/src/Spectron.Emulation/Screen/ScreenSize.cs @@ -3,32 +3,32 @@ namespace OldBit.Spectron.Emulation.Screen; public static class ScreenSize { /// - /// Standard top border size is 64 lines. + /// The standard top border size is 64 lines. /// public const int BorderTop = 64; /// - /// Standard left border size is 48 pixels. + /// The standard left border size is 48 pixels. /// public const int BorderLeft = 48; /// - /// Standard right border size is 48 pixels. + /// The standard right border size is 48 pixels. /// public const int BorderRight = 48; /// - /// Standard bottom border size is 56 lines. + /// The standard bottom border size is 56 lines. /// public const int BorderBottom = 56; /// - /// The width of the screen content is 256 pixels, e.g. 32 columns of 8 pixels. + /// The width of the screen content is 256 pixels, e.g., 32 columns of 8 pixels. /// public const int ContentWidth = 256; /// - /// The height of the screen content is 192 pixels, e.g. 24 rows of 8 pixels. + /// The height of the screen content is 192 pixels, e.g., 24 rows of 8 pixels. /// public const int ContentHeight = 192; } \ No newline at end of file diff --git a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs index 17f715a6..54c24764 100644 --- a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs +++ b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs @@ -33,6 +33,7 @@ internal Emulator CreateEmulator(SzxFile snapshot) SzxHeader.MachineId16K => ComputerType.Spectrum16K, SzxHeader.MachineId48K => ComputerType.Spectrum48K, SzxHeader.MachineId128K => ComputerType.Spectrum128K, + SzxHeader.MachineIdTc2048 => ComputerType.Timex2048, _ => throw new NotSupportedException($"Snapshot hardware mode not supported: {snapshot.Header.MachineId}") }; @@ -47,8 +48,7 @@ internal Emulator CreateEmulator(SzxFile snapshot) LoadJoystick(emulator.JoystickManager, snapshot.Joystick); LoadTape(emulator.TapeManager, snapshot.Tape); LoadAyRegisters(emulator.AudioManager, snapshot.Ay); - - // TODO: Load the rest of the snapshot + LoadTimex(emulator.UlaTimex, snapshot.TimexSinclair); return emulator; } @@ -77,6 +77,7 @@ private static SzxFile CreateSnapshot(Emulator emulator, CompressionLevel compre ComputerType.Spectrum16K => SzxHeader.MachineId16K, ComputerType.Spectrum48K => SzxHeader.MachineId48K, ComputerType.Spectrum128K => SzxHeader.MachineId128K, + ComputerType.Timex2048 => SzxHeader.MachineIdTc2048, _ => snapshot.Header.MachineId }; @@ -87,6 +88,7 @@ private static SzxFile CreateSnapshot(Emulator emulator, CompressionLevel compre SaveJoystick(emulator.JoystickManager, snapshot); SaveTape(emulator.TapeManager, snapshot, compressionLevel); SaveAyRegisters(emulator.AudioManager, snapshot); + SaveTimex(emulator.UlaTimex, snapshot); if (emulator.RomType.IsCustomRom()) { @@ -239,6 +241,16 @@ private static void LoadAyRegisters(AudioManager audioManager, AyBlock? ay) audioManager.Ay.LoadRegisters(ay.CurrentRegister, ay.Registers); } + private static void LoadTimex(UlaTimex? ulaTimex, TimexSinclairBlock? timexSinclair) + { + if (ulaTimex == null || timexSinclair == null) + { + return; + } + + ulaTimex.WritePort(UlaTimex.ControlPort, timexSinclair.PortFF); + } + private static void SaveRegisters(Z80 cpu, Z80RegsBlock registers) { registers.AF = cpu.Registers.AF; @@ -384,4 +396,17 @@ private static void SaveAyRegisters(AudioManager audioManager, SzxFile snapshot) snapshot.Ay.Registers[i] = audioManager.Ay.Registers[i]; } } + + private static void SaveTimex(UlaTimex? ulaTimex, SzxFile snapshot) + { + if (ulaTimex == null) + { + return; + } + + snapshot.TimexSinclair = new TimexSinclairBlock + { + PortFF = ulaTimex.ReadPort(UlaTimex.ControlPort) ?? 0 + }; + } } \ No newline at end of file diff --git a/src/Spectron.Emulation/Spectron.Emulation.csproj b/src/Spectron.Emulation/Spectron.Emulation.csproj index 71df38f1..908c3cf8 100644 --- a/src/Spectron.Emulation/Spectron.Emulation.csproj +++ b/src/Spectron.Emulation/Spectron.Emulation.csproj @@ -32,6 +32,8 @@ + + diff --git a/src/Spectron.Emulation/State/Components/TimexState.cs b/src/Spectron.Emulation/State/Components/TimexState.cs new file mode 100644 index 00000000..b30b6d81 --- /dev/null +++ b/src/Spectron.Emulation/State/Components/TimexState.cs @@ -0,0 +1,9 @@ +using MemoryPack; + +namespace OldBit.Spectron.Emulation.State.Components; + +[MemoryPackable] +public sealed partial record TimexState +{ + public byte PortFF { get; set; } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/State/StateManager.cs b/src/Spectron.Emulation/State/StateManager.cs index 1009068d..8f05dc3c 100644 --- a/src/Spectron.Emulation/State/StateManager.cs +++ b/src/Spectron.Emulation/State/StateManager.cs @@ -35,6 +35,7 @@ public Emulator CreateEmulator(StateSnapshot snapshot) LoadInterface1(emulator.Interface1, emulator.MicrodriveManager, snapshot.Interface1); LoadBeta128(emulator.Beta128, emulator.DiskDriveManager, snapshot.Beta128); LoadPrinter(emulator.Printer, snapshot.Printer); + LoadTimex(emulator.UlaTimex, snapshot.Timex); LoadOther(emulator, snapshot); return emulator; @@ -58,6 +59,7 @@ public static StateSnapshot CreateSnapshot(Emulator emulator, bool isTimeMachine SaveInterface1(emulator.Interface1, emulator.MicrodriveManager, isTimeMachine, snapshot); SaveBeta128(emulator.Beta128, emulator.DiskDriveManager, isTimeMachine, snapshot); SavePrinter(emulator.Printer, snapshot); + SaveTimex(emulator.UlaTimex, snapshot); SaveOther(emulator, snapshot); if (emulator.RomType.IsCustomRom()) @@ -260,6 +262,19 @@ private static void SaveBeta128(Beta128Device beta128, DiskDriveManager diskDriv private static void SavePrinter(ZxPrinter printer, StateSnapshot stateSnapshot) => stateSnapshot.Printer = new PrinterState(printer.IsEnabled); + private static void SaveTimex(UlaTimex? ulaTimex, StateSnapshot stateSnapshot) + { + if (ulaTimex == null) + { + return; + } + + stateSnapshot.Timex = new TimexState + { + PortFF = ulaTimex.ReadPort(UlaTimex.ControlPort) ?? 0 + }; + } + private static void SaveOther(Emulator emulator, StateSnapshot stateSnapshot) => stateSnapshot.BorderColor = emulator.ScreenBuffer.LastBorderColor; @@ -454,4 +469,14 @@ private static void LoadPrinter(ZxPrinter printer, PrinterState? printerState) printer.IsEnabled = printerState.IsZxPrinterEnabled; } + + private static void LoadTimex(UlaTimex? ulaTimex, TimexState? timexState) + { + if (timexState == null || ulaTimex == null) + { + return; + } + + ulaTimex.WritePort(UlaTimex.ControlPort, timexState.PortFF); + } } \ No newline at end of file diff --git a/src/Spectron.Emulation/State/StateSnapshot.cs b/src/Spectron.Emulation/State/StateSnapshot.cs index 6066a7b3..2f5f38dd 100644 --- a/src/Spectron.Emulation/State/StateSnapshot.cs +++ b/src/Spectron.Emulation/State/StateSnapshot.cs @@ -35,6 +35,8 @@ public sealed partial class StateSnapshot public PrinterState? Printer { get; set; } + public TimexState? Timex { get; set; } + public byte[] Serialize() => MemoryPackSerializer.Serialize(this); public static StateSnapshot? Load(string filePath) diff --git a/src/Spectron.Emulation/Tape/Loader/Files/2048.szx b/src/Spectron.Emulation/Tape/Loader/Files/2048.szx new file mode 100644 index 00000000..195ca0fb Binary files /dev/null and b/src/Spectron.Emulation/Tape/Loader/Files/2048.szx differ diff --git a/src/Spectron.Emulation/Tape/Loader/Loader.cs b/src/Spectron.Emulation/Tape/Loader/Loader.cs index e5998f79..b80c4d71 100644 --- a/src/Spectron.Emulation/Tape/Loader/Loader.cs +++ b/src/Spectron.Emulation/Tape/Loader/Loader.cs @@ -14,6 +14,7 @@ public sealed class Loader(SzxSnapshot szxSnapshot) private const string Loader16ResourceName = "OldBit.Spectron.Emulation.Tape.Loader.Files.16.szx"; private const string Loader48ResourceName = "OldBit.Spectron.Emulation.Tape.Loader.Files.48.szx"; private const string Loader128ResourceName = "OldBit.Spectron.Emulation.Tape.Loader.Files.128.szx"; + private const string LoaderTimex2048ResourceName = "OldBit.Spectron.Emulation.Tape.Loader.Files.2048.szx"; public Emulator EnterLoadCommand(ComputerType computerType) { @@ -22,6 +23,7 @@ public Emulator EnterLoadCommand(ComputerType computerType) ComputerType.Spectrum16K => Loader16ResourceName, ComputerType.Spectrum48K => Loader48ResourceName, ComputerType.Spectrum128K => Loader128ResourceName, + ComputerType.Timex2048 => LoaderTimex2048ResourceName, _ => throw new NotSupportedException($"The computer type '{computerType}' is not supported.") }; diff --git a/src/Spectron.Recorder/MediaRecorder.cs b/src/Spectron.Recorder/MediaRecorder.cs index 5781ecc5..12907921 100644 --- a/src/Spectron.Recorder/MediaRecorder.cs +++ b/src/Spectron.Recorder/MediaRecorder.cs @@ -10,6 +10,7 @@ namespace OldBit.Spectron.Recorder; public sealed class MediaRecorder : IDisposable { + private readonly FrameBuffer _frameBuffer; private readonly RecorderMode _recorderMode; private readonly string _filePath; private readonly RecorderOptions _options; @@ -23,11 +24,13 @@ public sealed class MediaRecorder : IDisposable private bool _isRecordingActive; public MediaRecorder( + FrameBuffer frameBuffer, RecorderMode recorderMode, string filePath, RecorderOptions options, ILogger logger) { + _frameBuffer = frameBuffer; _recorderMode = recorderMode; _filePath = filePath; _options = options; @@ -156,7 +159,7 @@ private void ProcessVideo() var tempVideoFilePath = $"{_filePath}.temp.mp4"; var tempAudioFilePath = $"{_filePath}.temp.wav"; - var videoProcessor = new VideoProcessor(_options, tempVideoFilePath, _videoRecordedFilePath, tempAudioFilePath); + var videoProcessor = new VideoProcessor(_frameBuffer, _options, tempVideoFilePath, _videoRecordedFilePath, tempAudioFilePath); videoProcessor.Process(); FileHelper.TryDeleteFile(_audioRecordedFilePath); diff --git a/src/Spectron.Recorder/Video/VideoProcessor.cs b/src/Spectron.Recorder/Video/VideoProcessor.cs index 8f8ce702..482c1376 100644 --- a/src/Spectron.Recorder/Video/VideoProcessor.cs +++ b/src/Spectron.Recorder/Video/VideoProcessor.cs @@ -19,22 +19,29 @@ internal sealed class VideoProcessor : IDisposable private readonly string _rawRecordingFilePath; private readonly string _audioFilePath; private readonly int _frameSizeInBytes; + private readonly FrameBuffer _emulatorFrameBuffer; private readonly byte[] _frameBuffer; private readonly SKBitmap _bitmap; - public VideoProcessor(RecorderOptions options, string outputFilePath, string rawRecordingFilePath, string audioFilePath) + public VideoProcessor( + FrameBuffer frameBuffer, + RecorderOptions options, + string outputFilePath, + string rawRecordingFilePath, + string audioFilePath) { + _emulatorFrameBuffer = frameBuffer; _options = options; _outputFilePath = outputFilePath; _rawRecordingFilePath = rawRecordingFilePath; _audioFilePath = audioFilePath; - _frameSizeInBytes = Marshal.SizeOf() * FrameBuffer.Width * FrameBuffer.Height; + _frameSizeInBytes = Marshal.SizeOf() * _emulatorFrameBuffer.Width * _emulatorFrameBuffer.Height; _frameBuffer = new byte[_frameSizeInBytes]; _bitmap = new SKBitmap( - FrameBuffer.Width, - FrameBuffer.Height, + _emulatorFrameBuffer.Width, + _emulatorFrameBuffer.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul); } @@ -104,11 +111,11 @@ private void ConvertImagesToVideo(string tempWorkingDir) { var inputPattern = Path.Combine(tempWorkingDir, $"{FileNamePrefix}%d.png"); - var height = FrameBuffer.Height - + var height = _emulatorFrameBuffer.Height - (ScreenSize.BorderTop - _options.BorderTop) - (ScreenSize.BorderBottom - _options.BorderBottom); - var width = FrameBuffer.Width - + var width = _emulatorFrameBuffer.Width - (ScreenSize.BorderLeft - _options.BorderLeft) - (ScreenSize.BorderRight - _options.BorderRight); diff --git a/src/Spectron.Recorder/Video/VideoRecorder.cs b/src/Spectron.Recorder/Video/VideoRecorder.cs index b99350a9..3e6dab34 100644 --- a/src/Spectron.Recorder/Video/VideoRecorder.cs +++ b/src/Spectron.Recorder/Video/VideoRecorder.cs @@ -6,7 +6,7 @@ namespace OldBit.Spectron.Recorder.Video; internal class VideoRecorder(string filePath) : IDisposable { - private readonly int _frameSizeInBytes = Marshal.SizeOf() * FrameBuffer.Width * FrameBuffer.Height; + private readonly int _sizeOfColor = Marshal.SizeOf(); private Stream? _stream; private VideoProcessor? _videoGenerator; @@ -18,11 +18,13 @@ internal void AppendFrame(FrameBuffer frameBuffer) return; } + var frameSizeInBytes = _sizeOfColor * frameBuffer.Width * frameBuffer.Height; + unsafe { fixed (Color* bufferPtr = &frameBuffer.Pixels[0]) { - var span = new Span(bufferPtr, _frameSizeInBytes); + var span = new Span(bufferPtr, frameSizeInBytes); _stream?.Write(span); } diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml index 8dcbf1a2..927b2fa7 100644 --- a/src/Spectron/Controls/MainMenu.axaml +++ b/src/Spectron/Controls/MainMenu.axaml @@ -49,6 +49,7 @@ + diff --git a/src/Spectron/Controls/NativeMainMenu.cs b/src/Spectron/Controls/NativeMainMenu.cs index 82306a12..67550503 100644 --- a/src/Spectron/Controls/NativeMainMenu.cs +++ b/src/Spectron/Controls/NativeMainMenu.cs @@ -262,7 +262,8 @@ private NativeMenuItem CreateEmulatorMenu() [ _computerTypes[ComputerType.Spectrum16K], _computerTypes[ComputerType.Spectrum48K], - _computerTypes[ComputerType.Spectrum128K] + _computerTypes[ComputerType.Spectrum128K], + _computerTypes[ComputerType.Timex2048], ] }, @@ -798,6 +799,7 @@ private void CreateComputerTypeMenu() new { Type = ComputerType.Spectrum16K, DisplayName = "ZX Spectrum 16" }, new { Type = ComputerType.Spectrum48K, DisplayName = "ZX Spectrum 48" }, new { Type = ComputerType.Spectrum128K, DisplayName = "ZX Spectrum 128" }, + new { Type = ComputerType.Timex2048, DisplayName = "Timex Computer 2048" }, }; foreach (var computer in computers) diff --git a/src/Spectron/Screen/FrameBufferConverter.cs b/src/Spectron/Screen/FrameBufferConverter.cs index a209a167..e2d2c659 100644 --- a/src/Spectron/Screen/FrameBufferConverter.cs +++ b/src/Spectron/Screen/FrameBufferConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; @@ -11,9 +12,6 @@ namespace OldBit.Spectron.Screen; /// internal sealed class FrameBufferConverter : IDisposable { - private const int ZoomX = 4; // Number of horizontal pixels, check the below code if changing value - private const int ZoomY = 4; // Number of vertical pixels - private Border _border = BorderSizes.Full; private int _startFrameBufferRow; @@ -21,24 +19,25 @@ internal sealed class FrameBufferConverter : IDisposable private int _startFrameBufferCol; private int _endFrameBufferCol; + private readonly FrameBuffer _frameBuffer; + internal WriteableBitmap ScreenBitmap { get; private set; } - internal FrameBufferConverter() + internal FrameBufferConverter(FrameBuffer frameBuffer, BorderSize borderSize) { - SetBorderSize(BorderSize.Full); - ScreenBitmap = CreateBitmap(); + _frameBuffer = frameBuffer; + SetBorderSize(borderSize); } - internal void UpdateBitmap(FrameBuffer frameBuffer) + internal void UpdateBitmap() { using var lockedBitmap = ScreenBitmap.Lock(); var targetAddress = lockedBitmap.Address; - var rowBytes = lockedBitmap.RowBytes; for (var frameBufferRow = _startFrameBufferRow; frameBufferRow <= _endFrameBufferRow; frameBufferRow++) { - var rowOffset = frameBufferRow * FrameBuffer.Width; + var rowOffset = frameBufferRow * _frameBuffer.Width; for (var frameBufferCol = _startFrameBufferCol; frameBufferCol <= _endFrameBufferCol; frameBufferCol++) { @@ -46,37 +45,19 @@ internal void UpdateBitmap(FrameBuffer frameBuffer) unsafe { - fixed (Color* color = &frameBuffer.Pixels[pixelIndex]) + fixed (Color* color = &_frameBuffer.Pixels[pixelIndex]) { var pixelColor = *(uint*)color; - - // Replicate pixels horizontally, unrolled loop for better performance. - // IMPORTANT: This needs to be in sync with ZoomX value *(uint*)targetAddress = pixelColor; - *(uint*)(targetAddress + 4) = pixelColor; - *(uint*)(targetAddress + 8) = pixelColor; - *(uint*)(targetAddress + 12) = pixelColor; - targetAddress += 4 * ZoomX; + targetAddress += 4; } } } - - var previousLine = targetAddress - rowBytes; - - // Replicate the previous line vertically, no need to unroll this loop - for (var y = 0; y < ZoomY - 1; y++) - { - unsafe - { - Buffer.MemoryCopy(previousLine.ToPointer(), targetAddress.ToPointer(), rowBytes, rowBytes); - } - - targetAddress += rowBytes; - } } } + [MemberNotNull(nameof(ScreenBitmap))] internal void SetBorderSize(BorderSize borderSize) { _border = borderSize switch @@ -88,23 +69,26 @@ internal void SetBorderSize(BorderSize borderSize) _ => BorderSizes.Full, }; + // In hi-res mode the frame buffer border is doubled, so the skip amount scales accordingly. + var borderMultiplier = _frameBuffer.Width / (ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight); + _startFrameBufferRow = BorderSizes.Max.Top - _border.Top; - _endFrameBufferRow = FrameBuffer.Height - (BorderSizes.Max.Bottom - _border.Bottom) - 1; - _startFrameBufferCol = BorderSizes.Max.Left - _border.Left; - _endFrameBufferCol = FrameBuffer.Width - (BorderSizes.Max.Right - _border.Right) - 1; + _endFrameBufferRow = _frameBuffer.Height - (BorderSizes.Max.Bottom - _border.Bottom) - 1; + _startFrameBufferCol = (BorderSizes.Max.Left - _border.Left) * borderMultiplier; + _endFrameBufferCol = _frameBuffer.Width - (BorderSizes.Max.Right - _border.Right) * borderMultiplier - 1; ScreenBitmap = CreateBitmap(); } private WriteableBitmap CreateBitmap() { - var height = FrameBuffer.Height - (BorderSizes.Max.Top - _border.Top) - (BorderSizes.Max.Bottom - _border.Bottom); - var width = FrameBuffer.Width - (BorderSizes.Max.Left - _border.Left) - (BorderSizes.Max.Right - _border.Right); + var height = _endFrameBufferRow - _startFrameBufferRow + 1; + var width = _endFrameBufferCol - _startFrameBufferCol + 1; return new WriteableBitmap( new PixelSize( - width * ZoomX, - height * ZoomY), + width, + height), new Vector(96, 96), PixelFormats.Rgba8888); } diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Display.cs b/src/Spectron/ViewModels/MainWindowViewModel.Display.cs index 8375fd33..51ffed96 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Display.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Display.cs @@ -13,8 +13,8 @@ private void HandleChangeBorderSize(BorderSize borderSize) { BorderSize = borderSize; - _frameBufferConverter.SetBorderSize(borderSize); - SpectrumScreen = _frameBufferConverter.ScreenBitmap; + _frameBufferConverter?.SetBorderSize(borderSize); + SpectrumScreen = _frameBufferConverter?.ScreenBitmap; } private void HandleChangeScreenEffect(ScreenEffect screenEffect) @@ -54,4 +54,21 @@ partial void OnIsNativeMenuEnabledChanged(bool value) NativeMenu.SetMenu(MainWindow, _nativeMainMenu.Empty()); } } + + private void InitializeFrameBuffer() + { + if (Emulator == null) + { + return; + } + + _frameBufferConverter = new FrameBufferConverter(Emulator.ScreenBuffer.FrameBuffer, BorderSize); + SpectrumScreen = _frameBufferConverter.ScreenBitmap; + + Emulator.ScreenBuffer.FrameBufferChanged += (_, _) => + { + _frameBufferConverter = new FrameBufferConverter(Emulator.ScreenBuffer.FrameBuffer, BorderSize); + SpectrumScreen = _frameBufferConverter.ScreenBitmap; + }; + } } \ No newline at end of file diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Emulator.cs b/src/Spectron/ViewModels/MainWindowViewModel.Emulator.cs index 37c4f7a5..aac9592b 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Emulator.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Emulator.cs @@ -150,6 +150,8 @@ private void Initialize(Emulator emulator, bool shouldResume = true) _debuggerViewModel?.ConfigureEmulator(Emulator); + InitializeFrameBuffer(); + Emulator.Start(); if (shouldResume) diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Recording.cs b/src/Spectron/ViewModels/MainWindowViewModel.Recording.cs index 8538ab59..516024d1 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Recording.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Recording.cs @@ -38,6 +38,7 @@ private async Task HandleStartAudioRecordingAsync() if (file != null && Emulator != null) { _mediaRecorder = new MediaRecorder( + Emulator.ScreenBuffer.FrameBuffer, RecorderMode.Audio, file.Path.LocalPath, GetRecorderOptions(), @@ -81,6 +82,7 @@ private async Task HandleStartVideoRecordingAsync() if (file != null && Emulator != null) { _mediaRecorder = new MediaRecorder( + Emulator.ScreenBuffer.FrameBuffer, RecorderMode.AudioVideo, file.Path.LocalPath, GetRecorderOptions(), diff --git a/src/Spectron/ViewModels/MainWindowViewModel.cs b/src/Spectron/ViewModels/MainWindowViewModel.cs index 2aa8d75a..487760c2 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.cs @@ -63,13 +63,13 @@ public partial class MainWindowViewModel : ObservableObject private readonly QuickSaveService _quickSaveService; private readonly ILogStore _logStore; private readonly ILogger _logger; - private readonly FrameBufferConverter _frameBufferConverter = new(); private readonly KeyboardHandler _keyboardHandler; private readonly Stopwatch _renderStopwatch = new(); private readonly FrameRateCalculator _frameRateCalculator = new(); private readonly ScreenshotViewModel _screenshotViewModel = new(); private Emulator? Emulator { get; set; } + private FrameBufferConverter? _frameBufferConverter; private Preferences _preferences = new(); private FavoritePrograms _favorites = new(); private TimeSpan _lastScreenRender = TimeSpan.Zero; @@ -384,8 +384,6 @@ public MainWindowViewModel( DiskDriveMenuViewModel = diskDriveMenuViewModel; recentFilesViewModel.OpenRecentFileAsync = async fileName => await HandleLoadFileAsync(fileName); - SpectrumScreen = _frameBufferConverter.ScreenBitmap; - tapeManager.TapeChanged += HandleTapeTapeChanged; microdriveManager.CartridgeChanged += HandleCartridgeChanged; diskDriveManager.DiskChanged += HandleFloppyDiskChanged; @@ -462,8 +460,7 @@ private void EmulatorFrameCompleted(FrameBuffer frameBuffer, AudioBuffer audioBu Dispatcher.UIThread.Post(() => { - _frameBufferConverter.UpdateBitmap(frameBuffer); - + _frameBufferConverter?.UpdateBitmap(); ScreenControl.InvalidateVisual(); }); diff --git a/src/Spectron/ViewModels/StatusBarViewModel.cs b/src/Spectron/ViewModels/StatusBarViewModel.cs index b77c3de5..cb9c0851 100644 --- a/src/Spectron/ViewModels/StatusBarViewModel.cs +++ b/src/Spectron/ViewModels/StatusBarViewModel.cs @@ -74,7 +74,7 @@ public partial class StatusBarViewModel : ObservableObject ComputerType.Spectrum16K => "16k", ComputerType.Spectrum48K => "48k", ComputerType.Spectrum128K => "128k", - ComputerType.Timex2048 => "Timex", + ComputerType.Timex2048 => "TC2048", _ => "Unknown" }; diff --git a/src/Spectron/Views/MainWindow.axaml b/src/Spectron/Views/MainWindow.axaml index d964c8f3..6d6e7f7c 100644 --- a/src/Spectron/Views/MainWindow.axaml +++ b/src/Spectron/Views/MainWindow.axaml @@ -86,29 +86,34 @@ - - - - - - + + + + + + + + diff --git a/tests/Spectron.Emulator.Tests/ContentTests.cs b/tests/Spectron.Emulator.Tests/ContentTests.cs index c177aff7..6d153bd6 100644 --- a/tests/Spectron.Emulator.Tests/ContentTests.cs +++ b/tests/Spectron.Emulator.Tests/ContentTests.cs @@ -6,34 +6,59 @@ namespace OldBit.Spectron.Emulator.Tests; public class ContentTests { [Theory] - [InlineData(0, 14336, 0x0000, 0x1800)] - [InlineData(1, 14344, 0x0002, 0x1802)] - [InlineData(15, 14456, 0x001E, 0x181E)] - [InlineData(16, 14560, 0x0100, 0x1800)] - [InlineData(3056, 57120, 0x17E0, 0x1AE0)] - [InlineData(3071, 57240, 0x17FE, 0x1AFE)] - public void ContentRenderer_ShouldBuildScreenEventsTableSpectrum48(int index, int expectedTicks, Word expectedBitmapAddress, Word expectedAttributeAddress) + [InlineData(0, 14336, 0x0000, 0x1800, 22576)] + [InlineData(1, 14344, 0x0002, 0x1802, 22592)] + [InlineData(15, 14456, 0x001E, 0x181E, 22816)] + [InlineData(16, 14560, 0x0100, 0x1800, 22928)] + [InlineData(3056, 57120, 0x17E0, 0x1AE0, 89808)] + [InlineData(3071, 57240, 0x17FE, 0x1AFE, 90048)] + public void ContentRenderer_ShouldBuildScreenEventsTableSpectrum48(int index, int expectedTicks, + Word expectedBitmapAddress, Word expectedAttributeAddress, int expectedFrameBufferIndex) { - var eventsTable = FastLookup.GetScreenRenderEvents(Hardware.Spectrum48K); + var eventsTable = FastLookup.GetScreenRenderEvents(Hardware.Spectrum48K, new FrameBuffer()); eventsTable[index].Ticks.ShouldBe(expectedTicks); eventsTable[index].BitmapAddress.ShouldBe(expectedBitmapAddress); eventsTable[index].AttributeAddress.ShouldBe(expectedAttributeAddress); + eventsTable[index].FrameBufferIndex.ShouldBe(expectedFrameBufferIndex); } [Theory] - [InlineData(0, 14362, 0x0000, 0x1800)] - [InlineData(1, 14370, 0x0002, 0x1802)] - [InlineData(15, 14482, 0x001E, 0x181E)] - [InlineData(16, 14590, 0x0100, 0x1800)] - [InlineData(3056, 57910, 0x17E0, 0x1AE0)] - [InlineData(3071, 58030, 0x17FE, 0x1AFE)] - public void ContentRenderer_ShouldBuildScreenEventsTableSpectrum128(int index, int expectedTicks, Word expectedBitmapAddress, Word expectedAttributeAddress) + [InlineData(0, 14362, 0x0000, 0x1800, 22224)] + [InlineData(1, 14370, 0x0002, 0x1802, 22240)] + [InlineData(15, 14482, 0x001E, 0x181E, 22464)] + [InlineData(16, 14590, 0x0100, 0x1800, 22576)] + [InlineData(3056, 57910, 0x17E0, 0x1AE0, 89456)] + [InlineData(3071, 58030, 0x17FE, 0x1AFE, 89696)] + public void ContentRenderer_ShouldBuildScreenEventsTableSpectrum128(int index, int expectedTicks, + Word expectedBitmapAddress, Word expectedAttributeAddress, int expectedFrameBufferIndex) { - var eventsTable = FastLookup.GetScreenRenderEvents(Hardware.Spectrum128K); + var eventsTable = FastLookup.GetScreenRenderEvents(Hardware.Spectrum128K, new FrameBuffer()); eventsTable[index].Ticks.ShouldBe(expectedTicks); eventsTable[index].BitmapAddress.ShouldBe(expectedBitmapAddress); eventsTable[index].AttributeAddress.ShouldBe(expectedAttributeAddress); + eventsTable[index].FrameBufferIndex.ShouldBe(expectedFrameBufferIndex); + } + + [Theory] + [InlineData(0, 14336, 0x0000, 0x1800, 45152)] + [InlineData(1, 14344, 0x0002, 0x1802, 45184)] + [InlineData(15, 14456, 0x001E, 0x181E, 45632)] + [InlineData(16, 14560, 0x0100, 0x1800, 45856)] + [InlineData(3056, 57120, 0x17E0, 0x1AE0, 179616)] + [InlineData(3071, 57240, 0x17FE, 0x1AFE, 180096)] + public void ContentRenderer_ShouldBuildScreenEventsTableTimexHiRes(int index, int expectedTicks, + Word expectedBitmapAddress, Word expectedAttributeAddress, int expectedFrameBufferIndex) + { + var frameBuffer = new FrameBuffer(); + frameBuffer.ChangeScreenMode(ScreenMode.TimexHiRes); + + var eventsTable = FastLookup.GetScreenRenderEvents(Hardware.Timex2048, frameBuffer, ScreenMode.TimexHiRes); + + eventsTable[index].Ticks.ShouldBe(expectedTicks); + eventsTable[index].BitmapAddress.ShouldBe(expectedBitmapAddress); + eventsTable[index].AttributeAddress.ShouldBe(expectedAttributeAddress); + eventsTable[index].FrameBufferIndex.ShouldBe(expectedFrameBufferIndex); } } \ No newline at end of file diff --git a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs index ddb761db..360ff3c9 100644 --- a/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs +++ b/tests/Spectron.Emulator.Tests/Devices/Audio/AudioManagerTests.cs @@ -10,7 +10,7 @@ public class AudioManagerTests public void AudioManager_ShouldCreateCorrectly() { var clock = new Clock(); - var audionManager = new AudioManager(clock, null, Hardware.Spectrum128K); + var audionManager = new AudioManager(clock, null, Hardware.Spectrum128K, port => (port & 0x01) == 0); audionManager.IsAySupported.ShouldBeTrue(); audionManager.StereoMode.ShouldBe(StereoMode.Mono); diff --git a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs index ebd55c4f..a3bb7160 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests128.cs @@ -17,7 +17,7 @@ public FloatingBusTests128() _memory = new Memory128K(new byte[16384], new byte[16384]); _clock = new Clock(); - _floatingBus = new FloatingBus(Hardware.Spectrum128K, _memory, _clock); + _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 4fcb2f31..cb866a88 100644 --- a/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs +++ b/tests/Spectron.Emulator.Tests/Devices/FloatingBusTests48.cs @@ -17,7 +17,7 @@ public FloatingBusTests48() _memory = new Memory48K(new byte[16384]); _clock = new Clock(); - _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, _clock); + _floatingBus = new FloatingBus(Hardware.Spectrum48K, _memory, _clock, port => (port & 0x01) == 0); } [Fact] diff --git a/tests/Spectron.Emulator.Tests/Fixtures/MemoryExtensions.cs b/tests/Spectron.Emulator.Tests/Fixtures/MemoryExtensions.cs index c275bca3..9e976edc 100644 --- a/tests/Spectron.Emulator.Tests/Fixtures/MemoryExtensions.cs +++ b/tests/Spectron.Emulator.Tests/Fixtures/MemoryExtensions.cs @@ -4,47 +4,38 @@ namespace OldBit.Spectron.Emulator.Tests.Fixtures; internal static class MemoryExtensions { - internal static byte[] ReadAll(this IEmulatorMemory memory) + extension(IEmulatorMemory memory) { - var result = new byte[65536]; - - for (var address = 0; address < 65536; address++) + internal byte[] ReadRange(int startAddress, int count) { - result[address] = memory.Read((Word)address); - } + var result = new byte[count]; - return result; - } + for (var i = 0; i < count; i++) + { + result[i] = memory.Read((Word)(startAddress + i)); + } - internal static byte[] ReadRange(this IEmulatorMemory memory, int startAddress, int count) - { - var result = new byte[count]; - - for (var i = 0; i < count; i++) - { - result[i] = memory.Read((Word)(startAddress + i)); + return result; } - return result; - } + internal byte[] ReadScreen() + { + var result = new byte[16384]; - internal static byte[] ReadScreen(this IEmulatorMemory memory) - { - var result = new byte[16384]; + for (Word address = 0; address < 16384; address++) + { + result[address] = memory.ReadScreen(address); + } - for (Word address = 0; address < 16384; address++) - { - result[address] = memory.ReadScreen(address); + return result; } - return result; - } - - internal static void Fill(this IEmulatorMemory memory, int startAddress, int count, byte value) - { - for (var i = 0; i < count; i++) + internal void Fill(int startAddress, int count, byte value) { - memory.Write((Word)(startAddress + i), value); + for (var i = 0; i < count; i++) + { + memory.Write((Word)(startAddress + i), value); + } } } } \ No newline at end of file diff --git a/tests/Spectron.Emulator.Tests/Screen/BorderTests.cs b/tests/Spectron.Emulator.Tests/Screen/BorderTests.cs index 2a6f2046..2228c6a6 100644 --- a/tests/Spectron.Emulator.Tests/Screen/BorderTests.cs +++ b/tests/Spectron.Emulator.Tests/Screen/BorderTests.cs @@ -9,6 +9,9 @@ public class BorderTests private readonly List _borderTicks = Border.BuildBorderTickRanges(Hardware.Spectrum48K.RetraceTicks, Hardware.Spectrum48K.BorderTop); + private readonly List _hiResBorderTicks = + Border.BuildBorderTickRanges(Hardware.Timex2048.RetraceTicks, Hardware.Timex2048.BorderTop, ScreenSize.ContentWidth * 2); + [Theory] [InlineData(0, 0, 151, 48)] // first top border line [InlineData(1, 200, 375, 352)] @@ -29,18 +32,42 @@ public void BorderTicksTable_ShouldHaveCorrectRange(int index, int startTick, in _borderTicks[index].StartTick.ShouldBe(startTick); _borderTicks[index].EndTick.ShouldBe(endTick); _borderTicks[index].StartPixel.ShouldBe(startPixel); + _borderTicks[index].Shift.ShouldBe(1); + } + + [Theory] + [InlineData(0, 0, 151, 96, 2)] + [InlineData(1, 200, 375, 704, 2)] + [InlineData(2, 424, 599, 1408, 2)] + [InlineData(3, 648, 823, 2112, 2)] + [InlineData(4, 872, 1047, 2816, 2)] + [InlineData(191, 28576, 28599, 90016, 2)] + [InlineData(501, 69192, 69367, 217536, 2)] + [InlineData(502, 69416, 69591, 218240, 2)] + [InlineData(503, 69640, 69815, 218944, 2)] + public void BorderTicksTable_ShouldHaveCorrectHiResRanges( + int index, + int startTick, + int endTick, + int startPixel, + int shift) + { + _hiResBorderTicks[index].StartTick.ShouldBe(startTick); + _hiResBorderTicks[index].EndTick.ShouldBe(endTick); + _hiResBorderTicks[index].StartPixel.ShouldBe(startPixel); + _hiResBorderTicks[index].Shift.ShouldBe(shift); } [Fact] public void BorderRenderer_ShouldSetBorderBlue() { - var screenBuffer = new FrameBuffer(SpectrumPalette.White); + var screenBuffer = new FrameBuffer(); var borderRenderer = new Border(Hardware.Spectrum48K, screenBuffer); borderRenderer.Update(SpectrumPalette.Blue); borderRenderer.Update(SpectrumPalette.Blue, 69888); - BorderShouldHaveColor(SpectrumPalette.Blue, screenBuffer.Pixels); + BorderShouldHaveColor(SpectrumPalette.Blue, screenBuffer.Pixels[..109823]); } [Fact] @@ -48,7 +75,7 @@ public void BorderRenderer_ShouldSetBorderRed() { var random = new Random(69888); - var screenBuffer = new FrameBuffer(SpectrumPalette.White); + var screenBuffer = new FrameBuffer(); var borderRenderer = new Border(Hardware.Spectrum48K, screenBuffer); borderRenderer.Update(SpectrumPalette.Red); @@ -59,13 +86,13 @@ public void BorderRenderer_ShouldSetBorderRed() borderRenderer.Update(SpectrumPalette.Red, ticks); } - BorderShouldHaveColor(SpectrumPalette.Red, screenBuffer.Pixels); + BorderShouldHaveColor(SpectrumPalette.Red, screenBuffer.Pixels[..109823]); } [Fact] public void BorderRenderer_ShouldMatchAquaplane() { - var screenBuffer = new FrameBuffer(SpectrumPalette.White); + var screenBuffer = new FrameBuffer(); var borderRenderer = new Border(Hardware.Spectrum48K, screenBuffer); borderRenderer.Update(SpectrumPalette.Cyan, 1); @@ -77,6 +104,21 @@ public void BorderRenderer_ShouldMatchAquaplane() screenBuffer.Pixels[50..351].ShouldAllBe(c => c == SpectrumPalette.Cyan); } + [Fact] + public void BorderRenderer_ShouldFillFullTopAndBottomBordersInTimexHiRes() + { + var screenBuffer = new FrameBuffer(); + screenBuffer.ChangeScreenMode(ScreenMode.TimexHiRes); + + var borderRenderer = new Border(Hardware.Timex2048, screenBuffer); + borderRenderer.ChangeScreenMode(ScreenMode.TimexHiRes); + borderRenderer.Update(SpectrumPalette.Red); + + borderRenderer.Update(SpectrumPalette.Red, Hardware.Timex2048.TicksPerFrame); + + HiResBorderShouldHaveColor(SpectrumPalette.Red, screenBuffer.Pixels); + } + private static void BorderShouldHaveColor(Color color, Color[] screenBuffer) { screenBuffer[..47].ShouldAllBe(c => c == SpectrumPalette.White); @@ -91,4 +133,35 @@ private static void BorderShouldHaveColor(Color color, Color[] screenBuffer) screenBuffer[90112..].ShouldAllBe(c => c == color); } -} \ No newline at end of file + + private static void HiResBorderShouldHaveColor(Color color, Color[] screenBuffer) + { + const int width = (ScreenSize.BorderLeft + ScreenSize.ContentWidth + ScreenSize.BorderRight) * 2; + const int topPixels = ScreenSize.BorderTop * width; + const int bottomStart = topPixels + ScreenSize.ContentHeight * width; + + screenBuffer[..95].ShouldAllBe(c => c == SpectrumPalette.White); + screenBuffer[96..topPixels].ShouldAllBe(c => c == color); + + for (var i = 0; i < ScreenSize.ContentHeight; i++) + { + screenBuffer + .Skip(topPixels + i * width) + .Take(ScreenSize.BorderLeft * 2) + .ShouldAllBe(c => c == color); + + screenBuffer + .Skip(topPixels + i * width + ScreenSize.BorderLeft * 2) + .Take(ScreenSize.ContentWidth * 2) + .ShouldAllBe(c => c == SpectrumPalette.White); + + screenBuffer + .Skip(topPixels + i * width + ScreenSize.BorderLeft * 2 + ScreenSize.ContentWidth * 2) + .Take(ScreenSize.BorderRight) + .ShouldAllBe(c => c == color); + } + + screenBuffer[bottomStart..(width * (ScreenSize.BorderTop + ScreenSize.ContentHeight + ScreenSize.BorderBottom))] + .ShouldAllBe(c => c == color); + } +} diff --git a/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs b/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs index f4bd2d96..9df7f86e 100644 --- a/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs +++ b/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs @@ -15,10 +15,10 @@ public void Test() var memory = new Memory48K(rom); var ulaPlus = new UlaPlus(); - var buffer = new ScreenBuffer(Hardware.Spectrum48K, memory, ulaPlus); + var screenBuffer = new ScreenBuffer(Hardware.Spectrum48K, memory, ulaPlus); - buffer.UpdateBorder(SpectrumPalette.Cyan, 224); - buffer.UpdateBorder(SpectrumPalette.White, 448); + screenBuffer.UpdateBorder(SpectrumPalette.Cyan, 224); + screenBuffer.UpdateBorder(SpectrumPalette.White, 448); // TODO: Write some tests for the frame buffer, not so easy }