From 2be31b675ce3088045986c6dea5e0d6d4c3fd026 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 11 Jul 2018 23:51:03 +0200 Subject: [PATCH] Started on breakpoint conditions, expression parser. --- Emux.GameBoy/Cpu/Breakpoint.cs | 36 ++++++ Emux.GameBoy/Cpu/GameBoyCpu.cs | 47 ++++++-- Emux.GameBoy/Emux.GameBoy.csproj | 1 + Emux/App.xaml.cs | 2 + Emux/Emux.csproj | 11 ++ Emux/Expressions/ExpressionLexer.cs | 163 +++++++++++++++++++++++++ Emux/Expressions/ExpressionParser.cs | 172 +++++++++++++++++++++++++++ Emux/Expressions/Terminal.cs | 30 +++++ Emux/Expressions/Token.cs | 26 ++++ Emux/Gui/BreakpointDialog.xaml | 25 ++++ Emux/Gui/BreakpointDialog.xaml.cs | 25 ++++ Emux/Gui/InstructionItem.cs | 16 ++- Emux/Gui/MainWindow.xaml.cs | 4 +- 13 files changed, 540 insertions(+), 18 deletions(-) create mode 100644 Emux.GameBoy/Cpu/Breakpoint.cs create mode 100644 Emux/Expressions/ExpressionLexer.cs create mode 100644 Emux/Expressions/ExpressionParser.cs create mode 100644 Emux/Expressions/Terminal.cs create mode 100644 Emux/Expressions/Token.cs create mode 100644 Emux/Gui/BreakpointDialog.xaml create mode 100644 Emux/Gui/BreakpointDialog.xaml.cs diff --git a/Emux.GameBoy/Cpu/Breakpoint.cs b/Emux.GameBoy/Cpu/Breakpoint.cs new file mode 100644 index 0000000..4175ddc --- /dev/null +++ b/Emux.GameBoy/Cpu/Breakpoint.cs @@ -0,0 +1,36 @@ +using System; + +namespace Emux.GameBoy.Cpu +{ + public class Breakpoint + { + public static readonly Predicate BreakAlways = _ => true; + + public Breakpoint(ushort offset) + : this(offset, BreakAlways) + { + } + + public Breakpoint(ushort offset, Predicate condition) + { + Offset = offset; + Condition = condition; + } + + public ushort Offset + { + get; + } + + public Predicate Condition + { + get; + set; + } + + public override string ToString() + { + return Offset.ToString("X4"); + } + } +} \ No newline at end of file diff --git a/Emux.GameBoy/Cpu/GameBoyCpu.cs b/Emux.GameBoy/Cpu/GameBoyCpu.cs index a6e0f23..0a7c140 100644 --- a/Emux.GameBoy/Cpu/GameBoyCpu.cs +++ b/Emux.GameBoy/Cpu/GameBoyCpu.cs @@ -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 _breakpoints = new Dictionary(); public GameBoyCpu(GameBoy device) { @@ -56,7 +60,6 @@ namespace Emux.GameBoy.Cpu Registers = new RegisterBank(); Alu = new GameBoyAlu(Registers); - Breakpoints = new HashSet(); EnableFrameLimit = true; new Thread(CpuLoop) @@ -107,14 +110,6 @@ namespace Emux.GameBoy.Cpu private set; } - /// - /// Gets a collection of memory addresses to break the execution on. - /// - public ISet Breakpoints - { - get; - } - /// /// 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 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() { diff --git a/Emux.GameBoy/Emux.GameBoy.csproj b/Emux.GameBoy/Emux.GameBoy.csproj index 5780f3c..b8f5d58 100644 --- a/Emux.GameBoy/Emux.GameBoy.csproj +++ b/Emux.GameBoy/Emux.GameBoy.csproj @@ -64,6 +64,7 @@ + diff --git a/Emux/App.xaml.cs b/Emux/App.xaml.cs index bfadf5f..ba8ac99 100644 --- a/Emux/App.xaml.cs +++ b/Emux/App.xaml.cs @@ -1,4 +1,6 @@ using System.Windows; +using Emux.Expressions; +using Emux.GameBoy.Cpu; using Emux.Properties; namespace Emux diff --git a/Emux/Emux.csproj b/Emux/Emux.csproj index 10ef90d..7a98071 100644 --- a/Emux/Emux.csproj +++ b/Emux/Emux.csproj @@ -64,6 +64,10 @@ + + + + AboutDialog.xaml @@ -71,6 +75,9 @@ AudioMixerWindow.xaml + + BreakpointDialog.xaml + CheatsWindow.xaml @@ -108,6 +115,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/Emux/Expressions/ExpressionLexer.cs b/Emux/Expressions/ExpressionLexer.cs new file mode 100644 index 0000000..ae85f8a --- /dev/null +++ b/Emux/Expressions/ExpressionLexer.cs @@ -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 Registers = new HashSet + { + "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 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 + "'."); + } + } +} \ No newline at end of file diff --git a/Emux/Expressions/ExpressionParser.cs b/Emux/Expressions/ExpressionParser.cs new file mode 100644 index 0000000..9754040 --- /dev/null +++ b/Emux/Expressions/ExpressionParser.cs @@ -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 CompileExpression(string code) + { + var lexer = new ExpressionLexer(new StringReader(code)); + var postFix = ToPostfix(lexer).ToArray(); + + var stack = new Stack(); + 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>(final, CpuParameter); + return lambda.Compile(); + } + + private static IEnumerable ToPostfix(ExpressionLexer lexer) + { + // Shunting yard algorithm to transform the infix expression into postfix for easier interpretation. + + var operatorStack = new Stack(); + 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(); + } + + + } +} \ No newline at end of file diff --git a/Emux/Expressions/Terminal.cs b/Emux/Expressions/Terminal.cs new file mode 100644 index 0000000..c74c5ee --- /dev/null +++ b/Emux/Expressions/Terminal.cs @@ -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 + } +} \ No newline at end of file diff --git a/Emux/Expressions/Token.cs b/Emux/Expressions/Token.cs new file mode 100644 index 0000000..0f25ec1 --- /dev/null +++ b/Emux/Expressions/Token.cs @@ -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})"; + } + } +} \ No newline at end of file diff --git a/Emux/Gui/BreakpointDialog.xaml b/Emux/Gui/BreakpointDialog.xaml new file mode 100644 index 0000000..d9c0eaf --- /dev/null +++ b/Emux/Gui/BreakpointDialog.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + +