Skip to content

Commit

Permalink
feature: memory search UI
Browse files Browse the repository at this point in the history
Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>
  • Loading branch information
maximilien-noal committed Sep 7, 2024
1 parent e074605 commit 1bc6ef1
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 20 deletions.
31 changes: 29 additions & 2 deletions src/Spice86.Shared/Utils/ConvertUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Spice86.Shared.Emulator.Memory;

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -91,6 +92,8 @@ public static uint BytesToInt32(byte[] data, int start) {
return (uint)((data[start] << 24 & 0xFF000000) | ((uint)data[start + 1] << 16 & 0x00FF0000) | ((uint)data[start + 2] << 8 & 0x0000FF00) | ((uint)data[start + 3] & 0x000000FF));
}

private const int HexadecimalByteDigitLength = 2;

/// <summary>
/// Converts a hexadecimal string to a byte array.
/// </summary>
Expand All @@ -99,12 +102,36 @@ public static uint BytesToInt32(byte[] data, int start) {
public static byte[] HexToByteArray(string valueString) {
byte[] res = new byte[valueString.Length / 2];
for (int i = 0; i < valueString.Length; i += 2) {
string hex = valueString.Substring(i, 2);
res[i / 2] = byte.Parse(hex, NumberStyles.HexNumber);
string hex = valueString.Substring(i, HexadecimalByteDigitLength);
res[i / HexadecimalByteDigitLength] = byte.Parse(hex, NumberStyles.HexNumber);
}

return res;
}

/// <summary>
/// Tries to convert a hexadecimal string to a byte array.
/// </summary>
/// <param name="valueString">The hexadecimal string to convert.</param>
/// <param name="bytes">The byte array representation of the hexadecimal string.</param>
/// <returns><c>False</c> if the conversion fails, <c>True</c> otherwise</returns>
public static bool TryParseHexToByteArray(string valueString, [NotNullWhen(true)] out byte[]? bytes) {
byte[] result = new byte[valueString.Length / 2];
for (int i = 0; i < valueString.Length; i += HexadecimalByteDigitLength) {
if(i + HexadecimalByteDigitLength > valueString.Length) {
bytes = null;
return false;
}
string hex = valueString.Substring(i, HexadecimalByteDigitLength);
if (!byte.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b)) {
bytes = null;
return false;
}
result[i / 2] = b;
}
bytes = result;
return true;
}

/// <summary> Sign extend value considering it is a 16 bit value </summary>
/// <param name="value"> </param>
Expand Down
17 changes: 7 additions & 10 deletions src/Spice86/Converters/InvalidNumberToQuestionMarkConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
internal class InvalidNumberToQuestionMarkConverter : IValueConverter {
/// <inheritdoc/>
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
switch (value)
{
switch (value) {
case null:
case long l when l == -1:
case int i when i == -1:
Expand All @@ -30,12 +29,10 @@ public object Convert(object? value, Type targetType, object? parameter, Culture

/// <inheritdoc/>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is string str && str == "?") {
return -1;
}
if (value is string longStr && long.TryParse(longStr, out long longValue)) {
return longValue;
}
return null;
return value switch {
string and "?" => -1,
string longStr when long.TryParse(longStr, out long longValue) => longValue,
_ => null
};
}
}
}
8 changes: 5 additions & 3 deletions src/Spice86/Spice86DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co
ClassicDesktopStyleApplicationLifetime? desktop = null;
ITextClipboard? textClipboard = null;
IHostStorageProvider? hostStorageProvider = null;
IUIDispatcher? uiThreadDispatcher = null;

if (!configuration.HeadlessMode) {
desktop = CreateDesktopApp();
uiThreadDispatcher = new UIDispatcher(Dispatcher.UIThread);
PerformanceViewModel performanceViewModel = new(state, pauseHandler);
mainWindow = new() {
PerformanceViewModel = performanceViewModel
Expand All @@ -156,7 +158,7 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co
hostStorageProvider = new HostStorageProvider(mainWindow.StorageProvider, configuration,
emulatorStateSerializer);
mainWindowViewModel = new MainWindowViewModel(
timer, new UIDispatcher(Dispatcher.UIThread), hostStorageProvider, textClipboard, configuration,
timer, uiThreadDispatcher, hostStorageProvider, textClipboard, configuration,
loggerService, pauseHandler);
}

Expand Down Expand Up @@ -236,11 +238,11 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co
}

