diff --git a/src/Spice86.Core/CLI/Configuration.cs b/src/Spice86.Core/CLI/Configuration.cs
index 6de5132aa..35986936f 100644
--- a/src/Spice86.Core/CLI/Configuration.cs
+++ b/src/Spice86.Core/CLI/Configuration.cs
@@ -137,4 +137,10 @@ public sealed class Configuration {
///
[Option(nameof(Mouse), Default = MouseType.Ps2, Required = false, HelpText = "Specify the type of mouse to use. Valid values are None, PS2 (default), and PS2Wheel")]
public MouseType Mouse { get; init; }
+
+ ///
+ /// Specify a C header file to be used for structure information
+ ///
+ [Option(nameof(StructureFile), Default = null, Required = false, HelpText = "Specify a C header file to be used for structure information")]
+ public string? StructureFile { get; init; }
}
\ No newline at end of file
diff --git a/src/Spice86.Core/Emulator/Function/IOverrideSupplier.cs b/src/Spice86.Core/Emulator/Function/IOverrideSupplier.cs
index 0455a786b..56803f538 100644
--- a/src/Spice86.Core/Emulator/Function/IOverrideSupplier.cs
+++ b/src/Spice86.Core/Emulator/Function/IOverrideSupplier.cs
@@ -16,6 +16,7 @@ public interface IOverrideSupplier {
/// The configuration.
/// The start address of the program.
/// The emulator machine.
+ ///
/// A dictionary containing the generated function information overrides.
public IDictionary GenerateFunctionInformations(
ILoggerService loggerService,
diff --git a/src/Spice86.Shared/Emulator/Memory/SegmentedAddress.cs b/src/Spice86.Shared/Emulator/Memory/SegmentedAddress.cs
index 440df8d85..d8ec304f6 100644
--- a/src/Spice86.Shared/Emulator/Memory/SegmentedAddress.cs
+++ b/src/Spice86.Shared/Emulator/Memory/SegmentedAddress.cs
@@ -3,6 +3,8 @@
using Spice86.Shared.Utils;
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Text.Json.Serialization;
///
@@ -140,4 +142,30 @@ public void Deconstruct(out ushort segment, out ushort offset) {
segment = Segment;
offset = Offset;
}
+
+ ///
+ /// Tries to parse a hexadecimal string in the format of segment:offset into a SegmentedAddress object.
+ ///
+ /// a hex string in the format of segment:offset
+ ///
+ /// true if s was converted successfully; otherwise, false.
+ public static bool TryParse(string? s, [NotNullWhen(true)] out SegmentedAddress segmentedAddress) {
+ if (string.IsNullOrWhiteSpace(s)) {
+ segmentedAddress = default;
+
+ return false;
+ }
+ string[] split = s.Split(":");
+ if (split.Length == 2
+ && ushort.TryParse(split[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort segment)
+ && ushort.TryParse(split[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort offset)) {
+ segmentedAddress = new SegmentedAddress(segment, offset);
+
+ return true;
+ }
+
+ segmentedAddress = default;
+
+ return false;
+ }
}
\ No newline at end of file
diff --git a/src/Spice86/Converters/SegmentedAddressConverter.cs b/src/Spice86/Converters/SegmentedAddressConverter.cs
new file mode 100644
index 000000000..066708a9f
--- /dev/null
+++ b/src/Spice86/Converters/SegmentedAddressConverter.cs
@@ -0,0 +1,38 @@
+namespace Spice86.Converters;
+
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+using Spice86.Shared.Emulator.Memory;
+
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+public partial class SegmentedAddressConverter : IValueConverter {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
+ return value switch {
+ null => null,
+ SegmentedAddress segmentedAddress => $"{segmentedAddress.Segment:X4}:{segmentedAddress.Offset:X4}",
+ _ => new BindingNotification(new InvalidCastException(), BindingErrorType.Error)
+ };
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
+ if (value is not string str || string.IsNullOrWhiteSpace(str)) {
+ return new SegmentedAddress();
+ }
+
+ Match match = SegmentedAddressRegex().Match(str);
+ if (match.Success) {
+ return new SegmentedAddress(
+ ushort.Parse(match.Groups[1].Value, NumberStyles.HexNumber, culture),
+ ushort.Parse(match.Groups[2].Value, NumberStyles.HexNumber, culture)
+ );
+ }
+
+ return null;
+ }
+
+ [GeneratedRegex(@"^([0-9A-Fa-f]{4}):([0-9A-Fa-f]{4})$")]
+ private static partial Regex SegmentedAddressRegex();
+}
\ No newline at end of file
diff --git a/src/Spice86/DataTemplates/DataTemplateProvider.cs b/src/Spice86/DataTemplates/DataTemplateProvider.cs
new file mode 100644
index 000000000..eaff0422e
--- /dev/null
+++ b/src/Spice86/DataTemplates/DataTemplateProvider.cs
@@ -0,0 +1,140 @@
+namespace Spice86.DataTemplates;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+using CommunityToolkit.Mvvm.Input;
+
+using Structurizer.Types;
+
+using System.Text;
+
+public static class DataTemplateProvider {
+ public static FuncDataTemplate StructureMemberValueTemplate { get; } = new(BuildStructureMemberValuePresenter);
+
+ private static Control? BuildStructureMemberValuePresenter(StructureMember? structureMember, INameScope scope) {
+ if (structureMember is null) {
+ return null;
+ }
+ if (structureMember.Type is {IsPointer: true, IsArray: false}) {
+ return new Button {
+ Content = FormatPointer(structureMember),
+ Command = new RelayCommand(() => throw new NotImplementedException("This should open a new memory view at the address the pointer points to")),
+ Classes = {"hyperlink"},
+ HorizontalAlignment = HorizontalAlignment.Right,
+ HorizontalContentAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0,0,5,0)
+ };
+ }
+
+ return new TextBlock {
+ Text = FormatValue(structureMember),
+ TextAlignment = TextAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0,0,5,0)
+ };
+ }
+
+ private static string FormatValue(StructureMember structureMember) {
+ if (structureMember.Members != null && structureMember.Type.Type != "char") {
+ return string.Empty;
+ }
+ if (structureMember.Type.EnumType != null) {
+ return FormatEnum(structureMember.Type.EnumType, structureMember.Data);
+ }
+ if (structureMember.Type is {IsPointer: true, Count: 1}) {
+ return FormatPointer(structureMember);
+ }
+
+ return structureMember.Type.Type switch {
+ "char" when structureMember.Type.IsArray => '"' + Encoding.ASCII.GetString(structureMember.Data) + '"',
+ "__int8" or "char" or "_BYTE" => FormatChar(structureMember.Data[0]),
+ "__int16" or "short" or "int" when structureMember.Type.Unsigned => FormatUnsignedShort(structureMember),
+ "__int16" or "short" or "int" => FormatShort(structureMember),
+ "__int32" or "long" when structureMember.Type.Unsigned => FormatUnsignedLong(structureMember),
+ "__int32" or "long" => FormatLong(structureMember),
+ _ => FormatHex(structureMember)
+ };
+ }
+
+ private static string FormatHex(StructureMember structureMember) {
+ switch (structureMember.Size) {
+ case 1:
+ return $"0x{structureMember.Data[0]:X2}";
+ case 2:
+ return $"0x{BitConverter.ToUInt16(structureMember.Data):X4}";
+ case 4:
+ return $"0x{BitConverter.ToUInt32(structureMember.Data):X8}";
+ default:
+ return "???";
+ }
+ }
+
+ private static string FormatPointer(StructureMember structureMember) {
+ Span bytes = structureMember.Data.AsSpan();
+ ushort targetSegment;
+ ushort targetOffset;
+ if (structureMember.Type.IsNear && bytes.Length == 2) {
+ targetSegment = 0;
+ targetOffset = BitConverter.ToUInt16(bytes);
+ } else if (bytes.Length == 4) {
+ targetSegment = BitConverter.ToUInt16(bytes[2..]);
+ targetOffset = BitConverter.ToUInt16(bytes[..2]);
+ } else {
+ throw new ArgumentException($"Invalid pointer size: {bytes.Length * 8}");
+ }
+
+ return structureMember.Type.IsNear
+ ? $"DS:{targetOffset:X4}"
+ : $"{targetSegment:X4}:{targetOffset:X4}";
+ }
+
+ private static string FormatEnum(EnumType enumType, byte[] bytes) {
+ uint value = enumType.MemberSize switch {
+ 1 => bytes[0],
+ 2 => BitConverter.ToUInt16(bytes),
+ 4 => BitConverter.ToUInt32(bytes),
+ _ => throw new NotSupportedException($"Enum member size {enumType.MemberSize} not supported")
+ };
+
+ if (enumType.Members.TryGetValue(value, out string? name)) {
+ return $"{name} [0x{value:X}]";
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(bytes), $"Enum value {value} not found in enum");
+ }
+
+ private static string FormatLong(StructureMember structureMember) {
+ int value = BitConverter.ToInt32(structureMember.Data);
+
+ return $"{value} [0x{value:X8}]";
+ }
+
+ private static string FormatUnsignedLong(StructureMember structureMember) {
+ uint value = BitConverter.ToUInt32(structureMember.Data);
+
+ return $"{value} [0x{value:X8}]";
+ }
+
+ private static string FormatShort(StructureMember structureMember) {
+ short value = BitConverter.ToInt16(structureMember.Data);
+
+ return $"{value} [0x{value:X4}]";
+ }
+
+ private static string FormatUnsignedShort(StructureMember structureMember) {
+ ushort value = BitConverter.ToUInt16(structureMember.Data);
+
+ return $"{value} [0x{value:X4}]";
+ }
+
+ private static string FormatChar(byte value) {
+ return value is < 32 or > 126
+ ? $"{value} [0x{value:X2}]"
+ : $"{value} '{(char)value}' [0x{value:X2}]";
+ }
+}
\ No newline at end of file
diff --git a/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs b/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
index c32e6e61f..84d2e6552 100644
--- a/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
@@ -8,6 +8,7 @@ namespace Spice86.DependencyInjection;
using Spice86.Core.CLI;
using Spice86.Infrastructure;
+using Spice86.Interfaces;
using Spice86.Logging;
using Spice86.Shared.Interfaces;
@@ -16,27 +17,30 @@ public static void AddConfiguration(this IServiceCollection serviceCollection, s
serviceCollection.AddSingleton();
serviceCollection.AddSingleton(serviceProvider => {
ICommandLineParser commandLineParser = serviceProvider.GetRequiredService();
+
return commandLineParser.ParseCommandLine(args);
});
}
public static void AddLogging(this IServiceCollection serviceCollection) {
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton((serviceProvider) => {
+ serviceCollection.AddSingleton(serviceProvider => {
Configuration configuration = serviceProvider.GetRequiredService();
LoggerService loggerService = new LoggerService(serviceProvider.GetRequiredService());
Startup.SetLoggingLevel(loggerService, configuration);
+
return loggerService;
});
}
-
+
public static void AddGuiInfrastructure(this IServiceCollection serviceCollection, TopLevel mainWindow) {
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton((_) => new UIDispatcher(Dispatcher.UIThread));
+ serviceCollection.AddSingleton(_ => new UIDispatcher(Dispatcher.UIThread));
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton((_) => mainWindow.StorageProvider);
+ serviceCollection.AddSingleton(_ => mainWindow.StorageProvider);
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton((_) => new TextClipboard(mainWindow.Clipboard));
+ serviceCollection.AddSingleton(_ => new TextClipboard(mainWindow.Clipboard));
+ serviceCollection.AddSingleton();
}
}
\ No newline at end of file
diff --git a/src/Spice86/Infrastructure/FilePoller.cs b/src/Spice86/Infrastructure/FilePoller.cs
new file mode 100644
index 000000000..079a4b7ee
--- /dev/null
+++ b/src/Spice86/Infrastructure/FilePoller.cs
@@ -0,0 +1,53 @@
+namespace Spice86.Infrastructure;
+
+using PropertyModels.Extensions;
+
+using System;
+using System.IO;
+using System.Timers;
+
+public class FilePoller {
+ private readonly string _filePath;
+ private ulong _lastHash;
+ private bool _fileChanged;
+ private readonly Timer _timer;
+ private readonly Action _actionToPerform;
+
+ public FilePoller(string filePath, Action actionToPerformOnChange, double interval = 1000) {
+ _filePath = filePath;
+ _actionToPerform = actionToPerformOnChange;
+ _timer = new Timer(interval);
+ _timer.Elapsed += CheckFileChange;
+ _timer.AutoReset = true;
+ }
+
+ public void Start() {
+ _timer.Start();
+ }
+
+ public void Stop() {
+ _timer.Stop();
+ }
+
+ private void CheckFileChange(object? sender, ElapsedEventArgs e) {
+ if (!File.Exists(_filePath))
+ return;
+
+ ulong currentHash = CalculateHash(_filePath);
+
+ if (_lastHash != 0 && _lastHash != currentHash) {
+ _fileChanged = !_fileChanged;
+ } else if (_lastHash == currentHash && _fileChanged) {
+ _fileChanged = false;
+ _actionToPerform();
+ }
+
+ _lastHash = currentHash;
+ }
+
+ private static ulong CalculateHash(string filePath) {
+ string fileContents = File.ReadAllText(filePath);
+
+ return fileContents.GetDeterministicHashCode64();
+ }
+}
\ No newline at end of file
diff --git a/src/Spice86/Infrastructure/StructureViewModelFactory.cs b/src/Spice86/Infrastructure/StructureViewModelFactory.cs
new file mode 100644
index 000000000..9031b0354
--- /dev/null
+++ b/src/Spice86/Infrastructure/StructureViewModelFactory.cs
@@ -0,0 +1,74 @@
+namespace Spice86.Infrastructure;
+
+using AvaloniaHex.Document;
+
+using Spice86.Core.CLI;
+using Spice86.Interfaces;
+using Spice86.Shared.Interfaces;
+using Spice86.ViewModels;
+
+using Structurizer;
+using Structurizer.Types;
+
+public class StructureViewModelFactory : IStructureViewModelFactory {
+ private readonly Hydrator? _hydrator;
+ private readonly Parser? _parser;
+ private readonly StructurizerSettings _structurizerSettings = new();
+ private StructureInformation? _structureInformation;
+ private readonly Configuration _configuration;
+ private readonly ILoggerService _logger;
+
+ public event EventHandler? StructureInformationChanged;
+
+ public StructureViewModelFactory(Configuration configuration, ILoggerService logger) {
+ _logger = logger;
+ _configuration = configuration;
+ if (!TryGetHeaderFilePath(out string headerFilePath)) {
+ return;
+ }
+ _parser = new Parser(_structurizerSettings);
+ _hydrator = new Hydrator(_structurizerSettings);
+
+ Parse(headerFilePath);
+ var poller = new FilePoller(headerFilePath, () => Parse(headerFilePath));
+ poller.Start();
+ }
+
+ public bool IsInitialized => _structureInformation != null && _hydrator != null;
+
+ public StructureViewModel CreateNew(IBinaryDocument data) {
+ if (_structureInformation == null || _hydrator == null) {
+ throw new InvalidOperationException("Factory not initialized.");
+ }
+
+ var viewModel = new StructureViewModel(_structureInformation, _hydrator, data);
+ StructureInformationChanged += viewModel.OnStructureInformationChanged;
+
+ return viewModel;
+ }
+
+ public void Parse(string headerFilePath) {
+ _logger.Information("Parsing {HeaderFilePath} for structure information", headerFilePath);
+ if (_parser == null) {
+ throw new InvalidOperationException("Factory not initialized.");
+ }
+ if (!File.Exists(headerFilePath)) {
+ throw new FileNotFoundException($"Specified structure file not found: '{headerFilePath}'");
+ }
+
+ string source = File.ReadAllText(headerFilePath);
+
+ _structureInformation = _parser.ParseSource(source);
+ StructureInformationChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private bool TryGetHeaderFilePath(out string headerFilePath) {
+ headerFilePath = string.Empty;
+ if (string.IsNullOrWhiteSpace(_configuration.StructureFile)) {
+ return false;
+ }
+ headerFilePath = _configuration.StructureFile;
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/Spice86/Interfaces/IStructureViewModelFactory.cs b/src/Spice86/Interfaces/IStructureViewModelFactory.cs
new file mode 100644
index 000000000..d824c3a19
--- /dev/null
+++ b/src/Spice86/Interfaces/IStructureViewModelFactory.cs
@@ -0,0 +1,11 @@
+namespace Spice86.Interfaces;
+
+using AvaloniaHex.Document;
+
+using Spice86.ViewModels;
+
+public interface IStructureViewModelFactory {
+ bool IsInitialized { get; }
+ StructureViewModel CreateNew(IBinaryDocument data);
+ void Parse(string headerFilePath);
+}
\ No newline at end of file
diff --git a/src/Spice86/Models/AddressChangedMessage.cs b/src/Spice86/Models/AddressChangedMessage.cs
new file mode 100644
index 000000000..e0cb0535b
--- /dev/null
+++ b/src/Spice86/Models/AddressChangedMessage.cs
@@ -0,0 +1,5 @@
+namespace Spice86.Models;
+
+public class AddressChangedMessage(ulong address) {
+ public ulong Address { get; } = address;
+}
\ No newline at end of file
diff --git a/src/Spice86/Spice86.csproj b/src/Spice86/Spice86.csproj
index 048ed7325..e5055afee 100644
--- a/src/Spice86/Spice86.csproj
+++ b/src/Spice86/Spice86.csproj
@@ -1,93 +1,97 @@
-
- Exe
- net8.0
- enable
- nullable
- enable
- true
- True
- 1591;1572;1573;1570;1587;1574
- true
- app.manifest
- true
-
-
-
- CS1591
- Spice86
- true
- 7.0.0
- Some breaking API changes (SegmentRegisters.cs), WIP new CFG_CPU, addtionnal memory/disasm views to the internal debugger, replaced UI DI framework with Microsoft.DI.
+
+ Exe
+ net8.0
+ enable
+ nullable
+ enable
+ true
+ True
+ 1591;1572;1573;1570;1587;1574
+ true
+ app.manifest
+ true
+
+
+
+ CS1591
+ Spice86
+ true
+ 7.0.0
+ Some breaking API changes (SegmentRegisters.cs), WIP new CFG_CPU, addtionnal memory/disasm views to the internal debugger, replaced UI DI framework with Microsoft.DI.
Kevin Ferrare, Maximilien Noal, Joris van Eijden, Artjom Vejsel
- Apache-2.0
- Reverse engineer and rewrite real mode DOS programs
- reverse-engineering;avalonia;debugger;assembly;emulator;cross-platform
- https://github.com/OpenRakis/Spice86
- https://github.com/OpenRakis/Spice86
- git
-
-
-
- lib\net8.0\libportaudio.dll
- Always
- True
-
-
+ Apache-2.0
+ Reverse engineer and rewrite real mode DOS programs
+ reverse-engineering;avalonia;debugger;assembly;emulator;cross-platform
+ https://github.com/OpenRakis/Spice86
+ https://github.com/OpenRakis/Spice86
+ git
+
-
-
- true
- true
- true
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+ true
+ true
+ true
+
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
+
+
+ lib\net8.0\libportaudio.dll
+ Always
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Spice86/Styles/Spice86.axaml b/src/Spice86/Styles/Spice86.axaml
index 8f72dff8b..1c8a7fbef 100644
--- a/src/Spice86/Styles/Spice86.axaml
+++ b/src/Spice86/Styles/Spice86.axaml
@@ -2,23 +2,21 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-
-
-
-
- This is highlighted
-
-
-
+
+
+
+ This is highlighted
+
+
@@ -30,7 +28,19 @@
diff --git a/src/Spice86/ViewModels/DebugWindowViewModel.cs b/src/Spice86/ViewModels/DebugWindowViewModel.cs
index b4c755f2a..9cad8126b 100644
--- a/src/Spice86/ViewModels/DebugWindowViewModel.cs
+++ b/src/Spice86/ViewModels/DebugWindowViewModel.cs
@@ -20,6 +20,7 @@ public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
private readonly IHostStorageProvider _storageProvider;
private readonly IUIDispatcherTimerFactory _uiDispatcherTimerFactory;
private readonly ITextClipboard _textClipboard;
+ private readonly IStructureViewModelFactory _structureViewModelFactory;
[ObservableProperty]
private DateTime? _lastUpdate;
@@ -35,7 +36,7 @@ public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private AvaloniaList _memoryViewModels = new();
-
+
[ObservableProperty]
private VideoCardViewModel _videoCardViewModel;
@@ -47,15 +48,16 @@ public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private AvaloniaList _disassemblyViewModels = new();
-
+
[ObservableProperty]
private SoftwareMixerViewModel _softwareMixerViewModel;
[ObservableProperty]
private CfgCpuViewModel _cfgCpuViewModel;
- public DebugWindowViewModel(ITextClipboard textClipboard, IHostStorageProvider storageProvider, IUIDispatcherTimerFactory uiDispatcherTimerFactory, IPauseStatus pauseStatus, IProgramExecutor programExecutor) {
+ public DebugWindowViewModel(ITextClipboard textClipboard, IHostStorageProvider storageProvider, IUIDispatcherTimerFactory uiDispatcherTimerFactory, IPauseStatus pauseStatus, IProgramExecutor programExecutor, IStructureViewModelFactory structureViewModelFactory) {
_programExecutor = programExecutor;
+ _structureViewModelFactory = structureViewModelFactory;
_storageProvider = storageProvider;
_textClipboard = textClipboard;
_uiDispatcherTimerFactory = uiDispatcherTimerFactory;
@@ -70,7 +72,7 @@ public DebugWindowViewModel(ITextClipboard textClipboard, IHostStorageProvider s
VideoCardViewModel = new(uiDispatcherTimerFactory);
CpuViewModel = new(uiDispatcherTimerFactory, pauseStatus);
MidiViewModel = new(uiDispatcherTimerFactory);
- MemoryViewModels.Add( new(this, textClipboard, uiDispatcherTimerFactory, storageProvider, pauseStatus, 0));
+ MemoryViewModels.Add(new(this, textClipboard, uiDispatcherTimerFactory, storageProvider, pauseStatus, 0, _structureViewModelFactory));
CfgCpuViewModel = new(uiDispatcherTimerFactory, new PerformanceMeasurer(), pauseStatus);
Dispatcher.UIThread.Post(ForceUpdate, DispatcherPriority.Background);
}
@@ -79,18 +81,20 @@ internal void CloseTab(IInternalDebugger internalDebuggerViewModel) {
switch (internalDebuggerViewModel) {
case MemoryViewModel memoryViewModel:
MemoryViewModels.Remove(memoryViewModel);
+
break;
case DisassemblyViewModel disassemblyViewModel:
DisassemblyViewModels.Remove(disassemblyViewModel);
+
break;
}
}
[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewMemoryView() {
- MemoryViewModels.Add(new MemoryViewModel(this, _textClipboard, _uiDispatcherTimerFactory, _storageProvider, _pauseStatus, 0));
+ MemoryViewModels.Add(new MemoryViewModel(this, _textClipboard, _uiDispatcherTimerFactory, _storageProvider, _pauseStatus, 0, _structureViewModelFactory));
}
-
+
[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewDisassemblyView() => DisassemblyViewModels.Add(new DisassemblyViewModel(this, _uiDispatcherTimerFactory, _pauseStatus));
@@ -109,7 +113,7 @@ public void NewMemoryView() {
private IEnumerable InternalDebuggers => new IInternalDebugger[] {
PaletteViewModel, CpuViewModel, VideoCardViewModel, MidiViewModel, SoftwareMixerViewModel, CfgCpuViewModel
- }
+ }
.Concat(DisassemblyViewModels)
.Concat(MemoryViewModels);
diff --git a/src/Spice86/ViewModels/MainWindowViewModel.cs b/src/Spice86/ViewModels/MainWindowViewModel.cs
index 36aa12173..ebe3acb96 100644
--- a/src/Spice86/ViewModels/MainWindowViewModel.cs
+++ b/src/Spice86/ViewModels/MainWindowViewModel.cs
@@ -40,6 +40,7 @@ public sealed partial class MainWindowViewModel : ViewModelBaseWithErrorDialog,
private readonly IUIDispatcherTimerFactory _uiDispatcherTimerFactory;
private readonly IAvaloniaKeyScanCodeConverter _avaloniaKeyScanCodeConverter;
private readonly IWindowService _windowService;
+ private readonly IStructureViewModelFactory _structureViewModelFactory;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ShowInternalDebuggerCommand))]
@@ -61,7 +62,7 @@ private IProgramExecutor? ProgramExecutor {
[ObservableProperty]
private Configuration _configuration;
-
+
private bool _disposed;
private bool _renderingTimerInitialized;
private Thread? _emulatorThread;
@@ -80,12 +81,13 @@ private IProgramExecutor? ProgramExecutor {
public event EventHandler? MouseButtonDown;
public event EventHandler? MouseButtonUp;
- public MainWindowViewModel(IWindowService windowService, IAvaloniaKeyScanCodeConverter avaloniaKeyScanCodeConverter, IProgramExecutorFactory programExecutorFactory, IUIDispatcher uiDispatcher, IHostStorageProvider hostStorageProvider, ITextClipboard textClipboard, IUIDispatcherTimerFactory uiDispatcherTimerFactory, Configuration configuration, ILoggerService loggerService) : base(textClipboard) {
+ public MainWindowViewModel(IWindowService windowService, IAvaloniaKeyScanCodeConverter avaloniaKeyScanCodeConverter, IProgramExecutorFactory programExecutorFactory, IUIDispatcher uiDispatcher, IHostStorageProvider hostStorageProvider, ITextClipboard textClipboard, IUIDispatcherTimerFactory uiDispatcherTimerFactory, Configuration configuration, ILoggerService loggerService, IStructureViewModelFactory structureViewModelFactory) : base(textClipboard) {
_avaloniaKeyScanCodeConverter = avaloniaKeyScanCodeConverter;
_windowService = windowService;
Configuration = configuration;
_programExecutorFactory = programExecutorFactory;
_loggerService = loggerService;
+ _structureViewModelFactory = structureViewModelFactory;
_hostStorageProvider = hostStorageProvider;
_uiDispatcher = uiDispatcher;
_uiDispatcherTimerFactory = uiDispatcherTimerFactory;
@@ -108,7 +110,7 @@ public async Task SaveBitmap() {
await _hostStorageProvider.SaveBitmapFile(Bitmap);
}
}
-
+
private bool _showCursor;
public bool ShowCursor {
@@ -154,7 +156,7 @@ internal void OnKeyDown(KeyEventArgs e) {
private string _asmOverrideStatus = "ASM Overrides: not used.";
private bool _isPaused;
-
+
public bool IsPaused {
get => _isPaused;
set {
@@ -223,7 +225,7 @@ public double? TimeMultiplier {
[RelayCommand(CanExecute = nameof(IsEmulatorRunning))]
public void ShowPerformance() => IsPerformanceVisible = !IsPerformanceVisible;
-
+
[RelayCommand]
public void ResetTimeMultiplier() => TimeMultiplier = Configuration.TimeMultiplier;
@@ -429,7 +431,7 @@ private void EmulatorThread() {
[RelayCommand(CanExecute = nameof(IsProgramExecutorNotNull))]
public async Task ShowInternalDebugger() {
if (ProgramExecutor is not null) {
- _debugViewModel = new DebugWindowViewModel(_textClipboard, _hostStorageProvider, _uiDispatcherTimerFactory, this, ProgramExecutor);
+ _debugViewModel = new DebugWindowViewModel(_textClipboard, _hostStorageProvider, _uiDispatcherTimerFactory, this, ProgramExecutor, _structureViewModelFactory);
await _windowService.ShowDebugWindow(_debugViewModel);
}
}
diff --git a/src/Spice86/ViewModels/MemoryViewModel.cs b/src/Spice86/ViewModels/MemoryViewModel.cs
index 5561507ef..38f3503e8 100644
--- a/src/Spice86/ViewModels/MemoryViewModel.cs
+++ b/src/Spice86/ViewModels/MemoryViewModel.cs
@@ -2,6 +2,9 @@
using Avalonia.Threading;
+using AvaloniaHex.Document;
+using AvaloniaHex.Editing;
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -11,6 +14,7 @@
using Spice86.Interfaces;
using Spice86.MemoryWrappers;
using Spice86.Shared.Utils;
+using Spice86.Views;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -21,10 +25,11 @@ public partial class MemoryViewModel : ViewModelBaseWithErrorDialog, IInternalDe
private IMemory? _memory;
private readonly DebugWindowViewModel _debugWindowViewModel;
private bool _needToUpdateBinaryDocument;
-
+ private readonly IStructureViewModelFactory _structureViewModelFactory;
+
[ObservableProperty]
- private DataMemoryDocument? _memoryBinaryDocument;
-
+ private DataMemoryDocument? _dataMemoryDocument;
+
public bool NeedsToVisitEmulator => _memory is null;
private uint? _startAddress = 0;
@@ -43,7 +48,7 @@ private void CloseTab() {
_debugWindowViewModel.CloseTab(this);
UpdateCanCloseTabProperty();
}
-
+
private bool GetIsMemoryRangeValid() {
if (_memory is null) {
return false;
@@ -52,7 +57,7 @@ private bool GetIsMemoryRangeValid() {
return StartAddress <= (EndAddress ?? _memory.Length)
&& EndAddress >= (StartAddress ?? 0);
}
-
+
public uint? StartAddress {
get => _startAddress;
set {
@@ -71,7 +76,7 @@ private void TryUpdateHeaderAndMemoryDocument() {
}
private uint? _endAddress = 0;
-
+
public uint? EndAddress {
get => _endAddress;
set {
@@ -89,22 +94,57 @@ public uint? EndAddress {
[NotifyCanExecuteChangedFor(nameof(EditMemoryCommand))]
private bool _isPaused;
+ [ObservableProperty]
+ private BitRange? _selectionRange;
+
+ public bool IsStructureInfoPresent => _structureViewModelFactory.IsInitialized;
+
private readonly IPauseStatus _pauseStatus;
private readonly IHostStorageProvider _storageProvider;
- public MemoryViewModel(DebugWindowViewModel debugWindowViewModel, ITextClipboard textClipboard, IUIDispatcherTimerFactory dispatcherTimerFactory, IHostStorageProvider storageProvider, IPauseStatus pauseStatus, uint startAddress, uint endAddress = 0) : base(textClipboard) {
+ public MemoryViewModel(DebugWindowViewModel debugWindowViewModel, ITextClipboard textClipboard, IUIDispatcherTimerFactory dispatcherTimerFactory, IHostStorageProvider storageProvider, IPauseStatus pauseStatus, uint startAddress, IStructureViewModelFactory structureViewModelFactory, uint endAddress = 0) : base(textClipboard) {
_debugWindowViewModel = debugWindowViewModel;
pauseStatus.PropertyChanged += PauseStatus_PropertyChanged;
_pauseStatus = pauseStatus;
_storageProvider = storageProvider;
IsPaused = _pauseStatus.IsPaused;
StartAddress = startAddress;
+ _structureViewModelFactory = structureViewModelFactory;
EndAddress = endAddress;
dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
UpdateCanCloseTabProperty();
debugWindowViewModel.MemoryViewModels.CollectionChanged += OnDebugViewModelCollectionChanged;
}
-
+
+ ///
+ /// Handles the event when the selection range within the HexEditor changes.
+ ///
+ /// The source of the event, expected to be of type .
+ /// The event arguments, not used in this method.
+ public void OnSelectionRangeChanged(object? sender, EventArgs e) {
+ SelectionRange = (sender as Selection)?.Range;
+ }
+
+ [RelayCommand(CanExecute = nameof(IsStructureInfoPresent))]
+ public void ShowStructureView() {
+ if (DataMemoryDocument == null) {
+ return;
+ }
+
+ // Use either the selected range or the entire document if no range is selected.
+ IBinaryDocument data;
+ if (SelectionRange is {ByteLength: > 1} bitRange) {
+ byte[] bytes = new byte[bitRange.ByteLength];
+ DataMemoryDocument.ReadBytes(bitRange.Start.ByteIndex, bytes);
+ data = new ByteArrayBinaryDocument(bytes);
+ } else {
+ data = DataMemoryDocument;
+ }
+ StructureViewModel structureViewModel = _structureViewModelFactory.CreateNew(data);
+ var structureWindow = new StructureView {DataContext = structureViewModel};
+ structureWindow.Show();
+ }
+
private void UpdateCanCloseTabProperty() {
CanCloseTab = _debugWindowViewModel.MemoryViewModels.Count > 1;
}
@@ -127,11 +167,11 @@ private void PauseStatus_PropertyChanged(object? sender, PropertyChangedEventArg
}
UpdateCanCloseTabProperty();
IsPaused = _pauseStatus.IsPaused;
- if(IsPaused) {
+ if (IsPaused) {
_needToUpdateBinaryDocument = true;
}
}
-
+
[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewMemoryView() {
_debugWindowViewModel.NewMemoryViewCommand.Execute(null);
@@ -142,9 +182,9 @@ private void UpdateBinaryDocument() {
if (_memory is null || StartAddress is null || EndAddress is null) {
return;
}
- MemoryBinaryDocument = new DataMemoryDocument(_memory, StartAddress.Value, EndAddress.Value);
- MemoryBinaryDocument.MemoryReadInvalidOperation -= OnMemoryReadInvalidOperation;
- MemoryBinaryDocument.MemoryReadInvalidOperation += OnMemoryReadInvalidOperation;
+ DataMemoryDocument = new DataMemoryDocument(_memory, StartAddress.Value, EndAddress.Value);
+ DataMemoryDocument.MemoryReadInvalidOperation -= OnMemoryReadInvalidOperation;
+ DataMemoryDocument.MemoryReadInvalidOperation += OnMemoryReadInvalidOperation;
}
private void OnMemoryReadInvalidOperation(Exception exception) {
@@ -178,6 +218,7 @@ public void EditMemory() {
private bool TryParseMemoryAddress(string? memoryAddress, [NotNullWhen(true)] out uint? address) {
if (string.IsNullOrWhiteSpace(memoryAddress)) {
address = null;
+
return false;
}
@@ -188,16 +229,19 @@ private bool TryParseMemoryAddress(string? memoryAddress, [NotNullWhen(true)] ou
ushort.TryParse(split[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort segment) &&
ushort.TryParse(split[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort offset)) {
address = MemoryUtils.ToPhysicalAddress(segment, offset);
+
return true;
}
} else if (uint.TryParse(memoryAddress, CultureInfo.InvariantCulture, out uint value)) {
address = value;
+
return true;
}
} catch (Exception e) {
Dispatcher.UIThread.Post(() => ShowError(e));
}
address = null;
+
return false;
}
@@ -212,7 +256,7 @@ _memory is null ||
!long.TryParse(MemoryEditValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long value)) {
return;
}
- MemoryBinaryDocument?.WriteBytes(address.Value, BitConverter.GetBytes(value));
+ DataMemoryDocument?.WriteBytes(address.Value, BitConverter.GetBytes(value));
IsEditingMemory = false;
}
@@ -229,4 +273,4 @@ public void Visit(T component) where T : IDebuggableComponent {
}
TryUpdateHeaderAndMemoryDocument();
}
-}
+}
\ No newline at end of file
diff --git a/src/Spice86/ViewModels/StructureViewModel.cs b/src/Spice86/ViewModels/StructureViewModel.cs
new file mode 100644
index 000000000..90ce4e2fc
--- /dev/null
+++ b/src/Spice86/ViewModels/StructureViewModel.cs
@@ -0,0 +1,179 @@
+namespace Spice86.ViewModels;
+
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Models.TreeDataGrid;
+using Avalonia.Media;
+
+using AvaloniaHex.Document;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+
+using Spice86.DataTemplates;
+using Spice86.MemoryWrappers;
+using Spice86.Models;
+using Spice86.Shared.Emulator.Memory;
+
+using Structurizer;
+using Structurizer.Types;
+
+///
+/// ViewModel for handling the structure view in the application. It manages the display and interaction
+/// with memory structures, including selection, filtering, and updating the view based on the selected structure.
+///
+public partial class StructureViewModel : ViewModelBase {
+ private readonly Hydrator _hydrator;
+ private readonly IBinaryDocument _originalMemory;
+ private readonly StructureInformation? _structureInformation;
+
+ [ObservableProperty]
+ private AvaloniaList _availableStructures;
+
+ [ObservableProperty]
+ private bool _isAddressableMemory;
+
+ [ObservableProperty]
+ private SegmentedAddress? _memoryAddress;
+
+ [ObservableProperty]
+ private StructType? _selectedStructure;
+
+ [ObservableProperty]
+ private AvaloniaList _structureMembers = new() {ResetBehavior = ResetBehavior.Remove};
+
+ [ObservableProperty]
+ private IBinaryDocument _structureMemory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The structure information containing available structures.
+ /// The hydrator used for creating structure members from binary data.
+ /// The binary document representing the memory to be displayed and interacted with.
+ public StructureViewModel(StructureInformation structureInformation, Hydrator hydrator, IBinaryDocument data) {
+ _structureInformation = structureInformation;
+ _hydrator = hydrator;
+ _structureMemory = data;
+ _originalMemory = data;
+ _availableStructures = new AvaloniaList(structureInformation.Structs.Values);
+ _isAddressableMemory = data is DataMemoryDocument;
+ Source = InitializeSource();
+ }
+
+ ///
+ /// Gets or sets the for the structure members.
+ /// This source is used to populate the hierarchical tree data grid in the UI, allowing for the
+ /// display and interaction with the structure members.
+ ///
+ public HierarchicalTreeDataGridSource Source { get; set; }
+
+ ///
+ /// Create the text that is displayed in the textbox when a structure is selected.
+ ///
+ public AutoCompleteSelector