mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
9.4 KiB
Python
243 lines
9.4 KiB
Python
"""Differential test of the portable FP/SIMD lowering (lifter simd='c') against Unicorn as a Cortex-A57.
|
|
|
|
Needs no game files. Enumerates every opcode bit pattern of the FP/SIMD data-processing space (bits 27..25 = 111),
|
|
the FP/SIMD load/store space and CRC32 with random register fields, and compares which encodings the lifter and
|
|
Unicorn accept. Then a stratified sample of encodings both accept is compiled and run with random interesting
|
|
operands (NaNs, infinities, denormals, +-0, saturation edges, random FPCR.DN/FZ/RMode/AHP): X0-X30, SP, NZCV, V0-V31
|
|
and, for stores, memory must match. FPSR is not compared; the lowering does not model the cumulative exception bits
|
|
or QC.
|
|
|
|
Adapted from LAS Recomp tools/test_simd.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
|
|
|
Usage: simd.py [--encodings N] [--cases K] [--seed S] [--jobs J] [--no-validity] [--filter HEX/MASK]
|
|
"""
|
|
import argparse
|
|
import functools
|
|
import os
|
|
import platform
|
|
import random
|
|
import struct
|
|
import sys
|
|
import time
|
|
import zlib
|
|
from collections import Counter, defaultdict
|
|
|
|
import unicorn as uc
|
|
from unicorn import arm64_const as A
|
|
|
|
from common import BUF, BUF_SIZE, ROOT, UC_INVALID, Emu, build_harness, diff_state, dis, mnemonic, rand_fpcr, \
|
|
rand_v, rand_x, run_harness
|
|
from lifter import Lifter, Unsupported
|
|
|
|
print = functools.partial(print, flush=True)
|
|
BASE_AREA = BUF + 0x8000
|
|
CODE, CODE_SIZE = 0x7210000000, 0x400000
|
|
|
|
|
|
def is_ldst(w):
|
|
return (w >> 25) & 1 == 0 and (w >> 27) & 1 == 1
|
|
|
|
|
|
def is_store(w):
|
|
if not is_ldst(w):
|
|
return False
|
|
if (w & 0x3B000000) == 0x18000000: # literal
|
|
return False
|
|
return not (w >> 22) & 1 # L bit, same position for structures, pairs and single registers
|
|
|
|
|
|
def is_literal(w):
|
|
return (w & 0x3F000000) == 0x1C000000
|
|
|
|
|
|
def class_key(w):
|
|
"""Opcode signature: register fields (Rd, Rn, Rm) cleared."""
|
|
if is_ldst(w):
|
|
return w & 0xFFE0FC00 if (w & 0xBE000000) == 0x0C000000 else w & 0xFFFFFC00
|
|
return w & 0xFFE0FC00
|
|
|
|
|
|
def candidates(r):
|
|
"""Every opcode bit pattern of the FP/SIMD data-processing and load/store spaces, random registers."""
|
|
out = []
|
|
for p in range(1 << 19):
|
|
hi, mid = p >> 15, p & 0x7FFF
|
|
rr = r.getrandbits(10)
|
|
out.append((hi << 28) | (0b111 << 25) | (mid << 10) | rr) # data processing
|
|
out.append((hi << 28) | (1 << 27) | (1 << 26) | (mid << 10) | rr) # loads/stores
|
|
for sf in (0, 1):
|
|
for C in (0, 1):
|
|
for sz in range(4):
|
|
for _ in range(4):
|
|
out.append((sf << 31) | (0b0011010110 << 21) | (r.getrandbits(5) << 16) | (0b010 << 13) |
|
|
(C << 12) | (sz << 10) | r.getrandbits(10))
|
|
return out
|
|
|
|
|
|
def validity(lifter, ws, emu):
|
|
"""Map word -> (lifter accepts, Unicorn accepts); words are placed at CODE + 4i."""
|
|
mu = emu.mu
|
|
res = {}
|
|
step = CODE_SIZE // 4
|
|
for base in range(0, len(ws), step):
|
|
chunk = ws[base:base + step]
|
|
mu.mem_write(CODE, struct.pack(f'<{len(chunk)}I', *chunk))
|
|
mu.ctl_remove_cache(CODE, CODE + CODE_SIZE)
|
|
state = list(zip(emu.xregs, [BASE_AREA] * 31)) + [(A.UC_ARM64_REG_SP, BASE_AREA)]
|
|
for i, w in enumerate(chunk):
|
|
pc = CODE + 4 * i
|
|
try:
|
|
lifter.lift(w, pc)
|
|
lok = True
|
|
except Unsupported:
|
|
lok = False
|
|
if i % 32 == 0:
|
|
mu.reg_write_batch(state)
|
|
try:
|
|
mu.emu_start(pc, pc + 4, count=1)
|
|
uok = True
|
|
except uc.UcError as e:
|
|
uok = e.errno not in UC_INVALID
|
|
res[w] = (lok, uok)
|
|
return res
|
|
|
|
|
|
def make_case(r, w):
|
|
fpcr = rand_fpcr(r)
|
|
v = [rand_v(r) for _ in range(32)]
|
|
nzcv = r.getrandbits(4) << 28
|
|
sp = BASE_AREA + r.randrange(0, 0x100, 16)
|
|
if is_ldst(w):
|
|
x = [BASE_AREA + r.randrange(0, 0x100) for _ in range(31)]
|
|
rm = (w >> 16) & 31
|
|
if rm < 31 and rm != (w >> 5) & 31:
|
|
reg_off = (w & 0x3B200C00) == 0x38200800 # register offset
|
|
post_reg = (w & 0xBF800000) in (0x0C800000, 0x0D800000)
|
|
if reg_off or post_reg:
|
|
x[rm] = r.randrange(0, 0x100)
|
|
else:
|
|
x = [rand_x(r) for _ in range(31)]
|
|
return fpcr, x, sp, nzcv, v
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument('--encodings', type=int, default=20000)
|
|
ap.add_argument('--cases', type=int, default=8)
|
|
ap.add_argument('--seed', type=int, default=1)
|
|
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4)
|
|
ap.add_argument('--no-validity', action='store_true', help='skip the full validity enumeration')
|
|
ap.add_argument('--filter', help='only encodings with (w & MASK) == VAL, as VAL/MASK in hex')
|
|
ap.add_argument('--show', type=int, default=40)
|
|
ap.add_argument('--cflags', default='-march=x86-64-v3' if platform.machine().lower() in ('amd64', 'x86_64') else '')
|
|
args = ap.parse_args()
|
|
r = random.Random(args.seed)
|
|
t0 = time.time()
|
|
lifter = Lifter(0, 'c')
|
|
emu = Emu([(CODE, bytes(CODE_SIZE))])
|
|
cands = candidates(r)
|
|
if args.filter:
|
|
val, mask = (int(x, 16) for x in args.filter.split('/'))
|
|
cands = [w for w in cands if (w & mask) == val]
|
|
if args.no_validity:
|
|
val = validity(lifter, r.sample(cands, min(len(cands), args.encodings * 4)), emu)
|
|
else:
|
|
val = validity(lifter, cands, emu)
|
|
agree = Counter()
|
|
only = defaultdict(list)
|
|
for w, (lok, uok) in val.items():
|
|
agree[(lok, uok)] += 1
|
|
if lok != uok:
|
|
only['lifter' if lok else 'unicorn'].append(w)
|
|
print(f'validity: {len(val)} encodings, both valid {agree[(True, True)]}, both invalid {agree[(False, False)]}, '
|
|
f'lifter-only {agree[(True, False)]}, unicorn-only {agree[(False, True)]} ({time.time() - t0:.0f}s)')
|
|
for kind in ('lifter', 'unicorn'):
|
|
ws = only[kind]
|
|
if ws:
|
|
print(f' {kind}-only by mnemonic: {dict(Counter(mnemonic(w) for w in ws).most_common(30))}')
|
|
for w in ws[:args.show // 2]:
|
|
print(f' {w:08x} {dis(w)}')
|
|
|
|
# Stratified sample of valid encodings: one per opcode signature first.
|
|
valid = [w for w, (lok, uok) in val.items() if lok and uok]
|
|
r.shuffle(valid)
|
|
seen, pick = set(), []
|
|
for w in valid:
|
|
k = class_key(w)
|
|
if k not in seen:
|
|
seen.add(k)
|
|
pick.append(w)
|
|
picked = set(pick)
|
|
pick = (pick + [w for w in valid if w not in picked])[:args.encodings]
|
|
# Placed at CODE + 4i; literal loads get an offset into the code region.
|
|
for i, w in enumerate(pick):
|
|
pc = CODE + 4 * i
|
|
if is_literal(w):
|
|
tgt = CODE + r.randrange(0, (len(pick) * 4 + 0x1000) & ~3, 4)
|
|
pick[i] = (w & ~(0x7FFFF << 5)) | ((((tgt - pc) >> 2) & 0x7FFFF) << 5)
|
|
code_bytes = struct.pack(f'<{len(pick)}I', *pick) + bytes(0x1000 + 64)
|
|
bodies = [lifter.lift(w, CODE + 4 * i).code for i, w in enumerate(pick)]
|
|
work = ROOT / 'build' / 'test' / 'simd'
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
t1 = time.time()
|
|
exe = build_harness(work, bodies, args.jobs, args.cflags)
|
|
print(f'compiled {len(bodies)} tests ({time.time() - t1:.0f}s)')
|
|
|
|
mu = emu.mu
|
|
mu.mem_write(CODE, code_bytes)
|
|
mu.ctl_remove_cache(CODE, CODE + CODE_SIZE)
|
|
buf0 = bytes(r.getrandbits(8) for _ in range(4096)) * (BUF_SIZE // 4096)
|
|
mu.mem_write(BUF, buf0)
|
|
code_path = work / 'code.bin'
|
|
code_path.write_bytes(code_bytes)
|
|
cases, expect = [], []
|
|
t2 = time.time()
|
|
for k, w in enumerate(pick):
|
|
pc = CODE + 4 * k
|
|
st = is_store(w)
|
|
for _ in range(args.cases):
|
|
fpcr, x, sp, nzcv, v = make_case(r, w)
|
|
try:
|
|
ox, osp, onz, opc, ov = emu.run(pc, fpcr, x, sp, nzcv, v)
|
|
except uc.UcError:
|
|
if st:
|
|
mu.mem_write(BUF, buf0)
|
|
continue
|
|
mh = 0
|
|
if st:
|
|
mh = zlib.crc32(bytes(mu.mem_read(BUF, BUF_SIZE)))
|
|
mu.mem_write(BUF, buf0)
|
|
cases.append((k, fpcr, x, sp, nzcv, v, st))
|
|
expect.append((ox, osp, onz, opc, ov, mh))
|
|
print(f'unicorn: {len(cases)} cases ({time.time() - t2:.0f}s)')
|
|
|
|
t3 = time.time()
|
|
recs, err = run_harness(exe, work, buf0, cases, [(CODE, code_path)])
|
|
print(f'harness: {len(recs)} of {len(cases)} cases ({time.time() - t3:.0f}s)')
|
|
if err:
|
|
k = cases[len(recs)][0] if len(recs) < len(cases) else None
|
|
print(f'harness failed: {err}' + (f', stopped at {pick[k]:08x} {dis(pick[k])}' if k is not None else ''))
|
|
|
|
fails, nfail = {}, 0
|
|
for case, want, got in zip(cases, expect, recs):
|
|
k, v, st = case[0], case[5], case[6]
|
|
d = diff_state(got, want, check_pc=False, check_mem=st)
|
|
if d:
|
|
nfail += 1
|
|
w = pick[k]
|
|
if w not in fails:
|
|
ins = [f'v{j}={v[j]:032x}' for j in sorted({(w >> 5) & 31, (w >> 16) & 31, w & 31})]
|
|
fails[w] = (case[1], d[:3], ins)
|
|
print(f'{len(pick)} encodings, {len(cases)} cases: {nfail} failing cases, {len(fails)} failing encodings '
|
|
f'({time.time() - t0:.0f}s total)')
|
|
if fails:
|
|
print(' failing by mnemonic:', dict(Counter(mnemonic(w) for w in fails).most_common(40)))
|
|
for w, (fpcr, d, ins) in list(fails.items())[:args.show]:
|
|
print(f' {w:08x} {dis(w):40} fpcr={fpcr:x} {"; ".join(d)}')
|
|
print(f' {" ".join(ins)}')
|
|
return 1 if fails or err else 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|