Started on breakpoint conditions, expression parser.

This commit is contained in:
Washi 2018-07-11 23:51:03 +02:00
parent e9bddd1353
commit 2be31b675c
13 changed files with 540 additions and 18 deletions

View File

@ -0,0 +1,36 @@
using System;
namespace Emux.GameBoy.Cpu
{
public class Breakpoint
{
public static readonly Predicate<GameBoyCpu> BreakAlways = _ => true;
public Breakpoint(ushort offset)
: this(offset, BreakAlways)
{
}
public Breakpoint(ushort offset, Predicate<GameBoyCpu> condition)
{
Offset = offset;
Condition = condition;
}
public ushort Offset
{
get;
}
public Predicate<GameBoyCpu> Condition
{
get;
set;
}
public override string ToString()
{
return Offset.ToString("X4");
}
}
}

View File

@ -38,14 +38,18 @@ namespace Emux.GameBoy.Cpu
private readonly GameBoy _device;
private readonly ManualResetEvent _continueSignal = new ManualResetEvent(false);
private readonly ManualResetEvent _terminateSignal = new ManualResetEvent(false);
private ulong _ticks;
private bool _break = true;
private bool _halt = false;
private readonly NativeTimer _frameTimer;
private readonly ManualResetEvent _frameStartSignal = new ManualResetEvent(false);
private readonly ManualResetEvent _breakSignal = new ManualResetEvent(false);
private TimeSpan _frameStartTime;
private ulong _frameStartTickCount;
private readonly IDictionary<ushort, Breakpoint> _breakpoints = new Dictionary<ushort, Breakpoint>();
public GameBoyCpu(GameBoy device)
{
@ -56,7 +60,6 @@ namespace Emux.GameBoy.Cpu
Registers = new RegisterBank();
Alu = new GameBoyAlu(Registers);
Breakpoints = new HashSet<ushort>();
EnableFrameLimit = true;
new Thread(CpuLoop)
@ -107,14 +110,6 @@ namespace Emux.GameBoy.Cpu
private set;
}
/// <summary>
/// Gets a collection of memory addresses to break the execution on.
/// </summary>
public ISet<ushort> Breakpoints
{
get;
}
/// <summary>
/// Gets or sets a value indicating whether the processor should limit the execution speed to the original GameBoy clock speed.
/// Disable this if experiencing heavy performance losses.
@ -197,7 +192,7 @@ namespace Emux.GameBoy.Cpu
}
}
if (Breakpoints.Contains(Registers.PC))
if (_breakpoints.TryGetValue(Registers.PC, out var breakpoint) && breakpoint.Condition(this))
_break = true;
} while (!_break);
@ -283,6 +278,38 @@ namespace Emux.GameBoy.Cpu
_continueSignal.Reset();
_terminateSignal.Set();
}
public Breakpoint SetBreakpoint(ushort address)
{
if (!_breakpoints.TryGetValue(address, out var breakpoint))
{
breakpoint = new Breakpoint(address);
_breakpoints.Add(address, breakpoint);
}
return breakpoint;
}
public void RemoveBreakpoint(ushort address)
{
_breakpoints.Remove(address);
}
public IEnumerable<Breakpoint> GetBreakpoints()
{
return _breakpoints.Values;
}
public Breakpoint GetBreakpointAtAddress(ushort address)
{
_breakpoints.TryGetValue(address, out var breakpoint);
return breakpoint;
}
public void ClearBreakpoints()
{
_breakpoints.Clear();
}
protected virtual void OnResumed()
{

View File

@ -64,6 +64,7 @@
<Compile Include="Cartridge\StreamedExternalMemory.cs" />
<Compile Include="Cheating\GamesharkCode.cs" />
<Compile Include="Cheating\GamesharkController.cs" />
<Compile Include="Cpu\Breakpoint.cs" />
<Compile Include="Cpu\GameBoyAlu.cs" />
<Compile Include="Cpu\InterruptFlags.cs" />
<Compile Include="Cpu\RegisterFlags.cs" />

View File

@ -1,4 +1,6 @@
using System.Windows;
using Emux.Expressions;
using Emux.GameBoy.Cpu;
using Emux.Properties;
namespace Emux

View File

@ -64,6 +64,10 @@
</ApplicationDefinition>
<Compile Include="DeviceEventArgs.cs" />
<Compile Include="DeviceManager.cs" />
<Compile Include="Expressions\ExpressionLexer.cs" />
<Compile Include="Expressions\ExpressionParser.cs" />
<Compile Include="Expressions\Terminal.cs" />
<Compile Include="Expressions\Token.cs" />
<Compile Include="Gui\AboutDialog.xaml.cs">
<DependentUpon>AboutDialog.xaml</DependentUpon>
</Compile>
@ -71,6 +75,9 @@
<Compile Include="Gui\AudioMixerWindow.xaml.cs">
<DependentUpon>AudioMixerWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Gui\BreakpointDialog.xaml.cs">
<DependentUpon>BreakpointDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Gui\CheatsWindow.xaml.cs">
<DependentUpon>CheatsWindow.xaml</DependentUpon>
</Compile>
@ -108,6 +115,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Gui\BreakpointDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Gui\CheatsWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace Emux.Expressions
{
public class ExpressionLexer
{
private static readonly ISet<string> Registers = new HashSet<string>
{
"PC", "SP", "AF", "BC", "DE", "HL", "A", "B", "C", "D", "E", "F", "H", "L",
"pc", "sp", "af", "bc", "de", "hl", "a", "b", "c", "d", "e", "f", "h", "l"
};
private const string HexCharacters = "0123456789ABCDEF";
private readonly TextReader _reader;
private Token _bufferedToken;
public ExpressionLexer(TextReader reader)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
}
public bool HasNext()
{
SkipWhitespaces();
return _reader.Peek() != -1;
}
public Token Peek()
{
if (_bufferedToken == null && HasNext())
ReadNextToken();
return _bufferedToken;
}
public Token Next()
{
if (_bufferedToken == null)
ReadNextToken();
var token = _bufferedToken;
_bufferedToken = null;
return token;
}
private void SkipWhitespaces()
{
while (true)
{
int c = _reader.Peek();
if (c == -1 || !char.IsWhiteSpace((char) c))
break;
_reader.Read();
}
}
private void ReadNextToken()
{
if (!HasNext())
throw new EndOfStreamException();
char c = (char) _reader.Peek();
_bufferedToken = char.IsLetterOrDigit(c) ? ReadNextWord() : ReadNextSymbol();
}
private Token ReadNextWord()
{
string word = ReadWhile(char.IsLetterOrDigit);
Terminal terminal;
if (Registers.Contains(word))
terminal = Terminal.Register;
else if (Regex.IsMatch(word, @"(0x[\da-zA-Z]+)|([\da-zA-Z]+h)"))
terminal = Terminal.Hexadecimal;
else if (Regex.IsMatch(word, @"\d+"))
terminal = Terminal.Decimal;
else
throw new SyntaxErrorException("Unrecognized word '" + word + "'.");
return new Token(terminal, word);
}
private string ReadWhile(Predicate<char> condition)
{
var builder = new StringBuilder();
while (true)
{
int p = _reader.Peek();
if (p == -1)
break;
char c = (char) p;
if (!condition(c))
break;
_reader.Read();
builder.Append(c);
}
return builder.ToString();
}
private Token ReadNextSymbol()
{
char c = (char) _reader.Read();
switch (c)
{
case '(':
return new Token(Terminal.LPar, "(");
case ')':
return new Token(Terminal.RPar, ")");
case '!':
return new Token(Terminal.Not, "!");
case '+':
return new Token(Terminal.Plus, "+");
case '-':
return new Token(Terminal.Minus, "-");
case '=':
return new Token(Terminal.Equals, "=");
case '>':
if (_reader.Peek() != '=')
return new Token(Terminal.GreaterThan, "=");
_reader.Read();
return new Token(Terminal.GreaterThanOrEqual, ">=");
case '<':
if (_reader.Peek() != '=')
return new Token(Terminal.Equals, "=");
_reader.Read();
return new Token(Terminal.LessThanOrEqual, ">=");
case '&':
if (_reader.Peek() != '&')
return new Token(Terminal.BitwiseAnd, "&");
_reader.Read();
return new Token(Terminal.BooleanAnd, "&&");
case '|':
if (_reader.Peek() != '|')
return new Token(Terminal.BitwiseOr, "|");
_reader.Read();
return new Token(Terminal.BooleanOr, "||");
}
throw new SyntaxErrorException("Unrecognized character '" + c + "'.");
}
}
}

