Fix bugs in heuristic function and code sequences

This commit is contained in:
Lucas Scharenbroich 2016-12-04 23:14:51 -06:00
parent 5fc5a254ab
commit 5fe12243ad
18 changed files with 425 additions and 45 deletions

View File

@ -0,0 +1,37 @@
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SpriteCompiler.Problem;
using SpriteCompiler.AI;
using System.Diagnostics;
using System.Collections.Generic;
using FluentAssertions;
namespace SpriteCompiler.Test
{
[TestClass]
public class HeuristicTests
{
private SpriteGeneratorHeuristicFunction heuristic = new SpriteGeneratorHeuristicFunction();
[TestMethod]
public void TestSmallGap()
{
// Create a test with $XX -- -- $XX with the Accumulator loaded with $XX. Optimal code is
// ^
// STA 3,s
// PHA = 7 cycles
var state = new SpriteGeneratorState(new[] { new SpriteByte(0x11, 0), new SpriteByte(0x11, 3)})
{
A = Register.Constant(0x0011),
S = Register.INITIAL_OFFSET,
P = SpriteGeneratorState.LONG_I
};
var h = heuristic.Eval(state);
h.Should().BeLessOrEqualTo(7);
}
}
}

View File

@ -35,7 +35,17 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="FluentAssertions, Version=4.17.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
<HintPath>..\packages\FluentAssertions.4.17.0\lib\net45\FluentAssertions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FluentAssertions.Core, Version=4.17.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
<HintPath>..\packages\FluentAssertions.4.17.0\lib\net45\FluentAssertions.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<Choose>
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
@ -50,6 +60,7 @@
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="HeuristicTests.cs" />
<Compile Include="RegisterTests.cs" />
<Compile Include="Tests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@ -60,6 +71,9 @@
<Name>SpriteCompiler</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentAssertions" version="4.17.0" targetFramework="net452" />
</packages>

View File

@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public abstract class AbstractAISearch<A, S, T, C>
@ -46,6 +47,7 @@
while (!fringe.Empty)
{
var node = fringe.Remove();
Console.WriteLine(string.Format("Removed {0} from the queue with g = {1}, c(n, n') = {2}", node.State, node.PathCost, node.StepCost));
if (problem.IsGoal(node.State))
{

View File

@ -43,5 +43,7 @@
pathCost = HasParent ? parent.PathCost.Add(value) : value;
}
}
virtual public C EstCost { get { return PathCost; } }
}
}

View File

@ -9,8 +9,17 @@
public HeuristicSearchNode(T node, S state)
: base(node, state)
{
Heuristic = new C();
}
public C Heuristic { get; set; }
public override C EstCost
{
get
{
return PathCost.Add(Heuristic);
}
}
}
}

View File

