Mesen2/UI/Debugger/ViewModels/TraceLoggerViewModel.cs

756 lines
26 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using Mesen.Config;
using Mesen.Debugger.Controls;
using Mesen.Debugger.Disassembly;
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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mesen.Debugger.ViewModels
{
public class TraceLoggerViewModel : DisposableViewModel, ISelectableModel
{
public TraceLoggerConfig Config { get; }
[Reactive] public TraceLoggerStyleProvider StyleProvider { get; set; }
[Reactive] public CodeLineData[] TraceLogLines { get; set; } = Array.Empty<CodeLineData>();
[Reactive] public int VisibleRowCount { get; set; } = 100;
[Reactive] public int ScrollPosition { get; set; } = 0;
[Reactive] public int MinScrollPosition { get; set; } = 0;
[Reactive] public int MaxScrollPosition { get; set; } = DebugApi.TraceLogBufferSize;
[Reactive] public bool IsLoggingToFile { get; set; } = false;
[Reactive] public List<TraceLoggerOptionTab> Tabs { get; set; } = new();
[Reactive] public TraceLoggerOptionTab SelectedTab { get; set; } = null!;
[Reactive] public string? TraceFile { get; set; } = null;
[Reactive] public bool AllowOpenTraceFile { get; private set; } = false;
[Reactive] public bool IsStartLoggingEnabled { get; set; }
[Reactive] public bool ShowByteCode { get; private set; }
[Reactive] public int SelectionStart { get; private set; }
[Reactive] public int SelectionEnd { get; private set; }
[Reactive] public int SelectionAnchor { get; private set; }
[Reactive] public int SelectedRow { get; private set; }
[Reactive] public List<ContextMenuAction> ToolbarItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> FileMenuItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> DebugMenuItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> SearchMenuItems { get; private set; } = new();
[Reactive] public List<ContextMenuAction> ViewMenuItems { get; private set; } = new();
public QuickSearchViewModel QuickSearch { get; } = new();
private DisassemblyViewer? _viewer = null;
public TraceLoggerViewModel()
{
Config = ConfigManager.Config.Debug.TraceLogger;
StyleProvider = new TraceLoggerStyleProvider(this);
if(Design.IsDesignMode) {
Tabs = new() { new TraceLoggerOptionTab(this, CpuType.Nes, Config.GetCpuConfig(CpuType.Nes), true) };
SelectedTab = Tabs[0];
return;
}
QuickSearch.OnFind += QuickSearch_OnFind;
AddDisposable(this.WhenAnyValue(x => x.QuickSearch.IsSearchBoxVisible).Subscribe(x => {
if(!QuickSearch.IsSearchBoxVisible) {
_viewer?.Focus();
}
}));
UpdateAvailableTabs();
AddDisposable(this.WhenAnyValue(x => x.ScrollPosition).Subscribe(x => {
ScrollPosition = Math.Max(MinScrollPosition, Math.Min(x, MaxScrollPosition));
UpdateLog();
}));
AddDisposable(this.WhenAnyValue(x => x.MinScrollPosition).Subscribe(x => {
ScrollPosition = Math.Max(MinScrollPosition, Math.Min(x, MaxScrollPosition));
}));
AddDisposable(this.WhenAnyValue(x => x.MaxScrollPosition).Subscribe(x => {
ScrollPosition = Math.Max(MinScrollPosition, Math.Min(x, MaxScrollPosition));
}));
AddDisposable(this.WhenAnyValue(x => x.IsLoggingToFile).Subscribe(x => {
AllowOpenTraceFile = !IsLoggingToFile && TraceFile != null;
}));
AddDisposable(this.WhenAnyValue(x => x.SelectionStart, x => x.SelectionEnd, x => x.SelectedRow, x => x.SelectionAnchor).Subscribe(x => {
SelectionStart = Math.Max(MinScrollPosition, Math.Min(DebugApi.TraceLogBufferSize - 1, SelectionStart));
SelectionEnd = Math.Max(MinScrollPosition, Math.Min(DebugApi.TraceLogBufferSize - 1, SelectionEnd));
SelectedRow = Math.Max(MinScrollPosition, Math.Min(DebugApi.TraceLogBufferSize - 1, SelectedRow));
SelectionAnchor = Math.Max(MinScrollPosition, Math.Min(DebugApi.TraceLogBufferSize - 1, SelectionAnchor));
}));
}
public void SetViewer(DisassemblyViewer viewer)
{
_viewer = viewer;
}
private void QuickSearch_OnFind(OnFindEventArgs e)
{
CodeLineData[] lines = GetCodeLines(0, DebugApi.TraceLogBufferSize);
string needle = e.SearchString.ToLowerInvariant();
int startRow = SelectedRow;
if(e.Direction == SearchDirection.Backward) {
startRow--;
} else if(e.SkipCurrent) {
startRow++;
}
int sign = e.Direction == SearchDirection.Backward ? -1 : 1;
for(int i = 0; i < lines.Length; i++) {
int lineIndex = (i * sign + startRow) % lines.Length;
if(lineIndex < 0) {
lineIndex += lines.Length;
}
if(lines[lineIndex].Text.Contains(needle, StringComparison.OrdinalIgnoreCase)) {
Dispatcher.UIThread.Post(() => {
ScrollToRowNumber(lineIndex);
SelectedRow = lineIndex;
SelectionStart = lineIndex;
SelectionEnd = lineIndex;
InvalidateVisual();
});
e.Success = true;
return;
}
}
e.Success = false;
}
public void InitializeMenu(Window wnd)
{
FileMenuItems = AddDisposables(new List<ContextMenuAction>() {
new ContextMenuAction() {
ActionType = ActionType.Exit,
OnClick = () => wnd.Close()
}
});
DebugMenuItems.AddRange(AddDisposables(DebugSharedActions.GetStepActions(wnd, () => MainWindowViewModel.Instance.RomInfo.ConsoleType.GetMainCpuType())));
ToolbarItems.AddRange(AddDisposables(DebugSharedActions.GetStepActions(wnd, () => MainWindowViewModel.Instance.RomInfo.ConsoleType.GetMainCpuType())));
ViewMenuItems = AddDisposables(new List<ContextMenuAction>() {
new ContextMenuAction() {
ActionType = ActionType.Refresh,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Refresh),
OnClick = () => UpdateLog()
},
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.ShowToolbar,
IsSelected = () => Config.ShowToolbar,
OnClick = () => Config.ShowToolbar = !Config.ShowToolbar
}
});
SearchMenuItems = AddDisposables(new List<ContextMenuAction>() {
new ContextMenuAction() {
ActionType = ActionType.Find,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Find),
OnClick = () => QuickSearch.Open()
},
new ContextMenuAction() {
ActionType = ActionType.FindPrev,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.FindPrev),
OnClick = () => QuickSearch.FindPrev()
},
new ContextMenuAction() {
ActionType = ActionType.FindNext,
Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.FindNext),
OnClick = () => QuickSearch.FindNext()
},
});
DebugShortcutManager.RegisterActions(wnd, FileMenuItems);
DebugShortcutManager.RegisterActions(wnd, DebugMenuItems);
DebugShortcutManager.RegisterActions(wnd, ViewMenuItems);
DebugShortcutManager.RegisterActions(wnd, SearchMenuItems);
}
public void InvalidateVisual()
{
TraceLogLines = (CodeLineData[])TraceLogLines.Clone();
}
public void UpdateAvailableTabs()
{
foreach(TraceLoggerOptionTab tab in Tabs) {
tab.Dispose();
}
List<TraceLoggerOptionTab> tabs = new();
RomInfo romInfo = EmuApi.GetRomInfo();
bool showEnableButton = romInfo.CpuTypes.Count > 1;
StyleProvider.SetConsoleType(romInfo.ConsoleType);
foreach(CpuType type in romInfo.CpuTypes) {
tabs.Add(AddDisposable(new TraceLoggerOptionTab(this, type, Config.GetCpuConfig(type), showEnableButton)));
}
Tabs = tabs;
SelectedTab = tabs[0];
UpdateOptions();
}
public void UpdateOptions()
{
bool forceEnable = Tabs.Count == 1;
bool isStartLoggingEnabled = forceEnable;
bool showByteCode = false;
RomInfo romInfo = EmuApi.GetRomInfo();
foreach(CpuType cpuType in romInfo.CpuTypes) {
showByteCode |= Config.GetCpuConfig(cpuType).ShowByteCode;
isStartLoggingEnabled |= Config.GetCpuConfig(cpuType).Enabled;
}
UpdateCoreOptions();
IsStartLoggingEnabled = isStartLoggingEnabled;
ShowByteCode = showByteCode;
UpdateLog();
}
public void UpdateCoreOptions()
{
RomInfo romInfo = EmuApi.GetRomInfo();
foreach(CpuType cpuType in romInfo.CpuTypes) {
TraceLoggerCpuConfig cfg = Config.GetCpuConfig(cpuType);
InteropTraceLoggerOptions options = new InteropTraceLoggerOptions() {
Enabled = romInfo.CpuTypes.Count == 1 || cfg.Enabled,
UseLabels = cfg.UseLabels,
IndentCode = cfg.IndentCode,
Format = Encoding.UTF8.GetBytes(cfg.UseCustomFormat ? cfg.Format : TraceLoggerOptionTab.GetAutoFormat(cfg, cpuType)),
Condition = Encoding.UTF8.GetBytes(cfg.Condition)
};
Array.Resize(ref options.Condition, 1000);
Array.Resize(ref options.Format, 1000);
DebugApi.SetTraceOptions(cpuType, options);
}
}
public void UpdateLog(bool scrollToBottom = false)
{
int traceSize = (int)DebugApi.GetExecutionTraceSize();
CodeLineData[] lines = GetCodeLines(ScrollPosition, VisibleRowCount);
Dispatcher.UIThread.Post(() => {
MinScrollPosition = Math.Min(MaxScrollPosition, DebugApi.TraceLogBufferSize - traceSize);
TraceLogLines = lines;
if(scrollToBottom) {
ScrollToBottom(false);
}
});
}
public void SetSelectedRow(int rowNumber)
{
rowNumber += ScrollPosition;
SelectionStart = rowNumber;
SelectionEnd = rowNumber;
SelectedRow = rowNumber;
SelectionAnchor = rowNumber;
InvalidateVisual();
}
public bool IsSelected(int rowNumber)
{
rowNumber += ScrollPosition;
return rowNumber >= SelectionStart && rowNumber <= SelectionEnd;
}
public void MoveCursor(int rowOffset, bool extendSelection)
{
int rowNumber = SelectedRow + rowOffset - ScrollPosition;
if(extendSelection) {
ResizeSelectionTo(rowNumber);
} else {
SetSelectedRow(rowNumber);
ScrollToRowNumber(rowNumber + ScrollPosition, rowOffset < 0 ? ScrollDisplayPosition.Top : ScrollDisplayPosition.Bottom);
}
}
public void ResizeSelectionTo(int rowNumber)
{
rowNumber += ScrollPosition;
if(SelectedRow == rowNumber) {
return;
}
bool anchorTop = SelectionAnchor == SelectionStart;
if(anchorTop) {
if(rowNumber < SelectionStart) {
SelectionEnd = SelectionStart;
SelectionStart = rowNumber;
} else {
SelectionEnd = rowNumber;
}
} else {
if(rowNumber < SelectionEnd) {
SelectionStart = rowNumber;
} else {
SelectionStart = SelectionEnd;
SelectionEnd = rowNumber;
}
}
ScrollDisplayPosition displayPos = SelectedRow < rowNumber ? ScrollDisplayPosition.Bottom : ScrollDisplayPosition.Top;
SelectedRow = rowNumber;
ScrollToRowNumber(rowNumber, displayPos);
InvalidateVisual();
}
public void Scroll(int offset)
{
ScrollPosition += offset;
}
public void ScrollToTop(bool extendSelection)
{
if(extendSelection) {
ResizeSelectionTo(-ScrollPosition);
} else {
ScrollPosition = 0;
SetSelectedRow(0);
}
}
public void ScrollToBottom(bool extendSelection)
{
if(extendSelection) {
ResizeSelectionTo(DebugApi.TraceLogBufferSize - 1 - ScrollPosition);
} else {
ScrollPosition = MaxScrollPosition;
SetSelectedRow(DebugApi.TraceLogBufferSize - 1);
}
}
public void SelectAll()
{
SelectionStart = 0;
SelectionEnd = DebugApi.TraceLogBufferSize - 1;
InvalidateVisual();
}
public void CopySelection()
{
StringBuilder sb = new();
int len = SelectionEnd - SelectionStart + 1;
CodeLineData[] lines = GetCodeLines(SelectionStart, len);
for(int i = 0; i < len; i++) {
string addrFormat = "X" + lines[i].CpuType.GetAddressSize();
sb.AppendLine(lines[i].GetAddressText(AddressDisplayType.CpuAddress, addrFormat).PadRight(6) + " " + lines[i].Text);
}
ApplicationHelper.GetMainWindow()?.Clipboard?.SetTextAsync(sb.ToString());
}
private bool IsRowVisible(int rowNumber)
{
return rowNumber > ScrollPosition && rowNumber < ScrollPosition + VisibleRowCount;
}
private void ScrollToRowNumber(int rowNumber, ScrollDisplayPosition position = ScrollDisplayPosition.Center)
{
if(IsRowVisible(rowNumber)) {
//Row is already visible, don't scroll
return;
}
switch(position) {
case ScrollDisplayPosition.Top: ScrollPosition = rowNumber; break;
case ScrollDisplayPosition.Center: ScrollPosition = rowNumber - (VisibleRowCount / 2) + 1; break;
case ScrollDisplayPosition.Bottom: ScrollPosition = rowNumber - VisibleRowCount + 1; break;
}
}
private CodeLineData[] GetCodeLines(int startIndex, int rowCount)
{
TraceRow[] rows = DebugApi.GetExecutionTrace((uint)(DebugApi.TraceLogBufferSize - startIndex - rowCount), (uint)rowCount);
List<CodeLineData> lines = new(rowCount);
CodeLineData emptyLine = new CodeLineData(CpuType.Snes);
int emptyLineCount = rowCount - rows.Length;
for(int i = 0; i < emptyLineCount; i++) {
lines.Add(emptyLine);
}
for(int i = rows.Length - 1; i >= 0; i--) {
lines.Add(new CodeLineData(rows[i].Type) {
Address = (int)rows[i].ProgramCounter,
AbsoluteAddress = new() { Address = -1 },
Text = rows[i].GetOutput(),
OpSize = rows[i].ByteCodeSize,
ByteCode = rows[i].GetByteCode()
});
}
return lines.ToArray();
}
public AddressInfo? GetSelectedRowAddress()
{
TraceRow[] rows = DebugApi.GetExecutionTrace(DebugApi.TraceLogBufferSize - (uint)SelectedRow - 1, 1);
if(rows.Length > 0) {
return new AddressInfo() {
Address = (int)rows[0].ProgramCounter,
Type = rows[0].Type.ToMemoryType()
};
}
return null;
}
}
public class TraceLoggerOptionTab : DisposableViewModel
{
public string TabName { get; set; } = "";
public Control HelpTooltip => ExpressionTooltipHelper.GetHelpTooltip(CpuType, false);
public Control FormatTooltip => GetFormatTooltip();
public CpuType CpuType { get; set; } = CpuType.Snes;
public string LogOptionsTitle { get; }
public bool ShowEnableButton { get; }
public bool ShowStatusFormat { get; }
public bool ShowIndentCode { get; }
public TraceLoggerCpuConfig Options { get; }
[Reactive] public string Format { get; set; } = "";
[Reactive] public bool IsConditionValid { get; set; } = true;
private TraceLoggerViewModel _traceLogger;
public TraceLoggerOptionTab(TraceLoggerViewModel traceLogger, CpuType cpuType, TraceLoggerCpuConfig options, bool showEnableButton)
{
_traceLogger = traceLogger;
CpuType = cpuType;
Options = options;
ShowEnableButton = showEnableButton;
ShowStatusFormat = cpuType != CpuType.Cx4 && cpuType != CpuType.NecDsp;
ShowIndentCode = cpuType != CpuType.Gsu;
AddDisposable(ReactiveHelper.RegisterRecursiveObserver(options, OnOptionsChanged));
UpdateFormat();
if(string.IsNullOrWhiteSpace(Options.Format)) {
//Set custom format to the default if it's empty
Options.Format = GetAutoFormat(Options, CpuType);
}
TabName = ResourceHelper.GetEnumText(cpuType);
LogOptionsTitle = string.Format(ResourceHelper.GetViewLabel(nameof(TraceLoggerWindow), "grpLogOptions"), ResourceHelper.GetEnumText(cpuType));
}
private void OnOptionsChanged(object? sender, PropertyChangedEventArgs e)
{
UpdateFormat();
if(e.PropertyName == nameof(TraceLoggerCpuConfig.UseCustomFormat) && Options.UseCustomFormat && string.IsNullOrWhiteSpace(Options.Format)) {
//Set custom format to the default if it's empty
Options.Format = Format;
}
if(Options.Enabled) {
//Auto-select current tab when enabled
_traceLogger.SelectedTab = this;
}
if(!string.IsNullOrWhiteSpace(Options.Condition)) {
DebugApi.EvaluateExpression(Options.Condition, CpuType, out EvalResultType result, false);
IsConditionValid = result == EvalResultType.Numeric || result == EvalResultType.Boolean;
} else {
IsConditionValid = true;
}
_traceLogger.UpdateOptions();
}
private void UpdateFormat()
{
if(!Options.UseCustomFormat) {
Format = GetAutoFormat(Options, CpuType);
}
}
public static string GetAutoFormat(TraceLoggerCpuConfig cfg, CpuType cpuType)
{
string format = "";
int alignValue = cpuType switch {
CpuType.Gba => 42,
CpuType.Ws => 36,
_ => 24
};
void addTag(bool condition, string formatText, int align = 0)
{
if(condition) {
format += formatText;
alignValue += align;
}
}
format += "[Disassembly]";
addTag(cfg.ShowEffectiveAddresses, "[EffectiveAddress]", 8);
addTag(cfg.ShowMemoryValues, " [MemoryValue,h]", 6);
format += "[Align," + alignValue.ToString() + "] ";
switch(cpuType) {
case CpuType.Snes:
case CpuType.Sa1:
addTag(cfg.ShowRegisters, "A:[A,4h] X:[X,4h] Y:[Y,4h] S:[SP,4h] D:[D,4h] DB:[DB,2h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "P:[P,h] ",
StatusFlagFormat.CompactText => "P:[P] ",
StatusFlagFormat.Text or _ => "P:[P,8] "
});
break;
case CpuType.Spc:
case CpuType.Nes:
case CpuType.Pce:
addTag(cfg.ShowRegisters, "A:[A,2h] X:[X,2h] Y:[Y,2h] S:[SP,2h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "P:[P,h] ",
StatusFlagFormat.CompactText => "P:[P] ",
StatusFlagFormat.Text or _ => "P:[P,8] "
});
break;
case CpuType.Gsu:
addTag(cfg.ShowRegisters, "SRC:[SRC,2h] DST:[DST,2h] R0:[R0,4h] R1:[R1,4h] R2:[R2,4h] R3:[R3,4h] R4:[R4,4h] R5:[R5,4h] R6:[R6,4h] R7:[R7,4h] R8:[R8,4h] R9:[R9,4h] R10:[R10,4h] R11:[R11,4h] R12:[R12,4h] R13:[R13,4h] R14:[R14,4h] R15:[R15,4h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "SFR:[SFR,h] ",
StatusFlagFormat.CompactText => "SFR:[SFR] ",
StatusFlagFormat.Text or _ => "SFR:[SFR,16] "
});
break;
case CpuType.Cx4:
addTag(cfg.ShowRegisters, "A:[A,6h] MAR:[MAR,6h] MDR:[MDR,6h] DPR:[DPR,6h] ML:[ML,6h] MH:[MH,6h] P:[P,4h] PB:[PB,4h] R0:[R0,6h] R1:[R1,6h] R2:[R2,6h] R3:[R3,6h] R4:[R4,6h] R5:[R5,6h] R6:[R6,6h] R7:[R7,6h] R8:[R8,6h] R9:[R9,6h] R10:[R10,6h] R11:[R11,6h] R12:[R12,6h] R13:[R13,6h] R14:[R14,6h] R15:[R15,6h] ");
addTag(cfg.ShowStatusFlags, "PS:[PS] ");
break;
case CpuType.NecDsp:
addTag(cfg.ShowRegisters, "A:[A,4h] ");
addTag(cfg.ShowStatusFlags, "[FlagsA] ");
addTag(cfg.ShowRegisters, "B:[B,4h] ");
addTag(cfg.ShowStatusFlags, "[FlagsB] ");
addTag(cfg.ShowRegisters, "K:[K,4h] L:[L,4h] M:[M,4h] N:[N,4h] RP:[RP,4h] DP:[DP,4h] DR:[DR,4h] SR:[SR,4h] TR:[TR,4h] TRB:[TRB,4h] ");
break;
case CpuType.Gameboy:
addTag(cfg.ShowRegisters, "A:[A,2h] B:[B,2h] C:[C,2h] D:[D,2h] E:[E,2h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "F:[PS,h] ",
StatusFlagFormat.CompactText => "F:[PS] ",
StatusFlagFormat.Text or _ => "F:[PS,4] "
});
addTag(cfg.ShowRegisters, "HL:[H,2h][L,2h] S:[SP,4h] ");
break;
case CpuType.Sms:
addTag(cfg.ShowRegisters, "A:[A,2h] B:[B,2h] C:[C,2h] D:[D,2h] E:[E,2h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "F:[PS,h] ",
StatusFlagFormat.CompactText => "F:[PS] ",
StatusFlagFormat.Text or _ => "F:[PS,8] "
});
addTag(cfg.ShowRegisters, "HL:[H,2h][L,2h] IX:[IX,4h] IY:[IY,4h] S:[SP,4h] ");
break;
case CpuType.St018:
case CpuType.Gba:
addTag(cfg.ShowRegisters, "R0:[R0,8h] R1:[R1,8h] R2:[R2,8h] R3:[R3,8h] R4:[R4,8h] R5:[R5,8h] R6:[R6,8h] R7:[R7,8h] R8:[R8,8h] R9:[R9,8h] R10:[R10,8h] R11:[R11,8h] R12:[R12,8h] R13:[R13,8h] R14:[R14,8h] R15:[R15,8h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "CPSR:[CPSR,8h] ",
StatusFlagFormat.CompactText => "CPSR:[CPSR] ",
StatusFlagFormat.Text or _ => "CPSR:[CPSR,7] "
});
addTag(cfg.ShowStatusFlags, "Mode: [Mode,3] ");
break;
case CpuType.Ws:
addTag(cfg.ShowRegisters, "AX:[AX,4h] BX:[BX,4h] CX:[CX,4h] DX:[DX,4h] DS:[DS,4h] ES:[ES,4h] SS:[SS,4h] SP:[SP,4h] BP:[BP,4h] SI:[SI,4h] DI:[DI,4h] ");
addTag(cfg.ShowStatusFlags, cfg.StatusFormat switch {
StatusFlagFormat.Hexadecimal => "F:[F,4h] ",
StatusFlagFormat.CompactText => "F:[F] ",
StatusFlagFormat.Text or _ => "F:[F,10] "
});
break;
}
addTag(cfg.ShowFramePosition, "V:[Scanline,3] H:[Cycle,3] ");
addTag(cfg.ShowFrameCounter, "Fr:[FrameCount] ");
addTag(cfg.ShowClockCounter, "Cycle:[CycleCount] ");
addTag(cfg.ShowByteCode, "BC:[ByteCode]");
return format.Trim();
}
private Control GetFormatTooltip()
{
StackPanel panel = new();
void addRow(string text) { panel.Children.Add(new TextBlock() { Text = text }); }
void addBoldRow(string text) { panel.Children.Add(new TextBlock() { Text = text, FontWeight = Avalonia.Media.FontWeight.Bold }); }
addBoldRow("Notes");
addRow("You can customize the output by enabling the 'Use custom format' option and manually editing the format.");
addRow(" ");
addRow("Tags can have their display format configured by using a comma and specifying the format options. e.g:");
addRow(" [Scanline,3] - display scanline in decimal, pad to always be 3 characters wide");
addRow(" [Scanline,h] - display scanline in hexadecimal");
addRow(" [Scanline,3h] - display scanline in decimal, pad to always be 3 characters wide");
addRow(" ");
addBoldRow("Common tags (all CPUs)");
addRow(" [ByteCode] - byte code for the instruction (1 to 3 bytes)");
addRow(" [Disassembly] - disassembly for the current instruction");
addRow(" [EffectiveAddress] - effective address used for indirect addressing modes");
addRow(" [MemoryValue] - value stored at the memory location referred to by the instruction");
addRow(" [PC] - program counter");
addRow(" [Cycle] - current horizontal cycle (H)");
addRow(" [HClock] - current horizontal cycle (H, in master clocks)");
addRow(" [Scanline] - current scanline (V)");
addRow(" [FrameCount] - current frame number");
addRow(" [CycleCount] - current CPU cycle (64-bit unsigned value)");
addRow(" [Align,X] - add spaces to ensure the line is X characters long");
addRow(" ");
addBoldRow("CPU-specific tags (" + ResourceHelper.GetEnumText(CpuType) + ")");
string[] tokens = CpuType switch {
CpuType.Snes or CpuType.Sa1 => new string[] { "A", "X", "Y", "D", "DB", "P", "SP" },
CpuType.Spc => new string[] { "A", "X", "Y", "P", "SP" },
CpuType.NecDsp => new string[] { "A", "B", "FlagsA", "FlagsB", "K", "L", "M", "N", "RP", "DP", "DR", "SR", "TR", "TRB" },
CpuType.Gsu => new string[] { "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", "SRC", "DST", "SFR" },
CpuType.Cx4 => new string[] { "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", "MAR", "MDR", "DPR", "ML", "MH", "PB", "P", "PS", "A" },
CpuType.Gameboy => new string[] { "A", "B", "C", "D", "E", "F", "H", "L", "PS", "SP" },
CpuType.Nes => new string[] { "A", "X", "Y", "P", "SP" },
CpuType.Pce => new string[] { "A", "X", "Y", "P", "SP" },
CpuType.Sms => new string[] { "A", "B", "C", "D", "E", "F", "H", "L", "IX", "IY", "A'", "B'", "C'", "D'", "E'", "F'", "H'", "L'", "I", "R", "PS", "SP" },
CpuType.Gba or CpuType.St018 => new string[] { "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", "CPSR" },
CpuType.Ws => new string[] { "AX", "BX", "CX", "DX", "CS", "IP", "SS", "SP", "BP", "DS", "ES", "SI", "DI", "F" },
_ => throw new Exception("unsupported cpu type")
};
Array.Sort(tokens);
Grid tokenGrid = new Grid() {
ColumnDefinitions = new("40, 40, 40, 40"),
RowDefinitions = new(string.Join(",", Enumerable.Repeat("Auto", (tokens.Length / 4) + 1))),
Margin = new Thickness(5, 0, 0, 0)
};
int col = 0;
int row = 0;
foreach(string token in tokens) {
TextBlock txt = new() { Text = $"[{token}]", Padding = new Thickness(0, 0, 5, 0) };
tokenGrid.Children.Add(txt);
Grid.SetColumn(txt, col);
Grid.SetRow(txt, row);
col++;
if(col == 4) {
col = 0;
row++;
}
}
panel.Children.Add(tokenGrid);
return panel;
}
}
public class TraceLoggerStyleProvider : ILineStyleProvider
{
private ConsoleType _consoleType = ConsoleType.Snes;
private TraceLoggerViewModel _model;
public TraceLoggerStyleProvider(TraceLoggerViewModel model)
{
_model = model;
}
public int AddressSize { get; private set; } = 6;
public int ByteCodeSize { get; private set; } = 4;
private LineProperties GetMainCpuStyle() { return new LineProperties() { AddressColor = null, LineBgColor = null }; }
private LineProperties GetSecondaryCpuStyle() { return new LineProperties() { AddressColor = Color.FromRgb(30, 145, 30), LineBgColor = Color.FromRgb(230, 245, 230) }; }
private LineProperties GetCoprocessorStyle() { return new LineProperties() { AddressColor = Color.FromRgb(30, 30, 145), LineBgColor = Color.FromRgb(230, 230, 245) }; }
public List<CodeColor> GetCodeColors(CodeLineData lineData, bool highlightCode, string addressFormat, Color? textColor, bool showMemoryValues)
{
return CodeHighlighting.GetCpuHighlights(lineData, highlightCode, addressFormat, textColor, showMemoryValues);
}
public LineProperties GetLineStyle(CodeLineData lineData, int lineIndex)
{
LineProperties props = lineData.CpuType switch {
CpuType.Spc => GetSecondaryCpuStyle(),
CpuType.NecDsp or CpuType.Sa1 or CpuType.Gsu or CpuType.Cx4 => GetCoprocessorStyle(),
CpuType.Gameboy => _consoleType == ConsoleType.Snes ? GetCoprocessorStyle() : GetMainCpuStyle(),
_ => GetMainCpuStyle(),
};
if(_model != null && lineData.HasAddress) {
int lineNumber = _model.ScrollPosition + lineIndex;
props.IsSelectedRow = lineNumber >= _model.SelectionStart && lineNumber <= _model.SelectionEnd;
props.IsActiveRow = _model.SelectedRow == lineNumber;
}
return props;
}
internal void SetConsoleType(ConsoleType consoleType)
{
_consoleType = consoleType;
AddressSize = consoleType.GetMainCpuType().GetAddressSize();
ByteCodeSize = consoleType.GetMainCpuType().GetByteCodeSize();
}
}
}