View File

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using Emux.GameBoy.Cpu;
namespace Emux.Expressions
{
public class ExpressionParser
{
private static readonly Terminal[] OperatorPrecedence =
{
Terminal.GreaterThan,
Terminal.GreaterThanOrEqual,
Terminal.LessThan,
Terminal.LessThanOrEqual,
Terminal.Equals,
Terminal.NotEquals,
Terminal.BitwiseAnd,
Terminal.BitwiseOr,
Terminal.LPar,
Terminal.RPar
};
private static readonly ParameterExpression CpuParameter = Expression.Parameter(typeof(GameBoyCpu));
public static Predicate<GameBoyCpu> CompileExpression(string code)
{
var lexer = new ExpressionLexer(new StringReader(code));
var postFix = ToPostfix(lexer).ToArray();
var stack = new Stack<Expression>();
foreach (var token in postFix)
{
switch (token.Terminal)
{
case Terminal.Register:
stack.Push(Expression.PropertyOrField(
Expression.Property(CpuParameter, "Registers"),
token.Text.ToUpperInvariant()));
break;
case Terminal.Hexadecimal:
var matches = Regex.Matches(token.Text, @"[\da-fA-F]+");
stack.Push(Expression.Constant(ushort.Parse(matches[matches.Count - 1].Value,
NumberStyles.HexNumber)));
break;
case Terminal.Decimal:
stack.Push(Expression.Constant(ushort.Parse(token.Text)));
break;
case Terminal.Equals:
stack.Push(Expression.Equal(stack.Pop(), stack.Pop()));
break;
case Terminal.NotEquals:
stack.Push(Expression.NotEqual(stack.Pop(), stack.Pop()));
break;
case Terminal.GreaterThan:
// Mirrored operator to accomodate for stack order.
stack.Push(Expression.LessThan(stack.Pop(), stack.Pop()));
break;
case Terminal.GreaterThanOrEqual:
// Mirrored operator to accomodate for stack order.
stack.Push(Expression.LessThanOrEqual(stack.Pop(), stack.Pop()));
break;
case Terminal.LessThan:
// Mirrored operator to accomodate for stack order.
stack.Push(Expression.GreaterThan(stack.Pop(), stack.Pop()));
break;
case Terminal.LessThanOrEqual:
// Mirrored operator to accomodate for stack order.
stack.Push(Expression.GreaterThanOrEqual(stack.Pop(), stack.Pop()));
break;
case Terminal.BitwiseAnd:
stack.Push(Expression.And(stack.Pop(), stack.Pop()));
break;
case Terminal.BitwiseOr:
stack.Push(Expression.Or(stack.Pop(), stack.Pop()));
break;
case Terminal.BooleanAnd:
var v2 = stack.Pop();
var v1 = stack.Pop();
stack.Push(Expression.AndAlso(v1, v2));
break;
case Terminal.BooleanOr:
v2 = stack.Pop();
v1 = stack.Pop();
stack.Push(Expression.OrElse(v1, v2));
break;
}
}
if (stack.Count >= 2)
throw new SyntaxErrorException("Expression contains unused terms.");
var final = stack.Pop();
var lambda = Expression.Lambda<Predicate<GameBoyCpu>>(final, CpuParameter);
return lambda.Compile();
}
private static IEnumerable<Token> ToPostfix(ExpressionLexer lexer)
{
// Shunting yard algorithm to transform the infix expression into postfix for easier interpretation.
var operatorStack = new Stack<Token>();
while (lexer.HasNext())
{
var current = lexer.Next();
switch (current.Terminal)
{
case Terminal.Decimal:
case Terminal.Hexadecimal:
case Terminal.Flag:
case Terminal.Register:
yield return current;
break;
case Terminal.LPar:
operatorStack.Push(current);
break;
case Terminal.RPar:
while (operatorStack.Peek().Terminal != Terminal.LPar)
yield return operatorStack.Pop();
operatorStack.Pop(); // Pop LPar
break;
default:
while (operatorStack.Count > 0)
{
var lastOperator = operatorStack.Peek();
int lastPrecedence = Array.IndexOf(OperatorPrecedence, lastOperator.Terminal);
int currentPrecedence = Array.IndexOf(OperatorPrecedence, current.Terminal);
if (lastPrecedence <= currentPrecedence)
{
yield return operatorStack.Pop();
}
else
{
break;
}
}
operatorStack.Push(current);
break;
}
}
while (operatorStack.Count > 0)
yield return operatorStack.Pop();
}
}
}

