diff --git a/src/Spice86.Core/CLI/Configuration.cs b/src/Spice86.Core/CLI/Configuration.cs
index a0ccedf65..6de5132aa 100644
--- a/src/Spice86.Core/CLI/Configuration.cs
+++ b/src/Spice86.Core/CLI/Configuration.cs
@@ -6,7 +6,7 @@ namespace Spice86.Core.CLI;
using Spice86.Core.Emulator.Function;
/// Configuration for spice86, that is what to run and how. Set on startup.
-public class Configuration {
+public sealed class Configuration {
///
/// Gets or sets whether the A20 gate is silenced. If true memory addresses will rollover above 1 MB.
///
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/SoftwareMixer.cs b/src/Spice86.Core/Emulator/Devices/Sound/SoftwareMixer.cs
index 55144ea5b..0adff1a2b 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/SoftwareMixer.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/SoftwareMixer.cs
@@ -27,13 +27,18 @@ internal void Register(SoundChannel soundChannel) {
Channels = _channels.ToFrozenDictionary();
}
+ ///
+ /// Gets or sets a value indicating whether the software mixer is paused. In paused state, the mixer will not render any sound.
+ ///
+ public bool IsPaused { get; set; }
+
///
/// Gets the sound channels in a read-only dictionary.
///
public FrozenDictionary Channels { get; private set; } = new Dictionary().ToFrozenDictionary();
internal int Render(Span data, SoundChannel channel) {
- if (channel.Volume == 0 || channel.IsMuted) {
+ if (channel.Volume == 0 || channel.IsMuted || IsPaused) {
_channels[channel].WriteSilence();
return data.Length;
}
diff --git a/src/Spice86.Core/Emulator/Devices/Timer/Timer.cs b/src/Spice86.Core/Emulator/Devices/Timer/Timer.cs
index 78866c3c7..fdeed0571 100644
--- a/src/Spice86.Core/Emulator/Devices/Timer/Timer.cs
+++ b/src/Spice86.Core/Emulator/Devices/Timer/Timer.cs
@@ -6,19 +6,15 @@
using Spice86.Shared.Interfaces;
using Spice86.Core.Emulator.Devices.ExternalInput;
-using Spice86.Core.Emulator.Devices.Video;
+using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Core.Emulator.IOPorts;
-using Spice86.Core.Emulator.Memory;
-using Spice86.Core.Emulator.VM;
-
-using System.ComponentModel;
///
/// Emulates a PIT8254 Programmable Interval Timer.
/// Triggers interrupt 8 on the CPU via the PIC.
/// https://k.lse.epita.fr/internals/8254_controller.html
///
-public class Timer : DefaultIOPortHandler, ITimeMultiplier {
+public class Timer : DefaultIOPortHandler, ITimeMultiplier, IDebuggableComponent {
private const int CounterRegisterZero = 0x40;
private const int CounterRegisterOne = 0x41;
private const int CounterRegisterTwo = 0x42;
@@ -119,4 +115,9 @@ private Counter GetCounterIndexFromPortNumber(int port) {
int counter = port & 0b11;
return GetCounter(counter);
}
+
+ ///
+ public void Accept(T emulatorDebugger) where T : IInternalDebugger {
+ emulatorDebugger.Visit(this);
+ }
}
diff --git a/src/Spice86.Core/Emulator/Function/Dump/MemoryDataExporter.cs b/src/Spice86.Core/Emulator/Function/Dump/MemoryDataExporter.cs
index a04cfc4a3..59356704f 100644
--- a/src/Spice86.Core/Emulator/Function/Dump/MemoryDataExporter.cs
+++ b/src/Spice86.Core/Emulator/Function/Dump/MemoryDataExporter.cs
@@ -46,6 +46,6 @@ private byte[] GenerateToolingCompliantRamDump() {
return _callbackHandler.ReplaceAllCallbacksInRamImage(_memory);
}
- return _memory.RamCopy;
+ return _memory.ReadRam();
}
}
\ No newline at end of file
diff --git a/src/Spice86.Core/Emulator/InternalDebugger/IInternalDebugger.cs b/src/Spice86.Core/Emulator/InternalDebugger/IInternalDebugger.cs
index eecbfa321..372a342db 100644
--- a/src/Spice86.Core/Emulator/InternalDebugger/IInternalDebugger.cs
+++ b/src/Spice86.Core/Emulator/InternalDebugger/IInternalDebugger.cs
@@ -1,5 +1,18 @@
namespace Spice86.Core.Emulator.InternalDebugger;
-public interface IInternalDebugger
-{
+
+///
+/// Interface for the internal debuggers implemented by the UI ViewModels.
+///
+public interface IInternalDebugger {
+ ///
+ /// Visit an emulator component that accepts the internal debugger.
+ ///
+ /// The emulator component that accepts the internal debugger
+ /// A class that implements the interface.
void Visit(T component) where T : IDebuggableComponent;
+
+ ///
+ /// Tells if the ViewModel for the internal debugger needs to visit the emulator. Either to get references to internal objects or refresh UI data.
+ ///
+ public bool NeedsToVisitEmulator { get; }
}
diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/Common/Callback/CallbackHandler.cs b/src/Spice86.Core/Emulator/InterruptHandlers/Common/Callback/CallbackHandler.cs
index ca178da3c..192765a4e 100644
--- a/src/Spice86.Core/Emulator/InterruptHandlers/Common/Callback/CallbackHandler.cs
+++ b/src/Spice86.Core/Emulator/InterruptHandlers/Common/Callback/CallbackHandler.cs
@@ -50,7 +50,7 @@ public void RunFromOverriden(int index) {
/// The memory bus.
/// A byte array representing the memory content with the Spice86 machine code for callbacks removed.
public byte[] ReplaceAllCallbacksInRamImage(IMemory memory) {
- ByteArrayBasedIndexable indexable = new ByteArrayBasedIndexable(memory.RamCopy);
+ ByteArrayBasedIndexable indexable = new ByteArrayBasedIndexable(memory.ReadRam());
MemoryAsmWriter memoryAsmWriter = new MemoryAsmWriter(indexable, new SegmentedAddress(0, 0), this);
foreach (ICallback callback in this.AllRunnables) {
memoryAsmWriter.CurrentAddress = callback.InstructionAddress;
diff --git a/src/Spice86.Core/Emulator/Memory/IMemory.cs b/src/Spice86.Core/Emulator/Memory/IMemory.cs
index 7905cdb82..af2430315 100644
--- a/src/Spice86.Core/Emulator/Memory/IMemory.cs
+++ b/src/Spice86.Core/Emulator/Memory/IMemory.cs
@@ -24,12 +24,22 @@ public interface IMemory : IIndexable, IByteReaderWriter, IDebuggableComponent {
A20Gate A20Gate { get; }
///
- /// Gets a copy of the current memory state.
+ /// Gets a copy of the current memory state, not triggering any breakpoints.
///
- byte[] RamCopy { get; }
+ /// The length of the byte array. Default is equal to the memory length.
+ /// Where to start in the memory. Default is 0.
+ /// A copy of the current memory state.
+ public byte[] ReadRam(uint length = 0, uint offset = 0);
///
- /// Returns a that represents the specified range of memory.
+ /// Writes an array of bytes to memory, not triggering any breakpoints.
+ ///
+ /// The array to copy data from.
+ /// Where to start in the memory. Default is 0.
+ public void WriteRam(byte[] array, uint offset = 0);
+
+ ///
+ /// Returns a that represents the specified range of memory. Will trigger memory read breakpoints.
///
/// The starting address of the memory range.
/// The length of the memory range.
@@ -47,12 +57,12 @@ public interface IMemory : IIndexable, IByteReaderWriter, IDebuggableComponent {
uint? SearchValue(uint address, int len, IList value);
///
- /// Allows write breakpoints to access the byte being written before it actually is.
+ /// Allows memory write breakpoints to access the byte being written before it actually is.
///
byte CurrentlyWritingByte { get; }
///
- /// Allow a class to register for a certain memory range.
+ /// Allow a memory mapped device to register for a certain memory range.
///
/// The start of the frameB
/// The size of the window
diff --git a/src/Spice86.Core/Emulator/Memory/Memory.cs b/src/Spice86.Core/Emulator/Memory/Memory.cs
index f02f9a5b0..c33855bb9 100644
--- a/src/Spice86.Core/Emulator/Memory/Memory.cs
+++ b/src/Spice86.Core/Emulator/Memory/Memory.cs
@@ -47,17 +47,22 @@ public Memory(IMemoryDevice baseMemory, bool is20ThAddressLineSilenced) {
///
public const uint EndOfHighMemoryArea = 0x10FFEF;
- ///
- /// Gets a copy of the current memory state, not triggering any breakpoints.
- ///
- public byte[] RamCopy {
- get {
- byte[] copy = new byte[_memoryDevices.Length];
- for (uint address = 0; address < copy.Length; address++) {
- copy[address] = _memoryDevices[address].Read(address);
- }
-
- return copy;
+ ///
+ public byte[] ReadRam(uint length = 0, uint offset = 0) {
+ if (length == 0) {
+ length = (uint)_memoryDevices.Length;
+ }
+ byte[] copy = new byte[length];
+ for (uint address = 0; address < copy.Length; address++) {
+ copy[address] = _memoryDevices[address + offset].Read(address + offset);
+ }
+ return copy;
+ }
+
+ ///
+ public void WriteRam(byte[] array, uint offset = 0) {
+ for (uint address = 0; address < array.Length; address++) {
+ _memoryDevices[address + offset].Write(address + offset, array[address]);
}
}
@@ -77,7 +82,7 @@ public byte this[uint address] {
}
///
- /// Allows write breakpoints to access the byte being written before it actually is.
+ /// Allows memory write breakpoints to access the byte being written before it actually is.
///
public byte CurrentlyWritingByte {
get;
@@ -97,10 +102,11 @@ public byte CurrentlyWritingByte {
public Span GetSpan(int address, int length) {
address = A20Gate.TransformAddress(address);
foreach (DeviceRegistration device in _devices) {
- if (address >= device.StartAddress && address + length <= device.EndAddress) {
- MemoryBreakpoints.MonitorRangeReadAccess((uint)address, (uint)(address + length));
- return device.Device.GetSpan(address, length);
+ if (address < device.StartAddress || address + length > device.EndAddress) {
+ continue;
}
+ MemoryBreakpoints.MonitorRangeReadAccess((uint)address, (uint)(address + length));
+ return device.Device.GetSpan(address, length);
}
throw new InvalidOperationException($"No Memory Device supports a span from {address} to {address + length}");
@@ -173,12 +179,7 @@ public override SegmentedAddressIndexer SegmentedAddress {
get;
}
- ///
- /// Allow a class to register for a certain memory range.
- ///
- /// The start of the frame
- /// The size of the window
- /// The memory device to use
+ ///
public void RegisterMapping(uint baseAddress, uint size, IMemoryDevice memoryDevice) {
uint endAddress = baseAddress + size;
if (endAddress >= _memoryDevices.Length) {
diff --git a/src/Spice86.Core/Emulator/VM/Machine.cs b/src/Spice86.Core/Emulator/VM/Machine.cs
index 67e8f311c..6f66b252f 100644
--- a/src/Spice86.Core/Emulator/VM/Machine.cs
+++ b/src/Spice86.Core/Emulator/VM/Machine.cs
@@ -242,9 +242,6 @@ public Machine(IGui? gui, State cpuState, IOPortDispatcher ioPortDispatcher, ILo
VgaCard = new VgaCard(gui, VgaRenderer, loggerService);
Timer = new Timer(CpuState, loggerService, DualPic, counterConfigurator, configuration.FailOnUnhandledPort);
- if (gui is not null) {
- gui.ProgrammableIntervalTimer = Timer;
- }
RegisterIoPortHandler(Timer);
Keyboard = new Keyboard(CpuState, Memory.A20Gate, DualPic, loggerService, gui, configuration.FailOnUnhandledPort);
RegisterIoPortHandler(Keyboard);
@@ -385,5 +382,6 @@ public void Accept(T emulatorDebugger) where T : IInternalDebugger {
VgaRegisters.Accept(emulatorDebugger);
MidiDevice.Accept(emulatorDebugger);
SoftwareMixer.Accept(emulatorDebugger);
+ Timer.Accept(emulatorDebugger);
}
}
\ No newline at end of file
diff --git a/src/Spice86.Shared/Interfaces/IGui.cs b/src/Spice86.Shared/Interfaces/IGui.cs
index 319bb96a3..9bdccc3bb 100644
--- a/src/Spice86.Shared/Interfaces/IGui.cs
+++ b/src/Spice86.Shared/Interfaces/IGui.cs
@@ -77,9 +77,4 @@ public interface IGui {
/// Indicate that a mouse button has been released.
///
event EventHandler? MouseButtonUp;
-
- ///
- /// Used by the UI to set or reset the time multiplier.
- ///
- ITimeMultiplier? ProgrammableIntervalTimer { set; }
}
\ No newline at end of file
diff --git a/src/Spice86/App.axaml b/src/Spice86/App.axaml
index eb600ef39..d8b443d53 100644
--- a/src/Spice86/App.axaml
+++ b/src/Spice86/App.axaml
@@ -3,15 +3,11 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
- xmlns:spice86="clr-namespace:Spice86"
RequestedThemeVariant="Default">
avares://Spice86/Assets#Roboto Mono
Consolas
-
-
-
diff --git a/src/Spice86/Assets/Debug.ico b/src/Spice86/Assets/Debug.ico
new file mode 100644
index 000000000..f1e25597f
Binary files /dev/null and b/src/Spice86/Assets/Debug.ico differ
diff --git a/src/Spice86/DependencyInjection/Composition.cs b/src/Spice86/DependencyInjection/Composition.cs
deleted file mode 100644
index 0e768f293..000000000
--- a/src/Spice86/DependencyInjection/Composition.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Spice86.DependencyInjection;
-
-using Spice86.Logging;
-using Spice86.Shared.Interfaces;
-using Pure.DI;
-
-///
-/// The DI composition root for both the UI and headless mode.
-///
-public partial class Composition {
- private static void Setup() =>
- DI.Setup(nameof(Composition))
- // Infrastructure
- .Bind().As(Lifetime.Singleton).To()
- .Bind().As(Lifetime.Singleton).To()
- // Composition Root
- .Root(nameof(Root));
-}
diff --git a/src/Spice86/Infrastructure/DebugWindowActivator.cs b/src/Spice86/Infrastructure/DebugWindowActivator.cs
deleted file mode 100644
index 334063945..000000000
--- a/src/Spice86/Infrastructure/DebugWindowActivator.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace Spice86.Infrastructure;
-
-using Spice86.ViewModels;
-using Spice86.Views;
-
-///
-internal class DebugWindowActivator : IDebugWindowActivator {
- private DebugWindow? _debugWindow;
-
- ///
- public void ActivateDebugWindow(DebugWindowViewModel viewModel) {
- if (_debugWindow is not null) {
- _debugWindow.Activate();
- return;
- }
- _debugWindow = new DebugWindow {
- DataContext = viewModel
- };
- _debugWindow.Show();
- _debugWindow.Closed += (_, _) => _debugWindow = null;
- }
-
- public void CloseDebugWindow() {
- _debugWindow?.Close();
- }
-}
diff --git a/src/Spice86/Infrastructure/HostStorageProvider.cs b/src/Spice86/Infrastructure/HostStorageProvider.cs
index 9d67e0bb1..c53a9dc10 100644
--- a/src/Spice86/Infrastructure/HostStorageProvider.cs
+++ b/src/Spice86/Infrastructure/HostStorageProvider.cs
@@ -59,6 +59,21 @@ public async Task SaveBitmapFile(WriteableBitmap bitmap) {
}
}
}
+
+ public async Task SaveBinaryFile(byte[] bytes) {
+ if (CanSave && CanPickFolder) {
+ FilePickerSaveOptions options = new() {
+ Title = "Save binary file",
+ SuggestedFileName = "dump.bin",
+ DefaultExtension = "bin",
+ SuggestedStartLocation = await TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
+ };
+ string? file = (await SaveFilePickerAsync(options))?.TryGetLocalPath();
+ if (!string.IsNullOrWhiteSpace(file)) {
+ await File.WriteAllBytesAsync(file, bytes);
+ }
+ }
+ }
public async Task DumpEmulatorStateToFile(Configuration configuration, IProgramExecutor programExecutor) {
if (CanSave && CanPickFolder) {
diff --git a/src/Spice86/Infrastructure/IDebugWindowActivator.cs b/src/Spice86/Infrastructure/IDebugWindowActivator.cs
deleted file mode 100644
index a65037982..000000000
--- a/src/Spice86/Infrastructure/IDebugWindowActivator.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Spice86.Infrastructure;
-
-using Spice86.Core.Emulator;
-using Spice86.Interfaces;
-using Spice86.ViewModels;
-
-///
-/// Activates or closes the Debug window
-///
-public interface IDebugWindowActivator {
- ///
- /// Activates the Window corresponding to the
- ///
- /// The viewModel for the debug view.
- void ActivateDebugWindow(DebugWindowViewModel viewModel);
-
- ///
- /// Closes the debug window
- ///
- void CloseDebugWindow();
-}
diff --git a/src/Spice86/Infrastructure/IHostStorageProvider.cs b/src/Spice86/Infrastructure/IHostStorageProvider.cs
index a8f29fe37..f204c2c92 100644
--- a/src/Spice86/Infrastructure/IHostStorageProvider.cs
+++ b/src/Spice86/Infrastructure/IHostStorageProvider.cs
@@ -82,4 +82,11 @@ public interface IHostStorageProvider {
/// The directory of the last file picked, if any.
/// The operation as an awaitable Task, containing the first picked file, or null.
Task PickExecutableFile(string lastExecutableDirectory);
+
+ ///
+ /// Spanws the file picker to save a binary file.
+ ///
+ /// The binary content of the file.
+ /// The operation as an awaitable Task.
+ Task SaveBinaryFile(byte[] bytes);
}
diff --git a/src/Spice86/Infrastructure/IWindowService.cs b/src/Spice86/Infrastructure/IWindowService.cs
new file mode 100644
index 000000000..e99d1d7e6
--- /dev/null
+++ b/src/Spice86/Infrastructure/IWindowService.cs
@@ -0,0 +1,19 @@
+namespace Spice86.Infrastructure;
+
+using Spice86.ViewModels;
+
+///
+/// Service used for showing the debug window.
+///
+public interface IWindowService {
+ ///
+ /// Shows the debug window.
+ ///
+ /// The used as DataContext in case the window needs to be created.
+ Task ShowDebugWindow(DebugWindowViewModel viewModel);
+
+ ///
+ /// Close the debug window.
+ ///
+ void CloseDebugWindow();
+}
\ No newline at end of file
diff --git a/src/Spice86/Infrastructure/WindowService.cs b/src/Spice86/Infrastructure/WindowService.cs
new file mode 100644
index 000000000..dde46da3f
--- /dev/null
+++ b/src/Spice86/Infrastructure/WindowService.cs
@@ -0,0 +1,50 @@
+namespace Spice86.Infrastructure;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+
+using Spice86.ViewModels;
+using Spice86.Views;
+
+///
+public class WindowService : IWindowService {
+ public void CloseDebugWindow() {
+ Dispatcher.UIThread.Post(() => {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime) {
+ return;
+ }
+ DebugWindow? debugWindow = lifetime.Windows.FirstOrDefault(x => x is DebugWindow) as DebugWindow;
+ debugWindow?.Close();
+ });
+ }
+
+ public async Task ShowDebugWindow(DebugWindowViewModel viewModel) {
+ await Dispatcher.UIThread.InvokeAsync(() => {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime) {
+ return;
+ }
+
+ bool foundWindow = false;
+ IReadOnlyList windows = lifetime.Windows;
+ foreach (WindowBase window in windows) {
+ if (window.DataContext?.GetType() != viewModel?.GetType()) {
+ continue;
+ }
+ foundWindow = true;
+ window.Activate();
+ break;
+ }
+
+ if (foundWindow) {
+ return;
+ }
+
+ WindowBase debugWindow = new DebugWindow {
+ DataContext = viewModel
+ };
+ debugWindow.Show();
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs b/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs
index 52300c45c..8d8b4e230 100644
--- a/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs
+++ b/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs
@@ -7,15 +7,22 @@
public class MemoryBinaryDocument : IBinaryDocument {
private readonly IMemory _memory;
- public MemoryBinaryDocument(IMemory memory) {
- _memory = memory;
+ private readonly uint _startAddress;
+ private readonly uint _endAddress;
+
+ public MemoryBinaryDocument(IMemory memory, uint startAddress, uint endAddress) {
IsReadOnly = false;
CanInsert = false;
CanRemove = false;
- ValidRanges = new MemoryReadOnlyBitRangeUnion(memory);
+ _startAddress = startAddress;
+ _endAddress = endAddress;
+ _memory = memory;
+ ValidRanges = new MemoryReadOnlyBitRangeUnion(0, _endAddress - _startAddress);
}
- public ulong Length => _memory.Length;
+ public event Action? MemoryReadInvalidOperation;
+
+ public ulong Length => _endAddress - _startAddress;
public bool IsReadOnly { get; }
public bool CanInsert { get; }
public bool CanRemove { get; }
@@ -28,8 +35,11 @@ public void InsertBytes(ulong offset, ReadOnlySpan buffer) {
}
public void ReadBytes(ulong offset, Span buffer) {
- for (int i = 0; i < buffer.Length; i++) {
- buffer[i] = _memory[(uint)(offset + (uint)i)];
+ try {
+ Span memRange = _memory.ReadRam((uint)buffer.Length, (uint)(_startAddress + offset));
+ memRange.CopyTo(buffer);
+ } catch (InvalidOperationException e) {
+ MemoryReadInvalidOperation?.Invoke(e);
}
}
@@ -38,8 +48,6 @@ public void RemoveBytes(ulong offset, ulong length) {
}
public void WriteBytes(ulong offset, ReadOnlySpan buffer) {
- for (int i = 0; i < buffer.Length; i++) {
- _memory[(uint)(offset + (uint)i)] = buffer[i];
- }
+ _memory.WriteRam(buffer.ToArray(), (uint)(_startAddress + offset));
}
}
\ No newline at end of file
diff --git a/src/Spice86/MemoryWrappers/MemoryReadOnlyBitRangeUnion.cs b/src/Spice86/MemoryWrappers/MemoryReadOnlyBitRangeUnion.cs
index 57b6ce66b..f147ad0b8 100644
--- a/src/Spice86/MemoryWrappers/MemoryReadOnlyBitRangeUnion.cs
+++ b/src/Spice86/MemoryWrappers/MemoryReadOnlyBitRangeUnion.cs
@@ -9,15 +9,23 @@
using System.Collections.Specialized;
public class MemoryReadOnlyBitRangeUnion : IReadOnlyBitRangeUnion {
- private readonly IMemory _memory;
-
- public MemoryReadOnlyBitRangeUnion(IMemory memory) {
- _memory = memory;
- EnclosingRange = new BitRange(0, _memory.Length * 8);
+ private readonly uint _startAddress;
+ private readonly uint _endAddress;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The start address of tha range of memory.
+ /// The end address of the range of memory. This end address is not included in the range.
+ public MemoryReadOnlyBitRangeUnion(uint startAddress, uint endAddress) {
+ _startAddress = startAddress;
+ _endAddress = endAddress;
+ // The endAddress is excluded from the range
+ EnclosingRange = new BitRange(startAddress, endAddress);
}
public BitRange EnclosingRange { get; }
- public int Count => (int)_memory.Length;
+ public int Count => (int)(_endAddress - _startAddress);
public event NotifyCollectionChangedEventHandler? CollectionChanged;
@@ -27,8 +35,7 @@ public bool Contains(BitLocation location) {
public BitRangeUnion.Enumerator GetEnumerator() {
var bitRangeUnion = new BitRangeUnion();
- // Multiply by 8 to convert from bytes to bits
- bitRangeUnion.Add(new BitRange(0, _memory.Length * 8));
+ bitRangeUnion.Add(new BitRange(_startAddress, _endAddress));
return bitRangeUnion.GetEnumerator();
}
@@ -41,7 +48,7 @@ public bool IsSuperSetOf(BitRange range) {
}
IEnumerator IEnumerable.GetEnumerator() {
- for (uint i = 0; i < _memory.Length; i++) {
+ for (uint i = _startAddress; i < _endAddress; i++) {
yield return EnclosingRange;
}
}
diff --git a/src/Spice86/Program.cs b/src/Spice86/Program.cs
index ad601e99d..8fc001efb 100644
--- a/src/Spice86/Program.cs
+++ b/src/Spice86/Program.cs
@@ -6,21 +6,19 @@
using Spice86.Core.CLI;
using Spice86.Core.Emulator;
-using Spice86.DependencyInjection;
using Spice86.Shared.Interfaces;
using Spice86.Infrastructure;
using Avalonia.Threading;
-using Spice86.Views;
+using Microsoft.Extensions.DependencyInjection;
+
+using Spice86.Logging;
using Spice86.ViewModels;
///
/// Entry point for Spice86 application.
///
public class Program {
- private readonly ILoggerService _loggerService;
- internal Program(ILoggerService loggerService) => _loggerService = loggerService;
-
///
/// Alternate entry point to use when injecting a class that defines C# overrides of the x86 assembly code found in the target DOS program.
///
@@ -44,42 +42,39 @@ public class Program {
[STAThread]
public static void Main(string[] args) {
Configuration configuration = CommandLineParser.ParseCommandLine(args);
- Program program = new Composition().Resolve();
- program.StartApp(configuration, args);
- }
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+
+ ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
+ ILoggerService loggerService = serviceProvider.GetRequiredService();
+ Startup.SetLoggingLevel(loggerService, configuration);
- private void StartApp(Configuration configuration, string[] args) {
- Startup.SetLoggingLevel(_loggerService, configuration);
if (!configuration.HeadlessMode) {
- StartMainWindow(configuration, _loggerService, args);
+ AppBuilder appBuilder = BuildAvaloniaApp();
+ ClassicDesktopStyleApplicationLifetime desktop = SetupWithClassicDesktopLifetime(appBuilder, args);
+ App? app = (App?)appBuilder.Instance;
+
+ if (app is null) {
+ return;
+ }
+
+ Views.MainWindow mainWindow = new();
+ using var mainWindowViewModel = new MainWindowViewModel(new WindowService(), new AvaloniaKeyScanCodeConverter(),
+ new ProgramExecutorFactory(configuration, loggerService),
+ new UIDispatcher(Dispatcher.UIThread), new HostStorageProvider(mainWindow.StorageProvider),
+ new TextClipboard(mainWindow.Clipboard), new UIDispatcherTimerFactory(), configuration, loggerService);
+ mainWindow.DataContext = mainWindowViewModel;
+ desktop.MainWindow = mainWindow;
+ desktop.Start(args);
}
else {
- StartConsole(configuration, _loggerService);
- }
- }
-
- private static void StartConsole(Configuration configuration, ILoggerService loggerService) {
- ProgramExecutor programExecutor = new(configuration, loggerService, null);
- programExecutor.Run();
- }
-
- private static void StartMainWindow(Configuration configuration, ILoggerService loggerService, string[] args) {
- AppBuilder appBuilder = BuildAvaloniaApp();
- ClassicDesktopStyleApplicationLifetime desktop = SetupWithClassicDesktopLifetime(appBuilder, args);
- App? app = (App?)appBuilder.Instance;
- if (app is null) {
- return;
+ ProgramExecutor programExecutor = new(configuration, loggerService, null);
+ programExecutor.Run();
}
- MainWindow mainWindow = new();
- using var mainWindowViewModel = new MainWindowViewModel(new AvaloniaKeyScanCodeConverter(),
- new ProgramExecutorFactory(configuration, loggerService), new DebugWindowActivator(),
- new UIDispatcher(Dispatcher.UIThread), new HostStorageProvider(mainWindow.StorageProvider),
- new TextClipboard(mainWindow.Clipboard), new UIDispatcherTimerFactory(), configuration, loggerService);
- mainWindow.DataContext = mainWindowViewModel;
- desktop.MainWindow = mainWindow;
- desktop.Start(args);
}
-
+
///
/// Configures and builds an Avalonia application instance.
///
diff --git a/src/Spice86/Spice86.csproj b/src/Spice86/Spice86.csproj
index b64729d38..0242faaf8 100644
--- a/src/Spice86/Spice86.csproj
+++ b/src/Spice86/Spice86.csproj
@@ -44,6 +44,7 @@
+
all
@@ -58,10 +59,6 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml b/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml
new file mode 100644
index 000000000..f7d157534
--- /dev/null
+++ b/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml.cs b/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml.cs
new file mode 100644
index 000000000..3aea4e3b2
--- /dev/null
+++ b/src/Spice86/UserControls/ErrorModalDialogUserControl.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+
+namespace Spice86.UserControls;
+
+public partial class ErrorModalDialogUserControl : UserControl {
+ public ErrorModalDialogUserControl() {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Spice86/ViewLocator.cs b/src/Spice86/ViewLocator.cs
deleted file mode 100644
index 9f722d462..000000000
--- a/src/Spice86/ViewLocator.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace Spice86;
-
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-
-using Spice86.ViewModels;
-
-public class ViewLocator : IDataTemplate {
- public Control Build(object? data) {
- if (data is null) {
- return new TextBlock { Text = "Not Found: " + data };
- }
- string? name = data.GetType().FullName!.Replace("ViewModel", "View");
- var type = Type.GetType(name);
-
- if (type != null) {
- return (Control)Activator.CreateInstance(type)!;
- }
- return new TextBlock { Text = "Not Found: " + name };
- }
-
- public bool Match(object? data) {
- return data is ViewModelBase;
- }
-}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/CpuViewModel.cs b/src/Spice86/ViewModels/CpuViewModel.cs
index a05b535ab..70640bf4f 100644
--- a/src/Spice86/ViewModels/CpuViewModel.cs
+++ b/src/Spice86/ViewModels/CpuViewModel.cs
@@ -1,11 +1,12 @@
namespace Spice86.ViewModels;
-using Avalonia.Controls;
+using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.InternalDebugger;
+using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Models.Debugging;
@@ -13,31 +14,33 @@ namespace Spice86.ViewModels;
using System.Reflection;
public partial class CpuViewModel : ViewModelBase, IInternalDebugger {
- private readonly IPauseStatus? _pauseStatus;
+ private readonly IPauseStatus _pauseStatus;
+ private State? _cpuState;
[ObservableProperty]
private StateInfo _state = new();
[ObservableProperty]
private CpuFlagsInfo _flags = new();
-
- public CpuViewModel() {
- if (!Design.IsDesignMode) {
- throw new InvalidOperationException("This constructor is not for runtime usage");
- }
- }
- public CpuViewModel(IPauseStatus pauseStatus) {
+ public CpuViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory, IPauseStatus pauseStatus) {
_pauseStatus = pauseStatus;
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (_cpuState is not null) {
+ VisitCpuState(_cpuState);
+ }
}
+ public bool NeedsToVisitEmulator => _cpuState is null;
+
public void Visit(T component) where T : IDebuggableComponent {
- if (component is State state) {
- VisitCpuState(state);
- }
+ _cpuState ??= component as State;
}
- private bool IsPaused => _pauseStatus?.IsPaused is true;
+ private bool IsPaused => _pauseStatus.IsPaused;
private void VisitCpuState(State state) {
if (!IsPaused) {
diff --git a/src/Spice86/ViewModels/DebugWindowViewModel.cs b/src/Spice86/ViewModels/DebugWindowViewModel.cs
index fe92ec6cf..54d400cf6 100644
--- a/src/Spice86/ViewModels/DebugWindowViewModel.cs
+++ b/src/Spice86/ViewModels/DebugWindowViewModel.cs
@@ -1,6 +1,6 @@
namespace Spice86.ViewModels;
-using Avalonia.Controls;
+using Avalonia.Collections;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -14,101 +14,108 @@ namespace Spice86.ViewModels;
using System.ComponentModel;
public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
- private readonly IPauseStatus? _pauseStatus;
- private readonly IProgramExecutor? _programExecutor;
+ private readonly IPauseStatus _pauseStatus;
+ private readonly IProgramExecutor _programExecutor;
+ private readonly IHostStorageProvider _storageProvider;
+ private readonly IUIDispatcherTimerFactory _uiDispatcherTimerFactory;
+ private readonly ITextClipboard _textClipboard;
[ObservableProperty]
private DateTime? _lastUpdate;
-
- [ObservableProperty]
- private int _selectedTab;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
+ [NotifyCanExecuteChangedFor(nameof(NewMemoryViewCommand))]
+ [NotifyCanExecuteChangedFor(nameof(NewDisassemblyViewCommand))]
private bool _isPaused;
[ObservableProperty]
- private PaletteViewModel? _paletteViewModel;
+ private PaletteViewModel _paletteViewModel;
[ObservableProperty]
- private MemoryViewModel? _memoryViewModel;
+ private AvaloniaList _memoryViewModels = new();
[ObservableProperty]
- private VideoCardViewModel? _videoCardViewModel;
+ private VideoCardViewModel _videoCardViewModel;
[ObservableProperty]
- private CpuViewModel? _cpuViewModel;
+ private CpuViewModel _cpuViewModel;
[ObservableProperty]
- private MidiViewModel? _midiViewModel;
+ private MidiViewModel _midiViewModel;
[ObservableProperty]
- private DisassemblyViewModel? _disassemblyViewModel;
+ private AvaloniaList _disassemblyViewModels = new();
[ObservableProperty]
- private SoftwareMixerViewModel? _softwareMixerViewModel;
+ private SoftwareMixerViewModel _softwareMixerViewModel;
- public DebugWindowViewModel() {
- if (!Design.IsDesignMode) {
- throw new InvalidOperationException("This constructor is not for runtime usage");
- }
- }
-
- public DebugWindowViewModel(IUIDispatcherTimerFactory uiDispatcherTimerFactory, IPauseStatus pauseStatus, IProgramExecutor programExecutor) {
+ public DebugWindowViewModel(ITextClipboard textClipboard, IHostStorageProvider storageProvider, IUIDispatcherTimerFactory uiDispatcherTimerFactory, IPauseStatus pauseStatus, IProgramExecutor programExecutor) {
_programExecutor = programExecutor;
+ _storageProvider = storageProvider;
+ _textClipboard = textClipboard;
+ _uiDispatcherTimerFactory = uiDispatcherTimerFactory;
_pauseStatus = pauseStatus;
IsPaused = _programExecutor.IsPaused;
_pauseStatus.PropertyChanged += OnPauseStatusChanged;
uiDispatcherTimerFactory.StartNew(TimeSpan.FromSeconds(1.0 / 30.0), DispatcherPriority.Normal, UpdateValues);
- DisassemblyViewModel = new(pauseStatus);
- PaletteViewModel = new();
- SoftwareMixerViewModel = new();
- VideoCardViewModel = new();
- CpuViewModel = new(pauseStatus);
- MidiViewModel = new();
- MemoryViewModel = new(pauseStatus);
- Dispatcher.UIThread.Post(() => programExecutor.Accept(this), DispatcherPriority.Background);
+ var disassemblyVm = new DisassemblyViewModel(this, uiDispatcherTimerFactory, pauseStatus);
+ DisassemblyViewModels.Add(disassemblyVm);
+ PaletteViewModel = new(uiDispatcherTimerFactory);
+ SoftwareMixerViewModel = new(uiDispatcherTimerFactory);
+ VideoCardViewModel = new(uiDispatcherTimerFactory);
+ CpuViewModel = new(uiDispatcherTimerFactory, pauseStatus);
+ MidiViewModel = new(uiDispatcherTimerFactory);
+ MemoryViewModels.Add( new(this, textClipboard, uiDispatcherTimerFactory, storageProvider, pauseStatus, 0));
+ Dispatcher.UIThread.Post(ForceUpdate, DispatcherPriority.Background);
}
-
- [RelayCommand]
- public void Pause() {
- if (_programExecutor is null || _pauseStatus is null) {
- return;
+
+ internal void CloseTab(IInternalDebugger internalDebuggerViewModel) {
+ switch (internalDebuggerViewModel) {
+ case MemoryViewModel memoryViewModel:
+ MemoryViewModels.Remove(memoryViewModel);
+ break;
+ case DisassemblyViewModel disassemblyViewModel:
+ DisassemblyViewModels.Remove(disassemblyViewModel);
+ break;
}
- _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = true;
}
[RelayCommand(CanExecute = nameof(IsPaused))]
- public void Continue() {
- if (_programExecutor is null || _pauseStatus is null) {
- return;
- }
- _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = false;
+ public void NewMemoryView() {
+ MemoryViewModels.Add(new MemoryViewModel(this, _textClipboard, _uiDispatcherTimerFactory, _storageProvider, _pauseStatus, 0));
}
- private void OnPauseStatusChanged(object? sender, PropertyChangedEventArgs e) => IsPaused = _pauseStatus?.IsPaused is true;
+ [RelayCommand(CanExecute = nameof(IsPaused))]
+ public void NewDisassemblyView() => DisassemblyViewModels.Add(new DisassemblyViewModel(this, _uiDispatcherTimerFactory, _pauseStatus));
[RelayCommand]
- public void ForceUpdate() {
- UpdateValues(this, EventArgs.Empty);
- }
+ public void Pause() => _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = true;
+
+ [RelayCommand(CanExecute = nameof(IsPaused))]
+ public void Continue() => _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = false;
- private void UpdateValues(object? sender, EventArgs e) {
- _programExecutor?.Accept(this);
+ private void OnPauseStatusChanged(object? sender, PropertyChangedEventArgs e) => IsPaused = _pauseStatus.IsPaused;
+
+ [RelayCommand]
+ public void ForceUpdate() => UpdateValues(this, EventArgs.Empty);
+
+ private void UpdateValues(object? sender, EventArgs e) => _programExecutor.Accept(this);
+
+ private IEnumerable InternalDebuggers => new IInternalDebugger[] {
+ PaletteViewModel, CpuViewModel, VideoCardViewModel, MidiViewModel, SoftwareMixerViewModel
}
+ .Concat(DisassemblyViewModels)
+ .Concat(MemoryViewModels);
public void Visit(T component) where T : IDebuggableComponent {
- PaletteViewModel?.Visit(component);
- DisassemblyViewModel?.Visit(component);
- CpuViewModel?.Visit(component);
- VideoCardViewModel?.Visit(component);
- MidiViewModel?.Visit((component));
- SoftwareMixerViewModel?.Visit(component);
- MemoryViewModel?.Visit(component);
+ if (NeedsToVisitEmulator) {
+ foreach (IInternalDebugger debugger in InternalDebuggers.Where(x => x.NeedsToVisitEmulator)) {
+ debugger.Visit(component);
+ }
+ }
LastUpdate = DateTime.Now;
}
- public void ShowColorPalette() {
- SelectedTab = 4;
- }
+ public bool NeedsToVisitEmulator => InternalDebuggers.Any(x => x.NeedsToVisitEmulator);
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/DisassemblyViewModel.cs b/src/Spice86/ViewModels/DisassemblyViewModel.cs
index 9b0ddb00a..a94fa08d8 100644
--- a/src/Spice86/ViewModels/DisassemblyViewModel.cs
+++ b/src/Spice86/ViewModels/DisassemblyViewModel.cs
@@ -1,7 +1,7 @@
namespace Spice86.ViewModels;
using Avalonia.Collections;
-using Avalonia.Controls;
+using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -12,26 +12,34 @@ namespace Spice86.ViewModels;
using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Core.Emulator.Memory;
+using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.MemoryWrappers;
using Spice86.Models.Debugging;
using Spice86.Shared.Utils;
+using System.Collections.Specialized;
using System.ComponentModel;
public partial class DisassemblyViewModel : ViewModelBase, IInternalDebugger {
- private readonly IPauseStatus? _pauseStatus;
+ private readonly IPauseStatus _pauseStatus;
+ private readonly DebugWindowViewModel _debugWindowViewModel;
private bool _needToUpdateDisassembly = true;
private IMemory? _memory;
private State? _state;
private IProgramExecutor? _programExecutor;
+ [ObservableProperty]
+ private string _header = "Disassembly View";
+
[ObservableProperty]
private AvaloniaList _instructions = new();
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StepInstructionCommand))]
[NotifyCanExecuteChangedFor(nameof(UpdateDisassemblyCommand))]
+ [NotifyCanExecuteChangedFor(nameof(GoToCsIpCommand))]
+ [NotifyCanExecuteChangedFor(nameof(NewDisassemblyViewCommand))]
private bool _isPaused;
[ObservableProperty]
@@ -40,29 +48,61 @@ public partial class DisassemblyViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private int _numberOfInstructionsShown = 50;
- [ObservableProperty]
private uint? _startAddress;
-
- public DisassemblyViewModel() {
- if (!Design.IsDesignMode) {
- throw new InvalidOperationException("This constructor is not for runtime usage");
+
+ public uint? StartAddress {
+ get => _startAddress;
+ set {
+ Header = value is null ? "" : $"0x{value:X}";
+ SetProperty(ref _startAddress, value);
}
}
- public DisassemblyViewModel(IPauseStatus pauseStatus) {
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(CloseTabCommand))]
+ private bool _canCloseTab;
+
+ public DisassemblyViewModel(DebugWindowViewModel debugWindowViewModel, IUIDispatcherTimerFactory dispatcherTimerFactory, IPauseStatus pauseStatus) {
+ _debugWindowViewModel = debugWindowViewModel;
_pauseStatus = pauseStatus;
IsPaused = pauseStatus.IsPaused;
_pauseStatus.PropertyChanged += OnPauseStatusChanged;
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ UpdateCanCloseTabProperty();
+ debugWindowViewModel.DisassemblyViewModels.CollectionChanged += OnDebugViewModelCollectionChanged;
+ }
+
+ private void UpdateCanCloseTabProperty() {
+ CanCloseTab = _debugWindowViewModel.DisassemblyViewModels.Count > 1;
+ }
+
+ private void OnDebugViewModelCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
+ UpdateCanCloseTabProperty();
+ }
+
+ [RelayCommand(CanExecute = nameof(CanCloseTab))]
+ private void CloseTab() {
+ _debugWindowViewModel.CloseTab(this);
+ UpdateCanCloseTabProperty();
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (_needToUpdateDisassembly && IsPaused) {
+ UpdateDisassembly();
+ }
}
private void OnPauseStatusChanged(object? sender, PropertyChangedEventArgs e) {
- IsPaused = _pauseStatus?.IsPaused is true;
+ IsPaused = _pauseStatus.IsPaused;
+ UpdateCanCloseTabProperty();
if (!IsPaused) {
return;
}
_needToUpdateDisassembly = true;
StartAddress ??= _state?.IpPhysicalAddress;
}
+
+ public bool NeedsToVisitEmulator => _memory is null || _state is null || _programExecutor is null;
public void Visit(T component) where T : IDebuggableComponent {
switch (component) {
@@ -83,6 +123,11 @@ public void Visit(T component) where T : IDebuggableComponent {
}
}
+ [RelayCommand(CanExecute = nameof(IsPaused))]
+ public void NewDisassemblyView() {
+ _debugWindowViewModel.NewDisassemblyViewCommand.Execute(null);
+ }
+
[RelayCommand(CanExecute = nameof(IsPaused))]
public void StepInstruction() => _programExecutor?.StepInstruction();
@@ -92,6 +137,7 @@ public void GoToCsIp() {
_needToUpdateDisassembly = true;
UpdateDisassemblyCommand.Execute(null);
}
+
[RelayCommand(CanExecute = nameof(IsPaused))]
private void UpdateDisassembly() {
if(_state is null || _memory is null || StartAddress is null) {
@@ -111,7 +157,8 @@ private void DecodeInstructions(State state, IMemory memory, EmulatedMemoryStrea
Decoder decoder, uint startAddress) {
int byteOffset = 0;
emulatedMemoryStream.Position = startAddress;
- while (Instructions.Count < NumberOfInstructionsShown) {
+ var instructions = new List();
+ while (instructions.Count < NumberOfInstructionsShown) {
long instructionAddress = emulatedMemoryStream.Position;
decoder.Decode(out Instruction instruction);
CpuInstructionInfo instructionInfo = new() {
@@ -133,9 +180,11 @@ private void DecodeInstructions(State state, IMemory memory, EmulatedMemoryStrea
if (instructionAddress == state.IpPhysicalAddress) {
instructionInfo.IsCsIp = true;
}
- Instructions.Add(instructionInfo);
+ instructions.Add(instructionInfo);
byteOffset += instruction.Length;
}
+ Instructions.Clear();
+ Instructions.AddRange(instructions);
}
private Decoder InitializeDecoder(CodeReader codeReader, uint currentIp) {
diff --git a/src/Spice86/ViewModels/MainWindowViewModel.cs b/src/Spice86/ViewModels/MainWindowViewModel.cs
index d0e85b07d..6609da7e1 100644
--- a/src/Spice86/ViewModels/MainWindowViewModel.cs
+++ b/src/Spice86/ViewModels/MainWindowViewModel.cs
@@ -27,30 +27,34 @@
using Avalonia.Media.Imaging;
using Avalonia.Platform;
+using Spice86.Core.Emulator.Devices.Sound;
+using Spice86.Core.Emulator.InternalDebugger;
+
using Spice86.Interfaces;
using Spice86.Shared.Diagnostics;
using Spice86.Infrastructure;
-using Spice86.Models.Debugging;
using Spice86.Shared.Emulator.Video;
using Timer = System.Timers.Timer;
///
-public sealed partial class MainWindowViewModel : ViewModelBase, IPauseStatus, IGui, IDisposable {
+public sealed partial class MainWindowViewModel : ViewModelBaseWithErrorDialog, IPauseStatus, IGui, IDisposable {
private const double ScreenRefreshHz = 60;
private readonly ILoggerService _loggerService;
private readonly IHostStorageProvider _hostStorageProvider;
- private readonly ITextClipboard _textClipboard;
private readonly IUIDispatcher _uiDispatcher;
- private readonly IDebugWindowActivator _debugWindowActivator;
private readonly IProgramExecutorFactory _programExecutorFactory;
private readonly IUIDispatcherTimerFactory _uiDispatcherTimerFactory;
- private readonly IAvaloniaKeyScanCodeConverter? _avaloniaKeyScanCodeConverter;
+ private readonly IAvaloniaKeyScanCodeConverter _avaloniaKeyScanCodeConverter;
+ private readonly IWindowService _windowService;
private IProgramExecutor? _programExecutor;
+ private SoftwareMixer? _softwareMixer;
+ private ITimeMultiplier? _pit;
private DebugWindowViewModel? _debugViewModel;
[ObservableProperty]
private Configuration _configuration;
+
private bool _disposed;
private bool _renderingTimerInitialized;
private Thread? _emulatorThread;
@@ -69,26 +73,20 @@ public sealed partial class MainWindowViewModel : ViewModelBase, IPauseStatus, I
public event EventHandler? MouseButtonDown;
public event EventHandler? MouseButtonUp;
- public ITimeMultiplier? ProgrammableIntervalTimer { private get; set; }
-
- public MainWindowViewModel(IAvaloniaKeyScanCodeConverter avaloniaKeyScanCodeConverter, IProgramExecutorFactory programExecutorFactory, IDebugWindowActivator debugWindowActivator, IUIDispatcher uiDispatcher, IHostStorageProvider hostStorageProvider, ITextClipboard textClipboard, IUIDispatcherTimerFactory uiDispatcherTimerFactory, Configuration configuration, ILoggerService loggerService) {
+ public MainWindowViewModel(IWindowService windowService, IAvaloniaKeyScanCodeConverter avaloniaKeyScanCodeConverter, IProgramExecutorFactory programExecutorFactory, IUIDispatcher uiDispatcher, IHostStorageProvider hostStorageProvider, ITextClipboard textClipboard, IUIDispatcherTimerFactory uiDispatcherTimerFactory, Configuration configuration, ILoggerService loggerService) : base(textClipboard) {
_avaloniaKeyScanCodeConverter = avaloniaKeyScanCodeConverter;
+ _windowService = windowService;
Configuration = configuration;
_programExecutorFactory = programExecutorFactory;
_loggerService = loggerService;
_hostStorageProvider = hostStorageProvider;
- _textClipboard = textClipboard;
_uiDispatcher = uiDispatcher;
- _debugWindowActivator = debugWindowActivator;
_uiDispatcherTimerFactory = uiDispatcherTimerFactory;
}
internal void OnMainWindowClosing() => _isAppClosing = true;
internal void OnKeyUp(KeyEventArgs e) {
- if (_avaloniaKeyScanCodeConverter is null) {
- return;
- }
KeyUp?.Invoke(this,
new KeyboardEventArgs((Key)e.Key,
false,
@@ -104,7 +102,7 @@ public async Task SaveBitmap() {
}
}
- private bool _showCursor = false;
+ private bool _showCursor;
public bool ShowCursor {
get => _showCursor;
@@ -134,9 +132,6 @@ public double Scale {
private WriteableBitmap? _bitmap;
internal void OnKeyDown(KeyEventArgs e) {
- if (_avaloniaKeyScanCodeConverter is null) {
- return;
- }
KeyDown?.Invoke(this,
new KeyboardEventArgs((Key)e.Key,
true,
@@ -151,8 +146,17 @@ internal void OnKeyDown(KeyEventArgs e) {
[ObservableProperty]
private string _asmOverrideStatus = "ASM Overrides: not used.";
- [ObservableProperty]
private bool _isPaused;
+
+ public bool IsPaused {
+ get => _isPaused;
+ set {
+ SetProperty(ref _isPaused, value);
+ if (_softwareMixer is not null) {
+ _softwareMixer.IsPaused = value;
+ }
+ }
+ }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartMostRecentlyUsedCommand))]
@@ -169,7 +173,6 @@ internal void OnKeyDown(KeyEventArgs e) {
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ShowPerformanceCommand))]
- [NotifyCanExecuteChangedFor(nameof(ShowColorPaletteCommand))]
[NotifyCanExecuteChangedFor(nameof(PauseCommand))]
[NotifyCanExecuteChangedFor(nameof(PlayCommand))]
[NotifyCanExecuteChangedFor(nameof(DumpEmulatorStateToFileCommand))]
@@ -250,7 +253,6 @@ private async Task RestartEmulatorWithNewProgram(string filePath) {
await _uiDispatcher.InvokeAsync(DisposeEmulator, DispatcherPriority.MaxValue);
IsMachineRunning = false;
_closeAppOnEmulatorExit = false;
- _debugWindowActivator.CloseDebugWindow();
RunEmulator();
}
@@ -261,27 +263,14 @@ public double? TimeMultiplier {
set {
if (value is not null) {
SetProperty(ref _timeMultiplier, value.Value);
- ProgrammableIntervalTimer?.SetTimeMultiplier(value.Value);
+ _pit?.SetTimeMultiplier(value.Value);
}
}
}
[RelayCommand(CanExecute = nameof(IsMachineRunning))]
public void ShowPerformance() => IsPerformanceVisible = !IsPerformanceVisible;
-
- [RelayCommand]
- public void ShowDebugWindow() {
- if(_debugViewModel is not null) {
- _debugWindowActivator.ActivateDebugWindow(_debugViewModel);
- }
- }
-
- [RelayCommand(CanExecute = nameof(IsMachineRunning))]
- public void ShowColorPalette() {
- ShowDebugWindow();
- _debugViewModel?.ShowColorPalette();
- }
-
+
[RelayCommand]
public void ResetTimeMultiplier() => TimeMultiplier = Configuration.TimeMultiplier;
@@ -351,13 +340,12 @@ private bool RunEmulator() {
AddOrReplaceMostRecentlyUsed(Configuration.Exe);
_lastExecutableDirectory = Configuration.CDrive;
StatusMessage = "Emulator starting...";
- if (Configuration is {UseCodeOverrideOption: true, OverrideSupplier: not null}) {
- AsmOverrideStatus = "ASM code overrides: enabled.";
- } else if(Configuration is {UseCodeOverride: false, OverrideSupplier: not null}) {
- AsmOverrideStatus = "ASM code overrides: only functions names will be referenced.";
- } else {
- AsmOverrideStatus = "ASM code overrides: none.";
- }
+ AsmOverrideStatus = Configuration switch {
+ { UseCodeOverrideOption: true, OverrideSupplier: not null } => "ASM code overrides: enabled.",
+ { UseCodeOverride: false, OverrideSupplier: not null } =>
+ "ASM code overrides: only functions names will be referenced.",
+ _ => "ASM code overrides: none."
+ };
SetLogLevel(Configuration.SilencedLogs ? "Silent" : _loggerService.LogLevelSwitch.MinimumLevel.ToString());
SetMainTitle();
RunMachine();
@@ -418,7 +406,6 @@ private void Dispose(bool disposing) {
if (!_disposed) {
_disposed = true;
if (disposing) {
- _debugWindowActivator.CloseDebugWindow();
_drawTimer.Stop();
_drawTimer.Dispose();
_uiDispatcher.Post(() => {
@@ -438,15 +425,6 @@ private void Dispose(bool disposing) {
private void DisposeEmulator() => _programExecutor?.Dispose();
- [RelayCommand]
- public async Task CopyToClipboard() {
- if(Exception is not null) {
- await _textClipboard.SetTextAsync(
- Newtonsoft.Json.JsonConvert.SerializeObject(
- new ExceptionInfo(Exception.TargetSite?.ToString(), Exception.Message, Exception.StackTrace)));
- }
- }
-
private bool _isInitLogLevelSet;
private string _currentLogLevel = "";
@@ -523,10 +501,21 @@ private void MachineThread() {
[ObservableProperty]
private bool _isPerformanceVisible;
+ [RelayCommand]
+ public async Task ShowInternalDebugger() {
+ if (_programExecutor is not null) {
+ _debugViewModel = new DebugWindowViewModel(_textClipboard, _hostStorageProvider, _uiDispatcherTimerFactory, this, _programExecutor);
+ await _windowService.ShowDebugWindow(_debugViewModel);
+ }
+ }
+
private void StartProgramExecutor() {
- _programExecutor = _programExecutorFactory.Create(this);
+ (IProgramExecutor ProgramExecutor, SoftwareMixer? SoftwareMixer, ITimeMultiplier? Pit) viewModelEmulatorDependencies = CreateEmulator();
+ _programExecutor = viewModelEmulatorDependencies.ProgramExecutor;
+ _softwareMixer = viewModelEmulatorDependencies.SoftwareMixer;
+ _pit = viewModelEmulatorDependencies.Pit;
PerformanceViewModel = new(_uiDispatcherTimerFactory, _programExecutor, new PerformanceMeasurer(), this);
- _debugViewModel = new DebugWindowViewModel(_uiDispatcherTimerFactory, this, _programExecutor);
+ _windowService.CloseDebugWindow();
TimeMultiplier = Configuration.TimeMultiplier;
_uiDispatcher.Post(() => IsMachineRunning = true);
_uiDispatcher.Post(() => StatusMessage = "Emulator started.");
@@ -535,6 +524,23 @@ private void StartProgramExecutor() {
_uiDispatcher.Post(() => CloseMainWindow?.Invoke(this, EventArgs.Empty));
}
}
+
+ private sealed class ViewModelEmulatorDependenciesVisitor : IInternalDebugger {
+ public SoftwareMixer? SoftwareMixer { get; private set; }
+ public ITimeMultiplier? Pit { get; private set; }
+ public void Visit(T component) where T : IDebuggableComponent {
+ SoftwareMixer ??= component as SoftwareMixer;
+ Pit ??= component as ITimeMultiplier;
+ }
+ public bool NeedsToVisitEmulator => SoftwareMixer is null || Pit is null;
+ }
+
+ private (IProgramExecutor ProgramExecutor, SoftwareMixer? SoftwareMixer, ITimeMultiplier? Pit) CreateEmulator() {
+ IProgramExecutor programExecutor = _programExecutorFactory.Create(this);
+ ViewModelEmulatorDependenciesVisitor visitor = new();
+ programExecutor.Accept(visitor);
+ return (programExecutor, visitor.SoftwareMixer, visitor.Pit);
+ }
public event EventHandler? CloseMainWindow;
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/MemoryViewModel.cs b/src/Spice86/ViewModels/MemoryViewModel.cs
index 67e1e7033..62aaedf56 100644
--- a/src/Spice86/ViewModels/MemoryViewModel.cs
+++ b/src/Spice86/ViewModels/MemoryViewModel.cs
@@ -1,47 +1,160 @@
namespace Spice86.ViewModels;
+using Avalonia.Threading;
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Core.Emulator.Memory;
+using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.MemoryWrappers;
using Spice86.Shared.Utils;
+using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-public partial class MemoryViewModel : ViewModelBase, IInternalDebugger {
+public partial class MemoryViewModel : ViewModelBaseWithErrorDialog, IInternalDebugger {
private IMemory? _memory;
+ private readonly DebugWindowViewModel _debugWindowViewModel;
+ private bool _needToUpdateBinaryDocument;
+
[ObservableProperty]
private MemoryBinaryDocument? _memoryBinaryDocument;
+
+ public bool NeedsToVisitEmulator => _memory is null;
+
+ private uint? _startAddress = 0;
+
+ [NotifyCanExecuteChangedFor(nameof(UpdateBinaryDocumentCommand))]
+ [NotifyCanExecuteChangedFor(nameof(DumpMemoryCommand))]
+ [ObservableProperty]
+ private bool _isMemoryRangeValid;
+
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(CloseTabCommand))]
+ private bool _canCloseTab;
+
+ [RelayCommand(CanExecute = nameof(CanCloseTab))]
+ private void CloseTab() {
+ _debugWindowViewModel.CloseTab(this);
+ UpdateCanCloseTabProperty();
+ }
+
+ private bool GetIsMemoryRangeValid() {
+ if (_memory is null) {
+ return false;
+ }
+
+ return StartAddress <= (EndAddress ?? _memory.Length)
+ && EndAddress >= (StartAddress ?? 0);
+ }
+
+ public uint? StartAddress {
+ get => _startAddress;
+ set {
+ SetProperty(ref _startAddress, value);
+ IsMemoryRangeValid = GetIsMemoryRangeValid();
+ TryUpdateHeaderAndMemoryDocument();
+ }
+ }
+
+ private void TryUpdateHeaderAndMemoryDocument() {
+ if (!UpdateBinaryDocumentCommand.CanExecute(null)) {
+ return;
+ }
+ Header = $"{StartAddress:X} - {EndAddress:X}";
+ UpdateBinaryDocumentCommand.Execute(null);
+ }
+
+ private uint? _endAddress = 0;
+
+ public uint? EndAddress {
+ get => _endAddress;
+ set {
+ SetProperty(ref _endAddress, value);
+ IsMemoryRangeValid = GetIsMemoryRangeValid();
+ TryUpdateHeaderAndMemoryDocument();
+ }
+ }
+
+ [ObservableProperty]
+ private string _header = "Memory View";
[ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(NewMemoryViewCommand))]
+ [NotifyCanExecuteChangedFor(nameof(EditMemoryCommand))]
private bool _isPaused;
private readonly IPauseStatus _pauseStatus;
+ private readonly IHostStorageProvider _storageProvider;
- public MemoryViewModel( IPauseStatus pauseStatus) {
+ public MemoryViewModel(DebugWindowViewModel debugWindowViewModel, ITextClipboard textClipboard, IUIDispatcherTimerFactory dispatcherTimerFactory, IHostStorageProvider storageProvider, IPauseStatus pauseStatus, uint startAddress, uint endAddress = 0) : base(textClipboard) {
+ _debugWindowViewModel = debugWindowViewModel;
pauseStatus.PropertyChanged += PauseStatus_PropertyChanged;
_pauseStatus = pauseStatus;
+ _storageProvider = storageProvider;
IsPaused = _pauseStatus.IsPaused;
+ StartAddress = startAddress;
+ EndAddress = endAddress;
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ UpdateCanCloseTabProperty();
+ debugWindowViewModel.MemoryViewModels.CollectionChanged += OnDebugViewModelCollectionChanged;
+ }
+
+ private void UpdateCanCloseTabProperty() {
+ CanCloseTab = _debugWindowViewModel.MemoryViewModels.Count > 1;
+ }
+
+ private void OnDebugViewModelCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
+ UpdateCanCloseTabProperty();
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (!_needToUpdateBinaryDocument) {
+ return;
+ }
+ UpdateBinaryDocument();
+ _needToUpdateBinaryDocument = false;
}
private void PauseStatus_PropertyChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName != nameof(IPauseStatus.IsPaused)) {
return;
}
+ UpdateCanCloseTabProperty();
IsPaused = _pauseStatus.IsPaused;
if(IsPaused) {
- UpdateBinaryDocument();
+ _needToUpdateBinaryDocument = true;
}
}
+
+ [RelayCommand(CanExecute = nameof(IsPaused))]
+ public void NewMemoryView() {
+ _debugWindowViewModel.NewMemoryViewCommand.Execute(null);
+ }
+ [RelayCommand(CanExecute = nameof(IsMemoryRangeValid))]
private void UpdateBinaryDocument() {
- if (_memory is not null) {
- MemoryBinaryDocument = new MemoryBinaryDocument(_memory);
+ if (_memory is null || StartAddress is null || EndAddress is null) {
+ return;
+ }
+ MemoryBinaryDocument = new MemoryBinaryDocument(_memory, StartAddress.Value, EndAddress.Value);
+ MemoryBinaryDocument.MemoryReadInvalidOperation -= OnMemoryReadInvalidOperation;
+ MemoryBinaryDocument.MemoryReadInvalidOperation += OnMemoryReadInvalidOperation;
+ }
+
+ private void OnMemoryReadInvalidOperation(Exception exception) {
+ Dispatcher.UIThread.Post(() => ShowError(exception));
+ }
+
+ [RelayCommand(CanExecute = nameof(IsMemoryRangeValid))]
+ private async Task DumpMemory() {
+ if (_memory is not null && StartAddress is not null && EndAddress is not null) {
+ await _storageProvider.SaveBinaryFile(_memory.GetData(StartAddress.Value, EndAddress.Value - StartAddress.Value));
}
}
@@ -82,26 +195,24 @@ private bool TryParseMemoryAddress(string? memoryAddress, [NotNullWhen(true)] ou
return true;
}
} catch (Exception e) {
- ShowError(e);
+ Dispatcher.UIThread.Post(() => ShowError(e));
}
address = null;
return false;
}
[RelayCommand]
- public void CancelMemoryEdit() {
- IsEditingMemory = false;
- }
+ public void CancelMemoryEdit() => IsEditingMemory = false;
[RelayCommand]
public void ApplyMemoryEdit() {
if (!TryParseMemoryAddress(MemoryEditAddress, out uint? address) ||
MemoryEditValue is null ||
+ _memory is null ||
!long.TryParse(MemoryEditValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long value)) {
return;
}
MemoryBinaryDocument?.WriteBytes(address.Value, BitConverter.GetBytes(value));
- UpdateBinaryDocument();
IsEditingMemory = false;
}
@@ -109,7 +220,13 @@ public void Visit(T component) where T : IDebuggableComponent {
if (component is not IMemory memory) {
return;
}
- _memory ??= memory;
- MemoryBinaryDocument ??= new MemoryBinaryDocument(memory);
+
+ if (_memory is null) {
+ _memory = memory;
+ if (EndAddress is 0) {
+ EndAddress = _memory.Length;
+ }
+ }
+ TryUpdateHeaderAndMemoryDocument();
}
}
diff --git a/src/Spice86/ViewModels/MidiViewModel.cs b/src/Spice86/ViewModels/MidiViewModel.cs
index 28fec4390..08eb1bc6e 100644
--- a/src/Spice86/ViewModels/MidiViewModel.cs
+++ b/src/Spice86/ViewModels/MidiViewModel.cs
@@ -1,24 +1,36 @@
namespace Spice86.ViewModels;
+using Avalonia.Threading;
+
using CommunityToolkit.Mvvm.ComponentModel;
using Spice86.Core.Emulator.Devices.Sound.Midi;
using Spice86.Core.Emulator.InternalDebugger;
+using Spice86.Infrastructure;
using Spice86.Models.Debugging;
public partial class MidiViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private MidiInfo _midi = new();
- public void Visit(T component) where T : IDebuggableComponent {
- if(component is Midi midi) {
- VisitExternalMidiDevice(midi);
+ private Midi? _externalMidiDevice;
+
+ public MidiViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory) {
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (_externalMidiDevice is null) {
+ return;
}
+ Midi.LastPortRead = _externalMidiDevice.LastPortRead;
+ Midi.LastPortWritten = _externalMidiDevice.LastPortWritten;
+ Midi.LastPortWrittenValue = _externalMidiDevice.LastPortWrittenValue;
}
-
- private void VisitExternalMidiDevice(Midi externalMidiDevice) {
- Midi.LastPortRead = externalMidiDevice.LastPortRead;
- Midi.LastPortWritten = externalMidiDevice.LastPortWritten;
- Midi.LastPortWrittenValue = externalMidiDevice.LastPortWrittenValue;
+
+ public void Visit(T component) where T : IDebuggableComponent {
+ _externalMidiDevice ??= component as Midi;
}
+
+ public bool NeedsToVisitEmulator => _externalMidiDevice is null;
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/PaletteViewModel.cs b/src/Spice86/ViewModels/PaletteViewModel.cs
index 4742a859e..92d2f9cfa 100644
--- a/src/Spice86/ViewModels/PaletteViewModel.cs
+++ b/src/Spice86/ViewModels/PaletteViewModel.cs
@@ -1,6 +1,7 @@
namespace Spice86.ViewModels;
using Avalonia.Collections;
+using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Threading;
@@ -9,11 +10,14 @@ namespace Spice86.ViewModels;
using Spice86.Core.Emulator.Devices.Video;
using Spice86.Core.Emulator.InternalDebugger;
+using Spice86.Infrastructure;
using Spice86.Shared.Emulator.Video;
public partial class PaletteViewModel : ViewModelBase, IInternalDebugger {
private ArgbPalette? _argbPalette;
- public PaletteViewModel() {
+
+ public PaletteViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory) {
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
Dispatcher.UIThread.Post(() => {
for (int i = 0; i < 256; i++) {
_palette.Add(new (){Fill = new SolidColorBrush()});
@@ -21,6 +25,13 @@ public PaletteViewModel() {
});
}
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (_argbPalette is null) {
+ return;
+ }
+ UpdateColors(_argbPalette);
+ }
+
[ObservableProperty]
private AvaloniaList _palette = new();
@@ -34,16 +45,15 @@ private void UpdateColors(ArgbPalette palette) {
fill.Color = Color.FromRgb(rgb.R, rgb.G, rgb.B);
}
}
- } catch {
+ } catch(IndexOutOfRangeException) {
//A read during emulation provoked an OutOfRangeException (for example, in the DAC).
// Ignore it.
}
}
+ public bool NeedsToVisitEmulator => _argbPalette is null;
+
public void Visit(T component) where T : IDebuggableComponent {
_argbPalette ??= component as ArgbPalette;
- if (_argbPalette is not null) {
- UpdateColors(_argbPalette);
- }
}
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/PerformanceViewModel.cs b/src/Spice86/ViewModels/PerformanceViewModel.cs
index 00392b152..fea3d032a 100644
--- a/src/Spice86/ViewModels/PerformanceViewModel.cs
+++ b/src/Spice86/ViewModels/PerformanceViewModel.cs
@@ -15,27 +15,21 @@
public partial class PerformanceViewModel : ViewModelBase, IInternalDebugger {
private State? _state;
- private readonly IPerformanceMeasurer? _performanceMeasurer;
- private readonly IPauseStatus? _pauseStatus;
+ private readonly IPerformanceMeasurer _performanceMeasurer;
+ private readonly IPauseStatus _pauseStatus;
[ObservableProperty]
private double _averageInstructionsPerSecond;
-
- public PerformanceViewModel() {
- if (!Design.IsDesignMode) {
- throw new InvalidOperationException("This constructor is not for runtime usage");
- }
- }
-
- public PerformanceViewModel(IUIDispatcherTimerFactory iuiDispatcherTimerFactory, IDebuggableComponent programExecutor, IPerformanceMeasurer performanceMeasurer, IPauseStatus pauseStatus) {
+
+ public PerformanceViewModel(IUIDispatcherTimerFactory uiDispatcherTimerFactory, IDebuggableComponent programExecutor, IPerformanceMeasurer performanceMeasurer, IPauseStatus pauseStatus) : base() {
_pauseStatus = pauseStatus;
programExecutor.Accept(this);
_performanceMeasurer = performanceMeasurer;
- iuiDispatcherTimerFactory.StartNew(TimeSpan.FromSeconds(1.0 / 30.0), DispatcherPriority.MaxValue, UpdatePerformanceInfo);
+ uiDispatcherTimerFactory.StartNew(TimeSpan.FromSeconds(1.0 / 30.0), DispatcherPriority.MaxValue, UpdatePerformanceInfo);
}
private void UpdatePerformanceInfo(object? sender, EventArgs e) {
- if (_state is null || _performanceMeasurer is null || _pauseStatus is { IsPaused: true }) {
+ if (_state is null || _pauseStatus.IsPaused) {
return;
}
@@ -48,6 +42,8 @@ public void Visit(T component) where T : IDebuggableComponent {
_state ??= component as State;
}
+ public bool NeedsToVisitEmulator => _state is null;
+
[ObservableProperty]
private double _instructionsExecuted;
diff --git a/src/Spice86/ViewModels/SoftwareMixerViewModel.cs b/src/Spice86/ViewModels/SoftwareMixerViewModel.cs
index cf7d933c5..aaa836dad 100644
--- a/src/Spice86/ViewModels/SoftwareMixerViewModel.cs
+++ b/src/Spice86/ViewModels/SoftwareMixerViewModel.cs
@@ -1,21 +1,38 @@
namespace Spice86.ViewModels;
using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spice86.Core.Emulator.Devices.Sound;
using Spice86.Core.Emulator.InternalDebugger;
+using Spice86.Infrastructure;
using Spice86.Models.Debugging;
using System.ComponentModel;
public partial class SoftwareMixerViewModel : ViewModelBase, IInternalDebugger {
private readonly Dictionary _channelInfos = new();
+ private SoftwareMixer? _softwareMixer;
[ObservableProperty]
private AvaloniaList _channels = new();
+
+ public bool NeedsToVisitEmulator => _softwareMixer is null;
+
+ public SoftwareMixerViewModel(IUIDispatcherTimerFactory uiDispatcherTimerFactory) {
+ uiDispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if (_softwareMixer is null) {
+ return;
+ }
+ UpdateChannels(_softwareMixer);
+ }
[RelayCommand]
private void ResetStereoSeparation(object? parameter) {
@@ -55,8 +72,6 @@ private void OnChannelPropertyChanged(object? sender, PropertyChangedEventArgs e
}
public void Visit(T component) where T : IDebuggableComponent {
- if (component is SoftwareMixer mixer) {
- UpdateChannels(mixer);
- }
+ _softwareMixer ??= component as SoftwareMixer;
}
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/VideoCardViewModel.cs b/src/Spice86/ViewModels/VideoCardViewModel.cs
index 2d7a4e8fa..4de89f736 100644
--- a/src/Spice86/ViewModels/VideoCardViewModel.cs
+++ b/src/Spice86/ViewModels/VideoCardViewModel.cs
@@ -1,24 +1,38 @@
namespace Spice86.ViewModels;
+using Avalonia.Controls;
+using Avalonia.Threading;
+
using CommunityToolkit.Mvvm.ComponentModel;
using Spice86.Core.Emulator.Devices.Video;
using Spice86.Core.Emulator.InternalDebugger;
+using Spice86.Infrastructure;
using Spice86.Models.Debugging;
public partial class VideoCardViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private VideoCardInfo _videoCard = new();
+ private IVgaRenderer? _vgaRenderer;
+ private IVideoState? _videoState;
- public void Visit(T component) where T : IDebuggableComponent {
- switch (component) {
- case IVgaRenderer vgaRenderer:
- VisitVgaRenderer(vgaRenderer);
- break;
- case IVideoState videoState:
- VisitVideoState(videoState);
- break;
+ public bool NeedsToVisitEmulator => _vgaRenderer is null || _videoState is null;
+
+ public VideoCardViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory) {
+ dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
+ }
+
+ private void UpdateValues(object? sender, EventArgs e) {
+ if(_vgaRenderer is null || _videoState is null) {
+ return;
}
+ VisitVgaRenderer(_vgaRenderer);
+ VisitVideoState(_videoState);
+ }
+
+ public void Visit(T component) where T : IDebuggableComponent {
+ _vgaRenderer ??= component as IVgaRenderer;
+ _videoState ??= component as IVideoState;
}
private void VisitVgaRenderer(IVgaRenderer vgaRenderer) {
@@ -29,134 +43,139 @@ private void VisitVgaRenderer(IVgaRenderer vgaRenderer) {
}
private void VisitVideoState(IVideoState videoState) {
- VideoCard.GeneralMiscellaneousOutputRegister = videoState.GeneralRegisters.MiscellaneousOutput.Value;
- VideoCard.GeneralClockSelect = videoState.GeneralRegisters.MiscellaneousOutput.ClockSelect;
- VideoCard.GeneralEnableRam = videoState.GeneralRegisters.MiscellaneousOutput.EnableRam;
- VideoCard.GeneralVerticalSize = videoState.GeneralRegisters.MiscellaneousOutput.VerticalSize;
- VideoCard.GeneralHorizontalSyncPolarity = videoState.GeneralRegisters.MiscellaneousOutput.HorizontalSyncPolarity;
- VideoCard.GeneralVerticalSyncPolarity = videoState.GeneralRegisters.MiscellaneousOutput.VerticalSyncPolarity;
- VideoCard.GeneralIoAddressSelect = videoState.GeneralRegisters.MiscellaneousOutput.IoAddressSelect;
- VideoCard.GeneralOddPageSelect = videoState.GeneralRegisters.MiscellaneousOutput.EvenPageSelect;
- VideoCard.GeneralInputStatusRegister0 = videoState.GeneralRegisters.InputStatusRegister0.Value;
- VideoCard.GeneralCrtInterrupt = videoState.GeneralRegisters.InputStatusRegister0.CrtInterrupt;
- VideoCard.GeneralSwitchSense = videoState.GeneralRegisters.InputStatusRegister0.SwitchSense;
- VideoCard.GeneralInputStatusRegister1 = videoState.GeneralRegisters.InputStatusRegister1.Value;
- VideoCard.GeneralDisplayDisabled = videoState.GeneralRegisters.InputStatusRegister1.DisplayDisabled;
- VideoCard.GeneralVerticalRetrace = videoState.GeneralRegisters.InputStatusRegister1.VerticalRetrace;
+ try {
+ VideoCard.GeneralMiscellaneousOutputRegister = videoState.GeneralRegisters.MiscellaneousOutput.Value;
+ VideoCard.GeneralClockSelect = videoState.GeneralRegisters.MiscellaneousOutput.ClockSelect;
+ VideoCard.GeneralEnableRam = videoState.GeneralRegisters.MiscellaneousOutput.EnableRam;
+ VideoCard.GeneralVerticalSize = videoState.GeneralRegisters.MiscellaneousOutput.VerticalSize;
+ VideoCard.GeneralHorizontalSyncPolarity = videoState.GeneralRegisters.MiscellaneousOutput.HorizontalSyncPolarity;
+ VideoCard.GeneralVerticalSyncPolarity = videoState.GeneralRegisters.MiscellaneousOutput.VerticalSyncPolarity;
+ VideoCard.GeneralIoAddressSelect = videoState.GeneralRegisters.MiscellaneousOutput.IoAddressSelect;
+ VideoCard.GeneralOddPageSelect = videoState.GeneralRegisters.MiscellaneousOutput.EvenPageSelect;
+ VideoCard.GeneralInputStatusRegister0 = videoState.GeneralRegisters.InputStatusRegister0.Value;
+ VideoCard.GeneralCrtInterrupt = videoState.GeneralRegisters.InputStatusRegister0.CrtInterrupt;
+ VideoCard.GeneralSwitchSense = videoState.GeneralRegisters.InputStatusRegister0.SwitchSense;
+ VideoCard.GeneralInputStatusRegister1 = videoState.GeneralRegisters.InputStatusRegister1.Value;
+ VideoCard.GeneralDisplayDisabled = videoState.GeneralRegisters.InputStatusRegister1.DisplayDisabled;
+ VideoCard.GeneralVerticalRetrace = videoState.GeneralRegisters.InputStatusRegister1.VerticalRetrace;
- VideoCard.DacReadIndex = videoState.DacRegisters.IndexRegisterReadMode;
- VideoCard.DacWriteIndex = videoState.DacRegisters.IndexRegisterWriteMode;
- VideoCard.DacPixelMask = videoState.DacRegisters.PixelMask;
- VideoCard.DacData = videoState.DacRegisters.DataPeek;
+ VideoCard.DacReadIndex = videoState.DacRegisters.IndexRegisterReadMode;
+ VideoCard.DacWriteIndex = videoState.DacRegisters.IndexRegisterWriteMode;
+ VideoCard.DacPixelMask = videoState.DacRegisters.PixelMask;
+ VideoCard.DacData = videoState.DacRegisters.DataPeek;
- VideoCard.AttributeControllerColorSelect = videoState.AttributeControllerRegisters.ColorSelectRegister.Value;
- VideoCard.AttributeControllerOverscanColor = videoState.AttributeControllerRegisters.OverscanColor;
- VideoCard.AttributeControllerAttributeModeControl = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.Value;
- VideoCard.AttributeControllerVideoOutput45Select = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.VideoOutput45Select;
- VideoCard.AttributeControllerPixelWidth8 = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.PixelWidth8;
- VideoCard.AttributeControllerPixelPanningCompatibility = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.PixelPanningCompatibility;
- VideoCard.AttributeControllerBlinkingEnabled = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.BlinkingEnabled;
- VideoCard.AttributeControllerLineGraphicsEnabled = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.LineGraphicsEnabled;
- VideoCard.AttributeControllerMonochromeEmulation = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.MonochromeEmulation;
- VideoCard.AttributeControllerGraphicsMode = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.GraphicsMode;
- VideoCard.AttributeControllerColorPlaneEnable = videoState.AttributeControllerRegisters.ColorPlaneEnableRegister.Value;
- VideoCard.AttributeControllerHorizontalPixelPanning = videoState.AttributeControllerRegisters.HorizontalPixelPanning;
+ VideoCard.AttributeControllerColorSelect = videoState.AttributeControllerRegisters.ColorSelectRegister.Value;
+ VideoCard.AttributeControllerOverscanColor = videoState.AttributeControllerRegisters.OverscanColor;
+ VideoCard.AttributeControllerAttributeModeControl = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.Value;
+ VideoCard.AttributeControllerVideoOutput45Select = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.VideoOutput45Select;
+ VideoCard.AttributeControllerPixelWidth8 = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.PixelWidth8;
+ VideoCard.AttributeControllerPixelPanningCompatibility = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.PixelPanningCompatibility;
+ VideoCard.AttributeControllerBlinkingEnabled = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.BlinkingEnabled;
+ VideoCard.AttributeControllerLineGraphicsEnabled = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.LineGraphicsEnabled;
+ VideoCard.AttributeControllerMonochromeEmulation = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.MonochromeEmulation;
+ VideoCard.AttributeControllerGraphicsMode = videoState.AttributeControllerRegisters.AttributeControllerModeRegister.GraphicsMode;
+ VideoCard.AttributeControllerColorPlaneEnable = videoState.AttributeControllerRegisters.ColorPlaneEnableRegister.Value;
+ VideoCard.AttributeControllerHorizontalPixelPanning = videoState.AttributeControllerRegisters.HorizontalPixelPanning;
- VideoCard.CrtControllerAddressWrap = videoState.CrtControllerRegisters.CrtModeControlRegister.AddressWrap;
- VideoCard.CrtControllerBytePanning = videoState.CrtControllerRegisters.PresetRowScanRegister.BytePanning;
- VideoCard.CrtControllerByteWordMode = videoState.CrtControllerRegisters.CrtModeControlRegister.ByteWordMode;
- VideoCard.CrtControllerCharacterCellHeightRegister = videoState.CrtControllerRegisters.MaximumScanlineRegister.Value;
- VideoCard.CrtControllerCharacterCellHeight = videoState.CrtControllerRegisters.MaximumScanlineRegister.MaximumScanline;
- VideoCard.CrtControllerCompatibilityModeSupport = videoState.CrtControllerRegisters.CrtModeControlRegister.CompatibilityModeSupport;
- VideoCard.CrtControllerCompatibleRead = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.CompatibleRead;
- VideoCard.CrtControllerCountByFour = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.CountByFour;
- VideoCard.CrtControllerCountByTwo = videoState.CrtControllerRegisters.CrtModeControlRegister.CountByTwo;
- VideoCard.CrtControllerCrtcScanDouble = videoState.CrtControllerRegisters.MaximumScanlineRegister.CrtcScanDouble;
- VideoCard.CrtControllerCrtModeControl = videoState.CrtControllerRegisters.CrtModeControlRegister.Value;
- VideoCard.CrtControllerCursorEnd = videoState.CrtControllerRegisters.TextCursorEndRegister.Value;
- VideoCard.CrtControllerCursorLocationHigh = videoState.CrtControllerRegisters.TextCursorLocationHigh;
- VideoCard.CrtControllerCursorLocationLow = videoState.CrtControllerRegisters.TextCursorLocationLow;
- VideoCard.CrtControllerCursorStart = videoState.CrtControllerRegisters.TextCursorStartRegister.Value;
- VideoCard.CrtControllerDisableTextCursor = videoState.CrtControllerRegisters.TextCursorStartRegister.DisableTextCursor;
- VideoCard.CrtControllerDisableVerticalInterrupt = videoState.CrtControllerRegisters.VerticalSyncEndRegister.DisableVerticalInterrupt;
- VideoCard.CrtControllerDisplayEnableSkew = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.DisplayEnableSkew;
- VideoCard.CrtControllerDoubleWordMode = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.DoubleWordMode;
- VideoCard.CrtControllerEndHorizontalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.Value;
- VideoCard.CrtControllerEndHorizontalDisplay = videoState.CrtControllerRegisters.HorizontalDisplayEnd;
- VideoCard.CrtControllerEndHorizontalRetrace = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.Value;
- VideoCard.CrtControllerEndVerticalBlanking = videoState.CrtControllerRegisters.VerticalBlankingEnd;
- VideoCard.CrtControllerHorizontalBlankingEnd = videoState.CrtControllerRegisters.HorizontalBlankingEndValue;
- VideoCard.CrtControllerHorizontalSyncDelay = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.HorizontalSyncDelay;
- VideoCard.CrtControllerHorizontalSyncEnd = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.HorizontalSyncEnd;
- VideoCard.CrtControllerHorizontalTotal = videoState.CrtControllerRegisters.HorizontalTotal;
- VideoCard.CrtControllerLineCompareRegister = videoState.CrtControllerRegisters.LineCompare;
- VideoCard.CrtControllerLineCompare = videoState.CrtControllerRegisters.LineCompareValue;
- VideoCard.CrtControllerOffset = videoState.CrtControllerRegisters.Offset;
- VideoCard.CrtControllerOverflow = videoState.CrtControllerRegisters.OverflowRegister.Value;
- VideoCard.CrtControllerPresetRowScan = videoState.CrtControllerRegisters.PresetRowScanRegister.PresetRowScan;
- VideoCard.CrtControllerPresetRowScanRegister = videoState.CrtControllerRegisters.PresetRowScanRegister.Value;
- VideoCard.CrtControllerRefreshCyclesPerScanline = videoState.CrtControllerRegisters.VerticalSyncEndRegister.RefreshCyclesPerScanline;
- VideoCard.CrtControllerSelectRowScanCounter = videoState.CrtControllerRegisters.CrtModeControlRegister.SelectRowScanCounter;
- VideoCard.CrtControllerStartAddress = videoState.CrtControllerRegisters.ScreenStartAddress;
- VideoCard.CrtControllerStartAddressHigh = videoState.CrtControllerRegisters.ScreenStartAddressHigh;
- VideoCard.CrtControllerStartAddressLow = videoState.CrtControllerRegisters.ScreenStartAddressLow;
- VideoCard.CrtControllerStartHorizontalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingStart;
- VideoCard.CrtControllerStartHorizontalRetrace = videoState.CrtControllerRegisters.HorizontalSyncStart;
- VideoCard.CrtControllerStartVerticalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingStart;
- VideoCard.CrtControllerTextCursorEnd = videoState.CrtControllerRegisters.TextCursorEndRegister.TextCursorEnd;
- VideoCard.CrtControllerTextCursorLocation = videoState.CrtControllerRegisters.TextCursorLocation;
- VideoCard.CrtControllerTextCursorSkew = videoState.CrtControllerRegisters.TextCursorEndRegister.TextCursorSkew;
- VideoCard.CrtControllerTextCursorStart = videoState.CrtControllerRegisters.TextCursorStartRegister.TextCursorStart;
- VideoCard.CrtControllerTimingEnable = videoState.CrtControllerRegisters.CrtModeControlRegister.TimingEnable;
- VideoCard.CrtControllerUnderlineLocation = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.Value;
- VideoCard.CrtControllerUnderlineScanline = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.UnderlineScanline;
- VideoCard.CrtControllerVerticalBlankingStart = videoState.CrtControllerRegisters.VerticalBlankingStartValue;
- VideoCard.CrtControllerVerticalDisplayEnd = videoState.CrtControllerRegisters.VerticalDisplayEndValue;
- VideoCard.CrtControllerVerticalDisplayEndRegister = videoState.CrtControllerRegisters.VerticalDisplayEnd;
- VideoCard.CrtControllerVerticalRetraceEnd = videoState.CrtControllerRegisters.VerticalSyncEndRegister.Value;
- VideoCard.CrtControllerVerticalRetraceStart = videoState.CrtControllerRegisters.VerticalSyncStart;
- VideoCard.CrtControllerVerticalSyncStart = videoState.CrtControllerRegisters.VerticalSyncStartValue;
- VideoCard.CrtControllerVerticalTimingHalved = videoState.CrtControllerRegisters.CrtModeControlRegister.VerticalTimingHalved;
- VideoCard.CrtControllerVerticalTotal = videoState.CrtControllerRegisters.VerticalTotalValue;
- VideoCard.CrtControllerVerticalTotalRegister = videoState.CrtControllerRegisters.VerticalTotal;
- VideoCard.CrtControllerWriteProtect = videoState.CrtControllerRegisters.VerticalSyncEndRegister.WriteProtect;
+ VideoCard.CrtControllerAddressWrap = videoState.CrtControllerRegisters.CrtModeControlRegister.AddressWrap;
+ VideoCard.CrtControllerBytePanning = videoState.CrtControllerRegisters.PresetRowScanRegister.BytePanning;
+ VideoCard.CrtControllerByteWordMode = videoState.CrtControllerRegisters.CrtModeControlRegister.ByteWordMode;
+ VideoCard.CrtControllerCharacterCellHeightRegister = videoState.CrtControllerRegisters.MaximumScanlineRegister.Value;
+ VideoCard.CrtControllerCharacterCellHeight = videoState.CrtControllerRegisters.MaximumScanlineRegister.MaximumScanline;
+ VideoCard.CrtControllerCompatibilityModeSupport = videoState.CrtControllerRegisters.CrtModeControlRegister.CompatibilityModeSupport;
+ VideoCard.CrtControllerCompatibleRead = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.CompatibleRead;
+ VideoCard.CrtControllerCountByFour = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.CountByFour;
+ VideoCard.CrtControllerCountByTwo = videoState.CrtControllerRegisters.CrtModeControlRegister.CountByTwo;
+ VideoCard.CrtControllerCrtcScanDouble = videoState.CrtControllerRegisters.MaximumScanlineRegister.CrtcScanDouble;
+ VideoCard.CrtControllerCrtModeControl = videoState.CrtControllerRegisters.CrtModeControlRegister.Value;
+ VideoCard.CrtControllerCursorEnd = videoState.CrtControllerRegisters.TextCursorEndRegister.Value;
+ VideoCard.CrtControllerCursorLocationHigh = videoState.CrtControllerRegisters.TextCursorLocationHigh;
+ VideoCard.CrtControllerCursorLocationLow = videoState.CrtControllerRegisters.TextCursorLocationLow;
+ VideoCard.CrtControllerCursorStart = videoState.CrtControllerRegisters.TextCursorStartRegister.Value;
+ VideoCard.CrtControllerDisableTextCursor = videoState.CrtControllerRegisters.TextCursorStartRegister.DisableTextCursor;
+ VideoCard.CrtControllerDisableVerticalInterrupt = videoState.CrtControllerRegisters.VerticalSyncEndRegister.DisableVerticalInterrupt;
+ VideoCard.CrtControllerDisplayEnableSkew = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.DisplayEnableSkew;
+ VideoCard.CrtControllerDoubleWordMode = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.DoubleWordMode;
+ VideoCard.CrtControllerEndHorizontalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingEndRegister.Value;
+ VideoCard.CrtControllerEndHorizontalDisplay = videoState.CrtControllerRegisters.HorizontalDisplayEnd;
+ VideoCard.CrtControllerEndHorizontalRetrace = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.Value;
+ VideoCard.CrtControllerEndVerticalBlanking = videoState.CrtControllerRegisters.VerticalBlankingEnd;
+ VideoCard.CrtControllerHorizontalBlankingEnd = videoState.CrtControllerRegisters.HorizontalBlankingEndValue;
+ VideoCard.CrtControllerHorizontalSyncDelay = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.HorizontalSyncDelay;
+ VideoCard.CrtControllerHorizontalSyncEnd = videoState.CrtControllerRegisters.HorizontalSyncEndRegister.HorizontalSyncEnd;
+ VideoCard.CrtControllerHorizontalTotal = videoState.CrtControllerRegisters.HorizontalTotal;
+ VideoCard.CrtControllerLineCompareRegister = videoState.CrtControllerRegisters.LineCompare;
+ VideoCard.CrtControllerLineCompare = videoState.CrtControllerRegisters.LineCompareValue;
+ VideoCard.CrtControllerOffset = videoState.CrtControllerRegisters.Offset;
+ VideoCard.CrtControllerOverflow = videoState.CrtControllerRegisters.OverflowRegister.Value;
+ VideoCard.CrtControllerPresetRowScan = videoState.CrtControllerRegisters.PresetRowScanRegister.PresetRowScan;
+ VideoCard.CrtControllerPresetRowScanRegister = videoState.CrtControllerRegisters.PresetRowScanRegister.Value;
+ VideoCard.CrtControllerRefreshCyclesPerScanline = videoState.CrtControllerRegisters.VerticalSyncEndRegister.RefreshCyclesPerScanline;
+ VideoCard.CrtControllerSelectRowScanCounter = videoState.CrtControllerRegisters.CrtModeControlRegister.SelectRowScanCounter;
+ VideoCard.CrtControllerStartAddress = videoState.CrtControllerRegisters.ScreenStartAddress;
+ VideoCard.CrtControllerStartAddressHigh = videoState.CrtControllerRegisters.ScreenStartAddressHigh;
+ VideoCard.CrtControllerStartAddressLow = videoState.CrtControllerRegisters.ScreenStartAddressLow;
+ VideoCard.CrtControllerStartHorizontalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingStart;
+ VideoCard.CrtControllerStartHorizontalRetrace = videoState.CrtControllerRegisters.HorizontalSyncStart;
+ VideoCard.CrtControllerStartVerticalBlanking = videoState.CrtControllerRegisters.HorizontalBlankingStart;
+ VideoCard.CrtControllerTextCursorEnd = videoState.CrtControllerRegisters.TextCursorEndRegister.TextCursorEnd;
+ VideoCard.CrtControllerTextCursorLocation = videoState.CrtControllerRegisters.TextCursorLocation;
+ VideoCard.CrtControllerTextCursorSkew = videoState.CrtControllerRegisters.TextCursorEndRegister.TextCursorSkew;
+ VideoCard.CrtControllerTextCursorStart = videoState.CrtControllerRegisters.TextCursorStartRegister.TextCursorStart;
+ VideoCard.CrtControllerTimingEnable = videoState.CrtControllerRegisters.CrtModeControlRegister.TimingEnable;
+ VideoCard.CrtControllerUnderlineLocation = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.Value;
+ VideoCard.CrtControllerUnderlineScanline = videoState.CrtControllerRegisters.UnderlineRowScanlineRegister.UnderlineScanline;
+ VideoCard.CrtControllerVerticalBlankingStart = videoState.CrtControllerRegisters.VerticalBlankingStartValue;
+ VideoCard.CrtControllerVerticalDisplayEnd = videoState.CrtControllerRegisters.VerticalDisplayEndValue;
+ VideoCard.CrtControllerVerticalDisplayEndRegister = videoState.CrtControllerRegisters.VerticalDisplayEnd;
+ VideoCard.CrtControllerVerticalRetraceEnd = videoState.CrtControllerRegisters.VerticalSyncEndRegister.Value;
+ VideoCard.CrtControllerVerticalRetraceStart = videoState.CrtControllerRegisters.VerticalSyncStart;
+ VideoCard.CrtControllerVerticalSyncStart = videoState.CrtControllerRegisters.VerticalSyncStartValue;
+ VideoCard.CrtControllerVerticalTimingHalved = videoState.CrtControllerRegisters.CrtModeControlRegister.VerticalTimingHalved;
+ VideoCard.CrtControllerVerticalTotal = videoState.CrtControllerRegisters.VerticalTotalValue;
+ VideoCard.CrtControllerVerticalTotalRegister = videoState.CrtControllerRegisters.VerticalTotal;
+ VideoCard.CrtControllerWriteProtect = videoState.CrtControllerRegisters.VerticalSyncEndRegister.WriteProtect;
- VideoCard.GraphicsDataRotate = videoState.GraphicsControllerRegisters.DataRotateRegister.Value;
- VideoCard.GraphicsRotateCount = videoState.GraphicsControllerRegisters.DataRotateRegister.RotateCount;
- VideoCard.GraphicsFunctionSelect = videoState.GraphicsControllerRegisters.DataRotateRegister.FunctionSelect;
- VideoCard.GraphicsBitMask = videoState.GraphicsControllerRegisters.BitMask;
- VideoCard.GraphicsColorCompare = videoState.GraphicsControllerRegisters.ColorCompare;
- VideoCard.GraphicsReadMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.ReadMode;
- VideoCard.GraphicsWriteMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.WriteMode;
- VideoCard.GraphicsOddEven = videoState.GraphicsControllerRegisters.GraphicsModeRegister.OddEven;
- VideoCard.GraphicsShiftRegisterMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.ShiftRegisterMode;
- VideoCard.GraphicsIn256ColorMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.In256ColorMode;
- VideoCard.GraphicsModeRegister = videoState.GraphicsControllerRegisters.GraphicsModeRegister.Value;
- VideoCard.GraphicsMiscellaneousGraphics = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.Value;
- VideoCard.GraphicsGraphicsMode = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.GraphicsMode;
- VideoCard.GraphicsChainOddMapsToEven = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.ChainOddMapsToEven;
- VideoCard.GraphicsMemoryMap = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.MemoryMap;
- VideoCard.GraphicsReadMapSelect = videoState.GraphicsControllerRegisters.ReadMapSelectRegister.PlaneSelect;
- VideoCard.GraphicsSetReset = videoState.GraphicsControllerRegisters.SetReset.Value;
- VideoCard.GraphicsColorDontCare = videoState.GraphicsControllerRegisters.ColorDontCare;
- VideoCard.GraphicsEnableSetReset = videoState.GraphicsControllerRegisters.EnableSetReset.Value;
+ VideoCard.GraphicsDataRotate = videoState.GraphicsControllerRegisters.DataRotateRegister.Value;
+ VideoCard.GraphicsRotateCount = videoState.GraphicsControllerRegisters.DataRotateRegister.RotateCount;
+ VideoCard.GraphicsFunctionSelect = videoState.GraphicsControllerRegisters.DataRotateRegister.FunctionSelect;
+ VideoCard.GraphicsBitMask = videoState.GraphicsControllerRegisters.BitMask;
+ VideoCard.GraphicsColorCompare = videoState.GraphicsControllerRegisters.ColorCompare;
+ VideoCard.GraphicsReadMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.ReadMode;
+ VideoCard.GraphicsWriteMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.WriteMode;
+ VideoCard.GraphicsOddEven = videoState.GraphicsControllerRegisters.GraphicsModeRegister.OddEven;
+ VideoCard.GraphicsShiftRegisterMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.ShiftRegisterMode;
+ VideoCard.GraphicsIn256ColorMode = videoState.GraphicsControllerRegisters.GraphicsModeRegister.In256ColorMode;
+ VideoCard.GraphicsModeRegister = videoState.GraphicsControllerRegisters.GraphicsModeRegister.Value;
+ VideoCard.GraphicsMiscellaneousGraphics = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.Value;
+ VideoCard.GraphicsGraphicsMode = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.GraphicsMode;
+ VideoCard.GraphicsChainOddMapsToEven = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.ChainOddMapsToEven;
+ VideoCard.GraphicsMemoryMap = videoState.GraphicsControllerRegisters.MiscellaneousGraphicsRegister.MemoryMap;
+ VideoCard.GraphicsReadMapSelect = videoState.GraphicsControllerRegisters.ReadMapSelectRegister.PlaneSelect;
+ VideoCard.GraphicsSetReset = videoState.GraphicsControllerRegisters.SetReset.Value;
+ VideoCard.GraphicsColorDontCare = videoState.GraphicsControllerRegisters.ColorDontCare;
+ VideoCard.GraphicsEnableSetReset = videoState.GraphicsControllerRegisters.EnableSetReset.Value;
- VideoCard.SequencerResetRegister = videoState.SequencerRegisters.ResetRegister.Value;
- VideoCard.SequencerSynchronousReset = videoState.SequencerRegisters.ResetRegister.SynchronousReset;
- VideoCard.SequencerAsynchronousReset = videoState.SequencerRegisters.ResetRegister.AsynchronousReset;
- VideoCard.SequencerClockingModeRegister = videoState.SequencerRegisters.ClockingModeRegister.Value;
- VideoCard.SequencerDotsPerClock = videoState.SequencerRegisters.ClockingModeRegister.DotsPerClock;
- VideoCard.SequencerShiftLoad = videoState.SequencerRegisters.ClockingModeRegister.ShiftLoad;
- VideoCard.SequencerDotClock = videoState.SequencerRegisters.ClockingModeRegister.HalfDotClock;
- VideoCard.SequencerShift4 = videoState.SequencerRegisters.ClockingModeRegister.Shift4;
- VideoCard.SequencerScreenOff = videoState.SequencerRegisters.ClockingModeRegister.ScreenOff;
- VideoCard.SequencerPlaneMask = videoState.SequencerRegisters.PlaneMaskRegister.Value;
- VideoCard.SequencerCharacterMapSelect = videoState.SequencerRegisters.CharacterMapSelectRegister.Value;
- VideoCard.SequencerCharacterMapA = videoState.SequencerRegisters.CharacterMapSelectRegister.CharacterMapA;
- VideoCard.SequencerCharacterMapB = videoState.SequencerRegisters.CharacterMapSelectRegister.CharacterMapB;
- VideoCard.SequencerSequencerMemoryMode = videoState.SequencerRegisters.MemoryModeRegister.Value;
- VideoCard.SequencerExtendedMemory = videoState.SequencerRegisters.MemoryModeRegister.ExtendedMemory;
- VideoCard.SequencerOddEvenMode = videoState.SequencerRegisters.MemoryModeRegister.OddEvenMode;
- VideoCard.SequencerChain4Mode = videoState.SequencerRegisters.MemoryModeRegister.Chain4Mode;
+ VideoCard.SequencerResetRegister = videoState.SequencerRegisters.ResetRegister.Value;
+ VideoCard.SequencerSynchronousReset = videoState.SequencerRegisters.ResetRegister.SynchronousReset;
+ VideoCard.SequencerAsynchronousReset = videoState.SequencerRegisters.ResetRegister.AsynchronousReset;
+ VideoCard.SequencerClockingModeRegister = videoState.SequencerRegisters.ClockingModeRegister.Value;
+ VideoCard.SequencerDotsPerClock = videoState.SequencerRegisters.ClockingModeRegister.DotsPerClock;
+ VideoCard.SequencerShiftLoad = videoState.SequencerRegisters.ClockingModeRegister.ShiftLoad;
+ VideoCard.SequencerDotClock = videoState.SequencerRegisters.ClockingModeRegister.HalfDotClock;
+ VideoCard.SequencerShift4 = videoState.SequencerRegisters.ClockingModeRegister.Shift4;
+ VideoCard.SequencerScreenOff = videoState.SequencerRegisters.ClockingModeRegister.ScreenOff;
+ VideoCard.SequencerPlaneMask = videoState.SequencerRegisters.PlaneMaskRegister.Value;
+ VideoCard.SequencerCharacterMapSelect = videoState.SequencerRegisters.CharacterMapSelectRegister.Value;
+ VideoCard.SequencerCharacterMapA = videoState.SequencerRegisters.CharacterMapSelectRegister.CharacterMapA;
+ VideoCard.SequencerCharacterMapB = videoState.SequencerRegisters.CharacterMapSelectRegister.CharacterMapB;
+ VideoCard.SequencerSequencerMemoryMode = videoState.SequencerRegisters.MemoryModeRegister.Value;
+ VideoCard.SequencerExtendedMemory = videoState.SequencerRegisters.MemoryModeRegister.ExtendedMemory;
+ VideoCard.SequencerOddEvenMode = videoState.SequencerRegisters.MemoryModeRegister.OddEvenMode;
+ VideoCard.SequencerChain4Mode = videoState.SequencerRegisters.MemoryModeRegister.Chain4Mode;
+ } catch (IndexOutOfRangeException) {
+ //A read during emulation provoked an OutOfRangeException (for example, in the DacRegisters).
+ // Ignore it.
+ }
}
}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/ViewModelBase.cs b/src/Spice86/ViewModels/ViewModelBase.cs
index 0c15585f8..0ce2b8cf7 100644
--- a/src/Spice86/ViewModels/ViewModelBase.cs
+++ b/src/Spice86/ViewModels/ViewModelBase.cs
@@ -1,20 +1,5 @@
-namespace Spice86.ViewModels;
+namespace Spice86.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-public partial class ViewModelBase : ObservableObject {
-
- [RelayCommand]
- public void ClearDialog() => IsDialogVisible = false;
-
- protected void ShowError(Exception e) {
- Exception = e.GetBaseException();
- IsDialogVisible = true;
- }
- [ObservableProperty]
- private bool _isDialogVisible;
-
- [ObservableProperty]
- private Exception? _exception;
-}
+public partial class ViewModelBase : ObservableObject;
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/ViewModelBaseWithErrorDialog.cs b/src/Spice86/ViewModels/ViewModelBaseWithErrorDialog.cs
new file mode 100644
index 000000000..9169e9898
--- /dev/null
+++ b/src/Spice86/ViewModels/ViewModelBaseWithErrorDialog.cs
@@ -0,0 +1,38 @@
+namespace Spice86.ViewModels;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+using Spice86.Infrastructure;
+using Spice86.Models.Debugging;
+
+using System.Diagnostics;
+
+public partial class ViewModelBaseWithErrorDialog : ViewModelBase {
+ protected readonly ITextClipboard _textClipboard;
+
+ protected ViewModelBaseWithErrorDialog(ITextClipboard textClipboard) => _textClipboard = textClipboard;
+
+ [RelayCommand]
+ public void ClearDialog() => IsDialogVisible = false;
+
+ protected void ShowError(Exception e) {
+ Exception = e.GetBaseException();
+ IsDialogVisible = true;
+ }
+ [ObservableProperty]
+ private bool _isDialogVisible;
+
+ [ObservableProperty]
+ private Exception? _exception;
+
+ [RelayCommand]
+ public async Task CopyExceptionToClipboard() {
+ if(Exception is not null) {
+ Exception.Demystify();
+ await _textClipboard.SetTextAsync(
+ Newtonsoft.Json.JsonConvert.SerializeObject(
+ new ExceptionInfo(Exception.TargetSite?.ToString(), Exception.Message, Exception.StackTrace)));
+ }
+ }
+}
diff --git a/src/Spice86/Views/DebugWindow.axaml b/src/Spice86/Views/DebugWindow.axaml
index facad9d18..0eb2b5982 100644
--- a/src/Spice86/Views/DebugWindow.axaml
+++ b/src/Spice86/Views/DebugWindow.axaml
@@ -3,21 +3,17 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:Spice86.Views"
xmlns:vm="using:Spice86.ViewModels"
- Title="Debug"
- Width="800"
- Height="600"
- MinWidth="800"
- d:DesignHeight="450"
- d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:DebugWindowViewModel"
- CanResize="True"
- Icon="/Assets/Spice86.ico"
WindowStartupLocation="CenterOwner"
+ ShowInTaskbar="True"
+ Title="Spice86 Debug Window"
+ Width="800"
+ Height="600"
+ Icon="/Assets/Debug.ico"
mc:Ignorable="d">
@@ -26,14 +22,12 @@
+
+
+
-
-
-
-
-
@@ -51,10 +45,30 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -71,7 +85,7 @@
Margin="5,0,0,0"
VerticalAlignment="Bottom"
Command="{Binding ForceUpdateCommand}"
- Content="Force Update" />
+ Content="Force Refresh" />
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Spice86/Views/DebugWindow.axaml.cs b/src/Spice86/Views/DebugWindow.axaml.cs
index 9fd0ccbb9..387b2d56e 100644
--- a/src/Spice86/Views/DebugWindow.axaml.cs
+++ b/src/Spice86/Views/DebugWindow.axaml.cs
@@ -2,8 +2,6 @@ namespace Spice86.Views;
using Avalonia.Controls;
-using Spice86.ViewModels;
-
public sealed partial class DebugWindow : Window {
public DebugWindow() {
InitializeComponent();
diff --git a/src/Spice86/Views/DisassemblyView.axaml b/src/Spice86/Views/DisassemblyView.axaml
index b1fe63f41..d06518316 100644
--- a/src/Spice86/Views/DisassemblyView.axaml
+++ b/src/Spice86/Views/DisassemblyView.axaml
@@ -10,39 +10,45 @@
-
+
-
-
-
-
-
+ ColumnDefinitions="Auto,Auto">
+
+
+
+
+
-
+
-
+
-
+
diff --git a/src/Spice86/Views/MainWindow.axaml b/src/Spice86/Views/MainWindow.axaml
index 5addeba07..5e565db4f 100644
--- a/src/Spice86/Views/MainWindow.axaml
+++ b/src/Spice86/Views/MainWindow.axaml
@@ -5,6 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:Spice86.Converters"
xmlns:views="clr-namespace:Spice86.Views"
+ xmlns:userControls="clr-namespace:Spice86.UserControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Spice86.Views.MainWindow"
Icon="/Assets/Spice86.ico"
@@ -16,7 +17,6 @@
CanResize="True">
-
@@ -33,8 +33,7 @@
\ No newline at end of file
diff --git a/src/Spice86/Views/MemoryView.axaml b/src/Spice86/Views/MemoryView.axaml
index 856761981..ef45bc8a7 100644
--- a/src/Spice86/Views/MemoryView.axaml
+++ b/src/Spice86/Views/MemoryView.axaml
@@ -8,6 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rendering="clr-namespace:AvaloniaHex.Rendering;assembly=AvaloniaHex"
xmlns:viewModels="clr-namespace:Spice86.ViewModels"
+ xmlns:userControls="clr-namespace:Spice86.UserControls"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
@@ -15,16 +16,30 @@
mc:Ignorable="d">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -88,27 +103,7 @@
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/tests/Spice86.Tests/MachineTest.cs b/tests/Spice86.Tests/MachineTest.cs
index 61b339fef..aa30f90dc 100644
--- a/tests/Spice86.Tests/MachineTest.cs
+++ b/tests/Spice86.Tests/MachineTest.cs
@@ -355,7 +355,7 @@ private byte[] GetExpected(string binName) {
[AssertionMethod]
private void CompareMemoryWithExpected(IMemory memory, byte[] expected, int start, int end) {
- byte[] actual = memory.RamCopy;
+ byte[] actual = memory.ReadRam();
actual[start..end].Should().BeEquivalentTo(expected[start..end]);
}