From e00ae24ab06324bf16ab50580bf31986027d5a61 Mon Sep 17 00:00:00 2001 From: Lucas Scharenbroich Date: Tue, 29 Nov 2016 00:20:29 -0600 Subject: [PATCH] Flesh out the sprite compiler state and successor functions. A bug exists that is causing no progress to be made resulting in an eventual OOM exception --- SpriteCompiler.Test/Tests.cs | 47 +++++ SpriteCompiler/AI/AStarComparer.cs | 2 +- SpriteCompiler/AI/AStarSearch.cs | 2 +- SpriteCompiler/AI/AbstractSearchNode.cs | 6 +- SpriteCompiler/AI/HeuristicSearchNode.cs | 2 +- SpriteCompiler/AI/InformedNodeExpander.cs | 2 +- SpriteCompiler/Problem/CodeSequence.cs | 108 ++++++++++- SpriteCompiler/Problem/SpriteByte.cs | 32 ++++ .../Problem/SpriteGeneratorSearchProblem.cs | 178 +++++++++++++++++- SpriteCompiler/SpriteCompiler.csproj | 1 + 10 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 SpriteCompiler/Problem/SpriteByte.cs diff --git a/SpriteCompiler.Test/Tests.cs b/SpriteCompiler.Test/Tests.cs index 8b2a184..b9a6f3e 100644 --- a/SpriteCompiler.Test/Tests.cs +++ b/SpriteCompiler.Test/Tests.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using SpriteCompiler.Problem; using SpriteCompiler.AI; +using System.Diagnostics; namespace SpriteCompiler.Test { @@ -22,5 +23,51 @@ namespace SpriteCompiler.Test // Assert : The initial state IS the goal state Assert.AreEqual(1, solution.Count()); } + + [TestMethod] + public void TestSingleByteSprite() + { + // Arrange + var problem = SpriteGeneratorSearchProblem.CreateSearchProblem(); + var search = SpriteGeneratorSearchProblem.Create(); + + // Act : solve the problem + var solution = search.Search(problem, new SpriteGeneratorState(new byte[] { 0xAA })); + + // Assert + // + // The fastest way to draw a single byte at the current location should be + // + // TCS + // SHORT A + // LDA #$AA + // STA 0,s + // LONG A = 14 cycles + + Assert.AreEqual(5, solution.Count()); + Assert.AreEqual(14, solution.First().PathCost); + + // Write out the solution + foreach (var step in solution) + { + Trace.WriteLine(step.Action.ToString()); + } + } + + /* + [TestMethod] + public void TestSingleWordSprite() + { + // Arrange + var problem = SpriteGeneratorSearchProblem.CreateSearchProblem(); + var search = SpriteGeneratorSearchProblem.Create(); + + // Act : solve the problem + var solution = search.Search(problem, new SpriteGeneratorState()); + + // Assert : The initial state IS the goal state + Assert.AreEqual(1, solution.Count()); + } + */ } } diff --git a/SpriteCompiler/AI/AStarComparer.cs b/SpriteCompiler/AI/AStarComparer.cs index e59e109..d86c9d1 100644 --- a/SpriteCompiler/AI/AStarComparer.cs +++ b/SpriteCompiler/AI/AStarComparer.cs @@ -8,7 +8,7 @@ namespace SpriteCompiler.AI { public class AStarComparator : IComparer where T : HeuristicSearchNode - where C : IPathCost + where C : IPathCost, new() { public int Compare(T x, T y) { diff --git a/SpriteCompiler/AI/AStarSearch.cs b/SpriteCompiler/AI/AStarSearch.cs index 97ebabd..5e5aa93 100644 --- a/SpriteCompiler/AI/AStarSearch.cs +++ b/SpriteCompiler/AI/AStarSearch.cs @@ -2,7 +2,7 @@ { public class AStarSearch : BestFirstSearch where T : HeuristicSearchNode - where C : IPathCost + where C : IPathCost, new() { public AStarSearch(AbstractAISearch search) : base(search) // , new AStarComparator()) diff --git a/SpriteCompiler/AI/AbstractSearchNode.cs b/SpriteCompiler/AI/AbstractSearchNode.cs index dcce7a6..ab76569 100644 --- a/SpriteCompiler/AI/AbstractSearchNode.cs +++ b/SpriteCompiler/AI/AbstractSearchNode.cs @@ -5,14 +5,14 @@ public abstract class AbstractSearchNode : ISearchNode where T : ISearchNode - where C : IPathCost + where C : IPathCost, new() { protected readonly S state; protected readonly T parent; protected A action = default(A); - protected C pathCost = default(C); - protected C stepCost = default(C); + protected C pathCost = new C(); + protected C stepCost = new C(); protected readonly int depth; public AbstractSearchNode(T node, S state) diff --git a/SpriteCompiler/AI/HeuristicSearchNode.cs b/SpriteCompiler/AI/HeuristicSearchNode.cs index 5f8a0aa..ef4ab9d 100644 --- a/SpriteCompiler/AI/HeuristicSearchNode.cs +++ b/SpriteCompiler/AI/HeuristicSearchNode.cs @@ -4,7 +4,7 @@ public class HeuristicSearchNode : AbstractSearchNode, ISearchNode where T : HeuristicSearchNode - where C : IPathCost + where C : IPathCost, new() { public HeuristicSearchNode(T node, S state) : base(node, state) diff --git a/SpriteCompiler/AI/InformedNodeExpander.cs b/SpriteCompiler/AI/InformedNodeExpander.cs index 0c8b0ab..138ae3e 100644 --- a/SpriteCompiler/AI/InformedNodeExpander.cs +++ b/SpriteCompiler/AI/InformedNodeExpander.cs @@ -5,7 +5,7 @@ namespace SpriteCompiler.AI { public abstract class InformedNodeExpander : INodeExpander where T : HeuristicSearchNode - where C : IPathCost + where C : IPathCost, new() { public abstract T CreateNode(T parent, S state); diff --git a/SpriteCompiler/Problem/CodeSequence.cs b/SpriteCompiler/Problem/CodeSequence.cs index b3187b3..1923c9b 100644 --- a/SpriteCompiler/Problem/CodeSequence.cs +++ b/SpriteCompiler/Problem/CodeSequence.cs @@ -9,9 +9,113 @@ namespace SpriteCompiler.Problem /// /// A set of code sequences that can be used to generate sprites /// - public class CodeSequence + public abstract class CodeSequence { + protected CodeSequence(int cycles) + { + CycleCount = cycles; + } + // Number of cycles that this code snippets takes to execute - public int CycleCount { get; } + public int CycleCount { get; private set; } + + // Function to generate a new state based on the code's operation + public abstract SpriteGeneratorState Apply(SpriteGeneratorState state); + + // Helper function for ToString implementations + protected string FormatLine(string label, string opcode, string operand, string comment) + { + return String.Format("{0}\t{1}\t{2}\t; {3}"); + } + } + + public sealed class MOVE_STACK : CodeSequence + { + private readonly int offset; + + public MOVE_STACK(int offset) + : base(offset == 0 ? 2 : 5) + { + this.offset = offset; + } + + public override SpriteGeneratorState Apply(SpriteGeneratorState state) + { + return state.Clone(_ => + { + _.A = _.A.Add(offset); + _.S = _.A; + }); + } + + public override string ToString() + { + if (offset == 0) + { + return FormatLine("", "TCS", "", "2 cycles"); + } + else + { + return String.Join("\n", + FormatLine("", "ADC", "#" + offset.ToString(), "3 cycles"), + FormatLine("", "TCS", "", "2 cycles") + ); + } + } + } + + public sealed class SHORT_M : CodeSequence + { + public SHORT_M() : base(3) { } + + public override SpriteGeneratorState Apply(SpriteGeneratorState state) + { + return state.Clone(_ => _.P |= 0x10); + } + + public override string ToString() + { + return FormatLine("", "SEP", "#$10", "3 cycles"); + } + } + + public sealed class LONG_M : CodeSequence + { + public LONG_M() : base(3) { } + + public override SpriteGeneratorState Apply(SpriteGeneratorState state) + { + return state.Clone(_ => _.P &= 0xEF); + } + + public override string ToString() + { + return FormatLine("", "REP", "#$10", "3 cycles"); + } + } + + public sealed class STACK_REL_8_BIT_IMMEDIATE_STORE : CodeSequence + { + private readonly byte value; + private readonly byte offset; + + public STACK_REL_8_BIT_IMMEDIATE_STORE(byte value, byte offset) : base(6) { this.value = value; this.offset = offset; } + + public override SpriteGeneratorState Apply(SpriteGeneratorState state) + { + return state.Clone(_ => + { + _.A = _.A.LoadConstant((_.A.Value & 0xFF00) | value); + _.RemoveByte((ushort)(offset + _.S.Value)); + }); + } + + public override string ToString() + { + return String.Join("\n", + FormatLine("", "LDA", "#$" + value.ToString("X2"), "2 cycles"), + FormatLine("", "STA", offset.ToString("X2") + ",s", "4 cycles") + ); + } } } diff --git a/SpriteCompiler/Problem/SpriteByte.cs b/SpriteCompiler/Problem/SpriteByte.cs new file mode 100644 index 0000000..5967837 --- /dev/null +++ b/SpriteCompiler/Problem/SpriteByte.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpriteCompiler.Problem +{ + public struct SpriteByte + { + private static byte DataToMask(byte data) + { + return (byte)((((data & 0xF0) == 0x00) ? 0xF0 : 0x00) | (((data & 0x0F) == 0x00) ? 0x0F : 0x00)); + } + + public SpriteByte(byte data, ushort offset) + : this(data, DataToMask(data), offset) + { + } + + public SpriteByte(byte data, byte mask, ushort offset) + { + Data = data; + Mask = mask; + Offset = offset; + } + + public byte Data { get; private set; } + public byte Mask { get; private set; } + public ushort Offset { get; private set; } + } +} diff --git a/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs b/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs index 2382277..bfd7bac 100644 --- a/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs +++ b/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs @@ -11,6 +11,11 @@ namespace SpriteCompiler.Problem { private readonly int value; + public IntegerPathCost() + : this(0) + { + } + private IntegerPathCost(int value) { this.value = value; @@ -40,14 +45,125 @@ namespace SpriteCompiler.Problem public class SpriteGeneratorState { public SpriteGeneratorState() + : this(new SpriteByte[0]) { } - public SpriteGeneratorState(byte[][] data, byte[][] mask) + public SpriteGeneratorState(byte[] data) + : this(data.Select((x, i) => new SpriteByte(x, (ushort)i))) { } - public bool IsEmpty { get { return true; } } + public SpriteGeneratorState(IEnumerable bytes) + { + Bytes = bytes.ToList(); + + // Initialize the CPU state + A = Register.INITIAL_OFFSET; // the address to draw the sprite is passed in, this is a run-time value + + X = Register.UNINITIALIZED; // the other registered are also undefined + Y = Register.UNINITIALIZED; + D = Register.UNINITIALIZED; + S = Register.UNINITIALIZED; + + P = 0x30; // Start in native mode (16 bit A/X/Y) with the carry clear + } + + private SpriteGeneratorState(SpriteGeneratorState other) + { + Bytes = new List(other.Bytes); + A = other.A; + X = other.X; + Y = other.Y; + D = other.D; + S = other.S; + P = other.P; + } + + public void RemoveByte(ushort offset) + { + var total = Bytes.RemoveAll(x => x.Offset == offset); + if (total != 1) + { + throw new ArgumentException(string.Format("Cannot remove: {0}", total)); + } + } + + public SpriteGeneratorState Clone(Action f = null) + { + var other = new SpriteGeneratorState(this); + + f?.Invoke(other); + + return other; + } + + public List Bytes { get; private set; } + public bool IsEmpty { get { return Bytes.Count == 0; } } + + public bool LongA { get { return (P & 0x10) == 0x10; } } + public bool LongI { get { return (P & 0x20) == 0x20; } } + + // Maintain the state of the execution + public Register A { get; set; } // Nullable because unknown values can be passed through the accumulator + public Register X { get; set; } + public Register Y { get; set; } + public Register D { get; set; } + public Register S { get; set; } // S is always an offset, not a literal number + + public byte P { get; set; } + } + + public class Register + { + public static readonly Register UNINITIALIZED = new Register(0, DataType.UNINITIALIZED); + public static readonly Register INITIAL_OFFSET = new Register(0, DataType.SCREEN_OFFSET); + + public enum DataType + { + UNINITIALIZED, + SCREEN_OFFSET, + LITERAL + } + + private Register(int value, DataType tag) + { + Value = value; + Tag = tag; + } + + public Register Clone() + { + return new Register(Value, Tag); + } + + public Register Add(int offset) + { + if (IsUninitialized) + { + throw new ArgumentException("Cannot add valued to uninitialized registers"); + } + + // Adding a value does not change the tag + return new Register(Value + offset, Tag); + } + + public Register LoadConstant(int value) + { + return new Register(value, DataType.LITERAL); + } + + public bool IsUninitialized { get { return DataType.UNINITIALIZED.Equals(Tag); } } + public bool IsScreenOffset { get { return DataType.SCREEN_OFFSET.Equals(Tag); } } + public bool IsLiteral { get { return DataType.LITERAL.Equals(Tag); } } + + public DataType Tag { get; private set; } + public int Value { get; private set; } + + public override string ToString() + { + return string.Format("{0} ({1})", Tag, Value.ToString("X4")); + } } public class SpriteGeneratorStepCost : IStepCostFunction @@ -71,7 +187,56 @@ namespace SpriteCompiler.Problem { public IDictionary Successors(SpriteGeneratorState state) { - return new Dictionary(); + var actions = new List(); + + // If the accumulator holds an offset then we could move to any byte position. We do + // not allow two consecutive MOVE_STACK operations + if (state.A.IsScreenOffset && !state.S.IsScreenOffset) + { + foreach (var datum in state.Bytes) + { + actions.Add(new MOVE_STACK(datum.Offset - state.A.Value)); + } + } + + // If the accumulator and stack are both initialized, only propose moves to locations + // before and after the current 256 byte window + if (state.A.IsScreenOffset && state.S.IsScreenOffset) + { + var addr = state.S.Value; + foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0)) + { + actions.Add(new MOVE_STACK(datum.Offset - state.A.Value)); + } + } + + // It is always permissible to move to/from 16 bit mode + if (state.LongA) + { + actions.Add(new SHORT_M()); + + // Add any possible 16-bit data manipulations + } + else + { + actions.Add(new LONG_M()); + + // Add any possible 8-bit manipulations + if (state.S.IsScreenOffset) + { + var addr = state.S.Value; + + // We can LDA #$XX / STA X,s for any values within 256 bytes of the curren address + foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) < 255)) + { + var offset = datum.Offset - addr; + actions.Add(new STACK_REL_8_BIT_IMMEDIATE_STORE((byte)offset, datum.Data)); + } + } + } + + // Run through the actions to create a dictionary + return actions.ToDictionary(x => x, x => x.Apply(state)); } } @@ -89,6 +254,11 @@ namespace SpriteCompiler.Problem : base(node, state) { } + + public override string ToString() + { + return (action == null) ? "NO ACTION" : action.ToString(); + } } public class SpriteGeneratorNodeExpander : InformedNodeExpander @@ -113,7 +283,7 @@ namespace SpriteCompiler.Problem public static ISearch Create() { - var problem = CreateSearchProblem(); + //var problem = CreateSearchProblem(); var strategy = new TreeSearch(new SpriteGeneratorNodeExpander()); return new AStarSearch(strategy); diff --git a/SpriteCompiler/SpriteCompiler.csproj b/SpriteCompiler/SpriteCompiler.csproj index bba1dac..05c05c3 100644 --- a/SpriteCompiler/SpriteCompiler.csproj +++ b/SpriteCompiler/SpriteCompiler.csproj @@ -69,6 +69,7 @@ +