View File

@ -0,0 +1,30 @@
namespace Emux.Expressions
{
public enum Terminal
{
Not,
Equals,
NotEquals,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
Plus,
Minus,
BooleanAnd,
BitwiseAnd,
BooleanOr,
BitwiseOr,
Register,
Flag,
Decimal,
Hexadecimal,
RPar,
LPar
}
}

26
Emux/Expressions/Token.cs Normal file
View File

@ -0,0 +1,26 @@
namespace Emux.Expressions
{
public class Token
{
public Token(Terminal terminal, string text)
{
Terminal = terminal;
Text = text;
}
public Terminal Terminal
{
get;
}
public string Text
{
get;
}
public override string ToString()
{
return $"{Text} ({Terminal})";
}
}
}

View File

@ -0,0 +1,25 @@
<Window x:Class="Emux.Gui.BreakpointDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Emux"
mc:Ignorable="d"
Title="Breakpoint" Height="124.415" Width="336.585" WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="5" Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Margin = "5" Content="OK" Width="75" Click="OkButtonOnClick" IsDefault="True"/>
<Button Margin = "5" Content="Cancel" Width="75" Click ="CancelButtonOnClick" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,25 @@
using System.Windows;
namespace Emux.Gui
{
/// <summary>
/// Interaction logic for BreakpointDialog.xaml
/// </summary>
public partial class BreakpointDialog : Window
{
public BreakpointDialog()
{
InitializeComponent();
}
private void OkButtonOnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void CancelButtonOnClick(object sender, RoutedEventArgs e)
{
Close();
}
}
}

