Skip to content

Commit

Permalink
Feature/multiple memory views (#716)
Browse files Browse the repository at this point in the history
* UI: refactored the error dialogs into a single one

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* feature: editable memory view range

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* feature: additional memory views (WIP)

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* Debugger: catch IndexOutOfRangeException

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* UI: Pause the software mixer on emulator Pause

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* UI:Additonnal memory and disasm views feature

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* fix: UI did not respond while visiting emulator

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* fix: MemoryView crashes, new DISASM view button

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* Debugger: refactored into a tab

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* removed disasm duplicated view datatemplate

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: Removed reflection bindings for commands

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: Configuration is a sealed class

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: removed ViewLocator antipattern

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: ViewModelBaseWithErrrorDialog

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: removed ctors for Designer from VMs

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* fix: UI reads memory without trigerring read breakpoints

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* fix: Edit Memory button reacts to pause status

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: merged DI branches in Program.cs

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: inverted if to reduce indentation

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* doc: corrected grammar

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: memory read/write methods without trigerring breakpoints

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: removed some unused ViewModel code

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: removed SelectedTab ObservableProperty

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: Debugger is its own window again

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* Expanded XML documentation, polished DebugWindow

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* feature: close Memory/Disassembly views

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* doc: documented the fact that IReadOnlyBitRangeUnion endAdress is not included

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* MemoryView: better layout (DockPanel)

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* fix: Close DebugWindow on new program launch

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* UI: Debug Window icon

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: removed unused usings.

Co-authored-by: Joris van Eijden <joris.vaneijden@gmail.com>

---------

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>
Co-authored-by: Joris van Eijden <joris.vaneijden@gmail.com>
  • Loading branch information
maximilien-noal and JorisVanEijden authored Jun 17, 2024
1 parent fb6285d commit 55d13fa
Show file tree
Hide file tree
Showing 44 changed files with 940 additions and 635 deletions.
2 changes: 1 addition & 1 deletion src/Spice86.Core/CLI/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Spice86.Core.CLI;
using Spice86.Core.Emulator.Function;

/// <summary> Configuration for spice86, that is what to run and how. Set on startup. </summary>
public class Configuration {
public sealed class Configuration {
/// <summary>
/// Gets or sets whether the A20 gate is silenced. If <c>true</c> memory addresses will rollover above 1 MB.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion src/Spice86.Core/Emulator/Devices/Sound/SoftwareMixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ internal void Register(SoundChannel soundChannel) {
Channels = _channels.ToFrozenDictionary();
}

/// <summary>
/// Gets or sets a value indicating whether the software mixer is paused. In paused state, the mixer will not render any sound.
/// </summary>
public bool IsPaused { get; set; }

/// <summary>
/// Gets the sound channels in a read-only dictionary.
/// </summary>
public FrozenDictionary<SoundChannel, AudioPlayer> Channels { get; private set; } = new Dictionary<SoundChannel, AudioPlayer>().ToFrozenDictionary();

internal int Render(Span<float> data, SoundChannel channel) {
if (channel.Volume == 0 || channel.IsMuted) {
if (channel.Volume == 0 || channel.IsMuted || IsPaused) {
_channels[channel].WriteSilence();
return data.Length;
}
Expand Down
13 changes: 7 additions & 6 deletions src/Spice86.Core/Emulator/Devices/Timer/Timer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Emulates a PIT8254 Programmable Interval Timer.<br/>
/// Triggers interrupt 8 on the CPU via the PIC.<br/>
/// https://k.lse.epita.fr/internals/8254_controller.html
/// </summary>
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;
Expand Down Expand Up @@ -119,4 +115,9 @@ private Counter GetCounterIndexFromPortNumber(int port) {
int counter = port & 0b11;
return GetCounter(counter);
}

/// <inheritdoc />
public void Accept<T>(T emulatorDebugger) where T : IInternalDebugger {
emulatorDebugger.Visit(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ private byte[] GenerateToolingCompliantRamDump() {
return _callbackHandler.ReplaceAllCallbacksInRamImage(_memory);
}

return _memory.RamCopy;
return _memory.ReadRam();
}
}
17 changes: 15 additions & 2 deletions src/Spice86.Core/Emulator/InternalDebugger/IInternalDebugger.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
namespace Spice86.Core.Emulator.InternalDebugger;
public interface IInternalDebugger
{

/// <summary>
/// Interface for the internal debuggers implemented by the UI ViewModels.
/// </summary>
public interface IInternalDebugger {
/// <summary>
/// Visit an emulator component that accepts the internal debugger.
/// </summary>
/// <param name="component">The emulator component that accepts the internal debugger</param>
/// <typeparam name="T">A class that implements the <see cref="IDebuggableComponent"/> interface.</typeparam>
void Visit<T>(T component) where T : IDebuggableComponent;

/// <summary>
/// Tells if the ViewModel for the internal debugger needs to visit the emulator. Either to get references to internal objects or refresh UI data.
/// </summary>
public bool NeedsToVisitEmulator { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void RunFromOverriden(int index) {
/// <param name="memory">The memory bus.</param>
/// <returns>A byte array representing the memory content with the Spice86 machine code for callbacks removed.</returns>
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;
Expand Down
20 changes: 15 additions & 5 deletions src/Spice86.Core/Emulator/Memory/IMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,22 @@ public interface IMemory : IIndexable, IByteReaderWriter, IDebuggableComponent {
A20Gate A20Gate { get; }

/// <summary>
/// Gets a copy of the current memory state.
/// Gets a copy of the current memory state, not triggering any breakpoints.
/// </summary>
byte[] RamCopy { get; }
/// <param name="length">The length of the byte array. Default is equal to the memory length.</param>
/// <param name="offset">Where to start in the memory. Default is <c>0</c>.</param>
/// <returns>A copy of the current memory state.</returns>
public byte[] ReadRam(uint length = 0, uint offset = 0);

/// <summary>
/// Returns a <see cref="Span{T}"/> that represents the specified range of memory.
/// Writes an array of bytes to memory, not triggering any breakpoints.
/// </summary>
/// <param name="array">The array to copy data from.</param>
/// <param name="offset">Where to start in the memory. Default is <c>0</c>.</param>
public void WriteRam(byte[] array, uint offset = 0);

/// <summary>
/// Returns a <see cref="Span{T}"/> that represents the specified range of memory. Will trigger memory read breakpoints.
/// </summary>
/// <param name="address">The starting address of the memory range.</param>
/// <param name="length">The length of the memory range.</param>
Expand All @@ -47,12 +57,12 @@ public interface IMemory : IIndexable, IByteReaderWriter, IDebuggableComponent {
uint? SearchValue(uint address, int len, IList<byte> value);

/// <summary>
/// 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.
/// </summary>
byte CurrentlyWritingByte { get; }

/// <summary>
/// Allow a class to register for a certain memory range.
/// Allow a memory mapped device to register for a certain memory range.
/// </summary>
/// <param name="baseAddress">The start of the frame</param>B
/// <param name="size">The size of the window</param>
Expand Down
43 changes: 22 additions & 21 deletions src/Spice86.Core/Emulator/Memory/Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,22 @@ public Memory(IMemoryDevice baseMemory, bool is20ThAddressLineSilenced) {
/// </summary>
public const uint EndOfHighMemoryArea = 0x10FFEF;

/// <summary>
/// Gets a copy of the current memory state, not triggering any breakpoints.
/// </summary>
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;
/// <inheritdoc />
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;
}

/// <inheritdoc />
public void WriteRam(byte[] array, uint offset = 0) {
for (uint address = 0; address < array.Length; address++) {
_memoryDevices[address + offset].Write(address + offset, array[address]);
}
}

Expand All @@ -77,7 +82,7 @@ public byte this[uint address] {
}

/// <summary>
/// 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.
/// </summary>
public byte CurrentlyWritingByte {
get;
Expand All @@ -97,10 +102,11 @@ public byte CurrentlyWritingByte {
public Span<byte> 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}");
Expand Down Expand Up @@ -173,12 +179,7 @@ public override SegmentedAddressIndexer SegmentedAddress {
get;
}

/// <summary>
/// Allow a class to register for a certain memory range.
/// </summary>
/// <param name="baseAddress">The start of the frame</param>
/// <param name="size">The size of the window</param>
/// <param name="memoryDevice">The memory device to use</param>
/// <inheritdoc/>
public void RegisterMapping(uint baseAddress, uint size, IMemoryDevice memoryDevice) {
uint endAddress = baseAddress + size;
if (endAddress >= _memoryDevices.Length) {
Expand Down
4 changes: 1 addition & 3 deletions src/Spice86.Core/Emulator/VM/Machine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -385,5 +382,6 @@ public void Accept<T>(T emulatorDebugger) where T : IInternalDebugger {
VgaRegisters.Accept(emulatorDebugger);
MidiDevice.Accept(emulatorDebugger);
SoftwareMixer.Accept(emulatorDebugger);
Timer.Accept(emulatorDebugger);
}
}
5 changes: 0 additions & 5 deletions src/Spice86.Shared/Interfaces/IGui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,4 @@ public interface IGui {
/// Indicate that a mouse button has been released.
/// </summary>
event EventHandler<MouseButtonEventArgs>? MouseButtonUp;

/// <summary>
/// Used by the UI to set or reset the time multiplier.
/// </summary>
ITimeMultiplier? ProgrammableIntervalTimer { set; }
}
4 changes: 0 additions & 4 deletions src/Spice86/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<Application.Resources>
<FontFamily x:Key="RobotoMonoFont">avares://Spice86/Assets#Roboto Mono</FontFamily>
<FontFamily x:Key="ConsolasFont">Consolas</FontFamily>
</Application.Resources>
<Application.DataTemplates>
<spice86:ViewLocator />
</Application.DataTemplates>
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme DensityStyle="Compact" />
Expand Down
Binary file added src/Spice86/Assets/Debug.ico
Binary file not shown.
18 changes: 0 additions & 18 deletions src/Spice86/DependencyInjection/Composition.cs

This file was deleted.

26 changes: 0 additions & 26 deletions src/Spice86/Infrastructure/DebugWindowActivator.cs

This file was deleted.

15 changes: 15 additions & 0 deletions src/Spice86/Infrastructure/HostStorageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 0 additions & 21 deletions src/Spice86/Infrastructure/IDebugWindowActivator.cs

This file was deleted.

7 changes: 7 additions & 0 deletions src/Spice86/Infrastructure/IHostStorageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,11 @@ public interface IHostStorageProvider {
/// <param name="lastExecutableDirectory">The directory of the last file picked, if any.</param>
/// <returns>The operation as an awaitable Task, containing the first picked file, or <c>null</c>.</returns>
Task<IStorageFile?> PickExecutableFile(string lastExecutableDirectory);

/// <summary>
/// Spanws the file picker to save a binary file.
/// </summary>
/// <param name="bytes">The binary content of the file.</param>
/// <returns>The operation as an awaitable Task.</returns>
Task SaveBinaryFile(byte[] bytes);
}
19 changes: 19 additions & 0 deletions src/Spice86/Infrastructure/IWindowService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Spice86.Infrastructure;

using Spice86.ViewModels;

/// <summary>
/// Service used for showing the debug window.
/// </summary>
public interface IWindowService {
/// <summary>
/// Shows the debug window.
/// </summary>
/// <param name="viewModel">The <see cref="DebugWindowViewModel"/> used as DataContext in case the window needs to be created.</param>
Task ShowDebugWindow(DebugWindowViewModel viewModel);

/// <summary>
/// Close the debug window.
/// </summary>
void CloseDebugWindow();
}
Loading

0 comments on commit 55d13fa

Please sign in to comment.