@ -12,6 +12,7 @@
public interface ISearchNode<A, S, T, C> : ISearchNode<C> where C : IPathCost<C>
{
A Action { get; set; }
C PathCost { get; }
C StepCost { get; set; }
int Depth { get; }
S State { get; }
@ -20,6 +21,6 @@
public interface ISearchNode<C>
{
C PathCost { get; }
C EstCost { get; }
}
}

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
namespace SpriteCompiler.AI
{
using System.Linq;
public abstract class InformedNodeExpander<A, S, T, C> : INodeExpander<A, S, T, C>
where T : HeuristicSearchNode<A, S, T, C>
where C : IPathCost<C>, new()
@ -11,16 +13,24 @@ namespace SpriteCompiler.AI
public IEnumerable<T> Expand(ISearchProblem<A, S, C> problem, T node)
{
foreach (var successor in problem.Successors(node.State))
var successors = problem.Successors(node.State);
// Debug
Console.WriteLine(String.Format("There are {0} successors for {1}", successors.Count(), node));
Console.WriteLine(String.Format("This node has a current path cost of {0}", node.PathCost));
foreach (var successor in successors)
{
var action = successor.Item1;
var state = successor.Item2;
var next = CreateNode(node, state);
next.Action = action;
next.Action = action;
next.StepCost = problem.StepCost(node.State, action, state);
next.Heuristic = problem.Heuristic(state);
Console.WriteLine(" Action = " + next.Action + ", g(n') = " + next.PathCost + ", h(n') = " + next.Heuristic);
yield return next;
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
namespace SpriteCompiler.AI
{

View File

@ -20,7 +20,8 @@
{
foreach (var item in items)
{
queue.Enqueue(item, item.PathCost);
Console.WriteLine("Enqueuing " + item + " with cost " + item.EstCost);
queue.Enqueue(item, item.EstCost);
}
}
@ -31,7 +32,7 @@
public void Enqueue(T item)
{
queue.Enqueue(item, item.PathCost);
queue.Enqueue(item, item.EstCost);
}
public T Remove()

View File

@ -22,6 +22,9 @@ namespace SpriteCompiler.Problem
// Function to generate a new state based on the code's operation
public abstract SpriteGeneratorState Apply(SpriteGeneratorState state);
// Funtion to emit the source code
public abstract string Emit();
// Helper function for ToString implementations
protected string FormatLine(string label, string opcode, string operand, string comment)
{
@ -49,6 +52,11 @@ namespace SpriteCompiler.Problem
}
public override string ToString()
{
return (offset == 0) ? "TSC" : ("ADC #" + offset.ToString() + " / TSC");
}
public override string Emit()
{
if (offset == 0)
{
@ -74,6 +82,11 @@ namespace SpriteCompiler.Problem
}
public override string ToString()
{
return "SEP #$10";
}
public override string Emit()
{
return FormatLine("", "SEP", "#$10", "3 cycles");
}
@ -89,6 +102,11 @@ namespace SpriteCompiler.Problem
}
public override string ToString()
{
return "REP #$10";
}
public override string Emit()
{
return FormatLine("", "REP", "#$10", "3 cycles");
}
@ -111,6 +129,11 @@ namespace SpriteCompiler.Problem
}
public override string ToString()
{
return "LDA #$" + value.ToString("X2") + " / STA " + offset.ToString("X2") + ",s";
}
public override string Emit()
{
return String.Join("\n",
FormatLine("", "LDA", "#$" + value.ToString("X2"), "2 cycles"),
@ -119,6 +142,32 @@ namespace SpriteCompiler.Problem
}
}
public sealed class STACK_REL_16_BIT_STORE : CodeSequence
{
private readonly ushort value;
private readonly byte offset;
public STACK_REL_16_BIT_STORE(ushort value, byte offset) : base(5) { this.value = value; this.offset = offset; }
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ =>
{
_.RemoveWord((ushort)(offset + _.S.Value));
});
}
public override string ToString()
{
return "STA " + offset.ToString("X2") + ",s";
}
public override string Emit()
{
return FormatLine("", "STA", offset.ToString("X2") + ",s", "5 cycles");
}
}
public sealed class STACK_REL_16_BIT_IMMEDIATE_STORE : CodeSequence
{
private readonly ushort value;
@ -136,6 +185,11 @@ namespace SpriteCompiler.Problem
}
public override string ToString()
{
return "LDA #$" + value.ToString("X4") + " / STA " + offset.ToString("X2") + ",s";
}
public override string Emit()
{
return String.Join("\n",
FormatLine("", "LDA", "#$" + value.ToString("X4"), "3 cycles"),
@ -144,6 +198,36 @@ namespace SpriteCompiler.Problem
}
}
public sealed class LOAD_16_BIT_IMMEDIATE_AND_PUSH : CodeSequence
{
private readonly ushort value;
public LOAD_16_BIT_IMMEDIATE_AND_PUSH(ushort value) : base(7) { this.value = value; }
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ =>
{
_.A = _.A.LoadConstant(value);
_.RemoveWord((ushort)(_.S.Value - 1));
_.S = _.S.Add(-2);
});
}
public override string ToString()
{
return "LDA #$" + value.ToString("X4") + " / PHA";
}
public override string Emit()
{
return String.Join("\n",
FormatLine("", "LDA", "#$" + value.ToString("X4"), "3 cycles"),
FormatLine("", "PHA", "", "4 cycles")
);
}
}
public sealed class PEA : CodeSequence
{
private readonly ushort value;
@ -154,16 +238,43 @@ namespace SpriteCompiler.Problem
{
return state.Clone(_ =>
{
_.S.Add(-2);
_.RemoveWord((ushort)(_.S.Value - 1));
_.S = _.S.Add(-2);
});
}
public override string ToString()
{
return "PEA $" + value.ToString("X4");
}
public override string Emit()
{
return FormatLine("", "PEA", "$" + value.ToString("X4"), "5 cycles");
}
}
public sealed class PHA : CodeSequence
{
public PHA() : base(4) { }
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ =>
{
_.RemoveWord((ushort)(_.S.Value - 1));
_.S = _.S.Add(-2);
});
}
public override string ToString()
{
return "PHA";
}
public override string Emit()
{
return FormatLine("", "PHA", "", "4 cycles");
}
}
}