View File

@ -9,6 +9,8 @@ namespace Emux.Gui
{
public class InstructionItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly GameBoy.GameBoy _gameBoy;
private readonly Z80Instruction _instruction;
@ -24,17 +26,22 @@ namespace Emux.Gui
public bool IsBreakpoint
{
get { return _gameBoy.Cpu.Breakpoints.Contains(_instruction.Offset); }
get { return _gameBoy.Cpu.GetBreakpointAtAddress(_instruction.Offset) != null; }
set
{
if (value)
_gameBoy.Cpu.Breakpoints.Add(_instruction.Offset);
_gameBoy.Cpu.SetBreakpoint(_instruction.Offset);
else
_gameBoy.Cpu.Breakpoints.Remove(_instruction.Offset);
_gameBoy.Cpu.RemoveBreakpoint(_instruction.Offset);
OnPropertyChanged(nameof(IsBreakpoint));
}
}
public Breakpoint Breakpoint
{
get { return _gameBoy.Cpu.GetBreakpointAtAddress(_instruction.Offset); }
}
public bool IsCurrentInstruction
{
get { return _gameBoy.Cpu.Registers.PC == Offset; }
@ -75,9 +82,6 @@ namespace Emux.Gui
get;
set;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{

View File

@ -308,7 +308,7 @@ namespace Emux.Gui
}
else
{
_currentDevice.Cpu.Breakpoints.Add(address);
_currentDevice.Cpu.SetBreakpoint(address);
}
}
}
@ -322,7 +322,7 @@ namespace Emux.Gui
private void ClearBreakpointsCommandOnExecuted(object sender, ExecutedRoutedEventArgs e)
{
_currentDevice.Cpu.Breakpoints.Clear();
_currentDevice.Cpu.ClearBreakpoints();
}
private void KeyPadCommandOnExecuted(object sender, ExecutedRoutedEventArgs e)