From 53561fbcc87e129a6d8a41ad43c9313d743b77c7 Mon Sep 17 00:00:00 2001 From: Lucas Scharenbroich Date: Fri, 16 Dec 2016 22:52:14 -0600 Subject: [PATCH] Simplify success function and improve heuristic accuracy --- SpriteCompiler/AI/InstrumentedNodeExpander.cs | 47 +++ .../Helpers/EnumerableExtensions.cs | 85 ++++ SpriteCompiler/Problem/CodeSequence.cs | 46 ++- .../SpriteGeneratorHeuristicFunction.cs | 109 ++++-- .../Problem/SpriteGeneratorState.cs | 44 ++- .../SpriteGeneratorSuccessorFunction.cs | 366 +++++++++++------- SpriteCompiler/SpriteCompiler.csproj | 2 + 7 files changed, 510 insertions(+), 189 deletions(-) create mode 100644 SpriteCompiler/AI/InstrumentedNodeExpander.cs create mode 100644 SpriteCompiler/Helpers/EnumerableExtensions.cs diff --git a/SpriteCompiler/AI/InstrumentedNodeExpander.cs b/SpriteCompiler/AI/InstrumentedNodeExpander.cs new file mode 100644 index 0000000..675c806 --- /dev/null +++ b/SpriteCompiler/AI/InstrumentedNodeExpander.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpriteCompiler.AI +{ + public static class IntrumentedParameters + { + public const string NODES_EXPANDED = "nodesExpanded"; + } + + public class InstrumentedNodeExpander : NodeExpanderDelegator + where T : ISearchNode + where C : ICost + { + private readonly IDictionary metrics = new Dictionary(); + private readonly INodeExpander expander; + + public InstrumentedNodeExpander(INodeExpander expander) + : base(expander) + { + ClearMetrics(); + } + + public override IEnumerable Expand(ISearchProblem problem, T node) + { + metrics[IntrumentedParameters.NODES_EXPANDED] += 1; + return base.Expand(problem, node); + } + + public int this[string key] + { + get + { + return metrics[key]; + } + } + + public void ClearMetrics() + { + metrics[IntrumentedParameters.NODES_EXPANDED] = 0; + } + } + +} diff --git a/SpriteCompiler/Helpers/EnumerableExtensions.cs b/SpriteCompiler/Helpers/EnumerableExtensions.cs new file mode 100644 index 0000000..3ccd5ab --- /dev/null +++ b/SpriteCompiler/Helpers/EnumerableExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpriteCompiler.Helpers +{ + public static class EnumerableExtensions + { + public static IEnumerable> Pairs(this IEnumerable source) + { + return source.Zip(source.Skip(1), (x, y) => Tuple.Create(x, y)); + } + + public static IEnumerable> GroupAdjacent(this IEnumerable source, Func comparer) + { + return GroupAdjacent(source, x => x, comparer); + } + + public static IEnumerable> GroupAdjacent(this IEnumerable source, Func keySelector) + { + return GroupAdjacent(source, keySelector, (a, b) => a.Equals(b)); + } + + public static IEnumerable> GroupAdjacent(this IEnumerable source, Func keySelector, Func comparer) + { + TKey last = default(TKey); + bool haveLast = false; + List list = new List(); + foreach (TSource s in source) + { + TKey k = keySelector(s); + if (haveLast) + { + if (!comparer(last, k)) + { + yield return new GroupOfAdjacent(list, last); + list = new List(); + list.Add(s); + last = k; + } + else + { + list.Add(s); + last = k; + } + } + else + { + list.Add(s); + last = k; + haveLast = true; + } + } + if (haveLast) + { + yield return new GroupOfAdjacent(list, last); + } + } + } + + public class GroupOfAdjacent : IEnumerable, IGrouping + { + public TKey Key { get; set; } + private List GroupList { get; set; } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((System.Collections.Generic.IEnumerable)this).GetEnumerator(); + } + + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() + { + foreach (var s in GroupList) + yield return s; + } + + public GroupOfAdjacent(List source, TKey key) + { + GroupList = source; + Key = key; + } + } +} diff --git a/SpriteCompiler/Problem/CodeSequence.cs b/SpriteCompiler/Problem/CodeSequence.cs index 1d6a72d..a4857ce 100644 --- a/SpriteCompiler/Problem/CodeSequence.cs +++ b/SpriteCompiler/Problem/CodeSequence.cs @@ -102,7 +102,11 @@ namespace SpriteCompiler.Problem public override SpriteGeneratorState Apply(SpriteGeneratorState state) { - return state.Clone(_ => _.P &= 0xEF); + return state.Clone(_ => + { + _.P &= 0xEF; + _.AllowModeChange = false; + }); } public override string ToString() @@ -122,7 +126,11 @@ namespace SpriteCompiler.Problem public override SpriteGeneratorState Apply(SpriteGeneratorState state) { - return state.Clone(_ => _.P |= 0x10); + return state.Clone(_ => + { + _.P |= 0x10; + _.AllowModeChange = false; + }); } public override string ToString() @@ -224,6 +232,40 @@ namespace SpriteCompiler.Problem } } + public sealed class STACK_REL_8_BIT_READ_MODIFY_PUSH : CodeSequence + { + private readonly byte value; + private readonly byte mask; + + public STACK_REL_8_BIT_READ_MODIFY_PUSH(byte value, byte mask) : base(11) { this.value = value; this.mask = mask; } + + public override SpriteGeneratorState Apply(SpriteGeneratorState state) + { + return state.Clone(_ => + { + _.A = Register.UNINITIALIZED; + _.RemoveByte((ushort)_.S.Value); + _.S.Add(-1); + }); + } + + public override string ToString() + { + return "LDA 0,s / AND #$" + mask.ToString("X2") + " / ORA #$" + value.ToString("X2") + " / PHA"; + } + + public override string Emit() + { + return String.Join("\n", + FormatLine("", "LDA", "0,s", "4 cycles"), + FormatLine("", "AND", "#$" + mask.ToString("X2"), "2 cycles"), + FormatLine("", "ORA", "#$" + value.ToString("X2"), "2 cycles"), + FormatLine("", "PHA", "", "3 cycles") + ); + } + } + + public sealed class STACK_REL_16_BIT_STORE : CodeSequence { private readonly byte offset; diff --git a/SpriteCompiler/Problem/SpriteGeneratorHeuristicFunction.cs b/SpriteCompiler/Problem/SpriteGeneratorHeuristicFunction.cs index e28d407..e38cf8e 100644 --- a/SpriteCompiler/Problem/SpriteGeneratorHeuristicFunction.cs +++ b/SpriteCompiler/Problem/SpriteGeneratorHeuristicFunction.cs @@ -1,8 +1,10 @@ namespace SpriteCompiler.Problem { using SpriteCompiler.AI; + using SpriteCompiler.Helpers; using System.Linq; using System; + using System.Collections.Generic; public sealed class SpriteGeneratorHeuristicFunction : IHeuristicFunction { @@ -39,6 +41,29 @@ return gapCost + spanCost; } + private const int MASKED_DATA = 0; + private const int SOLID_DATA = 1; + + private int ClassifyChunk(IEnumerable chunk) + { + return (chunk.First().Mask == 0x00) ? SOLID_DATA : MASKED_DATA; + } + + private bool AreEquivalent(SpriteByte left, SpriteByte right) + { + // Return true if the two bytes are in an equivalence class + return + // The have to be adjacent + ((right.Offset - left.Offset) == 1) && + ( + // First case: Both bytes are solid + ((left.Mask == 0x00) && (right.Mask == 0x00)) || + + // Second case: Both bytes are masked + ((left.Mask != 0x00) && (right.Mask != 0x00)) + ); + } + public IntegerCost Eval(SpriteGeneratorState state) { // An admissible heuistic that calculates a cost based on the gaps and runs in a sprite @@ -46,40 +71,62 @@ // An even-length run can be done, at best in 4 cycles/word // An odd-length run is even + 3 cycles/byte - if (state.IsEmpty) return 0; - - var offsets = state.RemainingBytes(); - var start = offsets[0]; - var stack = state.S.Value; - var curr = start; - var cost = 0; - - for (int i = 1; i < offsets.Count; i++) + // Easy case -- no data means no code + if (state.IsEmpty) { - var prev = curr; - curr = offsets[i]; - - if (prev == (curr - 1)) - { - continue; - } - - // Calculate the estimate cost - if (state.S.IsScreenOffset) - { - cost += SpanAndGapCost(stack, start, prev, curr); - } - else - { - cost += SpanAndGapCost(start, prev, curr); - } - - // Start a new sppan - start = curr; + return 0; } - // End with the span - cost += SpanAndGapCost(start, curr, curr); + // Get a list of all the bytes that have not been emitted + var remaining = state.RemainingBytes(); + + // Count the number of + // + // solid words + // solid bytes + // masked words + // masked bytes + // + // By grouping the remaining bytes into solid / masked runs + var chunks = remaining.GroupAdjacent((x, y) => AreEquivalent(x, y)); + + // Figure out the full range of offsets that need to be written + var range = remaining.Last().Offset - remaining.First().Offset; + + // At best, we can write 257 bytes before needing to move the stack (0,s to 16-bit save to ff,s). + var minStackMoves = range / 257; + + // Calculate a heuristic + var cost = 0; + + // If the stack is undefined, then there is at least a 2 cycle + // cost to initialize it + if (!state.S.IsScreenOffset) + { + cost += 2; + } + + cost += minStackMoves * 5; + + // Iterate through each chunk, determine which equivalence class the chunk is + // in and then assign a minimum score + foreach (var chunk in chunks) + { + var len = chunk.Count(); + + switch (ClassifyChunk(chunk)) + { + // Solid data costs at least 4 cycles / word + 3 cycles for a byte + case SOLID_DATA: + cost += 4 * (len / 2) + 3 * (len % 2); + break; + + // Masked data costs at least 16 cycles / word + 11 cycles for a byte + case MASKED_DATA: + cost += 16 * (len / 2) + 11 * (len % 2); + break; + } + } return cost; } diff --git a/SpriteCompiler/Problem/SpriteGeneratorState.cs b/SpriteCompiler/Problem/SpriteGeneratorState.cs index 18cf727..3b0cc3b 100644 --- a/SpriteCompiler/Problem/SpriteGeneratorState.cs +++ b/SpriteCompiler/Problem/SpriteGeneratorState.cs @@ -10,11 +10,31 @@ public static List DATASET = null; public static IDictionary DATASET_BY_OFFSET = null; + // Histogram of the byte data + public static IDictionary DATASET_SOLID_BYTES = null; + + // Histogram of all possible words -- includes overlaps, e.g. $11 $11 $11 $11 = ($1111, 3) + public static IDictionary DATASET_SOLID_WORDS = null; + public static SpriteGeneratorState Init(IEnumerable bytes) { - DATASET = bytes.ToList(); + DATASET = bytes.OrderBy(x => x.Offset).ToList(); DATASET_BY_OFFSET = DATASET.ToDictionary(x => (int)x.Offset, x => x); + DATASET_SOLID_BYTES = DATASET + .Where(x => x.Mask == 0x00) + .GroupBy(x => x.Data) + .ToDictionary(x => x.Key, x => x.Count()) + ; + + DATASET_SOLID_WORDS = DATASET + .Zip(DATASET.Skip(1), (x, y) => new { Left = x, Right = y }) + .Where(x => (x.Left.Offset == (x.Right.Offset - 1)) && x.Left.Mask == 0x00 && x.Right.Mask == 0x00) + .Select(x => (x.Right.Data << 8) | x.Left.Data) + .GroupBy(x => x) + .ToDictionary(x => (ushort)x.Key, x => x.Count()) + ; + return new SpriteGeneratorState(); } @@ -44,6 +64,8 @@ S = Register.UNINITIALIZED; P = 0x30; // Start in native mode (16 bit A/X/Y) with the carry clear + + AllowModeChange = true; } private SpriteGeneratorState(SpriteGeneratorState other) @@ -55,6 +77,7 @@ D = other.D; S = other.S; P = other.P; + AllowModeChange = other.AllowModeChange; } public override string ToString() @@ -66,18 +89,19 @@ { Closed.Add(offset); Closed.Add((ushort)(offset + 1)); + AllowModeChange = true; } public void RemoveByte(ushort offset) { Closed.Add(offset); + AllowModeChange = true; } - public List RemainingBytes() + public List RemainingBytes() { return DATASET - .Select(x => x.Offset) - .Where(x => !Closed.Contains(x)) + .Where(x => !Closed.Contains(x.Offset)) .ToList(); } @@ -112,6 +136,12 @@ public byte P { get; set; } + // Flag that is cleared whenever there is a switch from + // 8/16-bit mode. It is reset once a PHA or STA occurs. + // A PEA instruction has no effect. This gates allowable + // state transition to prevent long REP/SEP seqences. + public bool AllowModeChange { get; set; } + public const byte LONG_A = 0x10; public const byte LONG_I = 0x20; @@ -129,7 +159,8 @@ Y.Equals(other.Y) && D.Equals(other.D) && S.Equals(other.S) && - P.Equals(other.P) + P.Equals(other.P) && + AllowModeChange == other.AllowModeChange ; } @@ -141,7 +172,8 @@ Y.GetHashCode() + D.GetHashCode() + S.GetHashCode() + - P.GetHashCode() + P.GetHashCode() + + AllowModeChange.GetHashCode() ; } diff --git a/SpriteCompiler/Problem/SpriteGeneratorSuccessorFunction.cs b/SpriteCompiler/Problem/SpriteGeneratorSuccessorFunction.cs index 97b1b76..3d9e701 100644 --- a/SpriteCompiler/Problem/SpriteGeneratorSuccessorFunction.cs +++ b/SpriteCompiler/Problem/SpriteGeneratorSuccessorFunction.cs @@ -1,6 +1,7 @@ namespace SpriteCompiler.Problem { using SpriteCompiler.AI; + using SpriteCompiler.Helpers; using System; using System.Collections.Generic; using System.Linq; @@ -77,168 +78,120 @@ { public IEnumerable> Successors(SpriteGeneratorState state) { - // This is the work-horse of the compiler. For a given state we need to enumerate all of the - // potential next operations. - // - // 1. If there are 16-bits of data at then current offset, we can - // a. Use one of the cached valued in A/X/Y/D if they match (4 cycles) - // b. Use a PEA to push immediate values (5 cycles) - // c. Load a value into A/X/Y and then push (7 cycles, only feasible if the value appears elsewhere in the sprite) - // d. Load the value into D and then push (9 cycles, and leaves A = D) - // - // 2. Move the stack - // a. Add a value directly (7 cycles, A = unknown) - // b. Skip 1 byte (6 cycles, A = unknown TSC/DEC/TSC) - // c. Multiple skips (LDA X,s/AND/ORA/STA = 16/byte, ADC #/TCS/LDX #/PHX = 10/byte - // - // 3. Single-byte at the end of a solid run - // 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 always try to return actions that write data since that moves us toward the goal state - - // Get the list of remaining bytes by removing the closed list from the global sprite - var open = SpriteGeneratorState.DATASET.Where(x => !state.Closed.Contains(x.Offset)).ToList(); - var bytes = open.ToDictionary(x => x.Offset, x => x); + // Get the list of remaining bytes by removing the closed list from the global sprite dataset + var open = state.RemainingBytes(); + // If the open list is empty, there can be only one reaons -- we're in an 8-bit + // mode. + if (!open.Any()) + { + yield return state.Apply(new LONG_M()); + yield break; + } + + // Get the first byte -- if we are expanding a state we can safely assume there + // is still data to expand. + var firstByte = open.First(); + + // Identify the first run of solid bytes + var firstSolid = FirstSolidRun(open); + + // In an initial state, the stack has not been set, but the accumulator contains a screen + // offset address. There are three possible options + // + // 1. Set the stack to the accumulator value. This is optimal when there are very few + // data bytes to set and the overhead of moving the stack negates any benefit from + // using stack push instructions + // + // 2. Set the stack to the first offset of the open set. This can be optimal if there are a few + // bytes and moving the stack forward a bit can allow the code to reach them without needing + // a second stack adjustment. + // + // 3. Set the stack to the first, right-most offset that end a sequence of solid bytes + if (!state.S.IsScreenOffset && state.A.IsScreenOffset) + { + // If the first byte is within 255 bytes of the accumulator, propose setting + // the stack to the accumulator value + var delta = firstByte.Offset - state.A.Value; + if (delta >= 0 && delta < 256) + { + yield return state.Apply(new MOVE_STACK(0)); + } + + // If the first byte offset is not equal to the accumulator value, propose that + if (delta > 0) + { + yield return state.Apply(new MOVE_STACK(delta)); + } + + // Find the first edge of a solid run....TODO + if (firstSolid != null && firstSolid.Count >= 2) + { + yield return state.Apply(new MOVE_STACK(firstSolid.Last.Offset - state.A.Value)); + } + + yield break; + } + + // If the first byte is 256 bytes or more ahead of the current stack location, + // then we need to advance + var firstByteDistance = firstByte.Offset - state.S.Value; + if (state.S.IsScreenOffset && firstByteDistance >= 256) + { + // Go to the next byte, or the first solid edge + yield return state.Apply(new MOVE_STACK(firstByteDistance)); + + yield break; + } + + var bytes = open.ToDictionary(x => x.Offset, x => x); + // 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 - // If there is some data at the top of the stack, see what we can do - if (topWord.HasValue) + // If the top of the stack is a solid work, we can always emit a PEA regardless of 8/16-bit mode + if (topWord.HasValue && topWord.Value.Mask == 0x000) { - // First, the simple case -- the data has no mask - if (topWord.Value.Mask == 0x000) - { - // If any of the registers has the exact value we need, then it is always fastest to just push the value - if (state.LongA) - { - if (state.A.IsLiteral && state.A.Value == topWord.Value.Data) - { - yield return state.Apply(new PHA_16()); - } - else - { - yield return state.Apply(new PEA(topWord.Value.Data)); - yield return state.Apply(new LOAD_16_BIT_IMMEDIATE_AND_PUSH(topWord.Value.Data)); - } - } - // Otherwise, the only alternative is a PEA instruction - else - { - yield return state.Apply(new PEA(topWord.Value.Data)); - } - } + yield return state.Apply(new PEA(topWord.Value.Data)); } - // 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) + // First set of operations for when the accumulator is in 8-bit mode. We are basically limited + // to either + // + // 1. Switching to 16-bit mode + // 3. Using a PHA to push an 8-bit solid or masked value on the stack + // 4. Using a STA 0,s to store an 8-bit solid or masked value on the stack + + if (!state.LongA) { - if (topByte.Value.Mask == 0x00) + // The byte to be stored at the top of the stack has some special methods available + if (topByte.HasValue) { - if (!state.LongA) + var datum = topByte.Value; + + // Solid byte + if (datum.Mask == 0x00) { - if (state.A.IsLiteral && ((state.A.Value & 0xFF) == topByte.Value.Data)) + if (state.A.IsLiteral && ((state.A.Value & 0xFF) == datum.Data)) { yield return state.Apply(new PHA_8()); } else { - yield return state.Apply(new LOAD_8_BIT_IMMEDIATE_AND_PUSH(topByte.Value.Data)); + yield return state.Apply(new LOAD_8_BIT_IMMEDIATE_AND_PUSH(datum.Data)); + yield return state.Apply(new STACK_REL_8_BIT_IMMEDIATE_STORE(datum.Data, 0)); } } - } - } - // If the accumulator holds an offset then we could move to any byte position, but it is only beneficial to - // either - // - // 1. Set the stack to the current accumulator value - // 2. Set the stack to the start of a contiguous segment - // 3. Set the stack to the end of a contiguous segment - - // 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) - { - // If any of the open bytes are within 255 bytes of the accumulator, consider just - // setting the stack to the accumulator value - if (open.Any(x => (x.Offset - state.A.Value) >= 0 && (x.Offset - state.A.Value) < 256)) - { - yield return state.Apply(new MOVE_STACK(0)); - } - - for (var i = 0; i < open.Count; i++) - { - if (i == 0) + // Masked byte + else { - yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value)); - continue; - } - - if (i == open.Count - 1) - { - yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value)); - continue; - } - - if ((open[i].Offset - open[i - 1].Offset) > 1) - { - yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value)); + yield return state.Apply(new STACK_REL_8_BIT_READ_MODIFY_PUSH(datum.Data, datum.Mask)); } } - } - // It is always permissible to move to/from 16 bit mode - if (state.LongA) - { - yield return state.Apply(new SHORT_M()); - - // Add any possible 16-bit data manipulations - if (state.S.IsScreenOffset) - { - var addr = state.S.Value; - - // Look for consecutive bytes. The second byte can come from the DATASET - var local = open.Where(WithinRangeOf(addr, 256)).ToList(); - var words = local - .Where(x => SpriteGeneratorState.DATASET_BY_OFFSET.ContainsKey(x.Offset + 1)) - .Select(x => new { Low = x, High = SpriteGeneratorState.DATASET_BY_OFFSET[x.Offset + 1] }) - .ToList(); - - foreach (var word in words) - { - var offset = (byte)(word.Low.Offset - addr); - var data = (ushort)(word.Low.Data + (word.High.Data << 8)); - var mask = (ushort)(word.Low.Mask + (word.High.Mask << 8)); - - // Easy case when mask is empty - if (mask == 0x0000) - { - if (data == state.A.Value) - { - yield return state.Apply(new STACK_REL_16_BIT_STORE(offset)); - } - else - { - yield return state.Apply(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset)); - } - } - - // Otherwise there is really only one choice LDA / AND / ORA / STA sequence - else - { - yield return state.Apply(new STACK_REL_16_BIT_READ_MODIFY_WRITE(data, mask, offset)); - } - } - } - } - else - { - yield return state.Apply(new LONG_M()); - - // Add any possible 8-bit manipulations + // Otherwise, just store the next byte closest to the stack if (state.S.IsScreenOffset) { var addr = state.S.Value; @@ -268,27 +221,140 @@ } } } + + if (state.AllowModeChange) + { + yield return state.Apply(new LONG_M()); + } } - // 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.S.IsScreenOffset && state.LongA) + // Now consider what can be done when the accumulator is 16-bit. All of the stack + // manipulation happens here, too. + if (state.LongA) { - // If the accumulator already has a screen offset value, just calculate it - if (state.A.IsScreenOffset) + // Handle the special case of a value sitting right on top of the stack + if (topWord.HasValue) { - var addr = state.S.Value; - foreach (var datum in open.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0)) + var datum = topWord.Value; + + // First, the simple case -- the data has no mask + if (datum.Mask == 0x000) { - yield return state.Apply(new MOVE_STACK(datum.Offset - state.A.Value)); + if (state.A.IsLiteral && state.A.Value == datum.Data) + { + yield return state.Apply(new PHA_16()); + } + else + { + // Only consider this if the value appear in the sprite more than once + if (SpriteGeneratorState.DATASET_SOLID_WORDS[topWord.Value.Data] > 1) + { + yield return state.Apply(new LOAD_16_BIT_IMMEDIATE_AND_PUSH(topWord.Value.Data)); + } + } } } - // Otherwise, put the stack in the accumulator - else + + // If the stack is set, find the next word to store (just one to reduce branching factor) + if (state.S.IsScreenOffset) { - yield return state.Apply(new TSC()); + var addr = state.S.Value; + + var local = open.Where(WithinRangeOf(addr, 256)).ToList(); + var words = local + .Where(x => SpriteGeneratorState.DATASET_BY_OFFSET.ContainsKey(x.Offset + 1)) + .Select(x => new { Low = x, High = SpriteGeneratorState.DATASET_BY_OFFSET[x.Offset + 1] }) + .ToList(); + + foreach (var word in words) + { + var offset = (byte)(word.Low.Offset - addr); + var data = (ushort)(word.Low.Data + (word.High.Data << 8)); + var mask = (ushort)(word.Low.Mask + (word.High.Mask << 8)); + // Easy case when mask is empty + if (mask == 0x0000) + { + if (data == state.A.Value) + { + yield return state.Apply(new STACK_REL_16_BIT_STORE(offset)); + } + else + { + yield return state.Apply(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset)); + } + } + + // Otherwise there is really only one choice LDA / AND / ORA / STA sequence + else + { + yield return state.Apply(new STACK_REL_16_BIT_READ_MODIFY_WRITE(data, mask, offset)); + } + } + } + + if (state.AllowModeChange) + { + yield return state.Apply(new SHORT_M()); } } + + Done: + var z = 0; z += 1; + } + + private static bool IsSolidPair(Tuple pair) + { + return + (pair.Item1.Offset == (pair.Item2.Offset - 1)) && + pair.Item1.Mask == 0x00 && + pair.Item2.Mask == 0x00; + } + + private class SolidRun + { + private readonly SpriteByte first; + private readonly SpriteByte last; + + public SolidRun(SpriteByte first, SpriteByte last) + { + this.first = first; + this.last = last; + } + + public SpriteByte First { get { return first; } } + public SpriteByte Last { get { return last; } } + public int Count { get { return last.Offset - first.Offset + 1; } } + } + + private SolidRun FirstSolidRun(IEnumerable open) + { + bool trigger = false; + SpriteByte first = default(SpriteByte); + SpriteByte last = default(SpriteByte); + + foreach (var item in open) + { + if (item.Mask == 0x00 && !trigger) + { + first = item; + trigger = true; + } + + if (item.Mask != 0x00 && trigger) + { + return new SolidRun(first, last); + } + + last = item; + } + + // If we get to the end and are still sold, great + if (last.Mask == 0x00 && trigger) + { + return new SolidRun(first, last); + } + + return null; } private Func WithinRangeOf(int addr, int range) diff --git a/SpriteCompiler/SpriteCompiler.csproj b/SpriteCompiler/SpriteCompiler.csproj index dc62f51..0f4a7ae 100644 --- a/SpriteCompiler/SpriteCompiler.csproj +++ b/SpriteCompiler/SpriteCompiler.csproj @@ -68,6 +68,7 @@ + @@ -87,6 +88,7 @@ +