View File

@ -36,6 +36,11 @@
return new Register(Value + offset, Tag);
}
public static Register Constant(int value)
{
return new Register(value, DataType.LITERAL);
}
public Register LoadConstant(int value)
{
return new Register(value, DataType.LITERAL);
@ -50,7 +55,18 @@
public override string ToString()
{
return string.Format("{0} ({1})", Tag, Value.ToString("X4"));
switch (Tag)
{
default:
case DataType.UNINITIALIZED:
return " ----";
case DataType.SCREEN_OFFSET:
return "*" + Value.ToString("X4");
case DataType.LITERAL:
return " " + Value.ToString("X4");
}
}
public override bool Equals(object obj)

View File

@ -2,9 +2,28 @@
{
using SpriteCompiler.AI;
using System.Linq;
using System;
public sealed class SpriteGeneratorHeuristicFunction : IHeuristicFunction<SpriteGeneratorState, IntegerPathCost>
{
private static int SpanAndGapCost(int stack, int start, int end, int next)
{
var len = end - start + 1;
// If the span is within 255 bytes of the stack, there is no
// gap penalty and we base the cost off of sta xx,s instructions
var h1 = SpanAndGapCost(start, end, next);
var h2 = int.MaxValue;
if (stack <= end && (end - stack) < 256)
{
h2 = 5 * (len / 2) + 4 * (len % 2);
}
return Math.Min(h1, h2);
}
private static int SpanAndGapCost(int start, int end, int next)
{
// [start, end] is the span
@ -22,12 +41,10 @@
public IntegerPathCost Eval(SpriteGeneratorState state)
{
// An admissible heuistic calculates a cost based on the gaps and runs in a sprite
// An admissible heuistic that calculates a cost based on the gaps and runs in a sprite
//
// An even-length run can be done, at best in 4 cycles/word
// An odd-length run is even + 3 cycles/byte
//
// Each gap needs at least 5 cycles to cover (ADC # / TCS)
var count = state.Bytes.Count;
@ -35,6 +52,7 @@
var offsets = state.Bytes.Select(x => x.Offset).OrderBy(x => x).ToList();
var start = offsets[0];
var stack = state.S.Value;
var curr = start;
var cost = 0;
@ -49,7 +67,14 @@
}
// Calculate the estimate cost
cost += SpanAndGapCost(start, prev, curr);
if (state.S.IsScreenOffset)
{
cost += SpanAndGapCost(stack, start, prev, curr);
}
else
{
cost += SpanAndGapCost(start, prev, curr);
}
// Start a new sppan
start = curr;

View File

@ -42,6 +42,11 @@
P = other.P;
}
public override string ToString()
{
return String.Format("A = {0:X4}, X = {1}, Y = {2}, S = {3}, D = {4}, P = {5:X2}", A, X, Y, S, D, P);
}
public void RemoveWord(ushort offset)
{
var total = Bytes.RemoveAll(x => x.Offset == offset || x.Offset == (offset + 1));
@ -72,6 +77,10 @@
return other;
}
// A better state representation would be to have an array of offsets and a static
// data and mask array. Then the state is just the locations and registers, rather
// than a full copy of the data
public List<SpriteByte> Bytes { get; private set; }
public bool IsEmpty { get { return Bytes.Count == 0; } }
@ -87,6 +96,9 @@
public byte P { get; set; }
public const byte LONG_A = 0x10;
public const byte LONG_I = 0x20;
public override bool Equals(object obj)
{
return Equals(obj as SpriteGeneratorState);

View File

@ -5,6 +5,42 @@
using System.Collections.Generic;
using System.Linq;
public static class StateHelpers
{
public static byte? TryGetStackByte(this SpriteGeneratorState state, IDictionary<ushort, SpriteByte> data)
{
SpriteByte top;
if (state.S.IsScreenOffset && data.TryGetValue((ushort)state.S.Value, out top))
{
return top.Data;
}
return null;
}
public static ushort? TryGetStackWord(this SpriteGeneratorState state, IDictionary<ushort, SpriteByte> data)
{
return TryGetStackWord(state, data, 0);
}
public static ushort? TryGetStackWord(this SpriteGeneratorState state, IDictionary<ushort, SpriteByte> data, int offset)
{
SpriteByte high;
SpriteByte low;
if (state.S.IsScreenOffset && (state.S.Value + offset) > 0 && data.TryGetValue((ushort)(state.S.Value + offset), out high) && data.TryGetValue((ushort)(state.S.Value + offset - 1), out low))
{
return (ushort)(low.Data + (high.Data << 8));
}
return null;
}
public static Tuple<CodeSequence, SpriteGeneratorState> Apply(this SpriteGeneratorState state, CodeSequence code)
{
return Tuple.Create(code, code.Apply(state));
}
}
public sealed class SpriteGeneratorSuccessorFunction : ISuccessorFunction<CodeSequence, SpriteGeneratorState>
{
public IEnumerable<Tuple<CodeSequence, SpriteGeneratorState>> Successors(SpriteGeneratorState state)
@ -27,48 +63,80 @@
// a. If no registers are 8-bit, LDA #Imm/STA 0,s (8 cycles, sets Acc)
// b. If any reg is already 8-bit, LDA #imm/PHA (6 cycles)
//
// We al
var actions = new List<CodeSequence>();
// We always try to return actions that write data since that moves us toward the goal state
// Make it more convenient to get data by offset (this will probably be the representation of the state, eventually)
var bytes = state.Bytes.ToDictionary(x => x.Offset, x => x);
// If the accumulator holds an offset then we could move to any byte position.
if (state.A.IsScreenOffset && !state.S.IsScreenOffset)
// Get the current byte and current word that exist at the current stack location
var topByte = state.TryGetStackByte(bytes);
var topWord = state.TryGetStackWord(bytes);
var nextWord = state.TryGetStackWord(bytes, -2); // Also get the next value below the current word
// We can always perform a PEA regardless of the register widths
if (topWord.HasValue)
{
foreach (var datum in state.Bytes)
yield return state.Apply(new PEA(topWord.Value));
// If any of the registers happen to match the value, we can do an optimized PHA/X/Y/D operations. Any one
// PHx is as good as another and cannot affect the state, so just pick the first one.
if (state.LongA)
{
actions.Add(new MOVE_STACK(datum.Offset - state.A.Value));
if (state.A.IsLiteral && state.A.Value == topWord.Value)
{
yield return state.Apply(new PHA());
}
else
{
yield return state.Apply(new LOAD_16_BIT_IMMEDIATE_AND_PUSH(topWord.Value));
}
//else if (state.X.IsLiteral && state.X.Value == topWord.Value) { }
//else if (state.Y.IsLiteral && state.Y.Value == topWord.Value) { }
//else if (state.D.IsLiteral && state.D.Value == topWord.Value) { }
// If the top two workd match, it might be worthwhile to load the accumulator to start immediate PHAs
}
}
// If the accumulator and stack are both initialized, only propose moves to locations
// before and after the current 256 byte stack-relative window
if (state.A.IsScreenOffset && state.S.IsScreenOffset)
// If there is a valid byte, then we can look for an 8-bit push, or an immediate mode LDA #XX/STA 0,s
if (topByte.HasValue)
{
var addr = state.S.Value;
foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0))
if (!state.LongA)
{
actions.Add(new MOVE_STACK(datum.Offset - state.A.Value));
yield return state.Apply(new STACK_REL_8_BIT_IMMEDIATE_STORE(topByte.Value, 0));
}
}
// If the stack is valid on a word (consecutive bytes), when we can alway do a PEA
if (state.S.IsScreenOffset && state.S.Value > 0)
// If the accumulator holds an offset then we could move to any byte position, but it is only beneficial to
// move to the first or last byte of each span. So , take the first byte and then look for any
if (state.A.IsScreenOffset && !state.S.IsScreenOffset && state.LongA)
{
var addr = state.S.Value;
if (bytes.ContainsKey((ushort)addr) && bytes.ContainsKey((ushort)(addr - 1)))
for (var i = 0; i < state.Bytes.Count; i++)
{
var high = bytes[(ushort)addr].Data;
var low = bytes[(ushort)(addr - 1)].Data;
if (i == 0)
{
yield return state.Apply(new MOVE_STACK(state.Bytes[i].Offset - state.A.Value));
continue;
}
var word = (ushort)(low + (high << 8));
actions.Add(new PEA(word));
if (i == state.Bytes.Count - 1)
{
yield return state.Apply(new MOVE_STACK(state.Bytes[i].Offset - state.A.Value));
continue;
}
if ((state.Bytes[i].Offset - state.Bytes[i-1].Offset) > 1)
{
yield return state.Apply(new MOVE_STACK(state.Bytes[i].Offset - state.A.Value));
}
}
}
// It is always permissible to move to/from 16 bit mode
if (state.LongA)
{
actions.Add(new SHORT_M());
yield return state.Apply(new SHORT_M());
// Add any possible 16-bit data manipulations
if (state.S.IsScreenOffset)
@ -87,21 +155,21 @@
{
var offset = (byte)(word.Low.Offset - addr);
var data = (ushort)(word.Low.Data + (word.High.Data << 8));
actions.Add(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset));
}
// We can LDA #$XXXX / STA X,s for any values within 256 bytes of the current address
foreach (var datum in state.Bytes.Where(WithinRangeOf(addr, 256)))
{
var offset = (byte)(datum.Offset - addr);
actions.Add(new STACK_REL_8_BIT_IMMEDIATE_STORE(datum.Data, offset));
if (data == state.A.Value)
{
yield return state.Apply(new STACK_REL_16_BIT_STORE(data, offset));
}
else
{
yield return state.Apply(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset));
}
}
}
}
else
{
actions.Add(new LONG_M());
yield return state.Apply(new LONG_M());
// Add any possible 8-bit manipulations
if (state.S.IsScreenOffset)
@ -112,13 +180,21 @@
foreach (var datum in state.Bytes.Where(WithinRangeOf(addr, 256)))
{
var offset = datum.Offset - addr;
actions.Add(new STACK_REL_8_BIT_IMMEDIATE_STORE(datum.Data, (byte)offset));
yield return state.Apply(new STACK_REL_8_BIT_IMMEDIATE_STORE(datum.Data, (byte)offset));
}
}
}
// Run through the actions to create a dictionary
return actions.Select(x => Tuple.Create(x, x.Apply(state)));
// If the accumulator and stack are both initialized, only propose moves to locations
// before and after the current 256 byte stack-relative window
if (state.A.IsScreenOffset && state.S.IsScreenOffset && state.LongA)
{
var addr = state.S.Value;
foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0))
{
yield return state.Apply(new MOVE_STACK(datum.Offset - state.A.Value));
}
}
}
private Func<SpriteByte, bool> WithinRangeOf(int addr, int range)