DebugWindowViewModel? debugWindowViewModel = null;
if (textClipboard != null && hostStorageProvider != null) {
if (textClipboard != null && hostStorageProvider != null && uiThreadDispatcher != null) {
IMessenger messenger = WeakReferenceMessenger.Default;
debugWindowViewModel = new DebugWindowViewModel(state, memory,
midiDevice, videoState.DacRegisters.ArgbPalette, softwareMixer, vgaRenderer, videoState,
cfgCpu.ExecutionContextManager, messenger, textClipboard, hostStorageProvider,
cfgCpu.ExecutionContextManager, messenger, uiThreadDispatcher, textClipboard, hostStorageProvider,
new StructureViewModelFactory(configuration, loggerService, pauseHandler), pauseHandler);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Spice86/ViewModels/DebugWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public partial class DebugWindowViewModel : ViewModelBase,

public DebugWindowViewModel(State cpuState, IMemory memory, Midi externalMidiDevice,
ArgbPalette argbPalette, SoftwareMixer softwareMixer, IVgaRenderer vgaRenderer, VideoState videoState,
ExecutionContextManager executionContextManager, IMessenger messenger,
ExecutionContextManager executionContextManager, IMessenger messenger, IUIDispatcher uiDispatcher,
ITextClipboard textClipboard, IHostStorageProvider storageProvider,
IStructureViewModelFactory structureViewModelFactory, IPauseHandler pauseHandler) {
messenger.Register<AddViewModelMessage<DisassemblyViewModel>>(this);
Expand All @@ -73,7 +73,7 @@ public DebugWindowViewModel(State cpuState, IMemory memory, Midi externalMidiDev
VideoCardViewModel = new(vgaRenderer, videoState);
CpuViewModel = new(cpuState, pauseHandler);
MidiViewModel = new(externalMidiDevice);
MemoryViewModels.Add(new(memory, pauseHandler, messenger, textClipboard, storageProvider, structureViewModelFactory));
MemoryViewModels.Add(new(memory, pauseHandler, messenger, uiDispatcher, textClipboard, storageProvider, structureViewModelFactory));
CfgCpuViewModel = new(executionContextManager, pauseHandler, new PerformanceMeasurer());
}

Expand Down
146 changes: 144 additions & 2 deletions src/Spice86/ViewModels/MemoryViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,38 @@

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

public partial class MemoryViewModel : ViewModelWithErrorDialog {
private readonly IMemory _memory;
private readonly IStructureViewModelFactory _structureViewModelFactory;
private readonly IMessenger _messenger;
private readonly IPauseHandler _pauseHandler;
private readonly IUIDispatcher _uiDispatcher;

public enum MemorySearchDataType {
Binary,
Ascii,
}

[ObservableProperty]
private MemorySearchDataType _searchDataType;

public bool SearchDataTypeIsBinary => SearchDataType == MemorySearchDataType.Binary;

public bool SearchDataTypeIsAscii => SearchDataType == MemorySearchDataType.Ascii;

[RelayCommand]
private void SetSearchDataTypeToBinary() => SetSearchDataType(MemorySearchDataType.Binary);

private void SetSearchDataType(MemorySearchDataType searchDataType) {
SearchDataType = searchDataType;
OnPropertyChanged(nameof(SearchDataTypeIsBinary));
OnPropertyChanged(nameof(SearchDataTypeIsAscii));
}

[RelayCommand]
private void SetSearchDataTypeToAscii() => SetSearchDataType(MemorySearchDataType.Ascii);

[ObservableProperty]
private DataMemoryDocument? _dataMemoryDocument;
Expand Down Expand Up @@ -72,13 +98,119 @@ public uint? EndAddress {
TryUpdateHeaderAndMemoryDocument();
}
}

[RelayCommand(CanExecute = nameof(IsPaused))]
public async Task CopySelection() {
if (SelectionRange is not null && StartAddress is not null) {
byte[] memoryBytes = _memory.GetData(
(uint)(StartAddress.Value + SelectionRange.Value.Start.ByteIndex),
(uint)SelectionRange.Value.ByteLength);
string hexRepresentation = ConvertUtils.ByteArrayToHexString(memoryBytes);
await _textClipboard.SetTextAsync($"{hexRepresentation}").ConfigureAwait(false);
}
}

[ObservableProperty]
private string? _memorySearchValue;

private enum SearchDirection {
FirstOccurence,
Forward,
Backward,
}

private SearchDirection _searchDirection;

[RelayCommand]
private async Task PreviousOccurrence() {
if (SearchMemoryCommand.CanExecute(null)) {
_searchDirection = SearchDirection.Backward;
await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false);
}
}

[RelayCommand]
private async Task NextOccurrence() {
if (SearchMemoryCommand.CanExecute(null)) {
_searchDirection = SearchDirection.Forward;
await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false);
}
}

[RelayCommand]
private async Task FirstOccurrence() {
if (SearchMemoryCommand.CanExecute(null)) {
_searchDirection = SearchDirection.FirstOccurence;
await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false);
}
}

