Mesen2/UI/Debugger/ViewModels/EventViewerViewModel.cs

600 lines
19 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using DataBoxControl;
using Mesen.Config;
using Mesen.Debugger.Controls;
using Mesen.Debugger.Labels;
using Mesen.Debugger.Utilities;
using Mesen.Debugger.Windows;
using Mesen.Interop;
using Mesen.Localization;
using Mesen.Utilities;
using Mesen.ViewModels;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using System.Reflection;
namespace Mesen.Debugger.ViewModels
{
public class EventViewerViewModel : DisposableViewModel
{
public const int HdmaChannelFlag = 0x40;
[Reactive] public CpuType CpuType { get; set; }
[Reactive] public DynamicBitmap ViewerBitmap { get; private set; }
[Reactive] public ViewModelBase ConsoleConfig { get; set; }
[Reactive] public GridRowColumn? GridHighlightPoint { get; set; }
[Reactive] public bool ShowListView { get; set; }
[Reactive] public double MinListViewHeight { get; set; }
[Reactive] public double ListViewHeight { get; set; }
private DateTime _lastListRefresh = DateTime.MinValue;
[Reactive] public DebugEventInfo? SelectedEvent { get; set; }
[Reactive] public Rect SelectionRect { get; set; }
public EventViewerListViewModel ListView { get; }
public EventViewerConfig Config { get; }
[Reactive] public List<object> FileMenuItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> DebugMenuItems { get; private set; } = new();
[Reactive] public List<object> ViewMenuItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> ToolbarItems { get; private set; } = new();
private PictureViewer _picViewer;
private bool _refreshPending;
[Obsolete("For designer only")]
public EventViewerViewModel() : this(CpuType.Nes, new PictureViewer(), null!, null) { }
public EventViewerViewModel(CpuType cpuType, PictureViewer picViewer, DataBox listView, Window? wnd)
{
CpuType = cpuType;
ListView = new EventViewerListViewModel(this);
ListView.Selection.SelectionChanged += Selection_SelectionChanged;
_picViewer = picViewer;
Config = ConfigManager.Config.Debug.EventViewer;
ShowListView = Config.ShowListView;
InitBitmap(new FrameInfo() { Width = 1, Height = 1 });
InitForCpuType();
FileMenuItems = AddDisposables(new List<object>() {
new ContextMenuAction() {
ActionType = ActionType.Exit,
OnClick = () => wnd?.Close()
}
});
ViewMenuItems = AddDisposables(new List<object>() {
new ContextMenuAction() {
ActionType = ActionType.Refresh,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Refresh),
OnClick = () => RefreshData()
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.EnableAutoRefresh,
IsSelected = () => Config.AutoRefresh,
OnClick = () => Config.AutoRefresh = !Config.AutoRefresh
},
new ContextMenuAction() {
ActionType = ActionType.RefreshOnBreakPause,
IsSelected = () => Config.RefreshOnBreakPause,
OnClick = () => Config.RefreshOnBreakPause = !Config.RefreshOnBreakPause
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.ShowSettingsPanel,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ToggleSettingsPanel),
IsSelected = () => Config.ShowSettingsPanel,
OnClick = () => Config.ShowSettingsPanel = !Config.ShowSettingsPanel
},
new ContextMenuAction() {
ActionType = ActionType.ShowListView,
IsSelected = () => Config.ShowListView,
OnClick = () => Config.ShowListView = !Config.ShowListView
},
new ContextMenuAction() {
ActionType = ActionType.ShowToolbar,
IsSelected = () => Config.ShowToolbar,
OnClick = () => Config.ShowToolbar = !Config.ShowToolbar
},
new ContextMenuSeparator(),
new ContextMenuAction() {
ActionType = ActionType.ZoomIn,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ZoomIn),
OnClick = () => _picViewer.ZoomIn()
},
new ContextMenuAction() {
ActionType = ActionType.ZoomOut,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ZoomOut),
OnClick = () => _picViewer.ZoomOut()
},
});
if(Design.IsDesignMode || wnd == null) {
return;
}
AddDisposables(DebugShortcutManager.CreateContextMenu(_picViewer, GetContextMenuActions()));
AddDisposables(DebugShortcutManager.CreateContextMenu(listView, GetContextMenuActions()));
UpdateConfig();
RefreshData();
DebugMenuItems = AddDisposables(DebugSharedActions.GetStepActions(wnd, () => CpuType));
ToolbarItems = AddDisposables(DebugSharedActions.GetStepActions(wnd, () => CpuType));
AddDisposable(this.WhenAnyValue(x => x.CpuType).Subscribe(_ => {
InitForCpuType();
UpdateConfig();
RefreshData();
}));
AddDisposable(ReactiveHelper.RegisterRecursiveObserver(Config, (s, e) => {
UpdateConfig();
RefreshUi(false);
}));
AddDisposable(this.WhenAnyValue(x => x.ShowListView).Subscribe(showListView => {
Config.ShowListView = showListView;
ListViewHeight = showListView ? Config.ListViewHeight : 0;
MinListViewHeight = showListView ? 100 : 0;
RefreshUi(false);
}));
AddDisposable(this.WhenAnyValue(x => x.ListViewHeight).Subscribe(height => {
if(ShowListView) {
Config.ListViewHeight = height;
} else {
ListViewHeight = 0;
}
}));
DebugShortcutManager.RegisterActions(wnd, FileMenuItems);
DebugShortcutManager.RegisterActions(wnd, DebugMenuItems);
DebugShortcutManager.RegisterActions(wnd, ViewMenuItems);
}
private List<ContextMenuAction> GetContextMenuActions()
{
return new List<ContextMenuAction> {
new ContextMenuAction() {
ActionType = ActionType.ViewInDebugger,
IsEnabled = () => SelectedEvent != null,
HintText = () => SelectedEvent != null ? $"PC -${SelectedEvent.Value.ProgramCounter:X4}" : "",
OnClick = () => {
if(SelectedEvent != null) {
DebuggerWindow.OpenWindowAtAddress(CpuType, (int)SelectedEvent.Value.ProgramCounter);
}
}
},
new ContextMenuAction() {
ActionType = ActionType.ToggleBreakpoint,
IsEnabled = () => SelectedEvent != null,
HintText = () => SelectedEvent != null ? $"PC - ${SelectedEvent.Value.ProgramCounter:X4}" : "",
OnClick = () => {
if(SelectedEvent != null) {
int addr = (int)SelectedEvent.Value.ProgramCounter;
BreakpointManager.ToggleBreakpoint(new AddressInfo() { Address = addr, Type = CpuType.ToMemoryType() }, CpuType);
}
}
},
new ContextMenuAction() {
ActionType = ActionType.ToggleBreakpoint,
IsEnabled = () => SelectedEvent?.Flags.HasFlag(EventFlags.ReadWriteOp) == true,
HintText = () => SelectedEvent?.Flags.HasFlag(EventFlags.ReadWriteOp) == true ? $"Address - ${SelectedEvent.Value.Operation.Address:X4}" : "",
OnClick = () => {
if(SelectedEvent?.Flags.HasFlag(EventFlags.ReadWriteOp) == true) {
int addr = (int)SelectedEvent.Value.Operation.Address;
BreakpointManager.ToggleBreakpoint(new AddressInfo() { Address = addr, Type = CpuType.ToMemoryType() }, CpuType);
}
}
},
};
}
private void Selection_SelectionChanged(object? sender, Avalonia.Controls.Selection.SelectionModelSelectionChangedEventArgs<DebugEventViewModel?> e)
{
if(e.SelectedItems.Count > 0 && e.SelectedItems[0] is DebugEventViewModel evt) {
UpdateSelectedEvent(evt.RawEvent);
} else {
SelectionRect = default;
}
}
public void UpdateSelectedEvent(DebugEventInfo? evt)
{
if(evt != null) {
PixelPoint p = GetEventLocation(evt.Value);
SelectedEvent = evt;
SelectionRect = new Rect(p.X - 2, p.Y - 2, 6, 6);
} else {
SelectedEvent = null;
SelectionRect = default;
}
}
[MemberNotNull(nameof(EventViewerViewModel.ConsoleConfig))]
private void InitForCpuType()
{
ConsoleConfig = CpuType switch {
CpuType.Snes => Config.SnesConfig,
CpuType.Nes => Config.NesConfig,
CpuType.Gameboy => Config.GbConfig,
CpuType.Pce => Config.PceConfig,
CpuType.Sms => Config.SmsConfig,
CpuType.Gba => Config.GbaConfig,
CpuType.Ws => Config.WsConfig,
_ => throw new Exception("Invalid cpu type")
};
}
private void InitBitmap()
{
InitBitmap(DebugApi.GetEventViewerDisplaySize(CpuType));
}
[MemberNotNull(nameof(ViewerBitmap))]
private void InitBitmap(FrameInfo size)
{
if(ViewerBitmap == null || ViewerBitmap.Size.Width != size.Width || ViewerBitmap.Size.Height != size.Height) {
ViewerBitmap = new DynamicBitmap(new PixelSize((int)size.Width, (int)size.Height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
}
}
public void RefreshData(bool forAutoRefresh = false)
{
DebugApi.TakeEventSnapshot(CpuType, forAutoRefresh);
Dispatcher.UIThread.Post(() => {
SelectionRect = default;
SelectedEvent = null;
});
RefreshUi(forAutoRefresh);
}
public void RefreshUi(bool forAutoRefresh)
{
if(_refreshPending) {
return;
}
_refreshPending = true;
Dispatcher.UIThread.Post(() => {
InternalRefreshUi(forAutoRefresh);
_refreshPending = false;
});
}
private void InternalRefreshUi(bool forAutoRefresh)
{
if(Disposed) {
return;
}
InitBitmap();
using(var bitmapLock = ViewerBitmap.Lock()) {
DebugApi.GetEventViewerOutput(CpuType, bitmapLock.FrameBuffer.Address, (uint)(ViewerBitmap.Size.Width * ViewerBitmap.Size.Height * sizeof(UInt32)));
}
if(ShowListView) {
DateTime now = DateTime.Now;
if(!forAutoRefresh || (now - _lastListRefresh).TotalMilliseconds >= 66) {
_lastListRefresh = now;
ListView.RefreshList();
}
}
}
private PixelPoint GetEventLocation(DebugEventInfo evt)
{
return CpuType switch {
CpuType.Snes => new PixelPoint(evt.Cycle / 2, evt.Scanline * 2),
CpuType.Nes => new PixelPoint(evt.Cycle * 2, (evt.Scanline + 1) * 2),
CpuType.Gameboy => new PixelPoint(evt.Cycle * 2, evt.Scanline * 2),
CpuType.Pce => new PixelPoint(evt.Cycle, evt.Scanline * 2),
CpuType.Sms => new PixelPoint(evt.Cycle * 2, evt.Scanline * 2),
CpuType.Gba => new PixelPoint(evt.Cycle, evt.Scanline * 4),
CpuType.Ws => new PixelPoint(evt.Cycle * 2, evt.Scanline * 2),
_ => throw new Exception("Invalid cpu type")
};
}
public void UpdateHighlightPoint(PixelPoint p, DebugEventInfo? eventInfo)
{
if(eventInfo != null) {
//Snap the row/column highlight to the selected event
p = GetEventLocation(eventInfo.Value);
}
GridRowColumn result = new GridRowColumn() {
Y = p.Y / 2 * 2,
Width = 2,
Height = 2
};
int xPos;
int yPos;
switch(CpuType) {
case CpuType.Snes:
result.X = p.X;
xPos = result.X * 2;
yPos = result.Y / 2;
break;
case CpuType.Nes:
result.X = p.X / 2 * 2;
xPos = result.X / 2;
yPos = result.Y / 2 - 1;
break;
case CpuType.Gameboy:
result.X = p.X / 2 * 2;
xPos = result.X / 2;
yPos = result.Y / 2;
break;
case CpuType.Pce:
result.X = p.X;
xPos = result.X;
yPos = result.Y / 2;
break;
case CpuType.Sms:
result.X = p.X / 2 * 2;
xPos = result.X / 2;
yPos = result.Y / 2;
break;
case CpuType.Gba:
result.X = p.X;
xPos = result.X;
yPos = result.Y / 4;
break;
case CpuType.Ws:
result.X = p.X / 2 * 2;
xPos = result.X / 2;
yPos = result.Y / 2;
break;
default:
throw new Exception("Invalid cpu type");
}
result.DisplayValue = $"X: {xPos}\nY: {yPos}";
GridHighlightPoint = result;
}
public void UpdateConfig()
{
if(ConsoleConfig is SnesEventViewerConfig snesCfg) {
DebugApi.SetEventViewerConfig(CpuType, snesCfg.ToInterop());
} else if(ConsoleConfig is NesEventViewerConfig nesCfg) {
DebugApi.SetEventViewerConfig(CpuType, nesCfg.ToInterop());
} else if(ConsoleConfig is GbEventViewerConfig gbCfg) {
DebugApi.SetEventViewerConfig(CpuType, gbCfg.ToInterop());
} else if(ConsoleConfig is GbaEventViewerConfig gbaCfg) {
DebugApi.SetEventViewerConfig(CpuType, gbaCfg.ToInterop());
} else if(ConsoleConfig is PceEventViewerConfig pceCfg) {
DebugApi.SetEventViewerConfig(CpuType, pceCfg.ToInterop());
} else if(ConsoleConfig is SmsEventViewerConfig smsCfg) {
DebugApi.SetEventViewerConfig(CpuType, smsCfg.ToInterop());
} else if(ConsoleConfig is WsEventViewerConfig wsCfg) {
DebugApi.SetEventViewerConfig(CpuType, wsCfg.ToInterop());
}
}
public void EnableAllEventTypes()
{
foreach(PropertyInfo prop in ConsoleConfig.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
if(prop.PropertyType == typeof(EventViewerCategoryCfg)) {
((EventViewerCategoryCfg)prop.GetValue(ConsoleConfig)!).Visible = true;
}
}
}
public void DisableAllEventTypes()
{
foreach(PropertyInfo prop in ConsoleConfig.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
if(prop.PropertyType == typeof(EventViewerCategoryCfg)) {
((EventViewerCategoryCfg)prop.GetValue(ConsoleConfig)!).Visible = false;
}
}
}
public enum EventViewerTab
{
PpuView,
ListView,
}
public static string GetEventDetails(CpuType cpuType, DebugEventInfo evt, bool singleLine)
{
bool isDma = evt.DmaChannel >= 0 && (evt.Operation.Type == MemoryOperationType.DmaWrite || evt.Operation.Type == MemoryOperationType.DmaRead);
List<string> details = new List<string>();
if(evt.Flags.HasFlag(EventFlags.PreviousFrame)) {
details.Add("Previous frame");
}
if(evt.Flags.HasFlag(EventFlags.RegSecondWrite)) {
details.Add("Second register write");
} else if(evt.Flags.HasFlag(EventFlags.RegFirstWrite)) {
details.Add("First register write");
}
if(evt.Flags.HasFlag(EventFlags.HasTargetMemory)) {
details.Add("Target: " + evt.TargetMemory.MemType.GetShortName() + " $" + evt.TargetMemory.Address.ToString(evt.TargetMemory.MemType.GetFormatString()));
}
if(evt.Type == DebugEventType.Breakpoint && evt.BreakpointId >= 0) {
Breakpoint? bp = BreakpointManager.GetBreakpointById(evt.BreakpointId);
if(bp != null) {
string bpInfo = "Breakpoint - ";
bpInfo += "CPU: " + ResourceHelper.GetEnumText(bp.CpuType);
bpInfo += singleLine ? " - " : Environment.NewLine;
bpInfo += "Type: " + bp.ToReadableType();
bpInfo += singleLine ? " - " : Environment.NewLine;
bpInfo += "Addresses: " + bp.GetAddressString(true);
if(bp.Condition.Length > 0) {
bpInfo += singleLine ? " - " : Environment.NewLine;
bpInfo += "Condition: " + bp.Condition;
}
details.Add(bpInfo);
}
}
if(isDma) {
string dmaInfo = "";
if(cpuType == CpuType.Snes) {
bool indirectHdma = false;
bool isHdma = (evt.DmaChannel & EventViewerViewModel.HdmaChannelFlag) != 0;
if(isHdma) {
indirectHdma = evt.DmaChannelInfo.HdmaIndirectAddressing;
dmaInfo += "HDMA #" + (evt.DmaChannel & 0x07);
dmaInfo += indirectHdma ? " (indirect)" : "";
dmaInfo += singleLine ? " - " : Environment.NewLine;
dmaInfo += "Line Counter: $" + evt.DmaChannelInfo.HdmaLineCounterAndRepeat.ToString("X2");
} else {
dmaInfo += "DMA #" + (evt.DmaChannel & 0x07);
}
dmaInfo += singleLine ? " - " : Environment.NewLine;
dmaInfo += "Mode: " + evt.DmaChannelInfo.TransferMode;
dmaInfo += singleLine ? " - " : Environment.NewLine;
int aBusAddress;
if(indirectHdma) {
aBusAddress = (evt.DmaChannelInfo.HdmaBank << 16) | evt.DmaChannelInfo.TransferSize;
} else if(isHdma) {
aBusAddress = (evt.DmaChannelInfo.SrcBank << 16) | evt.DmaChannelInfo.HdmaTableAddress;
} else {
aBusAddress = (evt.DmaChannelInfo.SrcBank << 16) | evt.DmaChannelInfo.SrcAddress;
}
if(!evt.DmaChannelInfo.InvertDirection) {
dmaInfo += "$" + aBusAddress.ToString("X4") + " -> $" + (0x2100 | evt.DmaChannelInfo.DestAddress).ToString("X4");
} else {
dmaInfo += "$" + aBusAddress.ToString("X4") + " <- $" + (0x2100 | evt.DmaChannelInfo.DestAddress).ToString("X4");
}
} else {
dmaInfo = "DMA Channel #" + evt.DmaChannel;
}
details.Add(dmaInfo.Trim());
}
if(singleLine) {
if(details.Count > 1) {
return "[" + string.Join("] [", details) + "]";
} else if(details.Count == 1) {
return details[0];
} else {
return "";
}
} else {
return string.Join(Environment.NewLine, details);
}
}
}
public class DebugEventViewModel : INotifyPropertyChanged
{
private DebugEventInfo[] _events = Array.Empty<DebugEventInfo>();
private int _index;
private CpuType _cpuType;
public event PropertyChangedEventHandler? PropertyChanged;
public string ProgramCounter { get; set; } = "";
public string Scanline { get; set; } = "";
public string Cycle { get; set; } = "";
public string Type { get; set; } = "";
public string Address { get; set; } = "";
public string Value { get; set; } = "";
public string Details { get; set; } = "";
private UInt32 _color = 0;
public UInt32 Color
{
get
{
UpdateFields();
return _color;
}
}
public DebugEventInfo RawEvent => _events[_index];
public DebugEventViewModel(DebugEventInfo[] events, int index, CpuType cpuType)
{
Update(events, index, cpuType);
}
public void Update(DebugEventInfo[] events, int index, CpuType cpuType)
{
_cpuType = cpuType;
_events = events;
_index = index;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Color"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ProgramCounter"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Scanline"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Cycle"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Type"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Address"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Details"));
}
private void UpdateFields()
{
DebugEventInfo evt = _events[_index];
_color = evt.Color;
ProgramCounter = "$" + evt.ProgramCounter.ToString("X4");
Scanline = evt.Scanline.ToString();
Cycle = evt.Cycle.ToString();
string address = "";
bool isReadWriteOp = evt.Flags.HasFlag(EventFlags.ReadWriteOp);
if(isReadWriteOp) {
address += "$" + evt.Operation.Address.ToString("X4");
CodeLabel? label = LabelManager.GetLabel(new AddressInfo() { Address = (int)evt.Operation.Address, Type = _cpuType.ToMemoryType() });
if(label != null) {
address = label.Label + " (" + address + ")";
}
if(evt.RegisterId >= 0) {
string regName = evt.GetRegisterName();
if(string.IsNullOrEmpty(regName)) {
address += $" (${evt.RegisterId:X2})";
} else {
address += $" ({regName} - ${evt.RegisterId:X2})";
}
}
}
Address = address;
Value = isReadWriteOp ? "$" + evt.Operation.Value.ToString("X2") : "";
Type = ResourceHelper.GetEnumText(evt.Type);
if(isReadWriteOp) {
Type += evt.Operation.Type.IsWrite() ? " (W)" : " (R)";
}
Details = EventViewerViewModel.GetEventDetails(_cpuType, evt, true);
}
}
}