From 9c71209fb9502896f4f3c8f4f02d2d5b76912d6c Mon Sep 17 00:00:00 2001 From: MS Date: Tue, 20 Feb 2024 02:56:33 -0500 Subject: [PATCH] reccmp: HTML refactor and diff address display (#581) * reccmp: HTML refactor and diff address display * Restore the @@ range indicator --- .../isledecomp/compare/asm/parse.py | 8 +- tools/isledecomp/isledecomp/compare/core.py | 38 +- tools/isledecomp/isledecomp/compare/diff.py | 81 +++ tools/isledecomp/isledecomp/utils.py | 63 ++ tools/reccmp/reccmp.js | 605 ++++++++++++++++++ tools/reccmp/reccmp.py | 12 +- tools/reccmp/template.html | 300 +++------ tools/vtable/vtable.py | 11 +- 8 files changed, 878 insertions(+), 240 deletions(-) create mode 100644 tools/isledecomp/isledecomp/compare/diff.py create mode 100644 tools/reccmp/reccmp.js diff --git a/tools/isledecomp/isledecomp/compare/asm/parse.py b/tools/isledecomp/isledecomp/compare/asm/parse.py index f974931e..35451fc6 100644 --- a/tools/isledecomp/isledecomp/compare/asm/parse.py +++ b/tools/isledecomp/isledecomp/compare/asm/parse.py @@ -192,11 +192,13 @@ def replace_immediate(chunk: str) -> str: def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]: asm = [] - for inst in disassembler.disasm_lite(data, start_addr): + for raw_inst in disassembler.disasm_lite(data, start_addr): # Use heuristics to disregard some differences that aren't representative # of the accuracy of a function (e.g. global offsets) - result = self.sanitize(DisasmLiteInst(*inst)) + inst = DisasmLiteInst(*raw_inst) + result = self.sanitize(inst) + # mnemonic + " " + op_str - asm.append(" ".join(result)) + asm.append((hex(inst.address), " ".join(result))) return asm diff --git a/tools/isledecomp/isledecomp/compare/core.py b/tools/isledecomp/isledecomp/compare/core.py index b490762a..de332e43 100644 --- a/tools/isledecomp/isledecomp/compare/core.py +++ b/tools/isledecomp/isledecomp/compare/core.py @@ -12,6 +12,7 @@ from isledecomp.types import SymbolType from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences from .db import CompareDb, MatchInfo +from .diff import combined_diff from .lines import LinesDb @@ -307,8 +308,12 @@ def recomp_lookup(addr: int) -> Optional[str]: float_lookup=recomp_float, ) - orig_asm = orig_parse.parse_asm(orig_raw, match.orig_addr) - recomp_asm = recomp_parse.parse_asm(recomp_raw, match.recomp_addr) + orig_combined = orig_parse.parse_asm(orig_raw, match.orig_addr) + recomp_combined = recomp_parse.parse_asm(recomp_raw, match.recomp_addr) + + # Detach addresses from asm lines for the text diff. + orig_asm = [x[1] for x in orig_combined] + recomp_asm = [x[1] for x in recomp_combined] diff = difflib.SequenceMatcher(None, orig_asm, recomp_asm) ratio = diff.ratio() @@ -317,7 +322,9 @@ def recomp_lookup(addr: int) -> Optional[str]: # Check whether we can resolve register swaps which are actually # perfect matches modulo compiler entropy. is_effective_match = can_resolve_register_differences(orig_asm, recomp_asm) - unified_diff = difflib.unified_diff(orig_asm, recomp_asm, n=10) + unified_diff = combined_diff( + diff, orig_combined, recomp_combined, context_size=10 + ) else: is_effective_match = False unified_diff = [] @@ -352,9 +359,7 @@ def _compare_vtable(self, match: MatchInfo) -> DiffReport: [t for (t,) in struct.iter_unpack(" str: + def match_text(m: Optional[MatchInfo], raw_addr: Optional[int] = None) -> str: """Format the function reference at this vtable index as text. If we have not identified this function, we have the option to display the raw address. This is only worth doing for the original addr @@ -363,19 +368,18 @@ def match_text( should override the given function from the superclass, but we have not implemented this yet. """ - index = f"vtable0x{i*4:02x}" if m is not None: orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig" recomp = ( hex(m.recomp_addr) if m.recomp_addr is not None else "no recomp" ) - return f"{index:>12} : ({orig:10} / {recomp:10}) : {m.name}" + return f"({orig} / {recomp}) : {m.name}" if raw_addr is not None: - return f"{index:>12} : 0x{raw_addr:x} from orig not annotated." + return f"0x{raw_addr:x} from orig not annotated." - return f"{index:>12} : (no match)" + return "(no match)" orig_text = [] recomp_text = [] @@ -395,14 +399,22 @@ def match_text( ratio += 1 n_entries += 1 - orig_text.append(match_text(i, orig, raw_orig)) - recomp_text.append(match_text(i, recomp)) + index = f"vtable0x{i*4:02x}" + orig_text.append((index, match_text(orig, raw_orig))) + recomp_text.append((index, match_text(recomp))) ratio = ratio / float(n_entries) if n_entries > 0 else 0 # n=100: Show the entire table if there is a diff to display. # Otherwise it would be confusing if the table got cut off. - unified_diff = difflib.unified_diff(orig_text, recomp_text, n=100) + + sm = difflib.SequenceMatcher( + None, + [x[1] for x in orig_text], + [x[1] for x in recomp_text], + ) + + unified_diff = combined_diff(sm, orig_text, recomp_text, context_size=100) return DiffReport( match_type=SymbolType.VTABLE, diff --git a/tools/isledecomp/isledecomp/compare/diff.py b/tools/isledecomp/isledecomp/compare/diff.py new file mode 100644 index 00000000..3b78ecd3 --- /dev/null +++ b/tools/isledecomp/isledecomp/compare/diff.py @@ -0,0 +1,81 @@ +from difflib import SequenceMatcher +from typing import Dict, List, Tuple + +CombinedDiffInput = List[Tuple[str, str]] +CombinedDiffOutput = List[Tuple[str, List[Dict[str, Tuple[str, str]]]]] + + +def combined_diff( + diff: SequenceMatcher, + orig_combined: CombinedDiffInput, + recomp_combined: CombinedDiffInput, + context_size: int = 3, +) -> CombinedDiffOutput: + """We want to diff the original and recomp assembly. The "combined" assembly + input has two components: the address of the instruction and the assembly text. + We have already diffed the text only. This is the SequenceMatcher object. + The SequenceMatcher can generate "opcodes" that describe how to turn "Text A" + into "Text B". These refer to list indices of the original arrays, so we can + use those to create the final diff and include the address for each line of assembly. + This is almost the same procedure as the difflib.unified_diff function, but we + are reusing the already generated SequenceMatcher object. + """ + + unified_diff = [] + + for group in diff.get_grouped_opcodes(context_size): + subgroups = [] + + # Keep track of the addresses we've seen in this diff group. + # This helps create the "@@" line. (Does this have a name?) + # Do it this way because not every line in each list will have an + # address. If our context begins or ends on a line that does not + # have one, we will have an incomplete range string. + orig_addrs = set() + recomp_addrs = set() + + for code, i1, i2, j1, j2 in group: + if code == "equal": + # The sections are equal, so the list slices are guaranteed + # to have the same length. We only need the diffed value (asm text) + # from one of the lists, but we need the addresses from both. + # Use zip to put the two lists together and then take out what we want. + both = [ + (a, b, c) + for ((a, b), (c, _)) in zip( + orig_combined[i1:i2], recomp_combined[j1:j2] + ) + ] + + for orig_addr, _, recomp_addr in both: + if orig_addr is not None: + orig_addrs.add(orig_addr) + + if recomp_addr is not None: + recomp_addrs.add(recomp_addr) + + subgroups.append({"both": both}) + else: + for orig_addr, _ in orig_combined[i1:i2]: + if orig_addr is not None: + orig_addrs.add(orig_addr) + + for recomp_addr, _ in recomp_combined[j1:j2]: + if recomp_addr is not None: + recomp_addrs.add(recomp_addr) + + subgroups.append( + { + "orig": orig_combined[i1:i2], + "recomp": recomp_combined[j1:j2], + } + ) + + orig_sorted = sorted(orig_addrs) + recomp_sorted = sorted(recomp_addrs) + + diff_slug = f"@@ -{orig_sorted[0]},{orig_sorted[-1]} +{recomp_sorted[0]},{recomp_sorted[-1]} @@" + + unified_diff.append((diff_slug, subgroups)) + + return unified_diff diff --git a/tools/isledecomp/isledecomp/utils.py b/tools/isledecomp/isledecomp/utils.py index 9797e1b0..a0a623a7 100644 --- a/tools/isledecomp/isledecomp/utils.py +++ b/tools/isledecomp/isledecomp/utils.py @@ -5,7 +5,70 @@ import colorama +def print_combined_diff(udiff, plain: bool = False, show_both: bool = False): + if udiff is None: + return + + # We don't know how long the address string will be ahead of time. + # Set this value for each address to try to line things up. + padding_size = 0 + + for slug, subgroups in udiff: + if plain: + print("---") + print("+++") + print(slug) + else: + print(f"{colorama.Fore.RED}---") + print(f"{colorama.Fore.GREEN}+++") + print(f"{colorama.Fore.BLUE}{slug}") + print(colorama.Style.RESET_ALL, end="") + + for subgroup in subgroups: + equal = subgroup.get("both") is not None + + if equal: + for orig_addr, line, recomp_addr in subgroup["both"]: + padding_size = max(padding_size, len(orig_addr)) + if show_both: + print(f"{orig_addr} / {recomp_addr} : {line}") + else: + print(f"{orig_addr} : {line}") + else: + for orig_addr, line in subgroup["orig"]: + padding_size = max(padding_size, len(orig_addr)) + addr_prefix = ( + f"{orig_addr} / {'':{padding_size}}" if show_both else orig_addr + ) + + if plain: + print(f"{addr_prefix} : -{line}") + else: + print( + f"{addr_prefix} : {colorama.Fore.RED}-{line}{colorama.Style.RESET_ALL}" + ) + + for recomp_addr, line in subgroup["recomp"]: + padding_size = max(padding_size, len(recomp_addr)) + addr_prefix = ( + f"{'':{padding_size}} / {recomp_addr}" + if show_both + else recomp_addr + ) + + if plain: + print(f"{addr_prefix} : +{line}") + else: + print( + f"{addr_prefix} : {colorama.Fore.GREEN}+{line}{colorama.Style.RESET_ALL}" + ) + + # Newline between each diff subgroup. + print() + + def print_diff(udiff, plain): + """Print diff in difflib.unified_diff format.""" if udiff is None: return False diff --git a/tools/reccmp/reccmp.js b/tools/reccmp/reccmp.js new file mode 100644 index 00000000..293d1906 --- /dev/null +++ b/tools/reccmp/reccmp.js @@ -0,0 +1,605 @@ +// reccmp.js +/* global data */ + +// Unwrap array of functions into a dictionary with address as the key. +const dataDict = Object.fromEntries(data.map(row => [row.address, row])); + +function getDataByAddr(addr) { + return dataDict[addr]; +} + +// +// Pure functions +// + +function formatAsm(entries, addrOption) { + const output = []; + + const createTh = (text) => { + const th = document.createElement('th'); + th.innerText = text; + return th; + }; + + const createTd = (text, className = '') => { + const td = document.createElement('td'); + td.innerText = text; + td.className = className; + return td; + }; + + entries.forEach(obj => { + // These won't all be present. You get "both" for an equal node + // and orig/recomp for a diff. + const { both = [], orig = [], recomp = [] } = obj; + + output.push(...both.map(([addr, line, recompAddr]) => { + const tr = document.createElement('tr'); + tr.appendChild(createTh(addr)); + tr.appendChild(createTh(recompAddr)); + tr.appendChild(createTd(line)); + return tr; + })); + + output.push(...orig.map(([addr, line]) => { + const tr = document.createElement('tr'); + tr.appendChild(createTh(addr)); + tr.appendChild(createTh('')); + tr.appendChild(createTd(`-${line}`, 'diffneg')); + return tr; + })); + + output.push(...recomp.map(([addr, line]) => { + const tr = document.createElement('tr'); + tr.appendChild(createTh('')); + tr.appendChild(createTh(addr)); + tr.appendChild(createTd(`+${line}`, 'diffpos')); + return tr; + })); + }); + + return output; +} + +function getMatchPercentText(row) { + if ('stub' in row) { + return 'stub'; + } + + if ('effective' in row) { + return '100.00%*'; + } + + return (row.matching * 100).toFixed(2) + '%'; +} + +// Helper for this set/remove attribute block +function setBooleanAttribute(element, attribute, value) { + if (value) { + element.setAttribute(attribute, ''); + } else { + element.removeAttribute(attribute); + } +} + +// +// Global state +// + +class ListingState { + constructor() { + this._query = ''; + this._sortCol = 'address'; + this._filterType = 1; + this.sortDesc = false; + this.hidePerfect = false; + this.hideStub = false; + } + + get filterType() { + return parseInt(this._filterType); + } + + set filterType(value) { + value = parseInt(value); + if (value >= 1 && value <= 3) { + this._filterType = value; + } + } + + get query() { + return this._query; + } + + set query(value) { + // Normalize search string + this._query = value.toLowerCase().trim(); + } + + get sortCol() { + return this._sortCol; + } + + set sortCol(column) { + if (column === this._sortCol) { + this.sortDesc = !this.sortDesc; + } else { + this._sortCol = column; + } + } +} + +const StateProxy = { + set(obj, prop, value) { + if (prop === 'onsort') { + this._onsort = value; + return true; + } + + if (prop === 'onfilter') { + this._onfilter = value; + return true; + } + + obj[prop] = value; + + if (prop === 'sortCol' || prop === 'sortDesc') { + this._onsort(); + } else { + this._onfilter(); + } + return true; + } +}; + +const appState = new Proxy(new ListingState(), StateProxy); + +// +// Stateful functions +// + +function addrShouldAppear(addr) { + // Destructuring sets defaults for optional values from this object. + const { + effective = false, + stub = false, + diff = '', + name, + address, + matching + } = getDataByAddr(addr); + + if (appState.hidePerfect && (effective || matching >= 1)) { + return false; + } + + if (appState.hideStub && stub) { + return false; + } + + if (appState.query === '') { + return true; + } + + // Name/addr search + if (appState.filterType === 1) { + return ( + address.includes(appState.query) || + name.toLowerCase().includes(appState.query) + ); + } + + // no diff for review. + if (diff === '') { + return false; + } + + // special matcher for combined diff + const anyLineMatch = ([addr, line]) => line.toLowerCase().trim().includes(appState.query); + + // Flatten all diff groups for the search + const diffs = diff.map(([slug, subgroups]) => subgroups).flat(); + for (const subgroup of diffs) { + const { both = [], orig = [], recomp = [] } = subgroup; + + // If search includes context + if (appState.filterType === 2 && both.some(anyLineMatch)) { + return true; + } + + if (orig.some(anyLineMatch) || recomp.some(anyLineMatch)) { + return true; + } + } + + return false; +} + +// Row comparator function, using our chosen sort column and direction. +// -1 (A before B) +// 1 (B before A) +// 0 (equal) +function rowSortOrder(addrA, addrB) { + const objA = getDataByAddr(addrA); + const objB = getDataByAddr(addrB); + const valA = objA[appState.sortCol]; + const valB = objB[appState.sortCol]; + + if (valA > valB) { + return appState.sortDesc ? -1 : 1; + } else if (valA < valB) { + return appState.sortDesc ? 1 : -1; + } + + return 0; +} + +// +// Custom elements +// + +// Sets sort indicator arrow based on element attributes. +class SortIndicator extends window.HTMLElement { + static observedAttributes = ['data-sort']; + + attributeChangedCallback(name, oldValue, newValue) { + if (newValue === null) { + this.textContent = ''; + } else { + this.innerHTML = newValue === 'asc' ? '▲' : '▼'; + } + } +} + +// Wrapper for +class FuncRow extends window.HTMLTableRowElement { + static observedAttributes = ['expanded']; + + constructor() { + super(); + + this.onclick = evt => (this.expanded = !this.expanded); + } + + connectedCallback() { + if (this.address === null) { + return; + } + + if (this.querySelector('td')) { + return; + } + + const obj = getDataByAddr(this.address); + + const td0 = document.createElement('td'); + const td1 = document.createElement('td'); + const td2 = document.createElement('td'); + + td0.innerText = `${obj.address}`; + td1.innerText = `${obj.name}`; + td2.innerText = getMatchPercentText(obj); + + this.appendChild(td0); + this.appendChild(td1); + this.appendChild(td2); + } + + get address() { + return this.getAttribute('data-address'); + } + + get expanded() { + return this.getAttribute('expanded') !== null; + } + + set expanded(value) { + setBooleanAttribute(this, 'expanded', value); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name !== 'expanded') { + return; + } + + if (this.onchangeExpand) { + this.onchangeExpand(this.expanded); + } + } +} + +// Wrapper for +// Displays asm diff for the given @data-address value. +class FuncRowChild extends window.HTMLTableRowElement { + constructor() { + super(); + + const td = document.createElement('td'); + td.setAttribute('colspan', 3); + this.appendChild(td); + } + + connectedCallback() { + const td = this.querySelector('td'); + + const addr = this.getAttribute('data-address'); + + const obj = getDataByAddr(addr); + + if ('stub' in obj) { + const diff = document.createElement('div'); + diff.className = 'identical'; + diff.innerText = 'Stub. No diff.'; + td.appendChild(diff); + } else if (obj.diff.length === 0) { + const diff = document.createElement('div'); + diff.className = 'identical'; + diff.innerText = 'Identical function - no diff'; + td.appendChild(diff); + } else { + const dd = new DiffDisplay(); + dd.option = '1'; + dd.address = addr; + td.appendChild(dd); + } + } +} + +class DiffDisplayOptions extends window.HTMLElement { + static observedAttributes = ['data-option']; + + connectedCallback() { + if (this.shadowRoot !== null) { + return; + } + + const shadow = this.attachShadow({ mode: 'open' }); + shadow.innerHTML = ` + +
+ Address display: + + + + + + +
`; + + shadow.querySelectorAll('input[type=radio]').forEach(radio => { + const checked = this.option === radio.getAttribute('value'); + setBooleanAttribute(radio, 'checked', checked); + + radio.addEventListener('change', evt => (this.option = evt.target.value)); + }); + } + + set option(value) { + this.setAttribute('data-option', parseInt(value)); + } + + get option() { + return this.getAttribute('data-option') ?? 1; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name !== 'data-option') { + return; + } + + this.dispatchEvent(new Event('change')); + } +} + +class DiffDisplay extends window.HTMLElement { + static observedAttributes = ['data-option']; + + connectedCallback() { + if (this.querySelector('diff-display-options') !== null) { + return; + } + + const optControl = new DiffDisplayOptions(); + optControl.option = this.option; + optControl.addEventListener('change', evt => (this.option = evt.target.option)); + this.appendChild(optControl); + + const div = document.createElement('div'); + const obj = getDataByAddr(this.address); + + const createSingleCellRow = (text, className) => { + const tr = document.createElement('tr'); + const td = document.createElement('td'); + td.setAttribute('colspan', 3); + td.textContent = text; + td.className = className; + tr.appendChild(td); + return tr; + }; + + const groups = obj.diff; + groups.forEach(([slug, subgroups]) => { + const secondTable = document.createElement('table'); + secondTable.classList.add('diffTable', 'showOrig'); + + const tbody = document.createElement('tbody'); + secondTable.appendChild(tbody); + tbody.appendChild(createSingleCellRow('---', 'diffneg')); + tbody.appendChild(createSingleCellRow('+++', 'diffpos')); + tbody.appendChild(createSingleCellRow(slug, 'diffslug')); + + const diffs = formatAsm(subgroups, this.option); + for (const el of diffs) { + tbody.appendChild(el); + } + + div.appendChild(secondTable); + }); + + this.appendChild(div); + } + + get address() { + return this.getAttribute('data-address'); + } + + set address(value) { + this.setAttribute('data-address', value); + } + + get option() { + return this.getAttribute('data-option') ?? 1; + } + + set option(value) { + this.setAttribute('data-option', value); + } +} + +// Main application. +class ListingTable extends window.HTMLElement { + constructor() { + super(); + + // Redraw the table on any changes. + appState.onsort = () => this.sortRows(); + appState.onfilter = () => this.filterRows(); + + const input = this.querySelector('input[type=search]'); + input.oninput = evt => (appState.query = evt.target.value); + + const hidePerf = this.querySelector('input#cbHidePerfect'); + hidePerf.onchange = evt => (appState.hidePerfect = evt.target.checked); + hidePerf.checked = appState.hidePerfect; + + const hideStub = this.querySelector('input#cbHideStub'); + hideStub.onchange = evt => (appState.hideStub = evt.target.checked); + hideStub.checked = appState.hideStub; + + this.querySelectorAll('input[name=filterType]').forEach(radio => { + const checked = appState.filterType === parseInt(radio.getAttribute('value')); + setBooleanAttribute(radio, 'checked', checked); + + radio.onchange = evt => (appState.filterType = radio.getAttribute('value')); + }); + } + + setRowExpand(address, shouldExpand) { + const tbody = this.querySelector('tbody'); + const funcrow = tbody.querySelector(`tr.funcrow[data-address="${address}"]`); + if (funcrow === null) { + return; + } + + const existing = tbody.querySelector(`tr.diffRow[data-address="${address}"]`); + if (shouldExpand) { + if (existing === null) { + const diffrow = document.createElement('tr', { is: 'func-row-child' }); + diffrow.className = 'diffRow'; + diffrow.setAttribute('data-address', address); + // Insert the diff row after the parent func row. + tbody.insertBefore(diffrow, funcrow.nextSibling); + } + } else { + if (existing !== null) { + tbody.removeChild(existing); + } + } + } + + connectedCallback() { + const thead = this.querySelector('thead'); + const headers = thead.querySelectorAll('th'); + headers.forEach(th => { + const col = th.getAttribute('data-col'); + if (col) { + th.onclick = evt => (appState.sortCol = col); + } + }); + + const tbody = this.querySelector('tbody'); + + for (const row of data) { + const tr = document.createElement('tr', { is: 'func-row' }); + tr.className = 'funcrow'; + tr.setAttribute('data-address', row.address); + tr.onchangeExpand = shouldExpand => this.setRowExpand(row.address, shouldExpand); + tbody.appendChild(tr); + } + + this.sortRows(); + this.filterRows(); + } + + sortRows() { + const thead = this.querySelector('thead'); + const headers = thead.querySelectorAll('th'); + + // Update sort indicator + headers.forEach(th => { + const col = th.getAttribute('data-col'); + const indicator = th.querySelector('sort-indicator'); + if (appState.sortCol === col) { + indicator.setAttribute('data-sort', appState.sortDesc ? 'desc' : 'asc'); + } else { + indicator.removeAttribute('data-sort'); + } + }); + + // Select only the function rows and the diff child row. + // Exclude any nested tables used to *display* the diffs. + const tbody = this.querySelector('tbody'); + const rows = tbody.querySelectorAll('tr.funcrow[data-address], tr.diffRow[data-address]'); + + // Sort all rows according to chosen order + const newRows = Array.from(rows); + newRows.sort((rowA, rowB) => { + const addrA = rowA.getAttribute('data-address'); + const addrB = rowB.getAttribute('data-address'); + + // Diff row always sorts after its parent row + if (addrA === addrB && rowB.className === 'diffRow') { + return -1; + } + + return rowSortOrder(addrA, addrB); + }); + + // Replace existing rows with updated order + newRows.forEach(row => tbody.appendChild(row)); + } + + filterRows() { + const tbody = this.querySelector('tbody'); + const rows = tbody.querySelectorAll('tr.funcrow[data-address], tr.diffRow[data-address]'); + + rows.forEach(row => { + const addr = row.getAttribute('data-address'); + const hidden = !addrShouldAppear(addr); + setBooleanAttribute(row, 'hidden', hidden); + }); + + // Update row count + this.querySelector('#rowcount').textContent = `${tbody.querySelectorAll('tr.funcrow:not([hidden])').length}`; + } +} + +window.onload = () => { + window.customElements.define('listing-table', ListingTable); + window.customElements.define('diff-display', DiffDisplay); + window.customElements.define('diff-display-options', DiffDisplayOptions); + window.customElements.define('sort-indicator', SortIndicator); + window.customElements.define('func-row', FuncRow, { extends: 'tr' }); + window.customElements.define('func-row-child', FuncRowChild, { extends: 'tr' }); +}; diff --git a/tools/reccmp/reccmp.py b/tools/reccmp/reccmp.py index d135eb87..711375d6 100755 --- a/tools/reccmp/reccmp.py +++ b/tools/reccmp/reccmp.py @@ -10,7 +10,7 @@ from isledecomp import ( Bin, get_file_in_script_dir, - print_diff, + print_combined_diff, diff_json, percent_string, ) @@ -48,8 +48,12 @@ def gen_json(json_file: str, orig_file: str, data): def gen_html(html_file, data): + js_path = get_file_in_script_dir("reccmp.js") + with open(js_path, "r", encoding="utf-8") as f: + reccmp_js = f.read() + output_data = Renderer().render_path( - get_file_in_script_dir("template.html"), {"data": data} + get_file_in_script_dir("template.html"), {"data": data, "reccmp_js": reccmp_js} ) with open(html_file, "w", encoding="utf-8") as htmlfile: @@ -106,7 +110,7 @@ def print_match_verbose(match, show_both_addrs: bool = False, is_plain: bool = F f"{addrs}: {match.name} Effective 100%% match. (Differs in register allocation only)\n\n{ok_text} (still differs in register allocation)\n\n" ) else: - print_diff(match.udiff, is_plain) + print_combined_diff(match.udiff, is_plain, show_both_addrs) print( f"\n{match.name} is only {percenttext} similar to the original, diff above" @@ -282,7 +286,7 @@ def main(): html_obj["effective"] = True if match.udiff is not None: - html_obj["diff"] = "\n".join(match.udiff) + html_obj["diff"] = match.udiff if match.is_stub: html_obj["stub"] = True diff --git a/tools/reccmp/template.html b/tools/reccmp/template.html index aa323852..2dba27fe 100644 --- a/tools/reccmp/template.html +++ b/tools/reccmp/template.html @@ -43,15 +43,15 @@ background: #404040 !important; } - .funcrow:nth-child(odd), #listing th { + .funcrow:nth-child(odd of :not([hidden])), #listing > thead th { background: #282828; } - .funcrow:nth-child(even) { + .funcrow:nth-child(even of :not([hidden])) { background: #383838; } - #listing td, #listing th { + .funcrow > td, .diffRow > td, #listing > thead th { border: 1px #f0f0f0 solid; padding: 0.5em; word-break: break-all !important; @@ -65,233 +65,109 @@ color: #80FF80; } + .diffslug { + color: #8080FF; + } + .identical { font-style: italic; text-align: center; } - #sortind { + sort-indicator { margin: 0 0.5em; } .filters { + align-items: top; + display: flex; font-size: 10pt; - text-align: center; + justify-content: space-between; margin: 0.5em 0 1em 0; } + + .filters > fieldset { + /* checkbox and radio buttons v-aligned with text */ + align-items: center; + display: flex; + } + + .filters > fieldset > label { + margin-right: 10px; + } + + table.diffTable { + border-collapse: collapse; + } + + table.diffTable:not(:last-child) { + /* visual gap *between* diff context groups */ + margin-bottom: 40px; + } + + table.diffTable td, table.diffTable th { + border: 0 none; + padding: 0 10px 0 0; + } + + table.diffTable th { + /* don't break address if asm line is long */ + word-break: keep-all; + } + + diff-display[data-option="0"] th:nth-child(1) { + display: none; + } + + diff-display[data-option="0"] th:nth-child(2), + diff-display[data-option="1"] th:nth-child(2) { + display: none; + } + + label { + user-select: none; + } - +

Decompilation Status

- -
- - - - -
- - -
AddressNameMatching
+ + +
+
+ Options: + + + + +
+
+ Search filters on: + + + + + + +
+
+

Results:

+ + + + + + + + + + +
AddressNameMatching
+
diff --git a/tools/vtable/vtable.py b/tools/vtable/vtable.py index 2c47d1d5..eb09412c 100755 --- a/tools/vtable/vtable.py +++ b/tools/vtable/vtable.py @@ -7,7 +7,7 @@ import colorama from isledecomp.bin import Bin as IsleBin from isledecomp.compare import Compare as IsleCompare -from isledecomp.utils import print_diff +from isledecomp.utils import print_combined_diff # Ignore all compare-db messages. logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler()) @@ -53,13 +53,8 @@ def parse_args() -> argparse.Namespace: return args -def show_vtable_diff(udiff: List[str], verbose: bool = False, plain: bool = False): - lines = [ - line - for line in udiff - if verbose or line.startswith("+") or line.startswith("-") - ] - print_diff(lines, plain) +def show_vtable_diff(udiff: List, _: bool = False, plain: bool = False): + print_combined_diff(udiff, plain) def print_summary(vtable_count: int, problem_count: int):