diff --git a/src/Spectron.Emulation/Devices/Ula.cs b/src/Spectron.Emulation/Devices/Ula.cs index 80a765bc..87b146e6 100644 --- a/src/Spectron.Emulation/Devices/Ula.cs +++ b/src/Spectron.Emulation/Devices/Ula.cs @@ -32,8 +32,7 @@ public virtual void WritePort(Word address, byte value) return; } - var color = SpectrumPalette.GetBorderColor(value); - screenBuffer.UpdateBorder(color, clock.UlaTicks); + screenBuffer.UpdateBorder((byte)(value & 0x07), clock.UlaTicks); } internal virtual bool IsUlaPort(Word address) => (address & 0x01) == 0x00; diff --git a/src/Spectron.Emulation/Devices/UlaPlus.cs b/src/Spectron.Emulation/Devices/UlaPlus.cs index c6d9283a..1a7faf6d 100644 --- a/src/Spectron.Emulation/Devices/UlaPlus.cs +++ b/src/Spectron.Emulation/Devices/UlaPlus.cs @@ -25,12 +25,12 @@ internal bool IsActive set { field = value; - ActiveChanged?.Invoke(EventArgs.Empty); + ActiveChanged?.Invoke(this, EventArgs.Empty); } } - internal delegate void ActiveChangedEvent(EventArgs e); - internal event ActiveChangedEvent? ActiveChanged; + internal event EventHandler? ActiveChanged; + internal event EventHandler? PaletteChanged; public void WritePort(Word address, byte value) { @@ -56,6 +56,7 @@ public void WritePort(Word address, byte value) var color = ColorFromValue(value); PaletteColors[paletteIndex][colorIndex] = color; + PaletteChanged?.Invoke(this, EventArgs.Empty); break; case Register.ModeGroup: @@ -85,6 +86,9 @@ internal Color GetInkColor(byte attribute) return palette[colorIndex]; } + internal Color GetBorderColor(byte borderIndex, bool isHiRes = false) => + PaletteColors[isHiRes ? 1 : 0][(borderIndex & 0x07) | 8]; + internal Color GetPaperColor(byte attribute) { var paletteIndex = attribute >> 6; diff --git a/src/Spectron.Emulation/Devices/UlaTimex.cs b/src/Spectron.Emulation/Devices/UlaTimex.cs index b7ecbba9..853ca0bb 100644 --- a/src/Spectron.Emulation/Devices/UlaTimex.cs +++ b/src/Spectron.Emulation/Devices/UlaTimex.cs @@ -18,6 +18,7 @@ internal sealed class UlaTimex( internal ScreenMode ScreenMode { get; private set; } internal Color Paper { get; private set; } internal Color Ink { get; private set; } + internal byte PaperIndex { get; private set; } = 0x07; internal event EventHandler? ScreenModeChanged; @@ -62,30 +63,32 @@ public override void WritePort(Word address, byte value) 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, + 0b000_000 => SpectrumPalette.Black, // 000 + 0b001_000 => SpectrumPalette.BrightBlue, // 001 + 0b010_000 => SpectrumPalette.BrightRed, // 010 + 0b011_000 => SpectrumPalette.BrightMagenta, // 011 + 0b100_000 => SpectrumPalette.BrightGreen, // 100 + 0b101_000 => SpectrumPalette.BrightCyan, // 101 + 0b110_000 => SpectrumPalette.BrightYellow, // 110 + 0b111_000 => SpectrumPalette.BrightWhite, // 111 _ => 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, + 0b000_000 => SpectrumPalette.BrightWhite, // 111 + 0b001_000 => SpectrumPalette.BrightYellow, // 110 + 0b010_000 => SpectrumPalette.BrightCyan, // 101 + 0b011_000 => SpectrumPalette.BrightGreen, // 100 + 0b100_000 => SpectrumPalette.BrightMagenta, // 011 + 0b101_000 => SpectrumPalette.BrightRed, // 010 + 0b110_000 => SpectrumPalette.BrightBlue, // 001 + 0b111_000 => SpectrumPalette.Black, // 000 _ => throw new ArgumentOutOfRangeException(nameof(value)) }; + PaperIndex = (byte)(~(value >> 3) & 0x07); // Inverted to match the paper color index + if (_lastControlValue != value) { ScreenModeChanged?.Invoke(this, EventArgs.Empty); diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index cdf8eda3..e8f96e2d 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -242,7 +242,8 @@ private void AddEventHandlers() { Cpu.Clock.TicksAdded += (_, previousFrameTicks, _) => ScreenBuffer.UpdateScreen(previousFrameTicks / Clock.Multiplier); Cpu.BeforeInstruction += BeforeInstruction; - UlaPlus.ActiveChanged += _ => _invalidateScreen = true; + UlaPlus.ActiveChanged += (_, _) => InvalidateUlaPlus(); + UlaPlus.PaletteChanged += (_, _) => InvalidateUlaPlus(); Beta128.DiskActivity += _ => DiskDriveManager.OnDiskActivity(); if (Ula is UlaTimex ulaTimex) @@ -345,7 +346,15 @@ private void EndFrame() private void ToggleUlaPlus(bool value) { UlaPlus.IsEnabled = value; + + InvalidateUlaPlus(); + } + + private void InvalidateUlaPlus() + { _invalidateScreen = true; + + ScreenBuffer.RefreshBorder(Clock.UlaTicks); } private void BeforeInstruction(Word pc) diff --git a/src/Spectron.Emulation/Extensions/EmulatorStateExtensions.cs b/src/Spectron.Emulation/Extensions/EmulatorStateExtensions.cs index f7bde6df..0d6dfaf4 100644 --- a/src/Spectron.Emulation/Extensions/EmulatorStateExtensions.cs +++ b/src/Spectron.Emulation/Extensions/EmulatorStateExtensions.cs @@ -8,50 +8,65 @@ public static class EmulatorStateExtensions { private static readonly int[] Buffer = new int[ScreenSize.ContentHeight * ScreenSize.ContentWidth]; - public static int[] GetScreenshot(this StateSnapshot snapshot, bool isFlashOnFrame = false) + extension(StateSnapshot snapshot) { - var screenMemory = snapshot.ComputerType == ComputerType.Spectrum128K ? - snapshot.Memory.Banks[5] : - snapshot.Memory.Banks[0]; - - for (var line = 0; line < ScreenSize.ContentHeight; line++) + public int[] GetScreenshot(bool isFlashOnFrame = false) { - for (var column = 0; column < 32; column++) - { - var bitmapAddress = ScreenAddress.Calculate(column, line) - 0x4000; - var attributeAddress = ScreenAddress.CalculateAttribute(column, line) - 0x4000; + var screenMemory = snapshot.ComputerType == ComputerType.Spectrum128K ? + snapshot.Memory.Banks[5] : + snapshot.Memory.Banks[0]; - var bitmap = screenMemory[bitmapAddress]; - var attribute = screenMemory[attributeAddress]; + var isUlaPlus = snapshot.UlaPlus is { IsEnabled: true, IsActive: true }; - var attributeData = FastLookup.AttributeData[attribute]; - var isFlashOn = attributeData.IsFlashOn && isFlashOnFrame; + for (var line = 0; line < ScreenSize.ContentHeight; line++) + { + for (var column = 0; column < 32; column++) + { + var bitmapAddress = ScreenAddress.Calculate(column, line) - 0x4000; + var attributeAddress = ScreenAddress.CalculateAttribute(column, line) - 0x4000; - var bufferIndex = 256 * line + 8 * column; + var bitmap = screenMemory[bitmapAddress]; + var attribute = screenMemory[attributeAddress]; - for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) - { - Color color; + var attributeData = FastLookup.AttributeData[attribute]; + var isFlashOn = attributeData.IsFlashOn && isFlashOnFrame; - if (snapshot.UlaPlus != null && snapshot.UlaPlus.PaletteGroup != 0) - { - color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? - snapshot.UlaPlus.GetInkColor(attribute) : - snapshot.UlaPlus.GetPaperColor(attribute); - } - else + var bufferIndex = 256 * line + 8 * column; + + for (var bit = 0; bit < FastLookup.BitMasks.Length; bit++) { - color = (bitmap & FastLookup.BitMasks[bit]) != 0 ^ isFlashOn ? - attributeData.Ink : - attributeData.Paper; - } + Color color; - Buffer[bufferIndex + bit] = (int)color.Abgr; + if (isUlaPlus) + { + color = (bitmap & FastLookup.BitMasks[bit]) != 0 ? + snapshot.UlaPlus!.GetInkColor(attribute) : + snapshot.UlaPlus!.GetPaperColor(attribute); + } + else + { + color = (bitmap & FastLookup.BitMasks[bit]) != 0 ^ isFlashOn ? + attributeData.Ink : + attributeData.Paper; + } + + Buffer[bufferIndex + bit] = (int)color.Abgr; + } } } + + return Buffer; } - return Buffer; + public Color GetBorderColor() + { + if (snapshot.UlaPlus is { IsEnabled: true, IsActive: true } ulaPlus) + { + return ulaPlus.GetBorderColor(snapshot.Border); + } + + return SpectrumPalette.GetBorderColor(snapshot.Border); + } } extension(UlaPlusState ulaPlusState) @@ -71,5 +86,8 @@ private Color GetPaperColor(byte attribute) return ulaPlusState.PaletteColors[paletteIndex][colorIndex]; } + + private Color GetBorderColor(byte borderIndex) => + ulaPlusState.PaletteColors[0][(borderIndex & 0x07) | 8]; } } \ No newline at end of file diff --git a/src/Spectron.Emulation/Screen/ScreenBuffer.cs b/src/Spectron.Emulation/Screen/ScreenBuffer.cs index 6da15c4f..265691b7 100644 --- a/src/Spectron.Emulation/Screen/ScreenBuffer.cs +++ b/src/Spectron.Emulation/Screen/ScreenBuffer.cs @@ -8,14 +8,16 @@ public sealed class ScreenBuffer { private readonly Border _border; private readonly Content _content; + private readonly UlaPlus _ulaPlus; private bool _borderColorChanged = true; private Color? _lockedBorderColor; + private byte _lockedBorderColorIndex; private ScreenMode _screenMode = ScreenMode.Spectrum; public FrameBuffer FrameBuffer { get; } - internal Color LastBorderColor { get; private set; } = SpectrumPalette.White; + internal byte LastBorderColorIndex { get; private set; } = 0x07; public event EventHandler? FrameBufferChanged; @@ -23,6 +25,7 @@ internal ScreenBuffer(HardwareSettings hardware, IEmulatorMemory memory, UlaPlus { FrameBuffer = new FrameBuffer(); + _ulaPlus = ulaPlus; _border = new Border(hardware, FrameBuffer); _content = new Content(hardware, FrameBuffer, memory, ulaPlus); @@ -32,14 +35,15 @@ internal ScreenBuffer(HardwareSettings hardware, IEmulatorMemory memory, UlaPlus } } - internal void ChangeScreenMode(ScreenMode screenMode, Color ink, Color paper, int frameTicks) + internal void ChangeScreenMode(ScreenMode screenMode, Color ink, Color paper, byte paperIndex, int frameTicks) { _lockedBorderColor = screenMode.IsTimexHiRes() ? paper : null; + _lockedBorderColorIndex = paperIndex; _content.ChangeScreenMode(screenMode, ink, paper); _border.ChangeScreenMode(screenMode); - _border.Update(_lockedBorderColor ?? LastBorderColor, frameTicks); + _border.Update(BorderColor, frameTicks); if (_screenMode != screenMode) { @@ -55,13 +59,15 @@ internal void NewFrame() _content.NewFrame(); } - internal void EndFrame(int frameTicks) => _border.Update(_lockedBorderColor ?? LastBorderColor, frameTicks); + internal void EndFrame(int frameTicks) => _border.Update(BorderColor, frameTicks); - internal void UpdateBorder(Color borderColor, int frameTicks = 0) + internal void UpdateBorder(byte borderColor, int frameTicks = 0) { - if (LastBorderColor != borderColor) + borderColor &= 0x07; + + if (LastBorderColorIndex != borderColor) { - LastBorderColor = borderColor; + LastBorderColorIndex = borderColor; _borderColorChanged = true; } @@ -70,11 +76,33 @@ internal void UpdateBorder(Color borderColor, int frameTicks = 0) return; } - _border.Update(_lockedBorderColor ?? borderColor, frameTicks); + _border.Update(BorderColor, frameTicks); _borderColorChanged = false; } + internal void RefreshBorder(int frameTicks = 0) + { + _borderColorChanged = true; + + UpdateBorder(LastBorderColorIndex, frameTicks); + } + + private Color BorderColor + { + get + { + if (_ulaPlus is { IsEnabled: true, IsActive: true }) + { + return _lockedBorderColor != null ? + _ulaPlus.GetBorderColor(_lockedBorderColorIndex, isHiRes: true) : + _ulaPlus.GetBorderColor(LastBorderColorIndex); + } + + return _lockedBorderColor ?? SpectrumPalette.GetBorderColor(LastBorderColorIndex); + } + } + internal void Reset() { _border.Reset(); diff --git a/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs b/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs index 1650801c..c92ce6d8 100644 --- a/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs +++ b/src/Spectron.Emulation/Screen/ScreenMemoryHandler.cs @@ -70,6 +70,7 @@ internal void SetScreenMode(UlaTimex? ulaTimex, int frameTicks = 0) screenMode, ulaTimex?.Ink ?? SpectrumPalette.Black, ulaTimex?.Paper ?? SpectrumPalette.White, + ulaTimex?.PaperIndex ?? 0x07, frameTicks); } diff --git a/src/Spectron.Emulation/Screen/SpectrumPalette.cs b/src/Spectron.Emulation/Screen/SpectrumPalette.cs index 21eb58ac..661c66d5 100644 --- a/src/Spectron.Emulation/Screen/SpectrumPalette.cs +++ b/src/Spectron.Emulation/Screen/SpectrumPalette.cs @@ -35,9 +35,6 @@ public static class SpectrumPalette { 0b0000111, White } }; - internal static readonly Dictionary ReverseBorderColors = - BorderColors.ToDictionary(x => x.Value, x => x.Key); - private static readonly Dictionary PaperColors = new() { { 0b00000000, Black }, diff --git a/src/Spectron.Emulation/Snapshot/SnaSnapshot.cs b/src/Spectron.Emulation/Snapshot/SnaSnapshot.cs index 5b6ae763..e72571f6 100644 --- a/src/Spectron.Emulation/Snapshot/SnaSnapshot.cs +++ b/src/Spectron.Emulation/Snapshot/SnaSnapshot.cs @@ -41,7 +41,7 @@ internal void Save(string fileName, Emulator emulator) InterruptMode = (byte)emulator.Cpu.IM, Interrupt = (byte)((emulator.Cpu.IFF2 ? 0x04 : 0x00) | (emulator.Cpu.IFF1 ? 0x02 : 0x00)), - BorderColor = SpectrumPalette.ReverseBorderColors[emulator.ScreenBuffer.LastBorderColor] + BorderColor = emulator.ScreenBuffer.LastBorderColorIndex } }; @@ -123,10 +123,8 @@ internal static void Update(Emulator emulator, SnaFile snapshot, bool updateBord return; } - var borderColor = SpectrumPalette.GetBorderColor(snapshot.Header.BorderColor); - screenBuffer.Reset(); - screenBuffer.UpdateBorder(borderColor); + screenBuffer.UpdateBorder(snapshot.Header.BorderColor); } private Emulator CreateEmulator(SnaFile snapshot) diff --git a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs index bd07c3d7..4e9450e3 100644 --- a/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs +++ b/src/Spectron.Emulation/Snapshot/SzxSnapshot.cs @@ -182,10 +182,8 @@ private static void LoadMemory(IMemory memory, List ramPages, Spec private static void UpdateBorder(ScreenBuffer screenBuffer, SpecRegsBlock specRegs) { - var borderColor = SpectrumPalette.GetBorderColor(specRegs.Border); - screenBuffer.Reset(); - screenBuffer.UpdateBorder(borderColor); + screenBuffer.UpdateBorder(specRegs.Border); } private static void LoadUlaPlus(UlaPlus ulaPlus, PaletteBlock? palette) @@ -335,7 +333,7 @@ private static void SaveCustomRom(IMemory memory, SzxFile snapshot, CompressionL } private static void SaveSpectrumRegisters(ScreenBuffer screenBuffer, SpecRegsBlock specRegs) => - specRegs.Border = SpectrumPalette.ReverseBorderColors[screenBuffer.LastBorderColor]; + specRegs.Border = screenBuffer.LastBorderColorIndex; private static void SaveUlaPlus(UlaPlus ulaPlus, SzxFile snapshot) { diff --git a/src/Spectron.Emulation/Snapshot/Z80Snapshot.cs b/src/Spectron.Emulation/Snapshot/Z80Snapshot.cs index 09bf84fc..dabc50ba 100644 --- a/src/Spectron.Emulation/Snapshot/Z80Snapshot.cs +++ b/src/Spectron.Emulation/Snapshot/Z80Snapshot.cs @@ -42,7 +42,7 @@ internal void Save(string fileName, Emulator emulator) IFF2 = (byte)(emulator.Cpu.IFF2 ? 1 : 0), Flags1 = { - BorderColor = SpectrumPalette.ReverseBorderColors[emulator.ScreenBuffer.LastBorderColor], + BorderColor = emulator.ScreenBuffer.LastBorderColorIndex, }, Flags2 = { @@ -134,8 +134,7 @@ internal static void Update(Emulator emulator, Z80File snapshot, bool updateBord } screenBuffer.Reset(); - var borderColor = SpectrumPalette.GetBorderColor(snapshot.Header.Flags1.BorderColor); - screenBuffer.UpdateBorder(borderColor); + screenBuffer.UpdateBorder(snapshot.Header.Flags1.BorderColor); } private Emulator CreateEmulator(Z80File snapshot) diff --git a/src/Spectron.Emulation/State/StateSnapshot.cs b/src/Spectron.Emulation/State/StateSnapshot.cs index bc0faf07..79bf0d2c 100644 --- a/src/Spectron.Emulation/State/StateSnapshot.cs +++ b/src/Spectron.Emulation/State/StateSnapshot.cs @@ -9,7 +9,7 @@ public sealed partial class StateSnapshot { public ComputerType ComputerType { get; set; } - public Color BorderColor { get; set; } + public byte Border { get; set; } public CpuState Cpu { get; set; } = new(); diff --git a/src/Spectron.Emulation/State/StateSnapshotManager.cs b/src/Spectron.Emulation/State/StateSnapshotManager.cs index 16b0989d..14ad65f4 100644 --- a/src/Spectron.Emulation/State/StateSnapshotManager.cs +++ b/src/Spectron.Emulation/State/StateSnapshotManager.cs @@ -275,8 +275,10 @@ private static void SaveTimex(Ula ula, StateSnapshot stateSnapshot) }; } - private static void SaveOther(Emulator emulator, StateSnapshot stateSnapshot) => - stateSnapshot.BorderColor = emulator.ScreenBuffer.LastBorderColor; + private static void SaveOther(Emulator emulator, StateSnapshot stateSnapshot) + { + stateSnapshot.Border = emulator.ScreenBuffer.LastBorderColorIndex; + } private static void SaveCustomRom(Emulator emulator, StateSnapshot stateSnapshot) { @@ -385,8 +387,10 @@ private static void LoadTape(TapeManager tapeManager, TapeState? tapeState) private static void LoadOther(Emulator emulator, StateSnapshot stateSnapshot) { + var border = stateSnapshot.Border; + emulator.ScreenBuffer.Reset(); - emulator.ScreenBuffer.UpdateBorder(stateSnapshot.BorderColor); + emulator.ScreenBuffer.UpdateBorder(border); } private static void LoadAy(AudioManager audioManager, AyState? ayState) diff --git a/src/Spectron/ViewModels/TimeMachineViewModel.cs b/src/Spectron/ViewModels/TimeMachineViewModel.cs index 19bc7969..be0b1d7f 100644 --- a/src/Spectron/ViewModels/TimeMachineViewModel.cs +++ b/src/Spectron/ViewModels/TimeMachineViewModel.cs @@ -129,7 +129,8 @@ private void UpdatePreview() Marshal.Copy(screenshot, 0, bitmap.Address, screenshot.Length); } - ScreenBorderBrush = new SolidColorBrush(snapshot.BorderColor.Argb); + var borderColor = snapshot.GetBorderColor(); + ScreenBorderBrush = new SolidColorBrush(borderColor.Argb); PreviewControl?.InvalidateVisual(); } diff --git a/src/Spectron/Views/AboutView.axaml b/src/Spectron/Views/AboutView.axaml index b63529a1..d21470f6 100644 --- a/src/Spectron/Views/AboutView.axaml +++ b/src/Spectron/Views/AboutView.axaml @@ -49,7 +49,7 @@ - + diff --git a/tests/Spectron.Emulator.Tests/Extensions/EmulatorStateExtensionsTests.cs b/tests/Spectron.Emulator.Tests/Extensions/EmulatorStateExtensionsTests.cs new file mode 100644 index 00000000..de1687c5 --- /dev/null +++ b/tests/Spectron.Emulator.Tests/Extensions/EmulatorStateExtensionsTests.cs @@ -0,0 +1,140 @@ +using OldBit.Spectron.Emulation; +using OldBit.Spectron.Emulation.Extensions; +using OldBit.Spectron.Emulation.Screen; +using OldBit.Spectron.Emulation.State; +using OldBit.Spectron.Emulation.State.Components; + +namespace OldBit.Spectron.Emulator.Tests.Extensions; + +public class EmulatorStateExtensionsTests +{ + [Fact] + public void GetBorderColor_WhenUlaPlusDisabled_ReturnsSpectrumPaletteColor() + { + var snapshot = new StateSnapshot { Border = 2 }; + + snapshot.GetBorderColor().ShouldBe(SpectrumPalette.GetBorderColor(2)); + } + + [Fact] + public void GetBorderColor_WhenUlaPlusEnabledButInactive_ReturnsSpectrumPaletteColor() + { + var snapshot = new StateSnapshot + { + Border = 2, + UlaPlus = new UlaPlusState + { + IsEnabled = true, + IsActive = false, + PaletteColors = CreatePalette(), + }, + }; + + snapshot.GetBorderColor().ShouldBe(SpectrumPalette.GetBorderColor(2)); + } + + [Fact] + public void GetBorderColor_WhenUlaPlusEnabledAndActive_ReturnsUlaPlusPaletteColor() + { + var expected = new Color(0x12, 0x34, 0x56); + var palette = CreatePalette(); + palette[0][(2 & 0x07) | 8] = expected; + + var snapshot = new StateSnapshot + { + Border = 2, + UlaPlus = new UlaPlusState + { + IsEnabled = true, + IsActive = true, + PaletteColors = palette, + }, + }; + + snapshot.GetBorderColor().ShouldBe(expected); + } + + [Fact] + public void GetScreenshot_WhenUlaPlusActive_UsesUlaPlusPaletteRegardlessOfPaletteGroup() + { + // Bitmap all zeros -> every pixel is paper. Attribute 0 -> palette 0, paper colour index 8. + // PaletteGroup is 0 here, proving the renderer keys off IsActive, not PaletteGroup. + var expected = new Color(0x11, 0x22, 0x33); + var palette = CreatePalette(); + palette[0][8] = expected; + + var snapshot = new StateSnapshot + { + ComputerType = ComputerType.Spectrum48K, + UlaPlus = new UlaPlusState + { + IsEnabled = true, + IsActive = true, + PaletteGroup = 0, + PaletteColors = palette, + }, + }; + snapshot.Memory.SetBank(new byte[0xC000], pageNumber: 0); + + var screenshot = snapshot.GetScreenshot(); + + screenshot[0].ShouldBe((int)expected.Abgr); + } + + [Fact] + public void GetScreenshot_WhenUlaPlusInactive_UsesStandardPalette() + { + // Attribute 0b0000_1000 -> blue paper in the standard palette; bitmap all zeros -> all paper. + var memory = new byte[0xC000]; + memory[0x1800] = 0b0000_1000; + + var snapshot = new StateSnapshot + { + ComputerType = ComputerType.Spectrum48K, + UlaPlus = new UlaPlusState + { + IsEnabled = true, + IsActive = false, + PaletteGroup = 0x3F, + PaletteColors = CreatePalette(), + }, + }; + snapshot.Memory.SetBank(memory, pageNumber: 0); + + var screenshot = snapshot.GetScreenshot(); + + screenshot[0].ShouldBe((int)SpectrumPalette.Blue.Abgr); + } + + [Fact] + public void GetScreenshot_AfterSerializationRoundTrip_PreservesUlaPlusPalette() + { + // Mirrors the exact Time Machine path: Serialize -> Deserialize -> GetScreenshot. + var expected = new Color(0x11, 0x22, 0x33); + var palette = CreatePalette(); + palette[0][8] = expected; + + var original = new StateSnapshot + { + ComputerType = ComputerType.Spectrum48K, + UlaPlus = new UlaPlusState + { + IsEnabled = true, + IsActive = true, + PaletteGroup = 0x3F, + PaletteColors = palette, + }, + }; + original.Memory.SetBank(new byte[0xC000], pageNumber: 0); + + var snapshot = StateSnapshot.Deserialize(original.Serialize()); + + snapshot.ShouldNotBeNull(); + var screenshot = snapshot.GetScreenshot(); + + screenshot[0].ShouldBe((int)expected.Abgr); + } + + private static Color[][] CreatePalette() => + [new Color[16], new Color[16], new Color[16], new Color[16]]; +} diff --git a/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs b/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs index 9df7f86e..0391ce1f 100644 --- a/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs +++ b/tests/Spectron.Emulator.Tests/Screen/ScreenBufferTests.cs @@ -7,6 +7,11 @@ namespace OldBit.Spectron.Emulator.Tests.Screen; public class ScreenBufferTests { + private const int RegisterPort = 0xBF3B; + private const int DataPort = 0xFF3B; + + private static readonly Color BrightGreen = new(0x00, 0xFF, 0x00); + [Fact] public void Test() { @@ -17,9 +22,88 @@ public void Test() var screenBuffer = new ScreenBuffer(Hardware.Spectrum48K, memory, ulaPlus); - screenBuffer.UpdateBorder(SpectrumPalette.Cyan, 224); - screenBuffer.UpdateBorder(SpectrumPalette.White, 448); + screenBuffer.UpdateBorder(5, 224); + screenBuffer.UpdateBorder(7, 448); // TODO: Write some tests for the frame buffer, not so easy } -} \ No newline at end of file + + [Fact] + public void UpdateBorder_ShouldUseStandardColor_WhenUlaPlusIsNotActive() + { + var ulaPlus = new UlaPlus { IsEnabled = true }; + var screenBuffer = CreateScreenBuffer(ulaPlus); + + SetPaletteEntry(ulaPlus, entry: 8, color: 0xE0); + + screenBuffer.UpdateBorder(2); + screenBuffer.EndFrame(Hardware.Spectrum48K.TicksPerFrame); + + screenBuffer.FrameBuffer.Pixels[100..5000].ShouldAllBe(color => color == SpectrumPalette.Red); + } + + [Fact] + public void UpdateBorder_ShouldUseUlaPlusPalette_WhenUlaPlusIsActive() + { + var ulaPlus = new UlaPlus { IsEnabled = true }; + var screenBuffer = CreateScreenBuffer(ulaPlus); + + // BORDER 2 uses PAPER 2 of the first CLUT, which is palette entry 10 + SetPaletteEntry(ulaPlus, entry: 10, color: 0xE0); + Activate(ulaPlus); + + screenBuffer.UpdateBorder(2); + screenBuffer.EndFrame(Hardware.Spectrum48K.TicksPerFrame); + + screenBuffer.FrameBuffer.Pixels[100..5000].ShouldAllBe(color => color == BrightGreen); + } + + [Fact] + public void RefreshBorder_ShouldUpdateBorder_WhenUlaPlusPaletteChanges() + { + var ulaPlus = new UlaPlus { IsEnabled = true }; + var screenBuffer = CreateScreenBuffer(ulaPlus); + + Activate(ulaPlus); + screenBuffer.UpdateBorder(2); + + SetPaletteEntry(ulaPlus, entry: 10, color: 0xE0); + screenBuffer.RefreshBorder(); + screenBuffer.EndFrame(Hardware.Spectrum48K.TicksPerFrame); + + screenBuffer.FrameBuffer.Pixels[100..5000].ShouldAllBe(color => color == BrightGreen); + } + + [Fact] + public void UpdateBorder_ShouldUseSecondUlaPlusClut_WhenTimexHiResIsActive() + { + var ulaPlus = new UlaPlus { IsEnabled = true }; + var screenBuffer = CreateScreenBuffer(ulaPlus, Hardware.Timex2048); + + // Hi-res PAPER 5 uses palette entry 13 of the second CLUT + SetPaletteEntry(ulaPlus, entry: 16 + 13, color: 0xE0); + Activate(ulaPlus); + + screenBuffer.ChangeScreenMode( + ScreenMode.TimexHiRes, SpectrumPalette.Black, SpectrumPalette.White, paperIndex: 5, frameTicks: 0); + + screenBuffer.EndFrame(Hardware.Timex2048.TicksPerFrame); + + screenBuffer.FrameBuffer.Pixels[100..5000].ShouldAllBe(color => color == BrightGreen); + } + + private static ScreenBuffer CreateScreenBuffer(UlaPlus ulaPlus, HardwareSettings? hardware = null) => + new(hardware ?? Hardware.Spectrum48K, new Memory48K(new byte[16384]), ulaPlus); + + private static void SetPaletteEntry(UlaPlus ulaPlus, int entry, byte color) + { + ulaPlus.WritePort(RegisterPort, (byte)entry); + ulaPlus.WritePort(DataPort, color); + } + + private static void Activate(UlaPlus ulaPlus) + { + ulaPlus.WritePort(RegisterPort, 0x40); + ulaPlus.WritePort(DataPort, 0x01); + } +} diff --git a/tests/Spectron.Tests/TestFiles/quick-save.spectron b/tests/Spectron.Tests/TestFiles/quick-save.spectron index 64897fd4..8c472da1 100644 Binary files a/tests/Spectron.Tests/TestFiles/quick-save.spectron and b/tests/Spectron.Tests/TestFiles/quick-save.spectron differ diff --git a/tests/Spectron.Tests/TestFiles/test.spectron b/tests/Spectron.Tests/TestFiles/test.spectron index 64897fd4..98fe03f1 100644 Binary files a/tests/Spectron.Tests/TestFiles/test.spectron and b/tests/Spectron.Tests/TestFiles/test.spectron differ