[ObservableProperty]
private bool _isBusy;

[RelayCommand]
private void StartMemorySearch() {
IsSearchingMemory = true;
}

[ObservableProperty]
private bool _isSearchingMemory;

[ObservableProperty]
private uint? _addressOFoundOccurence;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GoToFoundOccurenceCommand))]
private bool _isAddressOfFoundOccurrenceValid;

[RelayCommand(CanExecute = nameof(IsPaused), FlowExceptionsToTaskScheduler = false, IncludeCancelCommand = true)]
private async Task SearchMemory(CancellationToken token) {
if (string.IsNullOrWhiteSpace(MemorySearchValue) || token.IsCancellationRequested) {
return;
}
try {
IsBusy = true;
int searchLength = (int)_memory.Length;
uint searchStartAddress = 0;

if (_searchDirection == SearchDirection.Forward && AddressOFoundOccurence is not null) {
searchStartAddress = AddressOFoundOccurence.Value + 1;
searchLength = (int)(_memory.Length - searchStartAddress);
} else if (_searchDirection == SearchDirection.Backward && AddressOFoundOccurence is not null) {
searchStartAddress = 0;
searchLength = (int)(AddressOFoundOccurence.Value - 1);
}
if(SearchDataType == MemorySearchDataType.Binary && ConvertUtils.TryParseHexToByteArray(MemorySearchValue, out byte[]? searchBytes)) {
AddressOFoundOccurence = await Task.Run(
() =>_memory.SearchValue(searchStartAddress, searchLength, searchBytes), token).ConfigureAwait(false);
} else if(SearchDataType == MemorySearchDataType.Ascii) {
searchBytes = Encoding.ASCII.GetBytes(MemorySearchValue);
AddressOFoundOccurence = await Task.Run(
() => _memory.SearchValue(searchStartAddress, searchLength, searchBytes), token).ConfigureAwait(false);
}
} finally {
await _uiDispatcher.InvokeAsync(() => {
IsBusy = false;
IsAddressOfFoundOccurrenceValid = AddressOFoundOccurence is not null;
});
}
}

[RelayCommand(CanExecute = nameof(IsAddressOfFoundOccurrenceValid))]
private void GoToFoundOccurence() {
if (AddressOFoundOccurence is not null && NewMemoryViewCommand.CanExecute(null)) {
CreateNewMemoryView(AddressOFoundOccurence);
}
}

[ObservableProperty]
private string _header = "Memory View";

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NewMemoryViewCommand))]
[NotifyCanExecuteChangedFor(nameof(EditMemoryCommand))]
[NotifyCanExecuteChangedFor(nameof(CopySelectionCommand))]
[NotifyCanExecuteChangedFor(nameof(SearchMemoryCommand))]
private bool _isPaused;

[ObservableProperty]
Expand All @@ -88,8 +220,11 @@ public uint? EndAddress {

private readonly IHostStorageProvider _storageProvider;

public MemoryViewModel(IMemory memory, IPauseHandler pauseHandler, IMessenger messenger, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, bool canCloseTab = false, uint startAddress = 0, uint endAddress = A20Gate.EndOfHighMemoryArea) : base(textClipboard) {
public MemoryViewModel(IMemory memory, IPauseHandler pauseHandler, IMessenger messenger, IUIDispatcher uiDispatcher,
ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory,
bool canCloseTab = false, uint startAddress = 0, uint endAddress = A20Gate.EndOfHighMemoryArea) : base(textClipboard) {
_pauseHandler = pauseHandler;
_uiDispatcher = uiDispatcher;
_memory = memory;
_pauseHandler.Pausing += OnPause;
IsPaused = pauseHandler.IsPaused;
Expand Down Expand Up @@ -143,9 +278,16 @@ private void OnPause() {

[RelayCommand(CanExecute = nameof(IsPaused))]
private void NewMemoryView() {
MemoryViewModel memoryViewModel = new(_memory, _pauseHandler, _messenger, _textClipboard,
CreateNewMemoryView();
}

private void CreateNewMemoryView(uint? startAddress = null) {
MemoryViewModel memoryViewModel = new(_memory, _pauseHandler, _messenger, _uiDispatcher, _textClipboard,
_storageProvider,
_structureViewModelFactory, canCloseTab: true);
if (startAddress is not null) {
memoryViewModel.StartAddress = startAddress;
}
_messenger.Send(new AddViewModelMessage<MemoryViewModel>(memoryViewModel));
}

Expand Down
Loading

0 comments on commit 1bc6ef1

Please sign in to comment.