mirror of
https://github.com/isledecomp/isle.git
synced 2024-11-29 19:16:05 -05:00
reccmp: HTML refactor and diff address display (#581)
* reccmp: HTML refactor and diff address display * Restore the @@ range indicator
This commit is contained in:
parent
ba8f2b1c0f
commit
9c71209fb9
8 changed files with 878 additions and 240 deletions
|
@ -192,11 +192,13 @@ def replace_immediate(chunk: str) -> str:
|
||||||
def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]:
|
def parse_asm(self, data: bytes, start_addr: Optional[int] = 0) -> List[str]:
|
||||||
asm = []
|
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
|
# Use heuristics to disregard some differences that aren't representative
|
||||||
# of the accuracy of a function (e.g. global offsets)
|
# 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
|
# mnemonic + " " + op_str
|
||||||
asm.append(" ".join(result))
|
asm.append((hex(inst.address), " ".join(result)))
|
||||||
|
|
||||||
return asm
|
return asm
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
from isledecomp.types import SymbolType
|
from isledecomp.types import SymbolType
|
||||||
from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences
|
from isledecomp.compare.asm import ParseAsm, can_resolve_register_differences
|
||||||
from .db import CompareDb, MatchInfo
|
from .db import CompareDb, MatchInfo
|
||||||
|
from .diff import combined_diff
|
||||||
from .lines import LinesDb
|
from .lines import LinesDb
|
||||||
|
|
||||||
|
|
||||||
|
@ -307,8 +308,12 @@ def recomp_lookup(addr: int) -> Optional[str]:
|
||||||
float_lookup=recomp_float,
|
float_lookup=recomp_float,
|
||||||
)
|
)
|
||||||
|
|
||||||
orig_asm = orig_parse.parse_asm(orig_raw, match.orig_addr)
|
orig_combined = orig_parse.parse_asm(orig_raw, match.orig_addr)
|
||||||
recomp_asm = recomp_parse.parse_asm(recomp_raw, match.recomp_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)
|
diff = difflib.SequenceMatcher(None, orig_asm, recomp_asm)
|
||||||
ratio = diff.ratio()
|
ratio = diff.ratio()
|
||||||
|
@ -317,7 +322,9 @@ def recomp_lookup(addr: int) -> Optional[str]:
|
||||||
# Check whether we can resolve register swaps which are actually
|
# Check whether we can resolve register swaps which are actually
|
||||||
# perfect matches modulo compiler entropy.
|
# perfect matches modulo compiler entropy.
|
||||||
is_effective_match = can_resolve_register_differences(orig_asm, recomp_asm)
|
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:
|
else:
|
||||||
is_effective_match = False
|
is_effective_match = False
|
||||||
unified_diff = []
|
unified_diff = []
|
||||||
|
@ -352,9 +359,7 @@ def _compare_vtable(self, match: MatchInfo) -> DiffReport:
|
||||||
[t for (t,) in struct.iter_unpack("<L", recomp_table)],
|
[t for (t,) in struct.iter_unpack("<L", recomp_table)],
|
||||||
)
|
)
|
||||||
|
|
||||||
def match_text(
|
def match_text(m: Optional[MatchInfo], raw_addr: Optional[int] = None) -> str:
|
||||||
i: int, m: Optional[MatchInfo], raw_addr: Optional[int] = None
|
|
||||||
) -> str:
|
|
||||||
"""Format the function reference at this vtable index as text.
|
"""Format the function reference at this vtable index as text.
|
||||||
If we have not identified this function, we have the option to
|
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
|
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
|
should override the given function from the superclass, but we have not
|
||||||
implemented this yet.
|
implemented this yet.
|
||||||
"""
|
"""
|
||||||
index = f"vtable0x{i*4:02x}"
|
|
||||||
|
|
||||||
if m is not None:
|
if m is not None:
|
||||||
orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig"
|
orig = hex(m.orig_addr) if m.orig_addr is not None else "no orig"
|
||||||
recomp = (
|
recomp = (
|
||||||
hex(m.recomp_addr) if m.recomp_addr is not None else "no 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:
|
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 = []
|
orig_text = []
|
||||||
recomp_text = []
|
recomp_text = []
|
||||||
|
@ -395,14 +399,22 @@ def match_text(
|
||||||
ratio += 1
|
ratio += 1
|
||||||
|
|
||||||
n_entries += 1
|
n_entries += 1
|
||||||
orig_text.append(match_text(i, orig, raw_orig))
|
index = f"vtable0x{i*4:02x}"
|
||||||
recomp_text.append(match_text(i, recomp))
|
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
|
ratio = ratio / float(n_entries) if n_entries > 0 else 0
|
||||||
|
|
||||||
# n=100: Show the entire table if there is a diff to display.
|
# n=100: Show the entire table if there is a diff to display.
|
||||||
# Otherwise it would be confusing if the table got cut off.
|
# 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(
|
return DiffReport(
|
||||||
match_type=SymbolType.VTABLE,
|
match_type=SymbolType.VTABLE,
|
||||||
|
|
81
tools/isledecomp/isledecomp/compare/diff.py
Normal file
81
tools/isledecomp/isledecomp/compare/diff.py
Normal file
|
@ -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
|
|
@ -5,7 +5,70 @@
|
||||||
import colorama
|
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):
|
def print_diff(udiff, plain):
|
||||||
|
"""Print diff in difflib.unified_diff format."""
|
||||||
if udiff is None:
|
if udiff is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
605
tools/reccmp/reccmp.js
Normal file
605
tools/reccmp/reccmp.js
Normal file
|
@ -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 <tr>
|
||||||
|
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 <tr>
|
||||||
|
// 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 = `
|
||||||
|
<style>
|
||||||
|
fieldset {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Address display:</legend>
|
||||||
|
<input type="radio" id="showNone" name="addrDisplay" value=0>
|
||||||
|
<label for="showNone">None</label>
|
||||||
|
<input type="radio" id="showOrig" name="addrDisplay" value=1>
|
||||||
|
<label for="showOrig">Original</label>
|
||||||
|
<input type="radio" id="showBoth" name="addrDisplay" value=2>
|
||||||
|
<label for="showBoth">Both</label>
|
||||||
|
</fieldset>`;
|
||||||
|
|
||||||
|
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' });
|
||||||
|
};
|
|
@ -10,7 +10,7 @@
|
||||||
from isledecomp import (
|
from isledecomp import (
|
||||||
Bin,
|
Bin,
|
||||||
get_file_in_script_dir,
|
get_file_in_script_dir,
|
||||||
print_diff,
|
print_combined_diff,
|
||||||
diff_json,
|
diff_json,
|
||||||
percent_string,
|
percent_string,
|
||||||
)
|
)
|
||||||
|
@ -48,8 +48,12 @@ def gen_json(json_file: str, orig_file: str, data):
|
||||||
|
|
||||||
|
|
||||||
def gen_html(html_file, 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(
|
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:
|
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"
|
f"{addrs}: {match.name} Effective 100%% match. (Differs in register allocation only)\n\n{ok_text} (still differs in register allocation)\n\n"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print_diff(match.udiff, is_plain)
|
print_combined_diff(match.udiff, is_plain, show_both_addrs)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"\n{match.name} is only {percenttext} similar to the original, diff above"
|
f"\n{match.name} is only {percenttext} similar to the original, diff above"
|
||||||
|
@ -282,7 +286,7 @@ def main():
|
||||||
html_obj["effective"] = True
|
html_obj["effective"] = True
|
||||||
|
|
||||||
if match.udiff is not None:
|
if match.udiff is not None:
|
||||||
html_obj["diff"] = "\n".join(match.udiff)
|
html_obj["diff"] = match.udiff
|
||||||
|
|
||||||
if match.is_stub:
|
if match.is_stub:
|
||||||
html_obj["stub"] = True
|
html_obj["stub"] = True
|
||||||
|
|
|
@ -43,15 +43,15 @@
|
||||||
background: #404040 !important;
|
background: #404040 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.funcrow:nth-child(odd), #listing th {
|
.funcrow:nth-child(odd of :not([hidden])), #listing > thead th {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
}
|
}
|
||||||
|
|
||||||
.funcrow:nth-child(even) {
|
.funcrow:nth-child(even of :not([hidden])) {
|
||||||
background: #383838;
|
background: #383838;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing td, #listing th {
|
.funcrow > td, .diffRow > td, #listing > thead th {
|
||||||
border: 1px #f0f0f0 solid;
|
border: 1px #f0f0f0 solid;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
word-break: break-all !important;
|
word-break: break-all !important;
|
||||||
|
@ -65,233 +65,109 @@
|
||||||
color: #80FF80;
|
color: #80FF80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diffslug {
|
||||||
|
color: #8080FF;
|
||||||
|
}
|
||||||
|
|
||||||
.identical {
|
.identical {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortind {
|
sort-indicator {
|
||||||
margin: 0 0.5em;
|
margin: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
|
align-items: top;
|
||||||
|
display: flex;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
text-align: center;
|
justify-content: space-between;
|
||||||
margin: 0.5em 0 1em 0;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>var data = {{{data}}};</script>
|
||||||
var data = {{{data}}};
|
<script>{{{reccmp_js}}}</script>
|
||||||
|
|
||||||
function diffCssClass(firstChar) {
|
|
||||||
return firstChar === '-' ? 'diffneg' : (firstChar === '+' ? 'diffpos' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function asmLineToDiv(line) {
|
|
||||||
const diff_line = document.createElement('div');
|
|
||||||
diff_line.className = diffCssClass(line[0]);
|
|
||||||
diff_line.innerText = line;
|
|
||||||
return diff_line;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAsm(asm) {
|
|
||||||
var lines = asm.split('\n');
|
|
||||||
return lines.filter(line => line.length > 0)
|
|
||||||
.map(asmLineToDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowClick() {
|
|
||||||
if (this.dataset.expanded === 'true') {
|
|
||||||
this.nextSibling.remove();
|
|
||||||
this.dataset.expanded = false;
|
|
||||||
} else {
|
|
||||||
var row = this.parentNode.insertBefore(document.createElement('tr'), this.nextSibling);
|
|
||||||
row.classList.add('diff');
|
|
||||||
var decCel = row.appendChild(document.createElement('td'));
|
|
||||||
decCel.colSpan = 3;
|
|
||||||
var diff = data[this.dataset.index].diff;
|
|
||||||
const stub = "stub" in data[this.dataset.index];
|
|
||||||
|
|
||||||
if (stub) {
|
|
||||||
diff = document.createElement('div');
|
|
||||||
diff.className = 'identical';
|
|
||||||
diff.innerText = 'Stub. No diff.';
|
|
||||||
decCel.appendChild(diff);
|
|
||||||
} else if (diff == '') {
|
|
||||||
diff = document.createElement('div');
|
|
||||||
diff.className = 'identical';
|
|
||||||
diff.innerText = 'Identical function - no diff';
|
|
||||||
decCel.appendChild(diff);
|
|
||||||
} else {
|
|
||||||
diff = formatAsm(diff);
|
|
||||||
for (const el of diff) {
|
|
||||||
decCel.appendChild(el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.dataset.expanded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAllDiffs() {
|
|
||||||
const collection = document.getElementsByClassName("diff");
|
|
||||||
for (var ele of collection) {
|
|
||||||
ele.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterOptions = { text: '', hidePerfect: false, hideStub: false };
|
|
||||||
|
|
||||||
function filter() {
|
|
||||||
closeAllDiffs();
|
|
||||||
|
|
||||||
var ltext = filterOptions.text.toLowerCase();
|
|
||||||
|
|
||||||
const collection = document.getElementsByClassName("funcrow");
|
|
||||||
var searchCount = 0;
|
|
||||||
for (var ele of collection) {
|
|
||||||
var eledata = data[ele.dataset.index];
|
|
||||||
|
|
||||||
const stubOk = (!filterOptions.hideStub || !("stub" in eledata));
|
|
||||||
|
|
||||||
const textOk = (ltext == ''
|
|
||||||
|| eledata.address.toLowerCase().includes(ltext)
|
|
||||||
|| eledata.name.toLowerCase().includes(ltext));
|
|
||||||
|
|
||||||
const perfOk = (!filterOptions.hidePerfect || (eledata.matching < 1));
|
|
||||||
|
|
||||||
if (stubOk && textOk && perfOk) {
|
|
||||||
ele.style.display = '';
|
|
||||||
searchCount++;
|
|
||||||
} else {
|
|
||||||
ele.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastSortedCol = -1;
|
|
||||||
var ascending = true;
|
|
||||||
|
|
||||||
function sortByColumn(column) {
|
|
||||||
closeAllDiffs();
|
|
||||||
|
|
||||||
if (column == lastSortedCol) {
|
|
||||||
ascending = !ascending;
|
|
||||||
}
|
|
||||||
lastSortedCol = column;
|
|
||||||
|
|
||||||
const collection = document.getElementsByClassName("funcrow");
|
|
||||||
|
|
||||||
var newOrder = [];
|
|
||||||
|
|
||||||
for (var ele of collection) {
|
|
||||||
var inserted = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < newOrder.length; i++) {
|
|
||||||
var cmpEle = newOrder[i];
|
|
||||||
|
|
||||||
var ourCol = ele.childNodes[column];
|
|
||||||
var cmpCol = cmpEle.childNodes[column];
|
|
||||||
|
|
||||||
if ((cmpCol.dataset.value > ourCol.dataset.value) == ascending) {
|
|
||||||
newOrder.splice(i, 0, ele);
|
|
||||||
inserted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inserted) {
|
|
||||||
newOrder.push(ele);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 1; i < newOrder.length; i++) {
|
|
||||||
newOrder[i - 1].after(newOrder[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortIndicator = document.getElementById('sortind');
|
|
||||||
if (!sortIndicator) {
|
|
||||||
sortIndicator = document.createElement('span');
|
|
||||||
sortIndicator.id = 'sortind';
|
|
||||||
}
|
|
||||||
sortIndicator.innerHTML = ascending ? '▲' : '▼';
|
|
||||||
|
|
||||||
var th = document.getElementById('listingheader').childNodes[column];
|
|
||||||
th.appendChild(sortIndicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
var listing = document.getElementById('listing');
|
|
||||||
|
|
||||||
const headers = listing.getElementsByTagName('th');
|
|
||||||
var headerCount = 0;
|
|
||||||
for (const header of headers) {
|
|
||||||
header.addEventListener('click', function(){
|
|
||||||
sortByColumn(this.dataset.column, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
header.dataset.column = headerCount;
|
|
||||||
headerCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.forEach((element, index) => {
|
|
||||||
var row = listing.appendChild(document.createElement('tr'));
|
|
||||||
var addrCel = row.appendChild(document.createElement('td'));
|
|
||||||
var nameCel = row.appendChild(document.createElement('td'));
|
|
||||||
var matchCel = row.appendChild(document.createElement('td'));
|
|
||||||
|
|
||||||
addrCel.innerText = addrCel.dataset.value = element.address;
|
|
||||||
nameCel.innerText = nameCel.dataset.value = element.name;
|
|
||||||
|
|
||||||
if ("stub" in element) {
|
|
||||||
matchCel.innerHTML = 'stub'
|
|
||||||
matchCel.dataset.value = -1;
|
|
||||||
} else {
|
|
||||||
var effectiveNote = (element.matching == 1 && element.diff != '') ? '*' : '';
|
|
||||||
matchCel.innerHTML = (element.matching * 100).toFixed(2) + '%' + effectiveNote;
|
|
||||||
matchCel.dataset.value = element.matching;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.classList.add('funcrow');
|
|
||||||
row.addEventListener('click', rowClick);
|
|
||||||
row.dataset.index = index;
|
|
||||||
row.dataset.expanded = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
var search = document.getElementById('search');
|
|
||||||
search.addEventListener('input', function (evt) {
|
|
||||||
filterOptions.text = search.value;
|
|
||||||
filter();
|
|
||||||
});
|
|
||||||
|
|
||||||
const cbHidePerfect = document.getElementById('cbHidePerfect');
|
|
||||||
cbHidePerfect.addEventListener('change', evt => {
|
|
||||||
filterOptions.hidePerfect = evt.target.checked;
|
|
||||||
filter();
|
|
||||||
})
|
|
||||||
|
|
||||||
const cbHideStub = document.querySelector('#cbHideStub');
|
|
||||||
cbHideStub.addEventListener('change', evt => {
|
|
||||||
filterOptions.hideStub = evt.target.checked;
|
|
||||||
filter();
|
|
||||||
})
|
|
||||||
|
|
||||||
sortByColumn(0);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<h1>Decompilation Status</h1>
|
<h1>Decompilation Status</h1>
|
||||||
<input id="search" type="search" placeholder="Search for offset or function name...">
|
<listing-table>
|
||||||
<div class="filters">
|
<input id="search" type="search" placeholder="Search for offset or function name...">
|
||||||
<label for="cbHidePerfect">Hide 100% match</label>
|
<div class="filters">
|
||||||
<input type="checkbox" id="cbHidePerfect" />
|
<fieldset>
|
||||||
<label for="cbHideStub">Hide stubs</label>
|
<legend>Options:</legend>
|
||||||
<input type="checkbox" id="cbHideStub" />
|
<input type="checkbox" id="cbHidePerfect" />
|
||||||
</div>
|
<label for="cbHidePerfect">Hide 100% match</label>
|
||||||
<table id="listing">
|
<input type="checkbox" id="cbHideStub" />
|
||||||
<tr id='listingheader'><th style='width: 20%'>Address</th><th style="width:60%">Name</th><th style='width: 20%'>Matching</th></tr>
|
<label for="cbHideStub">Hide stubs</label>
|
||||||
</table>
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Search filters on:</legend>
|
||||||
|
<input type="radio" name="filterType" id="filterName" value=1 checked />
|
||||||
|
<label for="filterName">Name/address</label>
|
||||||
|
<input type="radio" name="filterType" id="filterAsm" value=2 />
|
||||||
|
<label for="filterAsm">Asm output</label>
|
||||||
|
<input type="radio" name="filterType" id="filterDiff" value=3 />
|
||||||
|
<label for="filterDiff">Asm diffs only</label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<p>Results: <span id="rowcount"></span></p>
|
||||||
|
<table id="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="address" style="width: 20%">Address<sort-indicator/></th>
|
||||||
|
<th data-col="name" style="width: 60%">Name<sort-indicator/></th>
|
||||||
|
<th data-col="matching" style="width: 20%">Matching<sort-indicator/></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</listing-table>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import colorama
|
import colorama
|
||||||
from isledecomp.bin import Bin as IsleBin
|
from isledecomp.bin import Bin as IsleBin
|
||||||
from isledecomp.compare import Compare as IsleCompare
|
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.
|
# Ignore all compare-db messages.
|
||||||
logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler())
|
logging.getLogger("isledecomp.compare").addHandler(logging.NullHandler())
|
||||||
|
@ -53,13 +53,8 @@ def parse_args() -> argparse.Namespace:
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def show_vtable_diff(udiff: List[str], verbose: bool = False, plain: bool = False):
|
def show_vtable_diff(udiff: List, _: bool = False, plain: bool = False):
|
||||||
lines = [
|
print_combined_diff(udiff, plain)
|
||||||
line
|
|
||||||
for line in udiff
|
|
||||||
if verbose or line.startswith("+") or line.startswith("-")
|
|
||||||
]
|
|
||||||
print_diff(lines, plain)
|
|
||||||
|
|
||||||
|
|
||||||
def print_summary(vtable_count: int, problem_count: int):
|
def print_summary(vtable_count: int, problem_count: int):
|
||||||
|
|
Loading…
Reference in a new issue