From f87ac20f32b9c82cbba55291047050d9d1189ee6 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Sat, 17 Aug 2019 16:59:08 -0700 Subject: [PATCH] Add a string operand cache String operands used to be simple -- each line had 62 characters plus two hard-coded non-ASCII delimiters -- but now we're mixing character and hex data, so we can't use simple math to tell where the lines will break. We want to render them and keep the result around until some dependency changes, e.g. different delimiters or a change to the pseudo-op table. Also, cleaned up LineListGen a little. It had some methods that were declared static because they were expected to be shared, but that never happened. Also, fixed a bug in GatherEntityCounts where multi-line items were being scanned multiple times. --- SourceGen/FormattedOperandCache.cs | 141 ++++++++++++++++++ SourceGen/LineListGen.cs | 219 ++++++++++++++++++---------- SourceGen/MainController.cs | 47 +++--- SourceGen/PseudoOp.cs | 13 +- SourceGen/SourceGen.csproj | 1 + SourceGen/WpfGui/MainWindow.xaml.cs | 87 ++++++----- 6 files changed, 368 insertions(+), 140 deletions(-) create mode 100644 SourceGen/FormattedOperandCache.cs diff --git a/SourceGen/FormattedOperandCache.cs b/SourceGen/FormattedOperandCache.cs new file mode 100644 index 0000000..13abc6b --- /dev/null +++ b/SourceGen/FormattedOperandCache.cs @@ -0,0 +1,141 @@ +/* + * Copyright 2019 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using Asm65; + +namespace SourceGen { + /// + /// Holds a cache of formatted lines. + /// + /// + /// This is intended for multi-line items with lengths that are non-trivial to compute, + /// such as long comments (which have to do word wrapping) and strings (which may + /// be a mix of characters and hex data). The on-demand line formatter needs to be able + /// to render the Nth line of a multi-line string, and will potentially be very + /// inefficient if it has to render lies 0 through N-1 as well. (Imagine the list is + /// rendered from end to start...) Single-line items, and multi-line items that are + /// easy to generate at an arbitrary offset (dense hex), aren't stored here. + /// + /// The trick is knowing when the cached data must be invalidated. For example, a + /// fully formatted string line must be invalidated if: + /// - The Formatter changes (different delimiter definition) + /// - The FormatDescriptor changes (different length, different text encoding, different + /// type of string) + /// - The PseudoOpNames table changes (potentially altering the pseudo-op string used) + /// + /// Doing a full .equals() on the various items would reduce performance, so we use a + /// simple test on reference equality when possible, and expect that the client will try + /// to ensure that the various bits that are depended upon don't get replaced unnecessarily. + /// + public class FormattedOperandCache { + private const bool VERBOSE = false; + + private class FormattedStringEntry { + public List Lines { get; private set; } + public string PseudoOpcode { get; private set; } + + private Formatter mFormatter; + private FormatDescriptor mFormatDescriptor; + private PseudoOp.PseudoOpNames mPseudoOpNames; + + public FormattedStringEntry(List lines, string popcode, Formatter formatter, + FormatDescriptor formatDescriptor, PseudoOp.PseudoOpNames pseudoOpNames) { + // Can't be sure the list won't change, so duplicate it. + Lines = new List(lines.Count); + foreach (string str in lines) { + Lines.Add(str); + } + PseudoOpcode = popcode; + + mFormatter = formatter; + mFormatDescriptor = formatDescriptor; + mPseudoOpNames = pseudoOpNames; + } + + /// + /// Checks the entry's dependencies. + /// + /// + /// The data analyzer regenerates stuff in Anattribs, so we can't expect to have + /// the same FormatDescriptor object. + /// + /// True if the dependencies match. + public bool CheckDeps(Formatter formatter, FormatDescriptor formatDescriptor, + PseudoOp.PseudoOpNames pseudoOpNames) { + bool ok = (ReferenceEquals(mFormatter, formatter) && + ReferenceEquals(mPseudoOpNames, pseudoOpNames) && + mFormatDescriptor == formatDescriptor); + //if (!ok) { + // Debug.WriteLine("CheckDeps:" + + // (ReferenceEquals(mFormatter, formatter) ? "" : " fmt") + + // (ReferenceEquals(mPseudoOpNames, pseudoOpNames) ? "" : " pop") + + // (mFormatDescriptor == formatDescriptor ? "" : " dfd")); + //} + return ok; + } + } + + private Dictionary mStringEntries = + new Dictionary(); + + + /// + /// Retrieves the formatted string data for the specified offset. + /// + /// File offset. + /// Formatter dependency. + /// FormatDescriptor dependency. + /// PseudoOpNames dependency. + /// Pseudo-op for this string. + /// A reference to the string list. The caller must not modify the + /// list. + public List GetStringEntry(int offset, Formatter formatter, + FormatDescriptor formatDescriptor, PseudoOp.PseudoOpNames pseudoOpNames, + out string PseudoOpcode) { + PseudoOpcode = null; + if (!mStringEntries.TryGetValue(offset, out FormattedStringEntry entry)) { + return null; + } + if (!entry.CheckDeps(formatter, formatDescriptor, pseudoOpNames)) { + //Debug.WriteLine(" stale entry at +" + offset.ToString("x6")); + return null; + } + PseudoOpcode = entry.PseudoOpcode; + return entry.Lines; + } + + /// + /// Sets the string data entry for the specified offset. + /// + /// File offset. + /// String data. + /// Pseudo-opcode for this line. + /// Formatter dependency. + /// FormatDescriptor dependency. + /// PseudoOpNames dependency. + public void SetStringEntry(int offset, List lines, string pseudoOpcode, + Formatter formatter, FormatDescriptor formatDescriptor, + PseudoOp.PseudoOpNames pseudoOpNames) { + Debug.Assert(lines != null); + FormattedStringEntry fse = new FormattedStringEntry(lines, pseudoOpcode, + formatter, formatDescriptor, pseudoOpNames); + mStringEntries[offset] = fse; + } + } +} diff --git a/SourceGen/LineListGen.cs b/SourceGen/LineListGen.cs index f195304..cbe0860 100644 --- a/SourceGen/LineListGen.cs +++ b/SourceGen/LineListGen.cs @@ -63,6 +63,12 @@ namespace SourceGen { /// private PseudoOp.PseudoOpNames mPseudoOpNames; + /// + /// Cache of previously-formatted data. The data is stored with references to + /// dependencies, so it should not be necessary to explicitly clear this. + /// + private FormattedOperandCache mFormattedLineCache; + /// /// One of these per line of output in the display. It should be possible to draw @@ -421,6 +427,7 @@ namespace SourceGen { mPseudoOpNames = opNames; mLineList = new List(); + mFormattedLineCache = new FormattedOperandCache(); mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, false); @@ -481,12 +488,10 @@ namespace SourceGen { FormattedParts parts; switch (line.LineType) { case Line.Type.Code: - parts = GenerateInstructionLine(mProject, mFormatter, - line.FileOffset, line.OffsetSpan, mShowCycleCounts); + parts = GenerateInstructionLine(line.FileOffset, line.OffsetSpan); break; case Line.Type.Data: - parts = GenerateDataLine(mProject, mFormatter, mPseudoOpNames, - line.FileOffset, line.SubLineIndex); + parts = GenerateDataLine(line.FileOffset, line.SubLineIndex); break; case Line.Type.Blank: // Nothing to do. @@ -624,8 +629,7 @@ namespace SourceGen { List headerLines = GenerateHeaderLines(mProject, mFormatter, mPseudoOpNames); mLineList.InsertRange(0, headerLines); - GenerateLineList(mProject, mFormatter, mPseudoOpNames, - mProject.FileData, 0, mProject.FileData.Length - 1, mLineList); + GenerateLineList(0, mProject.FileData.Length - 1, mLineList); mDisplayList.ResetList(mLineList.Count); @@ -709,8 +713,7 @@ namespace SourceGen { // Create temporary list to hold new lines. Set the initial capacity to // the previous size, on the assumption that it won't change much. List newLines = new List(endIndex - startIndex + 1); - GenerateLineList(mProject, mFormatter, mPseudoOpNames, mProject.FileData, - startOffset, endOffset, newLines); + GenerateLineList(startOffset, endOffset, newLines); // Out with the old, in with the new. mLineList.RemoveRange(startIndex, endIndex - startIndex + 1); @@ -862,14 +865,10 @@ namespace SourceGen { /// options, like maximum per-line operand length, might affect how many lines /// are generated. /// - /// Project reference. - /// Output formatter. /// Offset of first byte. /// Offset of last byte. /// List to add output lines to. - private static void GenerateLineList(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames, byte[] data, int startOffset, int endOffset, - List lines) { + private void GenerateLineList(int startOffset, int endOffset, List lines) { //Debug.WriteLine("GenerateRange [+" + startOffset.ToString("x6") + ",+" + // endOffset.ToString("x6") + "]"); @@ -878,9 +877,9 @@ namespace SourceGen { // Find the previous status flags for M/X tracking. StatusFlags prevFlags = StatusFlags.AllIndeterminate; - if (proj.CpuDef.HasEmuFlag) { + if (mProject.CpuDef.HasEmuFlag) { for (int scanoff = startOffset - 1; scanoff >= 0; scanoff--) { - Anattrib attr = proj.GetAnattrib(scanoff); + Anattrib attr = mProject.GetAnattrib(scanoff); if (attr.IsInstructionStart) { prevFlags = attr.StatusFlags; // Apply the same tweak here that we do to curFlags below. @@ -900,17 +899,16 @@ namespace SourceGen { // partial-list generation. bool addBlank = false; if (startOffset > 0) { - int baseOff = DataAnalysis.GetBaseOperandOffset(proj, startOffset - 1); - if (proj.GetAnattrib(baseOff).DoesNotContinue) { + int baseOff = DataAnalysis.GetBaseOperandOffset(mProject, startOffset - 1); + if (mProject.GetAnattrib(baseOff).DoesNotContinue) { addBlank = true; } } - int offset = startOffset; while (offset <= endOffset) { - Anattrib attr = proj.GetAnattrib(offset); + Anattrib attr = mProject.GetAnattrib(offset); if (attr.IsInstructionStart && offset > 0 && - proj.GetAnattrib(offset - 1).IsData) { + mProject.GetAnattrib(offset - 1).IsData) { // Transition from data to code. (Don't add blank line for inline data.) lines.Add(GenerateBlankLine(offset)); } else if (addBlank) { @@ -921,20 +919,22 @@ namespace SourceGen { // Insert long comments and notes. These may span multiple display lines, // and require word-wrap, so it's easiest just to render them fully here. - if (proj.Notes.TryGetValue(offset, out MultiLineComment noteData)) { - List formatted = noteData.FormatText(formatter, "NOTE: "); + // TODO: integrate into FormattedOperandCache so we don't have to + // regenerate them unless they change. Use the MLC as the dependency. + if (mProject.Notes.TryGetValue(offset, out MultiLineComment noteData)) { + List formatted = noteData.FormatText(mFormatter, "NOTE: "); StringListToLines(formatted, offset, Line.Type.Note, noteData.BackgroundColor, lines); } - if (proj.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { - List formatted = longComment.FormatText(formatter, string.Empty); + if (mProject.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { + List formatted = longComment.FormatText(mFormatter, string.Empty); StringListToLines(formatted, offset, Line.Type.LongComment, longComment.BackgroundColor, lines); } if (attr.IsInstructionStart) { // Generate reg width directive, if necessary. - if (proj.CpuDef.HasEmuFlag) { + if (mProject.CpuDef.HasEmuFlag) { // Changing from "ambiguous but assumed short" to "definitively short" // merits a directive, notably at the start of the file. The tricky // part is that E=1 means definitively M=1 X=1. And maybe @@ -964,8 +964,8 @@ namespace SourceGen { // possible values. Having the operand capitalization match the // pseudo-op's feels reasonable. rwLine.Parts = FormattedParts.CreateDirective( - formatter.FormatPseudoOp(opNames.RegWidthDirective), - formatter.FormatPseudoOp(operandStr)); + mFormatter.FormatPseudoOp(mPseudoOpNames.RegWidthDirective), + mFormatter.FormatPseudoOp(operandStr)); lines.Add(rwLine); } prevFlags = curFlags; @@ -974,7 +974,7 @@ namespace SourceGen { // Look for embedded instructions. int len; for (len = 1; len < attr.Length; len++) { - if (proj.GetAnattrib(offset + len).IsInstructionStart) { + if (mProject.GetAnattrib(offset + len).IsInstructionStart) { break; } } @@ -1003,15 +1003,32 @@ namespace SourceGen { offset += len; } else { Debug.Assert(attr.DataDescriptor != null); - // TODO: replace this with something that caches expensive items like - // string operands; maybe have an out List that is null for the - // easy stuff - int numLines = - PseudoOp.ComputeRequiredLineCount(formatter, opNames, attr.DataDescriptor, - data, offset); - for (int i = 0; i < numLines; i++) { - Line line = new Line(offset, attr.Length, Line.Type.Data, i); - lines.Add(line); + if (attr.DataDescriptor.IsString) { + // See if we've already got this one. + List strLines = mFormattedLineCache.GetStringEntry(offset, + mFormatter, attr.DataDescriptor, mPseudoOpNames, out string popcode); + if (strLines == null) { + //Debug.WriteLine("FMT string at +" + offset.ToString("x6")); + strLines = PseudoOp.FormatStringOp(mFormatter, mPseudoOpNames, + attr.DataDescriptor, mProject.FileData, offset, + out popcode); + mFormattedLineCache.SetStringEntry(offset, strLines, popcode, + mFormatter, attr.DataDescriptor, mPseudoOpNames); + } + FormattedParts[] partsArray = GenerateStringLines(offset, + popcode, strLines); + for (int i = 0; i < strLines.Count; i++) { + Line line = new Line(offset, attr.Length, Line.Type.Data, i); + line.Parts = partsArray[i]; + lines.Add(line); + } + } else { + int numLines = PseudoOp.ComputeRequiredLineCount(mFormatter, + mPseudoOpNames,attr.DataDescriptor, mProject.FileData, offset); + for (int i = 0; i < numLines; i++) { + Line line = new Line(offset, attr.Length, Line.Type.Data, i); + lines.Add(line); + } } offset += attr.Length; } @@ -1025,7 +1042,7 @@ namespace SourceGen { // // It should not be possible for an address map change to appear in the middle // of an instruction or data item. - foreach (AddressMap.AddressMapEntry ent in proj.AddrMap) { + foreach (AddressMap.AddressMapEntry ent in mProject.AddrMap) { if (ent.Offset < startOffset || ent.Offset > endOffset) { continue; } @@ -1041,9 +1058,9 @@ namespace SourceGen { } Line topLine = lines[index]; Line newLine = new Line(topLine.FileOffset, 0, Line.Type.OrgDirective); - string addrStr = formatter.FormatHexValue(ent.Addr, 4); + string addrStr = mFormatter.FormatHexValue(ent.Addr, 4); newLine.Parts = FormattedParts.CreateDirective( - formatter.FormatPseudoOp(opNames.OrgDirective), addrStr); + mFormatter.FormatPseudoOp(mPseudoOpNames.OrgDirective), addrStr); lines.Insert(index, newLine); // Prepend a blank line if the previous line wasn't already blank, and this @@ -1067,11 +1084,11 @@ namespace SourceGen { /// /// Takes a list of strings and adds them to the Line list as long comments. /// - /// - /// - /// - /// - /// + /// String list. + /// File offset of item start. + /// What type of line this is. + /// Background color (for Notes). + /// Line list to add data to. private static void StringListToLines(List list, int offset, Line.Type lineType, Color color, List lines) { foreach (string str in list) { @@ -1083,17 +1100,16 @@ namespace SourceGen { } } - private static FormattedParts GenerateInstructionLine(DisasmProject proj, - Formatter formatter, int offset, int instrBytes, bool showCycleCounts) { - Anattrib attr = proj.GetAnattrib(offset); - byte[] data = proj.FileData; + private FormattedParts GenerateInstructionLine(int offset, int instrBytes) { + Anattrib attr = mProject.GetAnattrib(offset); + byte[] data = mProject.FileData; - string offsetStr = formatter.FormatOffset24(offset); + string offsetStr = mFormatter.FormatOffset24(offset); int addr = attr.Address; - string addrStr = formatter.FormatAddress(addr, !proj.CpuDef.HasAddr16); - string bytesStr = formatter.FormatBytes(data, offset, instrBytes); - string flagsStr = attr.StatusFlags.ToString(proj.CpuDef.HasEmuFlag); + string addrStr = mFormatter.FormatAddress(addr, !mProject.CpuDef.HasAddr16); + string bytesStr = mFormatter.FormatBytes(data, offset, instrBytes); + string flagsStr = attr.StatusFlags.ToString(mProject.CpuDef.HasEmuFlag); string attrStr = attr.ToAttrString(); string labelStr = string.Empty; @@ -1101,7 +1117,7 @@ namespace SourceGen { labelStr = attr.Symbol.Label; } - OpDef op = proj.CpuDef.GetOpDef(data[offset]); + OpDef op = mProject.CpuDef.GetOpDef(data[offset]); int operand = op.GetOperand(data, offset, attr.StatusFlags); int instrLen = op.GetLength(attr.StatusFlags); OpDef.WidthDisambiguation wdis = OpDef.WidthDisambiguation.None; @@ -1109,7 +1125,7 @@ namespace SourceGen { wdis = OpDef.GetWidthDisambiguation(instrLen, operand); } - string opcodeStr = formatter.FormatOpcode(op, wdis); + string opcodeStr = mFormatter.FormatOpcode(op, wdis); if (attr.Length != instrBytes) { // An instruction is embedded inside this one. Note that BRK is a two-byte // instruction, so don't freak out if you see it marked as embedded when a @@ -1156,22 +1172,22 @@ namespace SourceGen { // Format operand as directed. if (op.AddrMode == OpDef.AddressMode.BlockMove) { // Special handling for the double-operand block move. - string opstr1 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + string opstr1 = PseudoOp.FormatNumericOperand(mFormatter, mProject.SymbolTable, null, attr.DataDescriptor, operand >> 8, 1, PseudoOp.FormatNumericOpFlags.None); - string opstr2 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + string opstr2 = PseudoOp.FormatNumericOperand(mFormatter, mProject.SymbolTable, null, attr.DataDescriptor, operand & 0xff, 1, PseudoOp.FormatNumericOpFlags.None); formattedOperand = '#' + opstr1 + "," + '#' + opstr2; } else { - formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + formattedOperand = PseudoOp.FormatNumericOperand(mFormatter, mProject.SymbolTable, null, attr.DataDescriptor, operandForSymbol, operandLen, opFlags); } } else { // Show operand value in hex. if (op.AddrMode == OpDef.AddressMode.BlockMove) { - formattedOperand = '#' + formatter.FormatHexValue(operand >> 8, 2) + "," + - '#' + formatter.FormatHexValue(operand & 0xff, 2); + formattedOperand = '#' + mFormatter.FormatHexValue(operand >> 8, 2) + "," + + '#' + mFormatter.FormatHexValue(operand & 0xff, 2); } else { if (operandLen == 2) { // This is necessary for 16-bit operands, like "LDA abs" and "PEA val", @@ -1179,66 +1195,107 @@ namespace SourceGen { // but we don't want to show it here. operandForSymbol &= 0xffff; } - formattedOperand = formatter.FormatHexValue(operandForSymbol, operandLen * 2); + formattedOperand = mFormatter.FormatHexValue(operandForSymbol, operandLen * 2); } } - string operandStr = formatter.FormatOperand(op, formattedOperand, wdis); + string operandStr = mFormatter.FormatOperand(op, formattedOperand, wdis); - string eolComment = proj.Comments[offset]; - if (showCycleCounts) { + string eolComment = mProject.Comments[offset]; + if (mShowCycleCounts) { bool branchCross = (attr.Address & 0xff00) != (operandForSymbol & 0xff00); - int cycles = proj.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, attr.BranchTaken, - branchCross); + int cycles = mProject.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, + attr.BranchTaken, branchCross); if (cycles > 0) { eolComment = cycles.ToString() + " " + eolComment; } else { eolComment = (-cycles).ToString() + "+ " + eolComment; } } - string commentStr = formatter.FormatEolComment(eolComment); + string commentStr = mFormatter.FormatEolComment(eolComment); FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr); return parts; } - private static FormattedParts GenerateDataLine(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames, int offset, int subLineIndex) { - Anattrib attr = proj.GetAnattrib(offset); - byte[] data = proj.FileData; + private FormattedParts GenerateDataLine(int offset, int subLineIndex) { + Anattrib attr = mProject.GetAnattrib(offset); + byte[] data = mProject.FileData; string offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr; offsetStr = addrStr = bytesStr = flagsStr = attrStr = labelStr = opcodeStr = operandStr = commentStr = string.Empty; - PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(formatter, opNames, proj.SymbolTable, - null, attr.DataDescriptor, proj.FileData, offset, subLineIndex); + PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(mFormatter, mPseudoOpNames, + mProject.SymbolTable, null, attr.DataDescriptor, mProject.FileData, offset, + subLineIndex); if (subLineIndex == 0) { - offsetStr = formatter.FormatOffset24(offset); + offsetStr = mFormatter.FormatOffset24(offset); - addrStr = formatter.FormatAddress(attr.Address, !proj.CpuDef.HasAddr16); + addrStr = mFormatter.FormatAddress(attr.Address, !mProject.CpuDef.HasAddr16); if (attr.Symbol != null) { labelStr = attr.Symbol.Label; } - bytesStr = formatter.FormatBytes(data, offset, attr.Length); + bytesStr = mFormatter.FormatBytes(data, offset, attr.Length); attrStr = attr.ToAttrString(); - opcodeStr = formatter.FormatPseudoOp(pout.Opcode); + opcodeStr = mFormatter.FormatPseudoOp(pout.Opcode); + commentStr = mFormatter.FormatEolComment(mProject.Comments[offset]); } else { opcodeStr = " +"; } operandStr = pout.Operand; - if (subLineIndex == 0) { - commentStr = formatter.FormatEolComment(proj.Comments[offset]); - } - FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr); return parts; } + + private FormattedParts[] GenerateStringLines(int offset, string popcode, + List operands) { + FormattedParts[] partsArray = new FormattedParts[operands.Count]; + + Anattrib attr = mProject.GetAnattrib(offset); + byte[] data = mProject.FileData; + + string offsetStr, addrStr, bytesStr, attrStr, labelStr, opcodeStr, + operandStr, commentStr; + + for (int subLineIndex = 0; subLineIndex < operands.Count; subLineIndex++) { + if (subLineIndex == 0) { + offsetStr = mFormatter.FormatOffset24(offset); + + addrStr = mFormatter.FormatAddress(attr.Address, !mProject.CpuDef.HasAddr16); + if (attr.Symbol != null) { + labelStr = attr.Symbol.Label; + } else { + labelStr = string.Empty; + } + + bytesStr = mFormatter.FormatBytes(data, offset, attr.Length); + attrStr = attr.ToAttrString(); + + opcodeStr = mFormatter.FormatPseudoOp(popcode); + commentStr = mFormatter.FormatEolComment(mProject.Comments[offset]); + } else { + offsetStr = addrStr = bytesStr = attrStr = labelStr = commentStr = + string.Empty; + + opcodeStr = " +"; + } + + operandStr = operands[subLineIndex]; + + FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, + /*flags*/string.Empty, attrStr, labelStr, opcodeStr, operandStr, commentStr); + + partsArray[subLineIndex] = parts; + } + + return partsArray; + } } } diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index 0336749..284955f 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -2462,30 +2462,37 @@ namespace SourceGen { // A single line can span multiple offsets, each of which could have a // different hint. Note the code/data hints are only applied to the first // byte of each selected line, so we're not quite in sync with that. - for (int offset = line.FileOffset; offset < line.FileOffset + line.OffsetSpan; - offset++) { - switch (mProject.TypeHints[offset]) { - case CodeAnalysis.TypeHint.Code: - codeHints++; - break; - case CodeAnalysis.TypeHint.Data: - dataHints++; - break; - case CodeAnalysis.TypeHint.InlineData: - inlineDataHints++; - break; - case CodeAnalysis.TypeHint.NoHint: - noHints++; - break; - default: - Debug.Assert(false); - break; + // + // For multi-line items, the OffsetSpan of the first item covers the entire + // item (it's the same for all Line instances), so we only want to do this for + // the first entry. + if (line.SubLineIndex == 0) { + for (int offset = line.FileOffset; offset < line.FileOffset + line.OffsetSpan; + offset++) { + switch (mProject.TypeHints[offset]) { + case CodeAnalysis.TypeHint.Code: + codeHints++; + break; + case CodeAnalysis.TypeHint.Data: + dataHints++; + break; + case CodeAnalysis.TypeHint.InlineData: + inlineDataHints++; + break; + case CodeAnalysis.TypeHint.NoHint: + noHints++; + break; + default: + Debug.Assert(false); + break; + } } } } - //Debug.WriteLine("GatherEntityCounts (len=" + CodeListGen.Count + ") took " + - // (DateTime.Now - startWhen).TotalMilliseconds + " ms"); + //Debug.WriteLine("GatherEntityCounts (start=" + startIndex + " end=" + endIndex + + // " len=" + mMainWin.CodeDisplayList.Count + + // ") took " + (DateTime.Now - startWhen).TotalMilliseconds + " ms"); return new EntityCounts() { mCodeLines = codeLines, diff --git a/SourceGen/PseudoOp.cs b/SourceGen/PseudoOp.cs index 1d27d85..a1626f2 100644 --- a/SourceGen/PseudoOp.cs +++ b/SourceGen/PseudoOp.cs @@ -36,7 +36,7 @@ namespace SourceGen { private const int MAX_OPERAND_LEN = 64; /// - /// One piece of the operand. + /// One piece of the pseudo-instruction. /// public struct PseudoOut { /// @@ -58,6 +58,8 @@ namespace SourceGen { } } + #region PseudoOpNames + /// /// Pseudo-op name collection. Instances are immutable. /// @@ -238,6 +240,7 @@ namespace SourceGen { { "StrDci", ".dstr" } }); + #endregion PseudoOpNames /// /// Computes the number of lines of output required to hold the formatted output. @@ -248,6 +251,7 @@ namespace SourceGen { public static int ComputeRequiredLineCount(Formatter formatter, PseudoOpNames opNames, FormatDescriptor dfd, byte[] data, int offset) { if (dfd.IsString) { + Debug.Assert(false); // shouldn't be calling here anymore List lines = FormatStringOp(formatter, opNames, dfd, data, offset, out string popcode); return lines.Count; @@ -275,7 +279,7 @@ namespace SourceGen { /// Generates a pseudo-op statement for the specified data operation. /// /// For most operations, only one output line will be generated. For larger items, - /// like long comments, the value may be split into multiple lines. The sub-index + /// like dense hex, the value may be split into multiple lines. The sub-index /// indicates which line should be formatted. /// /// Format definition. @@ -305,6 +309,7 @@ namespace SourceGen { PseudoOut po = new PseudoOut(); if (dfd.IsString) { + Debug.Assert(false); // shouldn't be calling here anymore List lines = FormatStringOp(formatter, opNames, dfd, data, offset, out string popcode); po.Opcode = popcode; @@ -372,8 +377,8 @@ namespace SourceGen { /// File data. /// Offset, within data, of start of string. /// Pseudo-opcode string. - /// Array of strings. - private static List FormatStringOp(Formatter formatter, PseudoOpNames opNames, + /// Array of operand strings. + public static List FormatStringOp(Formatter formatter, PseudoOpNames opNames, FormatDescriptor dfd, byte[] data, int offset, out string popcode) { int hiddenLeadingBytes = 0; diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index fc28150..de56815 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -76,6 +76,7 @@ + diff --git a/SourceGen/WpfGui/MainWindow.xaml.cs b/SourceGen/WpfGui/MainWindow.xaml.cs index f8ab083..ab5c956 100644 --- a/SourceGen/WpfGui/MainWindow.xaml.cs +++ b/SourceGen/WpfGui/MainWindow.xaml.cs @@ -732,43 +732,60 @@ namespace SourceGen.WpfGui { // 50K: 10 seconds, 20K: 1.6 sec, 10K: 0.6 sec, 5K: 0.2 sec const int MAX_SEL_COUNT = 5000; - // The caller will clear the DisplayListSelection before calling here, so we - // need to clear the ListView selection to match, even if we're about to call - // SelectAll. If we don't, the SelectAll() call won't generate the necessary - // events, and our DisplayListSelection will get out of sync. - codeListView.SelectedItems.Clear(); + TaskTimer timer = new TaskTimer(); + timer.StartTask("TOTAL"); - if (sel.IsAllSelected()) { - Debug.WriteLine("SetSelection: re-selecting all items"); - codeListView.SelectAll(); - return; + try { + timer.StartTask("Clear"); + // The caller will clear the DisplayListSelection before calling here, so we + // need to clear the ListView selection to match, even if we're about to call + // SelectAll. If we don't, the SelectAll() call won't generate the necessary + // events, and our DisplayListSelection will get out of sync. + codeListView.SelectedItems.Clear(); + timer.EndTask("Clear"); + + if (sel.IsAllSelected()) { + Debug.WriteLine("SetSelection: re-selecting all items"); + timer.StartTask("SelectAll"); + codeListView.SelectAll(); + timer.EndTask("SelectAll"); + return; + } + + if (sel.Count > MAX_SEL_COUNT) { + // Too much for WPF ListView -- only restore the first item. + Debug.WriteLine("SetSelection: not restoring (" + sel.Count + " items)"); + codeListView.SelectedItems.Add(CodeDisplayList[sel.GetFirstSelectedIndex()]); + return; + } + + Debug.WriteLine("SetSelection: selecting " + sel.Count + " of " + + CodeDisplayList.Count); + + // Note: if you refresh the display list with F5, the selection will be lost. This + // appears to be a consequence of hitting a key -- changing from the built-in + // "Refresh" command to a locally defined "Re-analyze" command bound to F6 didn't + // change the behavior. Selecting "re-analyze" from the DEBUG menu doesn't lose + // the selection. + + timer.StartTask("tmpArray " + sel.Count); + DisplayList.FormattedParts[] tmpArray = new DisplayList.FormattedParts[sel.Count]; + int ai = 0; + foreach (int listIndex in sel) { + tmpArray[ai++] = CodeDisplayList[listIndex]; + } + timer.EndTask("tmpArray " + sel.Count); + + // Use a reflection call to provide the full set. This is much faster than + // adding the items one at a time to SelectedItems. (For one thing, it only + // invokes the SelectionChanged method once.) + timer.StartTask("Invoke"); + listViewSetSelectedItems.Invoke(codeListView, new object[] { tmpArray }); + timer.EndTask("Invoke"); + } finally { + timer.EndTask("TOTAL"); + //timer.DumpTimes("CodeListView_SetSelection"); } - - if (sel.Count > MAX_SEL_COUNT) { - // Too much for WPF ListView -- only restore the first item. - Debug.WriteLine("SetSelection: not restoring (" + sel.Count + " items)"); - codeListView.SelectedItems.Add(CodeDisplayList[sel.GetFirstSelectedIndex()]); - return; - } - - Debug.WriteLine("SetSelection: selecting " + sel.Count + " of " + - CodeDisplayList.Count); - - //DateTime startWhen = DateTime.Now; - - DisplayList.FormattedParts[] tmpArray = new DisplayList.FormattedParts[sel.Count]; - int ai = 0; - foreach (int listIndex in sel) { - tmpArray[ai++] = CodeDisplayList[listIndex]; - } - - // Use a reflection call to provide the full set. This is much faster than - // adding the items one at a time to SelectedItems. (For one thing, it only - // invokes the SelectionChanged method once.) - listViewSetSelectedItems.Invoke(codeListView, new object[] { tmpArray }); - - //Debug.WriteLine("SetSelection on " + sel.Count + " items took " + - // (DateTime.Now - startWhen).TotalMilliseconds + " ms"); } public void CodeListView_DebugValidateSelectionCount() {