View File

@ -1,9 +1,63 @@
namespace SpriteCompiler
{
using Fclp;
using SpriteCompiler.Problem;
using System;
using System.Collections.Generic;
using System.Linq;
public class ApplicationArguments
{
public List<string> Data { get; set; }
public List<string> Mask { get; set; }
}
public class Program
{
public static void WriteOutSolution(IEnumerable<SpriteGeneratorSearchNode> solution)
{
foreach (var step in solution.Skip(1))
{
Console.WriteLine(step.Action.Emit());
}
Console.WriteLine(string.Format("; Total Cost = {0} cycles", (int)solution.Last().PathCost));
}
static void Main(string[] args)
{
byte[] data = null;
Console.WriteLine(string.Join(", ", args.Select(s => "'" + s + "'")));
data = args.Select(s => Convert.ToByte(s, 16)).ToArray();
/*
return;
var p = new FluentCommandLineParser<ApplicationArguments>();
// specify which property the value will be assigned too.
p.Setup<List<string>>(arg => arg.Data)
.As('d', "data") // define the short and long option name
.Required() // using the standard fluent Api to declare this Option as required.
.Callback(d => d.Select(s => Convert.ToByte(s, 16)).ToArray());
p.Setup<List<string>>(arg => arg.Mask)
.As('m', "mask");
var result = p.Parse(args);
if (!result.HasErrors)
{
*/
var problem = SpriteGeneratorSearchProblem.CreateSearchProblem();
var search = SpriteGeneratorSearchProblem.Create();
var solution = search.Search(problem, new SpriteGeneratorState(data));
WriteOutSolution(solution);
//}
}
}
}

View File

@ -33,6 +33,10 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="FluentCommandLineParser, Version=1.4.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FluentCommandLineParser.1.4.3\lib\net35\FluentCommandLineParser.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Priority Queue, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\OptimizedPriorityQueue.4.0.0\lib\net45\Priority Queue.dll</HintPath>
<Private>True</Private>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentCommandLineParser" version="1.4.3" targetFramework="net452" />
<package id="OptimizedPriorityQueue" version="4.0.0" targetFramework="net452" />
</packages>