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 @@ + + + + + + + + + + +