Skip to content

Commit

Permalink
Feature/structure viewer (#772)
Browse files Browse the repository at this point in the history
* Add a structure viewer to the memory viewer.

* Don't crash without structure file

* Fix hyperlink style

* Replace fluent with semi

* More progress towards a functional structure viewer

* WIP progress, but stumped on hexview scroll issue

* Resolve merge conflicts

* update Structurizer

* Replace iffy FileSystemWatcher with own poller implementation

* Fix scroll issue with new document

* Refactor and document StructureViewModel

* Always allow address input

* Formatting, styling and cleanup

* Resolve merge conflicts
  • Loading branch information
JorisVanEijden authored Jul 11, 2024
1 parent 46eaf53 commit 2e8f178
Show file tree
Hide file tree
Showing 20 changed files with 904 additions and 141 deletions.
6 changes: 6 additions & 0 deletions src/Spice86.Core/CLI/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,10 @@ public sealed class Configuration {
/// </summary>
[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; }

/// <summary>
/// Specify a C header file to be used for structure information
/// </summary>
[Option(nameof(StructureFile), Default = null, Required = false, HelpText = "Specify a C header file to be used for structure information")]
public string? StructureFile { get; init; }
}
1 change: 1 addition & 0 deletions src/Spice86.Core/Emulator/Function/IOverrideSupplier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public interface IOverrideSupplier {
/// <param name="configuration">The configuration.</param>
/// <param name="programStartAddress">The start address of the program.</param>
/// <param name="machine">The emulator machine.</param>
/// <param name="loggerService"></param>
/// <returns>A dictionary containing the generated function information overrides.</returns>
public IDictionary<SegmentedAddress, FunctionInformation> GenerateFunctionInformations(
ILoggerService loggerService,
Expand Down
28 changes: 28 additions & 0 deletions src/Spice86.Shared/Emulator/Memory/SegmentedAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using Spice86.Shared.Utils;

using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Serialization;

/// <summary>
Expand Down Expand Up @@ -140,4 +142,30 @@ public void Deconstruct(out ushort segment, out ushort offset) {
segment = Segment;
offset = Offset;
}

/// <summary>
/// Tries to parse a hexadecimal string in the format of segment:offset into a SegmentedAddress object.
/// </summary>
/// <param name="s">a hex string in the format of segment:offset</param>
/// <param name="segmentedAddress"></param>
/// <returns>true if s was converted successfully; otherwise, false.</returns>
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;
}
}
38 changes: 38 additions & 0 deletions src/Spice86/Converters/SegmentedAddressConverter.cs
Original file line number Diff line number Diff line change
@@ -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();
}
140 changes: 140 additions & 0 deletions src/Spice86/DataTemplates/DataTemplateProvider.cs
Original file line number Diff line number Diff line change
@@ -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<StructureMember> 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<byte> 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}]";
}
}
14 changes: 9 additions & 5 deletions src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Spice86.DependencyInjection;

using Spice86.Core.CLI;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Logging;
using Spice86.Shared.Interfaces;

Expand All @@ -16,27 +17,30 @@ public static void AddConfiguration(this IServiceCollection serviceCollection, s
serviceCollection.AddSingleton<ICommandLineParser, CommandLineParser>();
serviceCollection.AddSingleton<Configuration>(serviceProvider => {
ICommandLineParser commandLineParser = serviceProvider.GetRequiredService<ICommandLineParser>();
return commandLineParser.ParseCommandLine(args);
});
}

public static void AddLogging(this IServiceCollection serviceCollection) {
serviceCollection.AddSingleton<ILoggerPropertyBag, LoggerPropertyBag>();
serviceCollection.AddSingleton<ILoggerService, LoggerService>((serviceProvider) => {
serviceCollection.AddSingleton<ILoggerService, LoggerService>(serviceProvider => {
Configuration configuration = serviceProvider.GetRequiredService<Configuration>();
LoggerService loggerService = new LoggerService(serviceProvider.GetRequiredService<ILoggerPropertyBag>());
Startup.SetLoggingLevel(loggerService, configuration);
return loggerService;
});
}

public static void AddGuiInfrastructure(this IServiceCollection serviceCollection, TopLevel mainWindow) {
serviceCollection.AddSingleton<IAvaloniaKeyScanCodeConverter, AvaloniaKeyScanCodeConverter>();
serviceCollection.AddSingleton<IWindowService, WindowService>();
serviceCollection.AddSingleton<IUIDispatcher, UIDispatcher>((_) => new UIDispatcher(Dispatcher.UIThread));
serviceCollection.AddSingleton<IUIDispatcher, UIDispatcher>(_ => new UIDispatcher(Dispatcher.UIThread));
serviceCollection.AddSingleton<IUIDispatcherTimerFactory, UIDispatcherTimerFactory>();
serviceCollection.AddSingleton<IStorageProvider>((_) => mainWindow.StorageProvider);
serviceCollection.AddSingleton<IStorageProvider>(_ => mainWindow.StorageProvider);
serviceCollection.AddSingleton<IHostStorageProvider, HostStorageProvider>();
serviceCollection.AddSingleton<ITextClipboard>((_) => new TextClipboard(mainWindow.Clipboard));
serviceCollection.AddSingleton<ITextClipboard>(_ => new TextClipboard(mainWindow.Clipboard));
serviceCollection.AddSingleton<IStructureViewModelFactory, StructureViewModelFactory>();
}
}
53 changes: 53 additions & 0 deletions src/Spice86/Infrastructure/FilePoller.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 2e8f178

Please sign in to comment.